Skip to main content
Glama
implementation.ts8.14 kB
/** * Core implementation for MCP Apps Host. * Handles server connection, tool calling, and AppBridge setup. */ import { RESOURCE_URI_META_KEY, AppBridge, PostMessageTransport, } from "@modelcontextprotocol/ext-apps/app-bridge"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js"; // Constants for MCP Apps const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"; /** * Extract UI resource URI from tool metadata */ function getToolUiResourceUri(tool: Tool): string | undefined { const meta = tool._meta as Record<string, unknown> | undefined; if (!meta) return undefined; // Check new format: _meta.ui.resourceUri const ui = meta.ui as Record<string, unknown> | undefined; if (ui?.resourceUri && typeof ui.resourceUri === "string") { return ui.resourceUri; } // Check legacy format: _meta["ui/resourceUri"] if (meta[RESOURCE_URI_META_KEY] && typeof meta[RESOURCE_URI_META_KEY] === "string") { return meta[RESOURCE_URI_META_KEY] as string; } return undefined; } const SANDBOX_PROXY_URL = new URL("http://localhost:8081/sandbox.html"); const IMPLEMENTATION = { name: "MCP Apps POC Host", version: "1.0.0" }; export const log = { info: console.log.bind(console, "[HOST]"), warn: console.warn.bind(console, "[HOST]"), error: console.error.bind(console, "[HOST]"), }; export interface ServerInfo { name: string; client: Client; tools: Map<string, Tool>; appHtmlCache: Map<string, string>; } export async function connectToServer(serverUrl: URL): Promise<ServerInfo> { const client = new Client(IMPLEMENTATION); log.info("Connecting to server:", serverUrl.href); await client.connect(new StreamableHTTPClientTransport(serverUrl)); log.info("Connection successful"); const name = client.getServerVersion()?.name ?? serverUrl.href; const toolsList = await client.listTools(); const tools = new Map(toolsList.tools.map((tool) => [tool.name, tool])); log.info("Server tools:", Array.from(tools.keys())); return { name, client, tools, appHtmlCache: new Map() }; } interface UiResourceData { html: string; csp?: { connectDomains?: string[]; resourceDomains?: string[]; }; } export interface ToolCallInfo { serverInfo: ServerInfo; tool: Tool; input: Record<string, unknown>; resultPromise: Promise<CallToolResult>; appResourcePromise?: Promise<UiResourceData>; } export function hasAppHtml( toolCallInfo: ToolCallInfo ): toolCallInfo is Required<ToolCallInfo> { return !!toolCallInfo.appResourcePromise; } export function callTool( serverInfo: ServerInfo, name: string, input: Record<string, unknown> ): ToolCallInfo { log.info("Calling tool", name, "with input", input); const resultPromise = serverInfo.client.callTool({ name, arguments: input, }) as Promise<CallToolResult>; const tool = serverInfo.tools.get(name); if (!tool) { throw new Error(`Unknown tool: ${name}`); } const toolCallInfo: ToolCallInfo = { serverInfo, tool, input, resultPromise }; const uiResourceUri = getToolUiResourceUri(tool); if (uiResourceUri) { toolCallInfo.appResourcePromise = getUiResource(serverInfo, uiResourceUri); } return toolCallInfo; } async function getUiResource( serverInfo: ServerInfo, uri: string ): Promise<UiResourceData> { log.info("Reading UI resource:", uri); const resource = await serverInfo.client.readResource({ uri }); if (!resource) { throw new Error(`Resource not found: ${uri}`); } if (resource.contents.length !== 1) { throw new Error(`Unexpected contents count: ${resource.contents.length}`); } const content = resource.contents[0]; if (content.mimeType !== RESOURCE_MIME_TYPE) { throw new Error(`Unsupported MIME type: ${content.mimeType}`); } const html = "blob" in content ? atob(content.blob) : content.text; const contentMeta = (content as any)._meta || (content as any).meta; const csp = contentMeta?.ui?.csp; return { html, csp }; } type SandboxProxyReadyNotification = { method: "ui/notifications/sandbox-proxy-ready"; }; export function loadSandboxProxy(iframe: HTMLIFrameElement): Promise<boolean> { if (iframe.src) return Promise.resolve(false); iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms"); const readyNotification: SandboxProxyReadyNotification["method"] = "ui/notifications/sandbox-proxy-ready"; const readyPromise = new Promise<boolean>((resolve) => { const listener = ({ source, data }: MessageEvent) => { if (source === iframe.contentWindow && data?.method === readyNotification) { log.info("Sandbox proxy loaded"); window.removeEventListener("message", listener); resolve(true); } }; window.addEventListener("message", listener); }); log.info("Loading sandbox proxy..."); iframe.src = SANDBOX_PROXY_URL.href; return readyPromise; } export async function initializeApp( iframe: HTMLIFrameElement, appBridge: AppBridge, { input, resultPromise, appResourcePromise }: Required<ToolCallInfo> ): Promise<void> { const appInitializedPromise = hookInitializedCallback(appBridge); await appBridge.connect( new PostMessageTransport(iframe.contentWindow!, iframe.contentWindow!) ); const { html, csp } = await appResourcePromise; log.info("Sending UI resource HTML to MCP App"); await appBridge.sendSandboxResourceReady({ html, csp }); log.info("Waiting for MCP App to initialize..."); await appInitializedPromise; log.info("MCP App initialized"); log.info("Sending tool call input to MCP App:", input); appBridge.sendToolInput({ arguments: input }); resultPromise.then( (result) => { log.info("Sending tool call result to MCP App:", result); appBridge.sendToolResult(result); }, (error) => { log.error("Tool call failed:", error); appBridge.sendToolCancelled({ reason: error instanceof Error ? error.message : String(error), }); } ); } function hookInitializedCallback(appBridge: AppBridge): Promise<void> { const oninitialized = appBridge.oninitialized; return new Promise<void>((resolve) => { appBridge.oninitialized = (...args) => { resolve(); appBridge.oninitialized = oninitialized; appBridge.oninitialized?.(...args); }; }); } export function newAppBridge( serverInfo: ServerInfo, iframe: HTMLIFrameElement ): AppBridge { const serverCapabilities = serverInfo.client.getServerCapabilities(); const appBridge = new AppBridge(serverInfo.client, IMPLEMENTATION, { openLinks: {}, serverTools: serverCapabilities?.tools, serverResources: serverCapabilities?.resources, }); appBridge.onmessage = async (params) => { log.info("Message from MCP App:", params); return {}; }; appBridge.onopenlink = async (params) => { log.info("Open link request:", params); window.open(params.url, "_blank", "noopener,noreferrer"); return {}; }; appBridge.onloggingmessage = (params) => { log.info("Log message from MCP App:", params); }; appBridge.onsizechange = async ({ width, height }) => { const style = getComputedStyle(iframe); const isBorderBox = style.boxSizing === "border-box"; const from: Keyframe = {}; const to: Keyframe = {}; if (width !== undefined) { if (isBorderBox) { width += parseFloat(style.borderLeftWidth) + parseFloat(style.borderRightWidth); } from.minWidth = `${iframe.offsetWidth}px`; iframe.style.minWidth = to.minWidth = `min(${width}px, 100%)`; } if (height !== undefined) { if (isBorderBox) { height += parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth); } from.height = `${iframe.offsetHeight}px`; iframe.style.height = to.height = `${height}px`; } iframe.animate([from, to], { duration: 300, easing: "ease-out" }); }; return appBridge; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jamesdowzard/mcp-apps-poc'

If you have feedback or need assistance with the MCP directory API, please join our Discord server