Skip to content

feat: serve React UI views from Python toolsets#11

Merged
ciaransweet merged 4 commits into
mainfrom
feat/toolset-ui-views
Jul 23, 2026
Merged

feat: serve React UI views from Python toolsets#11
ciaransweet merged 4 commits into
mainfrom
feat/toolset-ui-views

Conversation

@ciaransweet

Copy link
Copy Markdown
Contributor

What I am changing

Toolsets can now ship UI views — small frontend components (a map, a
gallery, a chart) that a UI-capable MCP host renders inline in the chat and
feeds the tool's result. This works in Claude web / mcp-ui clients and in this
repo's own Chainlit agent, so a tool answer can be an interactive map instead of
a wall of text, and clicking inside it drives the next tool call.

Crucially, the runtime stays pure-Python. A view is a build-time HTML bundle
(React/Vite); nothing new executes at call time. Adopting it is one export
(VIEWS = {tool_name: view_id}) plus a bundle — the credential and structured-
return contracts are unchanged, and a plain MCP client still gets the tool's
text/structuredContent as before (views are progressive enhancement).

Includes a worked example (stac-explorer) against the public Planetary
Computer STAC catalog: search → thumbnail gallery → pick → map with real data
tiles over the collection's extent.

How I did it

  • packages/mcp-runtime/src/mcp_runtime/views.py (new) — the whole runtime
    surface: serve each view as a ui://<toolset>/<view_id> MCP resource and
    stamp the owning tool's _meta with that URI (the mcp-ui / Apps-SDK
    convention). VIEWS and bundles are validated at build_server time.
  • packages/mcp-runtime/src/mcp_runtime/server.py — wire the two calls into
    build_server (~3 lines).
  • packages/mcp-agent/src/mcp_agent/web.py + public/elements/McpView.jsx
    — the Chainlit agent reads bundles via get_resources and matches them to
    tools by _meta (standard MCP, no bespoke endpoints), rendering each in a
    sandboxed iframe (srcDoc, opaque origin, allow-scripts only). Interactions
    post back as user messages so the loop advances the chat.
  • toolsets/stac-explorer/ (new) — the example: two async tools + a Vite +
    React ui/ (host bridge in ui/src/host.ts); show_map returns a georef'd
    XYZ tile_url from the catalog's mosaic tiler.
  • Toolingnew-toolset --with-ui scaffolds a ui/; scripts/build-views
    builds/typechecks all UIs; the Dockerfile gains a node stage that inlines
    views into the wheel; a CI ui job typechecks, bundles and validates
    VIEWS↔bundle wiring. GitHub Actions pinned to SHAs at latest majors.
  • Docs — "Toolset UI views" section in README.md, notes in CLAUDE.md.

How you can test it

Automated: ./scripts/test (view runtime tests in
packages/mcp-runtime/tests/test_views.py) and ./scripts/build-views
(typecheck + bundle every UI).

End to end (needs node + a chat provider, e.g. uv add langchain-openai
with PROVIDER_MODEL/PROVIDER_API_KEY set):

# build the example's view bundles
cd toolsets/stac-explorer/ui && npm install && npm run build && cd -

# terminal 1: the toolset server
TOOLSET=stac-explorer uv run mcp-serve

# terminal 2 (repo root): the Chainlit agent
uv run mcp-agent-web    # http://localhost:8080

In the chat: "find denver land cover data" → a thumbnail gallery renders →
click Show on map → the model calls show_map and a Leaflet map renders the
classified land-cover tiles over Colorado. Try "show me hawaii datasets" for
a collection without a tiler (the map falls back to the extent box, gracefully).

Standard-MCP check (no model): with the server running, tool _meta and the
ui:// resources are visible over plain MCP — uv run mcp-cli list shows the
tools, and a MultiServerMCPClient.get_resources() call returns the bundles.

🤖 Generated with Claude Code

ciaransweet and others added 4 commits July 22, 2026 20:41
A toolset can ship a view: a build-time HTML bundle a UI-capable host
renders in an iframe, fed the tool's structuredContent. Opt in with a
VIEWS ({tool_name: view_id}) export and a bundle at
<package>/views/<view_id>.html.

The runtime does two standard-MCP things: serves each view as a
ui://<toolset>/<view_id> resource and stamps the owning tool's _meta with
that URI (the mcp-ui / Apps-SDK convention). Nothing executes at call
time, so the runtime stays pure-Python. VIEWS entries and bundles are
validated at build_server time, the same gate as the ToolResult contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The agent reads each connected toolset's ui:// view bundles via
get_resources and matches them to tools by their _meta — standard MCP, no
bespoke endpoints. A view renders in a sandboxed iframe (srcDoc, opaque
origin, allow-scripts only), fed the tool's structuredContent; the
iframe's postMessage interactions come back as user messages via
sendUserMessage, so the loop advances the chat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A worked example of the view round-trip against the public Microsoft
Planetary Computer STAC catalog. search_collections returns a thumbnail
gallery; picking one drives show_map, which renders the collection's data
as map tiles (via the catalog's mosaic tiler) over its extent. The gallery
and map views are built from ui/ (Vite + React) into the package's views/
dir; interactions post back to advance the chat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- new-toolset --with-ui scaffolds a Vite + React ui/ with an example view
- build-views builds and typechecks every toolset UI
- Dockerfile gains a node build stage that inlines views into the wheel
- CI ui job typechecks, bundles and validates VIEWS<->bundle wiring
- document the view contract in README and CLAUDE
- pin all GitHub Actions to commit SHAs at their latest majors

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ciaransweet

Copy link
Copy Markdown
Contributor Author

Short lil demo of what the stac-explorer looks like.

Screen.Recording.2026-07-23.at.09.53.08.mov

@ciaransweet
ciaransweet requested a review from yellowcap July 23, 2026 08:55

@yellowcap yellowcap left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation looks solid, works well locally, except the planetary computer tiles. They are requested but fail. I assume that is because I dont have a key or so? Is this expected to work without keys? If not, maybe add to .env.example

@ciaransweet

Copy link
Copy Markdown
Contributor Author

Implementation looks solid, works well locally, except the planetary computer tiles. They are requested but fail. I assume that is because I dont have a key or so? Is this expected to work without keys? If not, maybe add to .env.example

No, it should work without keys, it's all public. Perhaps the dataset didn't have tiles? Did you get obvious errors to share? @yellowcap

@yellowcap

Copy link
Copy Markdown
Member

ok all good, it was blocked by a browser plugin 😅

@ciaransweet
ciaransweet merged commit 3acfcb5 into main Jul 23, 2026
7 checks passed
@ciaransweet
ciaransweet deleted the feat/toolset-ui-views branch July 23, 2026 15:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants