reshuffle, use -vanilla consistently
This commit is contained in:
parent
24dd9691de
commit
e4e7edf284
3 changed files with 2 additions and 2 deletions
21
examples/simple-host/example-host-vanilla.html
Normal file
21
examples/simple-host/example-host-vanilla.html
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<!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>
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
"scripts": {
|
||||
"start:server": "tsx server.ts",
|
||||
"start:mcp-server": "cd ../simple-server && npm install && npm run start",
|
||||
"build": "concurrently 'INPUT=example-host.html vite build' 'INPUT=example-host-react.html vite build' 'INPUT=sandbox.html vite build'",
|
||||
"build": "concurrently 'INPUT=example-host-vanilla.html vite build' 'INPUT=example-host-react.html vite build' 'INPUT=sandbox.html vite build'",
|
||||
"server": "bun server.ts",
|
||||
"start": "NODE_ENV=development npm run build && concurrently 'npm run start:server' 'npm run start:mcp-server'"
|
||||
},
|
||||
|
|
|
|||
159
examples/simple-host/src/example-host-vanilla.ts
Normal file
159
examples/simple-host/src/example-host-vanilla.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
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";
|
||||
|
||||
const SANDBOX_PROXY_URL = URL.parse("/sandbox.html", location.href)!;
|
||||
|
||||
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!" }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
Loading…
Reference in a new issue