Skip to content

MCP Apps

MCP Apps give you an interactive, visual interface on top of Bridge Town’s existing tools. When you or your agent calls a Bridge Town tool — list pull requests, schedule a model run, query data — you get the usual structured response. Apps that have an explicit presenter open only when that presenter is called; their data and mutation tools do not mount a card during an investigation. Other mapped tools retain their documented result behavior.

Apps are progressive enhancement: data tools keep their complete structured responses, and explicit presenters resolve bounded server results for display. Presentation does not repeat an operation, authorize a merge, or replace a required confirmation. Bridge Town ships 8 first-party apps.

The 8 built-in apps cover the most-used Bridge Town workflows. Every tool listed below was already part of the tool surface before apps were introduced. Seven apps are result_only — read-oriented widgets that render only the initial tool result. bsheet-editor is host_proxy: it may call its allowlisted tools directly from the iframe to support interactive editing, and it also receives MCP-driven refresh results for rehydration; refresh itself is not iframe-callable. Its allowlist excludes explain_bsheet_cell: Explain sends a chat prompt, and the agent’s next turn invokes that tool and renders the separate bsheet-cell-explain app. The result-only bsheet-viewer uses the same prompt flow and has no iframe-callable tool allowlist.

Normal input edits use update_bsheet_cells’s automatic save and calculation workflow. recalculate_bsheet remains a programmatic repair API, not an editor action.

AppStable / compatibility URILaunch-bound presenter
PR Workflowui://apps/pr-workflowpresent_pr_workflow
Run Outputui://apps/run-outputpresent_run_output
Query Data Gridui://apps/query-data-gridpresent_query_data_grid
Google Sheet Plannerui://apps/gsheet-plannerpresent_gsheet_planner
Scenario Compareui://apps/scenario-comparepresent_scenario_compare
Bsheet Editorui://apps/bsheet-editorpresent_bsheet_editor
Bsheet Viewerui://apps/bsheet-viewer— (readable compatibility resource; no launch binding)
Bsheet Cell Explainui://apps/bsheet-cell-explainpresent_bsheet_cell_explain

The catalog table shows each app’s stable identity and its explicit presenter. Data, polling, and mutation tools stay discoverable without launch bindings. Use them during investigation and ordinary analysis. Call the matching presenter when a visual is requested or materially improves the answer, usually once after analysis; honor requests for multiple distinct views. Presenting does not replace further analysis, repeat an operation, approve a merge, or bypass a required confirmation. Hosts control their thinking, tool-activity, and approval indicators.

All current app resources use a release-qualified cache key of the form ui://apps/<app-id>?release=sha256:<release_hash>, where <release_hash> is the reviewed HTML bundle digest. The stable URI remains a compatibility alias that resolves to the current release; the previous ui://apps/native-sheet-* identities also remain readable aliases. Hosts should use the URI they receive in _meta.ui.resourceUri and resources/list.

After a server release, ask the MCP host to reload or reconnect so it can fetch the current tools/list descriptors and release-qualified resource URIs. A host may retain tool definitions for an existing session; while that cache is stale, an old data-tool descriptor can still request an earlier app binding. The server keeps stable and known renamed resource URIs readable and resolves them to the current bundle, so an existing card can continue to load or refresh. A card already mounted in the host is not forcibly closed by a server deployment, and reconnecting does not guarantee that the host will render it.

When Apps are disabled or a host has no Apps UI, new tool definitions have no launch binding and no app resources are advertised. Tool calls still return their full text and structured JSON results. A host’s local cache may outlive that change until it refreshes; the server cannot control host activity or approval chrome.

When you call a Bridge Town tool, the response always includes the full text/JSON result — that never changes. In hosts that support MCP Apps, a _meta.ui.resourceUri annotation on the tool definition tells the host which app can render an interactive view of the result.

┌────────────┐ tool call ┌───────────────┐ text result ┌────────────┐
│ AI Agent │ ────────────► │ MCP Server │ ──────────────► │ Agent │
│ │ │ │ │ (always) │
└────────────┘ └───────────────┘ └────────────┘
│
_meta.ui.resourceUri (on tool definition)
│
▼
┌─────────────┐
│ App Bundle │ (only if host
│ (ui://) │ supports it)
└─────────────┘

Hosts that do not support ui:// resources simply ignore the _meta.ui annotation. The tool call, text result, and agent reasoning are unaffected — nothing breaks, nothing is hidden.

What you see depends on the specific host surface. Claude chat on claude.ai, Claude Desktop chat, and Cowork do not provide interchangeable evidence even when they share an Anthropic account or client family.

This status is generated from docs-site/src/data/mcp-apps-host-support.json; do not infer one Claude surface’s behavior from another.

Evidence baseline (2026-09-03): deployed source 8cb696f0a81fb4afe7c7068434e88b67acc519d0; catalog 21e339c9414143a70c061b13c43e8c44c0c7abf93b60ac1ff1fd740e3b76923d; Claude profile dddf08f3b9afa4f8b6d12580ad86886c4266f1e610277950d8818940515a93d9. The exact-release live-host gate is pending and remains owned by the release operator.

SurfaceProduction evidenceInteractive Apps statusRecovery limit
Claude chat (claude.ai)Production /health matched release 8cb696f0a81fb4afe7c7068434e88b67acc519d0 and reported the tool catalog plus the catalog hash above; exact-source tests and the server canary proved all eight App bundles, static bindings, and Claude-specific resource metadata. The live-host run did not reach a tool invocation because approved browser control was unavailable.Server delivery is proven; visible rendering of all eight Apps on this release is not yet production-proven. Earlier mixed outcomes must not be promoted to current-release support.A new chat or connector re-add can refresh host metadata, but neither guarantees a widget. Expand the error and capture the app, time, client version, tool result, and whether resources/read occurred. Invalid ui.domain is a server-metadata defect; raw JSON alone is not a stale-session diagnosis.
Claude Desktop chatNo release-bound live Claude Desktop chat session is retained. Release 8cb696f0a81fb4afe7c7068434e88b67acc519d0 proves only the shared deployed server and source contract for canonical App bindings, resources, structured output, and text fallback—not Desktop end-to-end behavior.Desktop tool/fallback behavior and interactive rendering are unverified for this release and must not be inferred from Claude.ai, Cowork, the shared server contract, or a simulator.Restarting Desktop or reloading its MCP configuration only starts a new client session; it does not guarantee App support or rendering. Check the Desktop version, initialize capability, mapped tool binding, and matching resources/read before classifying the failure.
CoworkOn deployed release 4750db4763b3effde73e9864cc6f56e61ada217d, Cowork identified through claude-code 2.1.252, omitted the UI extension, invoked get_native_sheet, and did not read ui://apps/native-sheet-editor. Static binding fix 5b9f2a45a517752d6cd299d9199b758a9b231421 was deployed in 8cb696f0a81fb4afe7c7068434e88b67acc519d0, but no later visible-render receipt exists.Tool execution and fallback are production-proven. Interactive rendering is not production-proven after the binding fix; a raw JSON or text result is a host-path observation, not proof of a stale connector.Reconnecting cannot make an incapable Cowork transport render Apps. Retain the client family/version and the mapped-tool/no-resource-read sequence, continue with the complete fallback, and wait for release-bound operator acceptance before claiming interactive support.

Other hosts receive the full structured and text result as normal. A host that does not implement ui:// rendering can safely ignore the _meta.ui binding.

Each app is a self-contained HTML bundle — no external scripts, no external stylesheets, no CDN dependencies. The entire app is served as a single ui:// resource, keeping rendering fast and avoiding CSP or network-isolation issues in sandboxed environments.

Use these checks to diagnose whether Apps are active server-side and whether the host would render them.

1. Verify Apps are active (full MCP handshake → tools/list → _meta.ui.resourceUri)

In production, Bridge Town uses stateful Streamable HTTP so Apps-capable hosts can keep session state and open the SSE server-to-client channel. App tool bindings are static discovery metadata whenever BRIDGE_TOWN_MCP_APPS_ENABLED=true; the initialize-time UI extension is recorded for diagnostics but is not an authorization or server-side admission gate. Hosts that cannot render Apps ignore the binding and use the complete text fallback.

Perform the session-aware handshake to verify the production path:

Terminal window
# 1. initialize — advertise MCP Apps capability and capture Mcp-Session-Id
curl -fsS -D /tmp/bt-mcp-headers -o /tmp/bt-mcp-init \
-X POST https://api.bridgetown.builders/mcp \
-H "Authorization: Bearer btk_YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","clientInfo":{"name":"Anthropic/ClaudeAI","version":"0.0.1"},"capabilities":{"extensions":{"io.modelcontextprotocol/ui":{"mimeTypes":["text/html;profile=mcp-app"]}}}}}'
SESSION_ID=$(awk 'tolower($1)=="mcp-session-id:" {print $2}' /tmp/bt-mcp-headers | tr -d '\r')
# 2. notifications/initialized — complete handshake
curl -fsS -X POST https://api.bridgetown.builders/mcp \
-H "Authorization: Bearer btk_YOUR_TOKEN" \
-H "Mcp-Session-Id: $SESSION_ID" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
# 3. tools/list — check for _meta.ui.resourceUri
curl -fsS -X POST https://api.bridgetown.builders/mcp \
-H "Authorization: Bearer btk_YOUR_TOKEN" \
-H "Mcp-Session-Id: $SESSION_ID" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
| grep -o '"resourceUri":"[^"]*"'

Note: The Mcp-Session-Id header is required after initialize. Without it, stateful production requests cannot see the Apps capability that was negotiated during the session setup.

Expected output (when Apps are active):

"resourceUri":"ui://apps/pr-workflow"
"resourceUri":"ui://apps/run-output"
...

If _meta.ui.resourceUri is absent in production:

  • Server flag disabled — BRIDGE_TOWN_MCP_APPS_ENABLED is not true on the deployed server
  • Server bug or stale deployment — the flag is set but static tool metadata is missing; Bridge Town’s scheduled production canary checks this independently of any single session

Whether the client advertised the UI extension is useful diagnostic evidence, but it does not control whether Bridge Town emits the static resource binding.

2. Verify ui:// resources are registered (resources/list)

Terminal window
curl -fsS -X POST https://api.bridgetown.builders/mcp \
-H "Authorization: Bearer btk_YOUR_TOKEN" \
-H "Mcp-Session-Id: $SESSION_ID" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"resources/list"}' \
| grep -o '"uri":"ui://apps/[^"]*"'

All 8 ui://apps/... entries — one per app in the App catalog table above — confirm the server-side app bundles are live. Compare the exact URI set against the table, not just the count: a duplicate or swapped entry can leave the count unchanged while a real bundle is missing.

3. Verify app bundle MIME type (resources/read)

Read a specific app resource and confirm both the MIME type and contents-level host metadata. To reproduce Claude’s projection, initialize the diagnostic session with clientInfo.name set to Anthropic/ClaudeAI (as the scheduled canary does):

Terminal window
curl -fsS -X POST https://api.bridgetown.builders/mcp \
-H "Authorization: Bearer btk_YOUR_TOKEN" \
-H "Mcp-Session-Id: $SESSION_ID" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"resources/read","params":{"uri":"ui://apps/pr-workflow"}}' \
| grep -Eo '"mimeType":"[^"]*"|"domain":"[^"]*"'

Expected MIME: "text/html;profile=mcp-app". For the canonical production endpoint, Claude’s content block must also contain "domain":"e8410557956977b970535d7e6b4ab13b.claudemcpcontent.com" with no scheme or path. ChatGPT receives its required unique HTTPS origin instead. The domain is optional for unknown clients and is projected from MCP clientInfo only for rendering compatibility, never authorization.

4. Verify tool call annotates results (tools/call → structuredContent)

Successful calls to an explicit presenter include _meta.ui.resourceUri for the selected App. structuredContent is standard MCP output for tools that define an output schema; data tools may return it without an App binding. The text content remains available regardless of host rendering support.

Terminal window
curl -fsS -X POST https://api.bridgetown.builders/mcp \
-H "Authorization: Bearer btk_YOUR_TOKEN" \
-H "Mcp-Session-Id: $SESSION_ID" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"present_run_output","arguments":{"view":"history","model_name":"YOUR_MODEL_NAME"}}}' \
| grep -o '"resourceUri":"[^"]*"\|"structuredContent":{[^}]*}'

The presenter result should include a release-qualified resourceUri and structuredContent. Data-tool results keep their text and structured data but do not carry the App binding. Apps feature flags and host capabilities control whether UI metadata and resources are advertised.

Apps appear active but don’t render in Claude.ai?

First expand any visible error. Unable to reach Bridge Town plus Invalid ui.domain format means the tool and resource-discovery paths worked, but Claude rejected resources/read content metadata. Capture the exact error and result.contents[0]._meta.ui.domain; do not classify it as a capability discovery or SSE failure. The Claude value must match the bare hash hostname above. Only continue to the SSE checks after domain rejection is excluded.

Bridge Town uses stateful Streamable HTTP in production. If tool definitions include _meta.ui.resourceUri but the iframe remains on “Connecting”, confirm the GET /mcp SSE channel is returning HTTP 200 for the same Mcp-Session-Id. A 405 on that channel means the iframe runtime cannot finish the handshake even though bundle discovery succeeded.

Do not classify a raw JSON or plain-text result as a stale session by default. The scheduled canary proves the server-side handshake, resource, metadata, and SSE contract; it does not prove that a particular host fetched or visibly rendered the bundle. Compare the observation with the generated host matrix above, then retain the mapped tool, client name/version, result metadata, matching resources/read (or its absence), visible error, and time.

Starting a new conversation or re-adding a connector is a diagnostic refresh, not a rendering guarantee. If the result remains plain text, use the complete fallback while support determines whether the gap is host capability, resource selection, content metadata, the SSE iframe handshake, or a tool-specific failure.

If you are building a custom MCP host or agent, you can choose to support ui:// resources or ignore them:

  • To support apps: Advertise extensions["io.modelcontextprotocol/ui"].mimeTypes containing "text/html;profile=mcp-app" in your initialize request so conformance and diagnostics reflect your rendering support. Bridge Town’s tool binding is static rather than capability-gated. When a tool definition includes _meta.ui.resourceUri, fetch that resource after the tool call and render the returned HTML in a sandboxed iframe or webview. The HTML is self-contained and safe to render.
  • To ignore apps: Ignore the _meta.ui.resourceUri binding and consume the standard structured and text outputs. Omitting the extension does not change tool behavior or authorize a different surface.

Apps never change the semantics of a tool call. A create_pull_request call returns the same pull request data whether or not an app renders it. Branch and pull-request operations are data-only: they do not carry an app launch binding in tool discovery or call results. To open a branch list, PR list or detail, selected diff, or operation receipt, pass the source result’s opaque presentation_ref to present_pr_workflow with its matching view. The server presents that bounded caller-owned snapshot, rechecks its current workspace/model access, and shows its captured revision and freshness; it does not rerun the operation.