Remove simple-host and simple-server examples
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 <noreply@anthropic.com>
This commit is contained in:
parent
4b5b0c7d79
commit
308537daac
22 changed files with 2 additions and 2093 deletions
|
|
@ -1,21 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
</style>
|
||||
<title>Example MCP-UI React Host</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="/src/example-host-react.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
#chat-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
<script src="/src/example-host-vanilla.ts" type="module"></script>
|
||||
<title>Example MCP View Host</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Example MCP View Host</h1>
|
||||
|
||||
<div id="controls"></div>
|
||||
<div id="chat-root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1 +0,0 @@
|
|||
<meta http-equiv="refresh" content="0; url=/example-host-react.html">
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<!-- Permissive CSP so nested content is not constrained by host CSP -->
|
||||
<meta http-equiv="Content-Security-Policy" content="
|
||||
default-src 'self';
|
||||
img-src * data: blob: 'unsafe-inline';
|
||||
media-src * blob: data:;
|
||||
font-src * blob: data:;
|
||||
script-src 'self'
|
||||
'wasm-unsafe-eval'
|
||||
'unsafe-inline'
|
||||
'unsafe-eval'
|
||||
blob: data: http://localhost:* https://localhost:*;
|
||||
style-src * blob: data: 'unsafe-inline';
|
||||
connect-src *;
|
||||
frame-src * blob: data: http://localhost:* https://localhost:*;
|
||||
base-uri 'self';
|
||||
" />
|
||||
<title>MCP-UI Proxy</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
iframe {
|
||||
background-color: transparent;
|
||||
border: 0px none transparent;
|
||||
padding: 0px;
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module" src="/src/sandbox.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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<string, unknown>;
|
||||
|
||||
/** 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
|
||||
* <AppRenderer
|
||||
* sandboxProxyUrl={new URL('http://localhost:8765/sandbox_proxy.html')}
|
||||
* client={mcpClient}
|
||||
* toolName="create-chart"
|
||||
* toolInput={{ data: [1, 2, 3], type: 'bar' }}
|
||||
* onUIAction={async (action) => {
|
||||
* 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<AppBridge | null>(null);
|
||||
const [iframeReady, setIframeReady] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const iframeRef = useRef<HTMLIFrameElement | null>(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 (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{error && (
|
||||
<div style={{ color: "red", padding: "1rem" }}>
|
||||
Error: {error.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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<void>;
|
||||
}> {
|
||||
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<void>((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<ToolUiResourceInfo | null> {
|
||||
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<string> {
|
||||
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;
|
||||
}
|
||||
|
|
@ -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<Client | null>(null);
|
||||
const [tools, setTools] = useState<Tool[]>([]);
|
||||
const [activeTools, setActiveTools] = useState<
|
||||
Array<{ id: string; name: string; input: Record<string, unknown> }>
|
||||
>([]);
|
||||
const [error, setError] = useState<string | null>(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<string, unknown>,
|
||||
) => {
|
||||
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 (
|
||||
<div style={{ padding: "20px" }}>
|
||||
<h1>Example MCP-UI React Host</h1>
|
||||
<div
|
||||
style={{
|
||||
color: "red",
|
||||
padding: "10px",
|
||||
border: "1px solid red",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
<strong>Error:</strong> {error}
|
||||
</div>
|
||||
<button onClick={() => window.location.reload()}>Retry</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!client) {
|
||||
return (
|
||||
<div style={{ padding: "20px" }}>
|
||||
<h1>Example MCP-UI React Host</h1>
|
||||
<p>Connecting to MCP server...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: "20px", fontFamily: "system-ui, sans-serif" }}>
|
||||
<h1>Example MCP-UI React Host</h1>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "20px",
|
||||
padding: "15px",
|
||||
backgroundColor: "#f5f5f5",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
<h2 style={{ marginTop: 0 }}>Available Tools</h2>
|
||||
{tools.length === 0 ? (
|
||||
<p>No tools available with UI resources.</p>
|
||||
) : (
|
||||
<div style={{ display: "flex", gap: "10px", flexWrap: "wrap" }}>
|
||||
{tools.map((tool) => (
|
||||
<button
|
||||
key={tool.name}
|
||||
onClick={() => {
|
||||
// Example input - in a real app, you'd have a form for this
|
||||
const input = tool.name.startsWith("pizza-")
|
||||
? { pizzaTopping: "Mushrooms" }
|
||||
: { message: "Hello from React!" };
|
||||
handleAddToolUI(tool.name, input);
|
||||
}}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
backgroundColor: "#007bff",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Add {tool.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
|
||||
{activeTools.map((tool) => (
|
||||
<div
|
||||
key={tool.id}
|
||||
style={{
|
||||
padding: "10px",
|
||||
border: "2px solid #ddd",
|
||||
borderRadius: "8px",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "10px",
|
||||
}}
|
||||
>
|
||||
<h3 style={{ margin: 0 }}>{tool.name}</h3>
|
||||
<button
|
||||
onClick={() => handleRemoveToolUI(tool.id)}
|
||||
style={{
|
||||
padding: "4px 12px",
|
||||
backgroundColor: "#dc3545",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AppRenderer
|
||||
sandboxProxyUrl={SANDBOX_PROXY_URL}
|
||||
client={client}
|
||||
toolName={tool.name}
|
||||
toolInput={tool.input}
|
||||
onmessage={handleMessage}
|
||||
onloggingmessage={handleLoggingMessage}
|
||||
onopenlink={handleOpenLink}
|
||||
onerror={(err) => handleError(tool.id, err)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{activeTools.length === 0 && (
|
||||
<p style={{ color: "#666", textAlign: "center", padding: "40px" }}>
|
||||
No tool UIs active. Click a button above to add one.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Mount the React app
|
||||
const root = document.getElementById("root");
|
||||
if (root) {
|
||||
createRoot(root).render(<ExampleApp />);
|
||||
} else {
|
||||
console.error("Root element not found");
|
||||
}
|
||||
|
|
@ -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<string, unknown>,
|
||||
) {
|
||||
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!" }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -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: {},
|
||||
},
|
||||
"*",
|
||||
);
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ReadResourceResult> => ({
|
||||
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<CallToolResult> => ({
|
||||
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<CallToolResult> => ({
|
||||
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<CallToolResult> => ({
|
||||
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<CallToolResult> => {
|
||||
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);
|
||||
});
|
||||
|
|
@ -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:
|
||||
*
|
||||
* <code>
|
||||
* npx esbuild src/ui-raw.ts --bundle --outfile=dist/ui-raw.js --minify --sourcemap --platform=browser
|
||||
* </code>
|
||||
*
|
||||
* 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<T extends Sendable, Result>({ method, params }: T) {
|
||||
const id = nextId++;
|
||||
window.parent.postMessage({ jsonrpc: "2.0", id, method, params }, "*");
|
||||
return new Promise<Result>((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<T extends Sendable>({ method, params }: T) {
|
||||
window.parent.postMessage({ jsonrpc: "2.0", method, params }, "*");
|
||||
},
|
||||
onNotification<T extends Sendable>(
|
||||
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<McpUiToolInputNotification>(
|
||||
"ui/notifications/tool-input",
|
||||
async (params) => {
|
||||
appendText(`Tool call input: ${JSON.stringify(params)}`);
|
||||
},
|
||||
);
|
||||
app.onNotification<McpUiToolResultNotification>(
|
||||
"ui/notifications/tool-result",
|
||||
async (params) => {
|
||||
appendText(`Tool call result: ${JSON.stringify(params)}`);
|
||||
},
|
||||
);
|
||||
app.onNotification<McpUiHostContextChangedNotification>(
|
||||
"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<McpUiInitializedNotification>({
|
||||
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<McpUiSizeChangeNotification>({
|
||||
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<CallToolRequest, CallToolResult>(
|
||||
{
|
||||
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<LoggingMessageNotification>({
|
||||
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);
|
||||
});
|
||||
|
|
@ -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<CallToolResult[]>([]);
|
||||
const [messages, setMessages] = useState<string[]>([]);
|
||||
|
||||
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 (
|
||||
<div style={{ color: "red" }}>Error connecting: {error.message}</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isConnected) {
|
||||
return <div>Connecting...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: "20px", fontFamily: "system-ui, sans-serif" }}>
|
||||
<h1>MCP UI Client (React)</h1>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
marginBottom: "20px",
|
||||
}}
|
||||
>
|
||||
<button onClick={handleGetWeather}>Get Weather (Tool)</button>
|
||||
|
||||
<button onClick={handleNotifyCart}>Notify Cart Updated</button>
|
||||
|
||||
<button onClick={handlePromptWeather}>Prompt Weather in Tokyo</button>
|
||||
|
||||
<button onClick={handleOpenLink}>Open Link to Google</button>
|
||||
</div>
|
||||
|
||||
{toolResults.length > 0 && (
|
||||
<div style={{ marginBottom: "20px" }}>
|
||||
<h2>Tool Results:</h2>
|
||||
{toolResults.map((result, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
padding: "10px",
|
||||
marginBottom: "10px",
|
||||
backgroundColor: result.isError ? "#fee" : "#efe",
|
||||
border: "1px solid #ccc",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
<strong>isError:</strong> {String(result.isError ?? false)}
|
||||
<br />
|
||||
<strong>content:</strong> {JSON.stringify(result.content)}
|
||||
<br />
|
||||
{result.structuredContent && (
|
||||
<>
|
||||
<strong>structuredContent:</strong>{" "}
|
||||
{JSON.stringify(result.structuredContent)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.length > 0 && (
|
||||
<div>
|
||||
<h2>Messages:</h2>
|
||||
{messages.map((msg, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
padding: "8px",
|
||||
marginBottom: "5px",
|
||||
backgroundColor: "#f5f5f5",
|
||||
border: "1px solid #ddd",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
{msg}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
const root = document.getElementById("root");
|
||||
if (!root) {
|
||||
throw new Error("Root element not found");
|
||||
}
|
||||
|
||||
createRoot(root).render(<McpClientApp />);
|
||||
});
|
||||
|
|
@ -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));
|
||||
});
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MCP UI Client (Raw)</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/ui-raw.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MCP UI Client (React)</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/ui-react.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue