From 308537daac9b54706d03278831496f3b3ba3a34a Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Tue, 2 Dec 2025 18:43:33 -0600 Subject: [PATCH] Remove simple-host and simple-server examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These legacy examples have been replaced by basic-host, basic-server-react, and basic-server-vanillajs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- examples/simple-host/example-host-react.html | 21 -- .../simple-host/example-host-vanilla.html | 21 -- examples/simple-host/index.html | 1 - examples/simple-host/package.json | 33 -- examples/simple-host/sandbox.html | 48 --- examples/simple-host/src/AppRenderer.tsx | 341 ------------------ examples/simple-host/src/app-host-utils.ts | 113 ------ .../simple-host/src/example-host-react.tsx | 264 -------------- .../simple-host/src/example-host-vanilla.ts | 163 --------- examples/simple-host/src/sandbox.ts | 69 ---- examples/simple-host/tsconfig.json | 25 -- examples/simple-host/vite.config.ts | 24 -- examples/simple-server/package.json | 31 -- examples/simple-server/server.ts | 297 --------------- examples/simple-server/src/ui-raw.ts | 235 ------------ examples/simple-server/src/ui-react.tsx | 187 ---------- examples/simple-server/src/ui-vanilla.ts | 131 ------- examples/simple-server/tsconfig.json | 20 - examples/simple-server/ui-raw.html | 12 - examples/simple-server/ui-react.html | 12 - examples/simple-server/ui-vanilla.html | 12 - examples/simple-server/vite.config.ts | 25 -- package-lock.json | 10 +- 23 files changed, 2 insertions(+), 2093 deletions(-) delete mode 100644 examples/simple-host/example-host-react.html delete mode 100644 examples/simple-host/example-host-vanilla.html delete mode 100644 examples/simple-host/index.html delete mode 100644 examples/simple-host/package.json delete mode 100644 examples/simple-host/sandbox.html delete mode 100644 examples/simple-host/src/AppRenderer.tsx delete mode 100644 examples/simple-host/src/app-host-utils.ts delete mode 100644 examples/simple-host/src/example-host-react.tsx delete mode 100644 examples/simple-host/src/example-host-vanilla.ts delete mode 100644 examples/simple-host/src/sandbox.ts delete mode 100644 examples/simple-host/tsconfig.json delete mode 100644 examples/simple-host/vite.config.ts delete mode 100644 examples/simple-server/package.json delete mode 100644 examples/simple-server/server.ts delete mode 100644 examples/simple-server/src/ui-raw.ts delete mode 100644 examples/simple-server/src/ui-react.tsx delete mode 100644 examples/simple-server/src/ui-vanilla.ts delete mode 100644 examples/simple-server/tsconfig.json delete mode 100644 examples/simple-server/ui-raw.html delete mode 100644 examples/simple-server/ui-react.html delete mode 100644 examples/simple-server/ui-vanilla.html delete mode 100644 examples/simple-server/vite.config.ts diff --git a/examples/simple-host/example-host-react.html b/examples/simple-host/example-host-react.html deleted file mode 100644 index fc71204..0000000 --- a/examples/simple-host/example-host-react.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - Example MCP-UI React Host - - -
- - - diff --git a/examples/simple-host/example-host-vanilla.html b/examples/simple-host/example-host-vanilla.html deleted file mode 100644 index cc4f2df..0000000 --- a/examples/simple-host/example-host-vanilla.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - Example MCP View Host - - -

Example MCP View Host

- -
-
- - \ No newline at end of file diff --git a/examples/simple-host/index.html b/examples/simple-host/index.html deleted file mode 100644 index 1bae39d..0000000 --- a/examples/simple-host/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/examples/simple-host/package.json b/examples/simple-host/package.json deleted file mode 100644 index 6c4dc32..0000000 --- a/examples/simple-host/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/simple-host", - "name": "@modelcontextprotocol/ext-apps-host", - "version": "1.0.0", - "type": "module", - "scripts": { - "start": "vite dev", - "start:server": "vite preview", - "build": "vite build" - }, - "dependencies": { - "@modelcontextprotocol/ext-apps": "../..", - "@modelcontextprotocol/sdk": "^1.22.0", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "zod": "^3.25.0" - }, - "devDependencies": { - "@types/express": "^5.0.0", - "@types/node": "^22.0.0", - "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.2", - "@vitejs/plugin-react": "^4.3.4", - "bun": "^1.3.2", - "concurrently": "^9.2.1", - "cors": "^2.8.5", - "express": "^5.1.0", - "prettier": "^3.6.2", - "vite": "^6.0.0", - "vite-plugin-singlefile": "^2.3.0", - "vitest": "^3.2.4" - } -} diff --git a/examples/simple-host/sandbox.html b/examples/simple-host/sandbox.html deleted file mode 100644 index f09d494..0000000 --- a/examples/simple-host/sandbox.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - MCP-UI Proxy - - - - - - diff --git a/examples/simple-host/src/AppRenderer.tsx b/examples/simple-host/src/AppRenderer.tsx deleted file mode 100644 index 146ec2d..0000000 --- a/examples/simple-host/src/AppRenderer.tsx +++ /dev/null @@ -1,341 +0,0 @@ -import { useEffect, useRef, useState } from "react"; - -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { - type CallToolResult, - type LoggingMessageNotification, - McpError, - ErrorCode, -} from "@modelcontextprotocol/sdk/types.js"; - -import { - AppBridge, - PostMessageTransport, -} from "@modelcontextprotocol/ext-apps/app-bridge"; - -import { - getToolUiResourceUri, - readToolUiResourceHtml, - setupSandboxProxyIframe, - ToolUiResourceInfo, -} from "./app-host-utils"; - -/** - * Props for the AppRenderer component. - */ -export interface AppRendererProps { - /** URL to the sandbox proxy HTML that will host the tool UI iframe */ - sandboxProxyUrl: URL; - - /** MCP client connected to the server providing the tool */ - client: Client; - - /** Name of the MCP tool to render UI for */ - toolName: string; - - /** Optional pre-fetched resource URI. If not provided, will be fetched via getToolUiResourceUri() */ - toolResourceUri?: string; - - /** Optional input arguments to pass to the tool UI once it's ready */ - toolInput?: Record; - - /** Optional result from tool execution to pass to the tool UI once it's ready */ - toolResult?: CallToolResult; - - onopenlink?: AppBridge["onopenlink"]; - onmessage?: AppBridge["onmessage"]; - onloggingmessage?: AppBridge["onloggingmessage"]; - - /** Callback invoked when an error occurs during setup or message handling */ - onerror?: (error: Error) => void; -} - -/** - * React component that renders an MCP tool's custom UI in a sandboxed iframe. - * - * This component manages the complete lifecycle of an MCP-UI tool: - * 1. Creates a sandboxed iframe with the proxy HTML - * 2. Establishes MCP communication channel between host and iframe - * 3. Fetches and loads the tool's UI resource (HTML) - * 4. Sends tool inputs and results to the UI when ready - * 5. Handles UI actions (intents, link opening, prompts, notifications) - * 6. Automatically resizes iframe based on content size changes - * - * @example - * ```tsx - * { - * if (action.type === 'intent') { - * // Handle intent request from UI - * console.log('Intent:', action.payload.intent); - * } - * }} - * onerror={(error) => console.error('UI Error:', error)} - * /> - * ``` - * - * **Architecture:** - * - Host (this component) ↔ Sandbox Proxy (iframe) ↔ Tool UI (nested iframe) - * - Communication uses MCP protocol over postMessage - * - Sandbox proxy provides CSP isolation for untrusted tool UIs - * - Standard MCP initialization flow determines when UI is ready - * - * **Lifecycle:** - * 1. `setupSandboxProxyIframe()` creates iframe and waits for proxy ready - * 2. Component creates `McpUiProxyServer` instance - * 3. Registers all handlers (BEFORE connecting to avoid race conditions) - * 4. Connects proxy to iframe via `MessageTransport` - * 5. MCP initialization completes → `onClientReady` callback fires - * 6. Fetches tool UI resource and sends to sandbox proxy - * 7. Sends tool inputs/results when iframe signals ready - * - * @param props - Component props - * @returns React element containing the sandboxed tool UI iframe - */ -export const AppRenderer = (props: AppRendererProps) => { - const { - client, - sandboxProxyUrl, - toolName, - toolResourceUri, - toolInput, - toolResult, - onmessage, - onopenlink, - onloggingmessage, - onerror, - } = props; - - // State - const [appBridge, setAppBridge] = useState(null); - const [iframeReady, setIframeReady] = useState(false); - const [error, setError] = useState(null); - const containerRef = useRef(null); - const iframeRef = useRef(null); - - // Use refs for callbacks to avoid effect re-runs when they change - const onmessageRef = useRef(onmessage); - const onopenlinkRef = useRef(onopenlink); - const onloggingmessageRef = useRef(onloggingmessage); - const onerrorRef = useRef(onerror); - - useEffect(() => { - onmessageRef.current = onmessage; - onopenlinkRef.current = onopenlink; - onloggingmessageRef.current = onloggingmessage; - onerrorRef.current = onerror; - }); - - useEffect(() => { - let mounted = true; - - const setup = async () => { - try { - const { iframe, onReady } = - await setupSandboxProxyIframe(sandboxProxyUrl); - - if (!mounted) return; - - iframeRef.current = iframe; - if (containerRef.current) { - containerRef.current.appendChild(iframe); - } - - await onReady; - - if (!mounted) return; - - const serverCapabilities = client.getServerCapabilities(); - const appBridge = new AppBridge( - client, - { - name: "Example MCP UI Host", - version: "1.0.0", - }, - { - openLinks: {}, - serverTools: serverCapabilities?.tools, - serverResources: serverCapabilities?.resources, - }, - ); - - // Step 3: Register ALL handlers BEFORE connecting (critical for avoiding race conditions) - - // Hook into the standard MCP initialization to know when the inner iframe is ready - appBridge.oninitialized = () => { - if (!mounted) return; - console.log("[Host] Inner iframe MCP client initialized"); - setIframeReady(true); - }; - - // Register handlers passed in via props - - appBridge.onmessage = (params, extra) => { - if (!onmessageRef.current) { - throw new McpError(ErrorCode.MethodNotFound, "Method not found"); - } - return onmessageRef.current(params, extra); - }; - appBridge.onopenlink = (params, extra) => { - if (!onopenlinkRef.current) { - throw new McpError(ErrorCode.MethodNotFound, "Method not found"); - } - return onopenlinkRef.current(params, extra); - }; - appBridge.onloggingmessage = (params) => { - if (!onloggingmessageRef.current) { - throw new McpError(ErrorCode.MethodNotFound, "Method not found"); - } - return onloggingmessageRef.current(params); - }; - - appBridge.onsizechange = async ({ width, height }) => { - if (iframeRef.current) { - if (width !== undefined) { - iframeRef.current.style.width = `${width}px`; - } - if (height !== undefined) { - iframeRef.current.style.height = `${height}px`; - } - } - }; - - // Step 4: NOW connect (triggers MCP initialization handshake) - // IMPORTANT: Pass iframe.contentWindow as BOTH target and source to ensure - // this proxy only responds to messages from its specific iframe - await appBridge.connect( - new PostMessageTransport( - iframe.contentWindow!, - iframe.contentWindow!, - ), - ); - - if (!mounted) return; - - // Step 5: Store proxy in state - setAppBridge(appBridge); - } catch (err) { - console.error("[AppRenderer] Error:", err); - if (!mounted) return; - const error = err instanceof Error ? err : new Error(String(err)); - setError(error); - onerrorRef.current?.(error); - } - }; - - setup(); - - return () => { - mounted = false; - // Cleanup: remove iframe from DOM - if ( - iframeRef.current && - containerRef.current?.contains(iframeRef.current) - ) { - containerRef.current.removeChild(iframeRef.current); - } - }; - }, [client, sandboxProxyUrl]); - - // Effect 2: Fetch and send UI resource - useEffect(() => { - if (!appBridge) return; - - let mounted = true; - - const fetchAndSendResource = async () => { - try { - // Get the resource URI (use prop if provided, otherwise fetch) - let resourceInfo: ToolUiResourceInfo; - - if (toolResourceUri) { - // When URI is provided directly, assume it's NOT OpenAI Apps SDK format - resourceInfo = { - uri: toolResourceUri, - }; - console.log( - `[Host] Using provided resource URI: ${resourceInfo.uri}`, - ); - } else { - console.log(`[Host] Fetching resource URI for tool: ${toolName}`); - const info = await getToolUiResourceUri(client, toolName); - if (!info) { - throw new Error( - `Tool ${toolName} has no UI resource (no ui/resourceUri or openai/outputTemplate in tool._meta)`, - ); - } - resourceInfo = info; - console.log(`[Host] Got resource URI: ${resourceInfo.uri}`); - } - - if (!resourceInfo.uri) { - throw new Error(`Tool ${toolName}: URI is undefined or empty`); - } - - if (!mounted) return; - - // Read the HTML content - console.log(`[Host] Reading resource HTML from: ${resourceInfo.uri}`); - const html = await readToolUiResourceHtml(client, { - uri: resourceInfo.uri, - }); - - if (!mounted) return; - - // Send the resource to the sandbox proxy - console.log("[Host] Sending sandbox resource ready"); - await appBridge.sendSandboxResourceReady({ html }); - } catch (err) { - if (!mounted) return; - const error = err instanceof Error ? err : new Error(String(err)); - setError(error); - onerrorRef.current?.(error); - } - }; - - fetchAndSendResource(); - - return () => { - mounted = false; - }; - }, [appBridge, toolName, toolResourceUri, client]); - - // Effect 3: Send tool input when ready - useEffect(() => { - if (appBridge && iframeReady && toolInput) { - console.log("[Host] Sending tool input:", toolInput); - appBridge.sendToolInput({ arguments: toolInput }); - } - }, [appBridge, iframeReady, toolInput]); - - // Effect 4: Send tool result when ready - useEffect(() => { - if (appBridge && iframeReady && toolResult) { - console.log("[Host] Sending tool result:", toolResult); - appBridge.sendToolResult(toolResult); - } - }, [appBridge, iframeReady, toolResult]); - - // Render - return ( -
- {error && ( -
- Error: {error.message} -
- )} -
- ); -}; diff --git a/examples/simple-host/src/app-host-utils.ts b/examples/simple-host/src/app-host-utils.ts deleted file mode 100644 index deee146..0000000 --- a/examples/simple-host/src/app-host-utils.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type { McpUiSandboxProxyReadyNotification } from "@modelcontextprotocol/ext-apps"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { Tool } from "@modelcontextprotocol/sdk/types.js"; - -const MCP_UI_RESOURCE_META_KEY = "ui/resourceUri"; - -export async function setupSandboxProxyIframe(sandboxProxyUrl: URL): Promise<{ - iframe: HTMLIFrameElement; - onReady: Promise; -}> { - const SANDBOX_PROXY_READY_METHOD: McpUiSandboxProxyReadyNotification["method"] = - "ui/notifications/sandbox-proxy-ready"; - - const iframe = document.createElement("iframe"); - iframe.style.width = "100%"; - iframe.style.height = "600px"; - iframe.style.border = "none"; - iframe.style.backgroundColor = "transparent"; - iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms"); - - const onReady = new Promise((resolve, _reject) => { - const initialListener = async (event: MessageEvent) => { - if (event.source === iframe.contentWindow) { - if (event.data && event.data.method === SANDBOX_PROXY_READY_METHOD) { - window.removeEventListener("message", initialListener); - resolve(); - } - } - }; - window.addEventListener("message", initialListener); - }); - - iframe.src = sandboxProxyUrl.href; - - return { iframe, onReady }; -} - -export type ToolUiResourceInfo = { - uri: string; -}; - -export async function getToolUiResourceUri( - client: Client, - toolName: string, -): Promise { - let tool: Tool | undefined; - let cursor: string | undefined = undefined; - do { - const toolsResult = await client.listTools({ cursor }); - tool = toolsResult.tools.find((t) => t.name === toolName); - cursor = toolsResult.nextCursor; - } while (!tool && cursor); - if (!tool) { - throw new Error(`tool ${toolName} not found`); - } - if (!tool._meta) { - return null; - } - - let uri: string; - if (MCP_UI_RESOURCE_META_KEY in tool._meta) { - uri = String(tool._meta[MCP_UI_RESOURCE_META_KEY]); - } else { - return null; - } - if (!uri.startsWith("ui://")) { - throw new Error( - `tool ${toolName} has unsupported output template URI: ${uri}`, - ); - } - return { uri }; -} - -export async function readToolUiResourceHtml( - client: Client, - opts: { - uri: string; - }, -): Promise { - const resource = await client.readResource({ uri: opts.uri }); - - if (!resource) { - throw new Error("UI resource not found: " + opts.uri); - } - if (resource.contents.length !== 1) { - throw new Error( - "Unsupported UI resource content length: " + resource.contents.length, - ); - } - const content = resource.contents[0]; - let html: string; - const isHtml = (t?: string) => t === "text/html;profile=mcp-app"; - - if ( - "text" in content && - typeof content.text === "string" && - isHtml(content.mimeType) - ) { - html = content.text; - } else if ( - "blob" in content && - typeof content.blob === "string" && - isHtml(content.mimeType) - ) { - html = atob(content.blob); - } else { - throw new Error( - "Unsupported UI resource content format: " + JSON.stringify(content), - ); - } - - return html; -} diff --git a/examples/simple-host/src/example-host-react.tsx b/examples/simple-host/src/example-host-react.tsx deleted file mode 100644 index 4156ce7..0000000 --- a/examples/simple-host/src/example-host-react.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import { useEffect, useState } from "react"; -import { createRoot } from "react-dom/client"; - -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { Tool } from "@modelcontextprotocol/sdk/types.js"; -import { AppRenderer, AppRendererProps } from "../src/AppRenderer"; -import { AppBridge } from "../../../dist/src/app-bridge"; - -// We use '[::1]' for the sandbox to ensure it's a different origin from 'localhost'. -const SANDBOX_PROXY_URL = new URL( - "/sandbox.html", - location.href.replace("localhost:", "[::1]:"), -); - -/** - * Example React application demonstrating the AppRenderer component. - * - * This shows how to: - * - Connect to an MCP server - * - List available tools - * - Render tool UIs using AppRenderer - * - Handle UI actions from the tool - */ -function ExampleApp() { - const [client, setClient] = useState(null); - const [tools, setTools] = useState([]); - const [activeTools, setActiveTools] = useState< - Array<{ id: string; name: string; input: Record }> - >([]); - const [error, setError] = useState(null); - - // Connect to MCP server on mount - useEffect(() => { - let mounted = true; - - async function connect() { - try { - const newClient = new Client({ - name: "MCP UI React Host", - version: "1.0.0", - }); - - const mcpServerUrl = new URL("http://localhost:3001/mcp"); - console.log( - "[React Host] Attempting SSE connection to", - mcpServerUrl.href, - ); - - try { - await newClient.connect(new SSEClientTransport(mcpServerUrl)); - console.log("[React Host] SSE connection successful"); - } catch (err) { - console.warn( - "[React Host] SSE connection failed, falling back to HTTP:", - err, - ); - await newClient.connect( - new StreamableHTTPClientTransport(mcpServerUrl), - ); - console.log("[React Host] HTTP connection successful"); - } - - if (!mounted) return; - - // List available tools - const toolsResult = await newClient.listTools(); - - if (!mounted) return; - - setClient(newClient); - setTools(toolsResult.tools); - } catch (err) { - if (!mounted) return; - const errorMsg = err instanceof Error ? err.message : String(err); - setError(`Failed to connect to MCP server: ${errorMsg}`); - console.error("[React Host] Connection error:", err); - } - } - - connect(); - - return () => { - mounted = false; - }; - }, []); - - const handleAddToolUI = ( - toolName: string, - input: Record, - ) => { - const id = `${toolName}-${Date.now()}`; - setActiveTools((prev) => [...prev, { id, name: toolName, input }]); - }; - - const handleRemoveToolUI = (id: string) => { - setActiveTools((prev) => prev.filter((t) => t.id !== id)); - }; - - const handleMessage: AppRendererProps["onmessage"] = async ( - params, - _extra, - ) => { - console.log("[React Host] Message:", params); - return {}; - }; - - const handleLoggingMessage: AppRendererProps["onloggingmessage"] = ( - params, - ) => { - console.log("[React Host] Logging message:", params); - }; - - const handleOpenLink: AppRendererProps["onopenlink"] = async ( - params, - _extra, - ) => { - console.log("[React Host] Open link request:", params); - window.open(params.url, "_blank", "noopener,noreferrer"); - return { isError: false }; - }; - - const handleError = (toolId: string, err: Error) => { - console.error(`[React Host] Error from tool ${toolId}:`, err); - setError(`Tool ${toolId}: ${err.message}`); - }; - - if (error) { - return ( -
-

Example MCP-UI React Host

-
- Error: {error} -
- -
- ); - } - - if (!client) { - return ( -
-

Example MCP-UI React Host

-

Connecting to MCP server...

-
- ); - } - - return ( -
-

Example MCP-UI React Host

- -
-

Available Tools

- {tools.length === 0 ? ( -

No tools available with UI resources.

- ) : ( -
- {tools.map((tool) => ( - - ))} -
- )} -
- -
- {activeTools.map((tool) => ( -
-
-

{tool.name}

- -
- - handleError(tool.id, err)} - /> -
- ))} - - {activeTools.length === 0 && ( -

- No tool UIs active. Click a button above to add one. -

- )} -
-
- ); -} - -// Mount the React app -const root = document.getElementById("root"); -if (root) { - createRoot(root).render(); -} else { - console.error("Root element not found"); -} diff --git a/examples/simple-host/src/example-host-vanilla.ts b/examples/simple-host/src/example-host-vanilla.ts deleted file mode 100644 index 249aabf..0000000 --- a/examples/simple-host/src/example-host-vanilla.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { - AppBridge, - PostMessageTransport, -} from "@modelcontextprotocol/ext-apps/app-bridge"; -import { - setupSandboxProxyIframe, - getToolUiResourceUri, - readToolUiResourceHtml, -} from "../src/app-host-utils"; - -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; -import { LoggingMessageNotificationSchema } from "@modelcontextprotocol/sdk/types.js"; -import { - McpUiOpenLinkRequestSchema, - McpUiMessageRequestSchema, - McpUiSizeChangeNotificationSchema, -} from "@modelcontextprotocol/ext-apps"; - -// We use '[::1]' for the sandbox to ensure it's a different origin from 'localhost'. -const SANDBOX_PROXY_URL = new URL( - "/sandbox.html", - location.href.replace("localhost:", "[::1]:"), -); - -window.addEventListener("load", async () => { - const client = new Client({ - name: "MCP UI Proxy Server", - version: "1.0.0", - }); - const mcpServerUrl = new URL("http://localhost:3001/mcp"); - console.log("[Client] Attempting SSE connection to", mcpServerUrl.href); - try { - await client.connect(new SSEClientTransport(mcpServerUrl)); - console.log("[Client] SSE connection successful"); - } catch (error) { - console.warn( - "[Client] SSE connection failed, falling back to HTTP:", - error, - ); - await client.connect(new StreamableHTTPClientTransport(mcpServerUrl)); - console.log("[Client] HTTP connection successful"); - } - - const tools = Object.fromEntries( - (await client.listTools()).tools.map((t) => [t.name, t]), - ); - - const controlsDiv = document.getElementById("controls") as HTMLDivElement; - const chatRootDiv = document.getElementById("chat-root") as HTMLDivElement; - - /** - * Creates a tool UI instance for the given tool name. - * This demonstrates the new simplified API where the host manages the full proxy lifecycle. - */ - async function createToolUI( - toolName: string, - toolInput: Record, - ) { - try { - // Step 1: Create iframe and wait for sandbox proxy ready - const { iframe, onReady } = - await setupSandboxProxyIframe(SANDBOX_PROXY_URL); - - chatRootDiv.appendChild(iframe); - - // Wait for sandbox proxy to be ready - await onReady; - - // Step 2: Create proxy server instance - const serverCapabilities = client.getServerCapabilities(); - const appBridge = new AppBridge( - client, - { - name: "Example MCP UI Host", - version: "1.0.0", - }, - { - openLinks: {}, - serverTools: serverCapabilities?.tools, - serverResources: serverCapabilities?.resources, - }, - ); - - // Step 3: Register handlers BEFORE connecting - appBridge.oninitialized = () => { - console.log("[Example] Inner iframe MCP client initialized"); - - // Send tool input once iframe is ready - appBridge.sendToolInput({ arguments: toolInput }); - }; - - appBridge.onopenlink = async ({ url }) => { - console.log("[Example] Open link requested:", url); - window.open(url, "_blank", "noopener,noreferrer"); - return { isError: false }; - }; - - appBridge.onmessage = async (params) => { - console.log("[Example] Message requested:", params); - return { isError: false }; - }; - - // Handle size changes by resizing the iframe - appBridge.onsizechange = ({ width, height }) => { - if (width !== undefined) { - iframe.style.width = `${width}px`; - } - if (height !== undefined) { - iframe.style.height = `${height}px`; - } - }; - - appBridge.onloggingmessage = async (params) => { - console.log("[Tool UI Log]", params); - }; - - // Step 4: Connect proxy to iframe (triggers MCP initialization) - // Pass iframe.contentWindow as both target and source for proper message filtering - await appBridge.connect( - new PostMessageTransport(iframe.contentWindow!, iframe.contentWindow!), - ); - - // Step 5: Fetch and send UI resource - const resourceInfo = await getToolUiResourceUri(client, toolName); - if (!resourceInfo) { - throw new Error(`Tool ${toolName} has no UI resource`); - } - - const html = await readToolUiResourceHtml(client, { - uri: resourceInfo.uri, - }); - await appBridge.sendSandboxResourceReady({ html }); - - console.log("[Example] Tool UI setup complete for:", toolName); - } catch (error) { - console.error("[Example] Error setting up tool UI:", error); - } - } - - // Create buttons for available tools - if ([...Object.keys(tools)].some((n) => n.startsWith("pizza-"))) { - for (const t of Object.values(tools)) { - if (t.name.startsWith("pizza-")) { - controlsDiv.appendChild( - Object.assign(document.createElement("button"), { - innerText: t.name, - onclick: () => createToolUI(t.name, { pizzaTopping: "Mushrooms" }), - }), - ); - } - } - } else { - controlsDiv.appendChild( - Object.assign(document.createElement("button"), { - innerText: "Add MCP UI View", - onclick: () => - createToolUI("create-ui-vanilla", { message: "Hello from Host!" }), - }), - ); - } -}); diff --git a/examples/simple-host/src/sandbox.ts b/examples/simple-host/src/sandbox.ts deleted file mode 100644 index 5b50704..0000000 --- a/examples/simple-host/src/sandbox.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { - McpUiSandboxProxyReadyNotification, - McpUiSandboxResourceReadyNotification, -} from "../../../dist/src/types"; - -if (window.self === window.top) { - throw new Error("This file is only to be used in an iframe sandbox."); -} -if (!document.referrer) { - throw new Error("No referrer, cannot validate embedding site."); -} -if (!document.referrer.match(/^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/)) { - throw new Error( - `Embedding domain not allowed in referrer ${document.referrer} (update the validation logic to allow your domain)`, - ); -} - -// Try and break out of this iframe -try { - window.top!.alert("If you see this, the sandbox is not setup securely."); - - throw new Error( - "Managed to break out of iframe, the sandbox is not setup securely.", - ); -} catch (e) { - // Ignore -} - -const inner = document.createElement("iframe"); -inner.style = "width:100%; height:100%; border:none;"; -inner.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms"); -document.body.appendChild(inner); - -window.addEventListener("message", async (event) => { - // Note: in production you'll also want to validate event.origin against your outer domain. - if (event.source === window.parent) { - if ( - event.data && - event.data.method === - ("ui/notifications/sandbox-resource-ready" as McpUiSandboxResourceReadyNotification["method"]) - ) { - const { html, sandbox } = event.data.params; - if (typeof sandbox === "string") { - inner.setAttribute("sandbox", sandbox); - } - if (typeof html === "string") { - inner.srcdoc = html; - } - } else { - if (inner && inner.contentWindow) { - inner.contentWindow.postMessage(event.data, "*"); - } - } - } else if (event.source === inner.contentWindow) { - // Relay messages from inner to parent - window.parent.postMessage(event.data, "*"); - } -}); - -// Notify parent that proxy is ready to receive HTML (distinct event) -window.parent.postMessage( - { - jsonrpc: "2.0", - method: - "ui/notifications/sandbox-proxy-ready" as McpUiSandboxProxyReadyNotification["method"], - params: {}, - }, - "*", -); diff --git a/examples/simple-host/tsconfig.json b/examples/simple-host/tsconfig.json deleted file mode 100644 index 5550165..0000000 --- a/examples/simple-host/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "lib": ["ES2021", "DOM"], - "jsx": "react-jsx", - "declaration": true, - "emitDeclarationOnly": true, - "outDir": "./dist", - "rootDir": "./", - "moduleResolution": "bundler", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true, - "resolveJsonModule": true - }, - "include": [ - "examples/**/*.ts", - "examples/**/*.tsx", - "src/**/*.ts", - "src/**/*.tsx" - ], - "exclude": ["node_modules", "dist"] -} diff --git a/examples/simple-host/vite.config.ts b/examples/simple-host/vite.config.ts deleted file mode 100644 index 14a4709..0000000 --- a/examples/simple-host/vite.config.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; - -export default defineConfig(({ mode }) => { - const isDevelopment = mode === "development"; - return { - plugins: [react()], - build: { - sourcemap: isDevelopment ? "inline" : undefined, - cssMinify: !isDevelopment, - minify: !isDevelopment, - rollupOptions: { - input: [ - "index.html", - "example-host-vanilla.html", - "example-host-react.html", - "sandbox.html", - ], - }, - outDir: `dist`, - emptyOutDir: false, - }, - }; -}); diff --git a/examples/simple-server/package.json b/examples/simple-server/package.json deleted file mode 100644 index 52eb470..0000000 --- a/examples/simple-server/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "simple-server", - "version": "1.0.0", - "private": true, - "type": "module", - "scripts": { - "start": "NODE_ENV=development npm run build && npm run server", - "build": "concurrently 'INPUT=ui-raw.html vite build' 'INPUT=ui-vanilla.html vite build' 'INPUT=ui-react.html vite build'", - "server": "bun server.ts" - }, - "dependencies": { - "@modelcontextprotocol/ext-apps": "../..", - "@modelcontextprotocol/sdk": "^1.22.0", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "zod": "^3.25.0" - }, - "devDependencies": { - "@types/express": "^5.0.0", - "@types/node": "^22.0.0", - "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.2", - "@vitejs/plugin-react": "^4.3.4", - "concurrently": "^9.2.1", - "cors": "^2.8.5", - "express": "^5.1.0", - "typescript": "^5.7.2", - "vite": "^6.0.0", - "vite-plugin-singlefile": "^2.3.0" - } -} diff --git a/examples/simple-server/server.ts b/examples/simple-server/server.ts deleted file mode 100644 index b3b19bf..0000000 --- a/examples/simple-server/server.ts +++ /dev/null @@ -1,297 +0,0 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import express, { Request, Response } from "express"; -import { randomUUID } from "node:crypto"; -import { z } from "zod"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { - CallToolResult, - isInitializeRequest, - ReadResourceResult, - Resource, -} from "@modelcontextprotocol/sdk/types.js"; -import { InMemoryEventStore } from "@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js"; -import cors from "cors"; -import path from "node:path"; -import fs from "node:fs/promises"; -import { fileURLToPath } from "node:url"; -import { RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// Load both UI HTML files from dist/ -const distDir = path.join(__dirname, "dist"); -const loadHtml = async (name: string) => { - const htmlPath = path.join(distDir, `${name}.html`); - return fs.readFile(htmlPath, "utf-8"); -}; - -// Create an MCP server with both UI tools -const getServer = async () => { - const server = new McpServer( - { - name: "simple-mcp-server", - version: "1.0.0", - }, - { capabilities: { logging: {} } }, - ); - - // Load HTML for both UIs - const [rawHtml, vanillaHtml, reactHtml] = await Promise.all([ - loadHtml("ui-raw"), - loadHtml("ui-vanilla"), - loadHtml("ui-react"), - ]); - - const registerResource = (resource: Resource, htmlContent: string) => { - server.registerResource( - resource.name, - resource.uri, - resource, - async (): Promise => ({ - contents: [ - { - uri: resource.uri, - mimeType: resource.mimeType, - text: htmlContent, - }, - ], - }), - ); - return resource; - }; - - { - const rawResource = registerResource( - { - name: "ui-raw-template", - uri: "ui://raw", - title: "Raw UI Template", - description: "A simple raw HTML UI", - mimeType: "text/html;profile=mcp-app", - }, - rawHtml, - ); - - server.registerTool( - "create-ui-raw", - { - title: "Raw UI", - description: "A tool that returns a raw HTML UI (no Apps SDK runtime)", - inputSchema: { - message: z.string().describe("Message to display"), - }, - _meta: { - [RESOURCE_URI_META_KEY]: rawResource.uri, - }, - }, - async ({ message }): Promise => ({ - content: [{ type: "text", text: JSON.stringify({ message }) }], - structuredContent: { message }, - }), - ); - } - - { - const vanillaResource = registerResource( - { - name: "ui-vanilla-template", - uri: "ui://vanilla", - title: "Vanilla UI Template", - description: "A simple vanilla JS UI", - mimeType: "text/html;profile=mcp-app", - }, - vanillaHtml, - ); - - server.registerTool( - "create-ui-vanilla", - { - title: "Vanilla UI", - description: "A tool that returns a vanilla TS + Apps SDK UI", - inputSchema: { - message: z.string().describe("Message to display"), - }, - _meta: { - [RESOURCE_URI_META_KEY]: vanillaResource.uri, - }, - }, - async ({ message }): Promise => ({ - content: [{ type: "text", text: JSON.stringify({ message }) }], - structuredContent: { message }, - }), - ); - } - - { - const reactResource = registerResource( - { - name: "ui-react-template", - uri: "ui://react", - title: "React UI Template", - description: "A React-based UI", - mimeType: "text/html;profile=mcp-app", - }, - reactHtml, - ); - - server.registerTool( - "create-ui-react", - { - title: "React UI", - description: "A tool that returns a React-based UI", - inputSchema: { - message: z.string().describe("Message to display"), - }, - _meta: { - [RESOURCE_URI_META_KEY]: reactResource.uri, - }, - }, - async ({ message }): Promise => ({ - content: [{ type: "text", text: JSON.stringify({ message }) }], - structuredContent: { message }, - }), - ); - } - - // --- Common tool: get-weather --- - server.registerTool( - "get-weather", - { - title: "Get Weather", - description: "Returns current weather for a location", - inputSchema: { - location: z.string().describe("Location to get weather for"), - }, - }, - async ({ location }): Promise => { - const temperature = 25; - const condition = "sunny"; - return { - content: [ - { - type: "text", - text: `The weather in ${location} is ${condition}, ${temperature}°C.`, - }, - ], - structuredContent: { temperature, condition }, - }; - }, - ); - - return server; -}; - -const MCP_PORT = process.env.MCP_PORT - ? parseInt(process.env.MCP_PORT, 10) - : 3001; - -const app = express(); -app.use(express.json()); -app.use( - cors({ - origin: "*", - exposedHeaders: ["Mcp-Session-Id"], - }), -); - -// Map to store transports by session ID -const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; - -const mcpPostHandler = async (req: Request, res: Response) => { - const sessionId = req.headers["mcp-session-id"] as string | undefined; - - try { - let transport: StreamableHTTPServerTransport; - if (sessionId && transports[sessionId]) { - transport = transports[sessionId]; - } else if (!sessionId && isInitializeRequest(req.body)) { - const eventStore = new InMemoryEventStore(); - transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - eventStore, - onsessioninitialized: (sessionId) => { - console.log(`Session initialized: ${sessionId}`); - transports[sessionId] = transport; - }, - }); - - transport.onclose = () => { - const sid = transport.sessionId; - if (sid && transports[sid]) { - console.log(`Session closed: ${sid}`); - delete transports[sid]; - } - }; - - const server = await getServer(); - await server.connect(transport); - await transport.handleRequest(req, res, req.body); - return; - } else { - res.status(400).json({ - jsonrpc: "2.0", - error: { code: -32000, message: "Bad Request: No valid session ID" }, - id: null, - }); - return; - } - - await transport.handleRequest(req, res, req.body); - } catch (error) { - console.error("Error handling MCP request:", error); - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: "2.0", - error: { code: -32603, message: "Internal server error" }, - id: null, - }); - } - } -}; - -app.post("/mcp", mcpPostHandler); - -app.get("/mcp", async (req: Request, res: Response) => { - const sessionId = req.headers["mcp-session-id"] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send("Invalid or missing session ID"); - return; - } - const transport = transports[sessionId]; - await transport.handleRequest(req, res); -}); - -app.delete("/mcp", async (req: Request, res: Response) => { - const sessionId = req.headers["mcp-session-id"] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send("Invalid or missing session ID"); - return; - } - try { - const transport = transports[sessionId]; - await transport.handleRequest(req, res); - } catch (error) { - console.error("Error handling session termination:", error); - if (!res.headersSent) { - res.status(500).send("Error processing session termination"); - } - } -}); - -app.listen(MCP_PORT, () => { - console.log(`MCP Server listening on http://localhost:${MCP_PORT}/mcp`); -}); - -process.on("SIGINT", async () => { - console.log("Shutting down..."); - for (const sessionId in transports) { - try { - await transports[sessionId].close(); - delete transports[sessionId]; - } catch (error) { - console.error(`Error closing session ${sessionId}:`, error); - } - } - process.exit(0); -}); diff --git a/examples/simple-server/src/ui-raw.ts b/examples/simple-server/src/ui-raw.ts deleted file mode 100644 index 1cf2b2e..0000000 --- a/examples/simple-server/src/ui-raw.ts +++ /dev/null @@ -1,235 +0,0 @@ -/** - * @file App that does NOT depend on Apps SDK runtime. - * - * The Raw UI example has no runtime dependency to the Apps SDK - * but still imports its types for static type safety. - * Types can be just stripped, e.g. w/ the command line: - * - * - * npx esbuild src/ui-raw.ts --bundle --outfile=dist/ui-raw.js --minify --sourcemap --platform=browser - * - * - * We implement a barebones JSON-RPC message sender/receiver (see `app` object below), - * but without timeouts or runtime type validation of any kind - * (for that, use the Apps SDK / see ui-vanilla.ts or ui-react.ts). - */ - -import type { - McpUiInitializeRequest, - McpUiInitializeResult, - McpUiInitializedNotification, - McpUiToolResultNotification, - McpUiHostContextChangedNotification, - McpUiToolInputNotification, - McpUiSizeChangeNotification, - McpUiMessageRequest, - McpUiMessageResult, - McpUiOpenLinkRequest, - McpUiOpenLinkResult, -} from "@modelcontextprotocol/ext-apps"; - -import type { - CallToolRequest, - CallToolResult, - JSONRPCMessage, - LoggingMessageNotification, -} from "@modelcontextprotocol/sdk/types.js"; - -const app = (() => { - type Sendable = { method: string; params: any }; - - let nextId = 1; - - return { - sendRequest({ method, params }: T) { - const id = nextId++; - window.parent.postMessage({ jsonrpc: "2.0", id, method, params }, "*"); - return new Promise((resolve, reject) => { - window.addEventListener("message", function listener(event) { - const data: JSONRPCMessage = event.data; - if (event.data?.id === id) { - window.removeEventListener("message", listener); - if (event.data?.result) { - resolve(event.data.result as Result); - } else if (event.data?.error) { - reject(new Error(event.data.error)); - } - } else { - reject(new Error(`Unsupported message: ${JSON.stringify(data)}`)); - } - }); - }); - }, - sendNotification({ method, params }: T) { - window.parent.postMessage({ jsonrpc: "2.0", method, params }, "*"); - }, - onNotification( - method: T["method"], - handler: (params: T["params"]) => void, - ) { - window.addEventListener("message", function listener(event) { - if (event.data?.method === method) { - handler(event.data.params); - } - }); - }, - }; -})(); - -window.addEventListener("load", async () => { - const root = document.getElementById("root")!; - const appendText = (textContent: string, opts = {}) => { - root.appendChild( - Object.assign(document.createElement("div"), { - textContent, - ...opts, - }), - ); - }; - const appendError = (error: unknown) => - appendText( - `Error: ${error instanceof Error ? error.message : String(error)}`, - { style: "color: red;" }, - ); - - app.onNotification( - "ui/notifications/tool-input", - async (params) => { - appendText(`Tool call input: ${JSON.stringify(params)}`); - }, - ); - app.onNotification( - "ui/notifications/tool-result", - async (params) => { - appendText(`Tool call result: ${JSON.stringify(params)}`); - }, - ); - app.onNotification( - "ui/notifications/host-context-changed", - async (params) => { - appendText(`Host context changed: ${JSON.stringify(params)}`); - }, - ); - - const initializeResult = await app.sendRequest< - McpUiInitializeRequest, - McpUiInitializeResult - >({ - method: "ui/initialize", - params: { - appCapabilities: {}, - appInfo: { name: "My UI", version: "1.0.0" }, - protocolVersion: "2025-06-18", - }, - }); - - appendText(`Initialize result: ${JSON.stringify(initializeResult)}`); - - app.sendNotification({ - method: "ui/notifications/initialized", - params: {}, - }); - - new ResizeObserver(() => { - const rect = ( - document.body.parentElement ?? document.body - ).getBoundingClientRect(); - const width = Math.ceil(rect.width); - const height = Math.ceil(rect.height); - app.sendNotification({ - method: "ui/notifications/size-change", - params: { width, height }, - }); - }).observe(document.body); - - root.appendChild( - Object.assign(document.createElement("button"), { - textContent: "Get Weather (Tool)", - onclick: async () => { - try { - const result = await app.sendRequest( - { - method: "tools/call", - params: { - name: "get-weather", - arguments: { location: "Tokyo" }, - }, - }, - ); - - appendText(`Weather tool result: ${JSON.stringify(result)}`); - } catch (e) { - appendError(e); - } - }, - }), - ); - - root.appendChild( - Object.assign(document.createElement("button"), { - textContent: "Notify Cart Updated", - onclick: async () => { - app.sendNotification({ - method: "notifications/message", - params: { - level: "info", - data: "cart-updated", - }, - }); - }, - }), - ); - - root.appendChild( - Object.assign(document.createElement("button"), { - textContent: "Prompt Weather in Tokyo", - onclick: async () => { - try { - const { isError } = await app.sendRequest< - McpUiMessageRequest, - McpUiMessageResult - >({ - method: "ui/message", - params: { - role: "user", - content: [ - { - type: "text", - text: "What is the weather in Tokyo?", - }, - ], - }, - }); - - appendText(`Message result: ${isError ? "error" : "success"}`); - } catch (e) { - appendError(e); - } - }, - }), - ); - - root.appendChild( - Object.assign(document.createElement("button"), { - textContent: "Open Link to Google", - onclick: async () => { - try { - const { isError } = await app.sendRequest< - McpUiOpenLinkRequest, - McpUiOpenLinkResult - >({ - method: "ui/open-link", - params: { - url: "https://www.google.com", - }, - }); - appendText(`Link result: ${isError ? "error" : "success"}`); - } catch (e) { - appendError(e); - } - }, - }), - ); - - console.log("Initialized with host info:", initializeResult.hostInfo); -}); diff --git a/examples/simple-server/src/ui-react.tsx b/examples/simple-server/src/ui-react.tsx deleted file mode 100644 index 45275ff..0000000 --- a/examples/simple-server/src/ui-react.tsx +++ /dev/null @@ -1,187 +0,0 @@ -/** - * @file App that demonstrates a few features using React + the Apps SDK. - */ -import { useState, useCallback } from "react"; -import { createRoot } from "react-dom/client"; -import { - useApp, - McpUiSizeChangeNotificationSchema, - McpUiToolResultNotificationSchema, -} from "@modelcontextprotocol/ext-apps/react"; -import type { - CallToolResult, - Implementation, -} from "@modelcontextprotocol/sdk/types.js"; - -const APP_INFO: Implementation = { - name: "MCP UI React Example Client", - version: "1.0.0", -}; - -export function McpClientApp() { - const [toolResults, setToolResults] = useState([]); - const [messages, setMessages] = useState([]); - - const { app, isConnected, error } = useApp({ - appInfo: APP_INFO, - capabilities: {}, - onAppCreated: (app) => { - app.ontoolresult = async (params) => { - setToolResults((prev) => [...prev, params]); - }; - }, - }); - - const handleGetWeather = useCallback(async () => { - if (!app) return; - try { - const result = await app.callServerTool({ - name: "get-weather", - arguments: { location: "Tokyo" }, - }); - setMessages((prev) => [ - ...prev, - `Weather tool result: ${JSON.stringify(result)}`, - ]); - } catch (e) { - setMessages((prev) => [...prev, `Tool call error: ${e}`]); - } - }, [app]); - - const handleNotifyCart = useCallback(async () => { - if (!app) return; - await app.sendLog({ level: "info", data: "cart-updated" }); - setMessages((prev) => [...prev, "Notification sent: cart-updated"]); - }, [app]); - - const handlePromptWeather = useCallback(async () => { - if (!app) return; - const signal = AbortSignal.timeout(5000); - try { - const { isError } = await app.sendMessage( - { - role: "user", - content: [ - { - type: "text", - text: "What is the weather in Tokyo?", - }, - ], - }, - { signal }, - ); - setMessages((prev) => [ - ...prev, - `Prompt result: ${isError ? "error" : "success"}`, - ]); - } catch (e) { - if (signal.aborted) { - setMessages((prev) => [...prev, "Prompt request timed out"]); - return; - } - setMessages((prev) => [...prev, `Prompt error: ${e}`]); - } - }, [app]); - - const handleOpenLink = useCallback(async () => { - if (!app) return; - const { isError } = await app.sendOpenLink({ - url: "https://www.google.com", - }); - setMessages((prev) => [ - ...prev, - `Open link result: ${isError ? "error" : "success"}`, - ]); - }, [app]); - - if (error) { - return ( -
Error connecting: {error.message}
- ); - } - - if (!isConnected) { - return
Connecting...
; - } - - return ( -
-

MCP UI Client (React)

- -
- - - - - - - -
- - {toolResults.length > 0 && ( -
-

Tool Results:

- {toolResults.map((result, i) => ( -
- isError: {String(result.isError ?? false)} -
- content: {JSON.stringify(result.content)} -
- {result.structuredContent && ( - <> - structuredContent:{" "} - {JSON.stringify(result.structuredContent)} - - )} -
- ))} -
- )} - - {messages.length > 0 && ( -
-

Messages:

- {messages.map((msg, i) => ( -
- {msg} -
- ))} -
- )} -
- ); -} - -window.addEventListener("load", () => { - const root = document.getElementById("root"); - if (!root) { - throw new Error("Root element not found"); - } - - createRoot(root).render(); -}); diff --git a/examples/simple-server/src/ui-vanilla.ts b/examples/simple-server/src/ui-vanilla.ts deleted file mode 100644 index f536a45..0000000 --- a/examples/simple-server/src/ui-vanilla.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * @file Demonstrate a few Apps SDK features. - * - * The vanilla (no React) UI uses the Apps SDK. - * - * The Apps SDK offers advantages over the Raw UI example, - * such as ability to set timeouts, strong runtime type validation - * and simpler methods for each request/response interaction. - */ -import { - App, - PostMessageTransport, - McpUiToolInputNotificationSchema, - McpUiToolResultNotificationSchema, - McpUiHostContextChangedNotificationSchema, -} from "@modelcontextprotocol/ext-apps"; - -window.addEventListener("load", async () => { - const root = document.getElementById("root")!; - const appendText = (textContent: string, opts = {}) => { - root.appendChild( - Object.assign(document.createElement("div"), { - textContent, - ...opts, - }), - ); - }; - const appendError = (error: unknown) => - appendText( - `Error: ${error instanceof Error ? error.message : String(error)}`, - { style: "color: red;" }, - ); - - const app = new App({ - name: "MCP UI Client (Vanilla)", - version: "1.0.0", - }); - - app.ontoolinput = (params) => { - appendText(`Tool call input received: ${JSON.stringify(params.arguments)}`); - }; - app.ontoolresult = ({ content, structuredContent, isError }) => { - appendText( - `Tool call result received: isError=${isError}, content=${content}, structuredContent=${JSON.stringify(structuredContent)}`, - ); - }; - app.onhostcontextchanged = (params) => { - appendText(`Host context changed: ${JSON.stringify(params)}`); - }; - - root.appendChild( - Object.assign(document.createElement("button"), { - textContent: "Get Weather (Tool)", - onclick: async () => { - try { - const result = await app.callServerTool({ - name: "get-weather", - arguments: { location: "Tokyo" }, - }); - appendText(`Weather tool result: ${JSON.stringify(result)}`); - } catch (e) { - appendError(e); - } - }, - }), - ); - - root.appendChild( - Object.assign(document.createElement("button"), { - textContent: "Notify Cart Updated", - onclick: async () => { - try { - await app.sendLog({ - level: "info", - data: "cart-updated", - }); - } catch (e) { - appendError(e); - } - }, - }), - ); - - root.appendChild( - Object.assign(document.createElement("button"), { - textContent: "Prompt Weather in Tokyo", - onclick: async () => { - const signal = AbortSignal.timeout(5000); - try { - const { isError } = await app.sendMessage( - { - role: "user", - content: [ - { - type: "text", - text: "What is the weather in Tokyo?", - }, - ], - }, - { signal }, - ); - appendText(`Prompt result: ${isError ? "error" : "success"}`); - } catch (e) { - if (signal.aborted) { - appendError("Prompt request timed out"); - return; - } - appendError(e); - } - }, - }), - ); - - root.appendChild( - Object.assign(document.createElement("button"), { - textContent: "Open Link to Google", - onclick: async () => { - try { - const { isError } = await app.sendOpenLink({ - url: "https://www.google.com", - }); - appendText(`Open link result: ${isError ? "error" : "success"}`); - } catch (e) { - appendError(e); - } - }, - }), - ); - - await app.connect(new PostMessageTransport(window.parent)); -}); diff --git a/examples/simple-server/tsconfig.json b/examples/simple-server/tsconfig.json deleted file mode 100644 index a4c834a..0000000 --- a/examples/simple-server/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"] -} diff --git a/examples/simple-server/ui-raw.html b/examples/simple-server/ui-raw.html deleted file mode 100644 index cf32084..0000000 --- a/examples/simple-server/ui-raw.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - MCP UI Client (Raw) - - -
- - - diff --git a/examples/simple-server/ui-react.html b/examples/simple-server/ui-react.html deleted file mode 100644 index c0e7fd7..0000000 --- a/examples/simple-server/ui-react.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - MCP UI Client (React) - - -
- - - diff --git a/examples/simple-server/ui-vanilla.html b/examples/simple-server/ui-vanilla.html deleted file mode 100644 index ecec6c8..0000000 --- a/examples/simple-server/ui-vanilla.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - MCP UI Client (Vanilla) - - -
- - - diff --git a/examples/simple-server/vite.config.ts b/examples/simple-server/vite.config.ts deleted file mode 100644 index 28d7e5b..0000000 --- a/examples/simple-server/vite.config.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; -import { viteSingleFile } from "vite-plugin-singlefile"; - -const INPUT = process.env.INPUT; -if (!INPUT) { - throw new Error("INPUT environment variable is not set"); -} - -const isDevelopment = process.env.NODE_ENV === "development"; - -export default defineConfig({ - plugins: [react(), viteSingleFile()], - build: { - sourcemap: isDevelopment ? "inline" : undefined, - cssMinify: !isDevelopment, - minify: !isDevelopment, - - rollupOptions: { - input: INPUT, - }, - outDir: `dist`, - emptyOutDir: false, - }, -}); diff --git a/package-lock.json b/package-lock.json index ae46820..b33cef7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -112,6 +112,7 @@ "examples/simple-host": { "name": "@modelcontextprotocol/ext-apps-host", "version": "1.0.0", + "extraneous": true, "dependencies": { "@modelcontextprotocol/ext-apps": "../..", "@modelcontextprotocol/sdk": "^1.22.0", @@ -137,6 +138,7 @@ }, "examples/simple-server": { "version": "1.0.0", + "extraneous": true, "dependencies": { "@modelcontextprotocol/ext-apps": "../..", "@modelcontextprotocol/sdk": "^1.22.0", @@ -477,10 +479,6 @@ "resolved": "examples/basic-host", "link": true }, - "node_modules/@modelcontextprotocol/ext-apps-host": { - "resolved": "examples/simple-host", - "link": true - }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.23.0", "license": "MIT", @@ -2841,10 +2839,6 @@ "dev": true, "license": "ISC" }, - "node_modules/simple-server": { - "resolved": "examples/simple-server", - "link": true - }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",