MCP · June 2026

Building MCP UI: what I learned from PlaceScout

I recently built PlaceScout as an MCP server. It searches 4.4 million UK places by category and location, surfaces contact info, and lets you save leads to a live dashboard. The point was to make local market prospecting work inside AI chat tools, not as a separate app you switch to.

Demo: PlaceScout MCP server with MCP UI rendering a live interactive leads dashboard directly inside ChatGPT — the AI searches UK places, saves leads, and opens a map, all without leaving the chat window.

This post is about what I learned building it. Specifically about MCP tools, MCP UI, and where I think this is going.


What MCP is

MCP (Model Context Protocol) is a standard for letting AI assistants call external tools. You define your tools with schemas. The AI client discovers them, calls them, and acts on the results. You don't write a custom integration per model.

Anthropic published the spec. It's now supported in Claude, ChatGPT, and others. From a developer's view it feels like building an API, except the AI is the client and it figures out which tool to call and when.


The tools in PlaceScout

PlaceScout exposes five main tools.

search_places does a hybrid semantic and keyword search across a taxonomy of 1,278 place categories. You pass a free-text description and a location. It runs two stages: first it finds matching categories (60% vector similarity, 40% BM25 full-text), then it filters places by those category IDs plus a location full-text search in DuckDB.

Why hybrid search rather than pure vector? Because pure vector search misses obvious keyword matches. If someone types "coffee shop" you want "Coffee Shop" to rank at the top, not just things that are semantically adjacent to it. BM25 handles that. The vector component handles intent ("places to grab a flat white" should still find coffee shops).

nearby_places finds places within a radius of seed place IDs, sorted by distance. Useful when you already have one target and want to find similar businesses nearby. It computes a centroid from the seed places and runs a DuckDB spatial query.

categories_by_location returns the most common categories in a location, ranked by frequency. The AI can call this before a targeted search to understand what's actually there. Useful for prompts like "what types of businesses are in Bristol?" before deciding what to prospect.

save_lead saves a place to the prospecting pipeline with a status (new, contacted, qualified, disqualified, converted) and optional notes. On save, it fires a server-sent event that the dashboard picks up for real-time sync.

dashboard is where MCP UI comes in.


How the dashboard works

When the AI calls dashboard, it returns an iframe embed pointing at /dashboard on the server. The MCP client renders it full-screen inside the chat.

The dashboard is a React SPA. It has a Leaflet map taking up the top 55% of the viewport, with pins for each saved lead. Below that is a searchable, paginated table. A sidebar handles status and category filtering. Clicking a pin shows a popup with the place name, status badge, category tags, and contact links.

Real-time sync works via an EventSource connection to /api/v2/events. When any client saves a lead (via the save_lead tool), the server fires an SSE event. The dashboard catches it and revalidates the leads list. So if you're running the AI in one window and watching the dashboard in another, the map updates without a refresh.

To make this work you need:


Generative UI

The execute_ui tool is a different approach. Instead of embedding a fixed React app, the AI writes Python code on the fly, and that code runs in a Pyodide WASM sandbox.

The code uses Prefab, a component library I've been building. Components include DataTable, BarChart, a Leaflet map embed, and reactive controls like Slider and Checkbox. The AI writes something like:

from prefab_ui.app import PrefabApp
from prefab_ui.components import Column, Heading, DataTable, DataTableColumn

with PrefabApp() as app:
    with Column(gap=4, css_class="p-4"):
        Heading("Coffee shops in Bristol with email addresses", level=2)
        DataTable(
            columns=[
                DataTableColumn(key="name", header="Name", sortable=True),
                DataTableColumn(key="locality", header="Locality"),
                DataTableColumn(key="email", header="Email"),
            ],
            rows=places,
            search=True, paginated=True, page_size=20,
        )

The data parameter takes a JSON string from a previous tool call. Keys become globals in the sandbox. So result["places"] is just available as places if you inject {"places": [...]}.

What makes this useful is that the AI can decide what to visualise based on the actual shape of the data it just fetched. If the search returned lots of places with Instagram handles, it can build a table showing just those. If you asked about categories, it can build a bar chart. No pre-built component for each specific view.

Pyodide runs real CPython. Full Python is supported inside the sandbox. The main constraint is startup time on cold calls, which is noticeable.


MCP app marketplaces

Anthropic and others are building MCP app marketplaces. Users will be able to discover and install MCP servers directly from inside their AI client. The server shows up as a set of tools the AI can use, with no separate app to download or onboard to.

For something like PlaceScout this means the full workflow lives inside the chat. User asks the AI to find physiotherapy clinics in Leeds with a website. AI calls search_places, gets results, calls execute_ui to render a contact table, then save_lead on the ones worth reaching out to. The leads dashboard opens inside the chat. Everything happens in one place.

We're early. Marketplaces are still being built out and distribution isn't guaranteed. But the direction is clear from what's being shipped.


What was hard

ChatGPT CSP. ChatGPT ignores the CSP you configure and applies its own. If your dashboard iframe makes PATCH or PUT requests back to your server, they'll be blocked. Build for this from the start rather than discovering it later.

Tool schema quality matters more than you'd expect. The AI calls the right tool based on your description and parameter names. A vague description gets the wrong tool called, or the right tool called with bad inputs. I rewrote the categories_by_location description twice before the AI started using it proactively rather than only when asked.

SSE + public HTTPS. Real-time dashboard sync via server-sent events requires a public URL. Local development with localhost doesn't work for testing from ChatGPT. I use ngrok and Sprites (a Fly.io-backed ephemeral service) for this.

Pyodide cold start. The first execute_ui call in a session takes a few seconds while Pyodide initialises. Subsequent calls in the same session are fast because the sandbox is reused. Worth setting that expectation in the tool description.


I'm doing more MCP development at timeship.dev and will write more as I build more things with it.

Have an MCP app in mind?

Tell us about your product, data, and the AI clients you want to support. We'll map the integration with you.

Discuss an MCP app