MCP Apps Bridge
Expose SLOP providers inside MCP Apps hosts (Claude, ChatGPT, Goose, VS Code Insiders) so the host model can subscribe to live state and call app affordances directly from chat.
The @slop-ai/mcp-apps-bridge package has two surfaces:
- Installed bridge — a stdio MCP server that discovers local SLOP providers, exposes
list_appsandopen_app, renders a generic MCP Apps view, and mirrors affordances as MCP tools. This is the main adoption path. - Custom MCP App helpers — lower-level primitives for app authors who want to ship a tailored iframe and MCP server:
createMcpAppsBridge,registerSlopView, andregisterSlopTools.
This guide is the developer-facing complement to the normative MCP Interoperability spec. For an end-to-end runnable custom MCP App example, see examples/mcp-apps-bridge.
When to use this
Section titled “When to use this”- Use the installed bridge when you already have SLOP providers and want them available in MCP clients with minimal setup.
- Use the custom MCP App helpers when you want a branded or app-specific iframe instead of the generic bridge view.
If your target host doesn’t support MCP Apps yet, use the Claude Code integration (proxy pattern) instead.
Installed bridge
Section titled “Installed bridge”Most users should start here:
npx -y @slop-ai/mcp-apps-bridgeConfigure the command in your MCP client:
{ "servers": { "slop": { "type": "stdio", "command": "npx", "args": ["-y", "@slop-ai/mcp-apps-bridge"] } }}Then ask the host model to call list_apps and open_app. The bridge handles discovery, the generic ui:// app view, model context updates, and generated SLOP action tools.
Custom MCP App architecture
Section titled “Custom MCP App architecture”┌─ host (VS Code Insiders / Claude / Goose) ─────────────────────────┐│ ││ MCP client ──stdio──▶ MCP server ││ │ │ ││ │ ├── SLOP provider (yours) ││ │ │ ││ │ ├── registerSlopView ── tool + ui:// ││ │ └── registerSlopTools ── one MCP tool ││ │ per affordance ││ ▼ ││ sandboxed iframe (open_*your_view*) ││ │ ││ └── createMcpAppsBridge ──ws──▶ same SLOP provider ││ ├── consumer mirrors tree ││ ├── projector → app.updateModelContext (debounced) ││ └── you render UI from consumer.getTree() │└─────────────────────────────────────────────────────────────────────┘In custom mode, a single Node process typically hosts the SLOP provider, the MCP server, and the WS endpoint. The iframe is a thin client.
Server: register the view + the tools
Section titled “Server: register the view + the tools”import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";import { registerSlopView, registerSlopTools,} from "@slop-ai/mcp-apps-bridge/server";import { createSlopServer } from "@slop-ai/server";import { bunHandler } from "@slop-ai/server/bun";import { readFile } from "node:fs/promises";
const PORT = 7411;const slop = createSlopServer({ id: "kanban", name: "Kanban" });// register your nodes + actions on slop ...
// Expose the SLOP provider over WebSocket (Bun example; use attachSlop for Node).const slopHandler = bunHandler(slop, { path: "/slop" });Bun.serve({ port: PORT, hostname: "127.0.0.1", fetch(req, srv) { return slopHandler.fetch(req, srv) ?? new Response("ok"); }, websocket: slopHandler.websocket,});
const RESOURCE_URI = "ui://my-app/view";const mcp = new McpServer( { name: "my-app", version: "0.1.0" }, { capabilities: { tools: { listChanged: true }, resources: {} } },);
registerSlopView(mcp, { toolName: "open_kanban", description: "Open a live view of the kanban board", resourceUri: RESOURCE_URI, resourceName: "Kanban View", html: () => readFile(new URL("./dist/iframe.html", import.meta.url), "utf8"), // CRITICAL for sandboxed hosts: whitelist the SLOP provider's WS origin. // Without this, VS Code's webview blocks the iframe's WS connection. connectDomains: [`ws://127.0.0.1:${PORT}`],});
await registerSlopTools(mcp, { url: `ws://127.0.0.1:${PORT}/slop`, uiResourceUri: RESOURCE_URI, // tags every tool with the iframe surface});
await mcp.connect(new StdioServerTransport());What registerSlopTools does: connects to the SLOP provider as a consumer, walks the affordances via affordancesToTools, and registers one MCP tool per (action, schema) group (so 1000 cards each with a delete affordance produce one delete MCP tool with a target parameter, not 1000). Resyncs on every patch and emits tools/listChanged.
Iframe: render + project
Section titled “Iframe: render + project”The iframe bundle is plain HTML/JS served by registerSlopView. Bundle it however you like (Bun, Vite, esbuild) — the demo uses a single-file HTML wrapper.
import { createMcpAppsBridge } from "@slop-ai/mcp-apps-bridge";import type { SlopNode } from "@slop-ai/consumer/browser";
const bridge = await createMcpAppsBridge({ provider: { mode: "ws", url: "ws://127.0.0.1:7411/slop" }, subscribe: { depth: -1, minSalience: 0.3 }, projection: { header: "# Kanban — live state from the iframe" },});
// Render UI from the consumer's mirrored tree.function render(tree: SlopNode | null) { /* your DOM updates */ }render(bridge.getTree());bridge.consumer.on("patch", () => render(bridge.getTree()));The bridge’s other mode is { mode: "postmessage" } for client-only setups where the SLOP provider runs inside the iframe via @slop-ai/client. Use ws mode whenever there’s a server-side authoritative state.
Provider modes
Section titled “Provider modes”| Mode | When to use | Tradeoffs |
|---|---|---|
ws | Server-side authoritative state. The MCP server, SLOP provider, and tool-registering consumer all run in one process. | Requires connectDomains for sandboxed hosts. The architecture every non-toy app wants. |
postmessage | Client-only / no backend. SLOP provider lives in the iframe via @slop-ai/client. | Model can’t act on state via registerSlopTools (server has no consumer to discover affordances from). State is iframe-local. |
How the model sees state
Section titled “How the model sees state”The bridge calls app.updateModelContext with a debounced markdown projection of the salience-filtered tree on every snapshot/patch. The projection includes the state tree (via formatTree) and the affordance list (via affordancesToTools). The model receives this in context on the next turn after the iframe opens — so the very first response after open_* won’t see state yet, but every subsequent turn will.
To validate: open the view, then ask a question whose answer depends on tree contents. The model should answer without making another tool call.
Caveats and known gotchas
Section titled “Caveats and known gotchas”- Sandboxed network. Hosts like VS Code’s webview default to “no network.” If you don’t pass
connectDomainstoregisterSlopView, yourwsiframe will silently fail to connect. The bridge surfaces this aserror: WebSocket connection failedin the iframe status line if you wrapcreateMcpAppsBridgein a try/catch. - First-turn latency.
app.updateModelContextlands asynchronously after the tool returns. The model that just calledopen_*won’t see state until its next turn. - Tool descriptions inline param info. The MCP SDK only accepts Zod schemas for
inputSchema, not raw JSON Schema.registerSlopToolsworks around this with a permissive passthrough schema and stuffs the SLOP affordance’s params + validtargetpaths into the tool description. The model handles this fine; future versions will convert SLOP JSON Schema → Zod for proper validation. tools/list_changedis chatty. Currently fires once per patch resync; in apps with high patch frequency, consider widening the bridge’s resync debounce (PRs welcome).- Big trees flood context. The default projection ships the entire salience-filtered tree as markdown on every patch. For apps with thousands of nodes use a stricter
subscribe.minSalienceand a customprojection.formatthat summarizes rather than emits every node. See the scaling extension.
Security
Section titled “Security”The host’s iframe sandbox and user-consent prompts are defense in depth. They are not the authorization boundary. Your SLOP provider must re-authorize every affordance invocation against live state and caller identity, exactly as it would for a direct WebSocket consumer.
For remote providers (production), use a short-lived token in the WS URL minted per-session by your backend. Don’t put bearer tokens in MCP tool content or anywhere model-visible — they end up in conversation transcripts.
Validating the installed bridge
Section titled “Validating the installed bridge”- Start a SLOP provider that registers through discovery.
- Register
npx -y @slop-ai/mcp-apps-bridgein VS Code, Claude Desktop, Goose, or the MCP Inspector. - Ask the host model to call
list_apps. - Ask it to open one of the discovered apps with
open_app. - Verify that the generic app view renders and generated action tools can mutate the provider.
Validating the custom example
Section titled “Validating the custom example”The custom MCP App smoke test we use:
- Build the demo:
cd examples/mcp-apps-bridge && bun run build. - Register the server in VS Code Insiders (Cmd+Shift+P →
MCP: Open User Configuration):{"servers": {"slop-kanban": {"type": "stdio","command": "bun","args": ["<absolute-path-to>/dist/server.js"]}}} - Reload the window, open Copilot Chat, ask:
Use the open_kanban tool. The iframe should render with statusconnected. This example does not exposelist_apps; it is already one specific MCP App server. - Ask:
What cards are in todo?— model should answer without another tool call. - Ask:
Add a card called "Ship it" to todo— the model callsadd_card, the iframe re-renders, and the next turn’s context reflects the new card.
If the iframe shows error: WebSocket connection failed, the most common cause is missing connectDomains (or VS Code holding a cached MCP server connection — Stop + Start the server in MCP: List Servers).
Future direction
Section titled “Future direction”When MCP standardizes event-driven resource subscriptions (tracked in the 2026 roadmap), the bridge will offer a third mode: "mcp-tunnel" that doesn’t require the iframe to open a WebSocket — all SLOP traffic flows over MCP’s own subscription channel. Until then, ws + connectDomains is the recommended pattern. See the “No formal MCP extension” entry for the planned SEP.