UNPKG

@assistant-ui/react

Version:

Open-source TypeScript/React library for building production-grade AI chat experiences

319 lines (318 loc) • 10.4 kB
import "./types.js"; import { invokeUserCallback, isRecord } from "@assistant-ui/core/internal"; // src/mcp-apps/bridge.ts const VALID_DISPLAY_MODES = [ "inline", "fullscreen", "pip" ]; function isMcpAppDisplayMode(value) { return VALID_DISPLAY_MODES.some((mode) => mode === value); } function isAppInfo(value) { return isRecord(value) && typeof value.name === "string" && typeof value.version === "string"; } function getAppCapabilities(value) { if (!isRecord(value)) return void 0; const { availableDisplayModes, ...rest } = value; return { ...rest, ...Array.isArray(availableDisplayModes) ? { availableDisplayModes: Object.values(availableDisplayModes).filter(isMcpAppDisplayMode) } : {} }; } function getInitializeParams(params) { if (!isRecord(params)) return {}; const appCapabilities = getAppCapabilities(params.appCapabilities); return { ...isAppInfo(params.appInfo) ? { appInfo: params.appInfo } : {}, ...appCapabilities ? { appCapabilities } : {}, ...typeof params.protocolVersion === "string" ? { protocolVersion: params.protocolVersion } : {} }; } const DEFAULT_HOST_INFO = { name: "assistant-ui", version: "0.1" }; const METHOD_ALIASES = { "ui/notifications/initialized": "notifications/initialized", "ui/notifications/size-changed": "notifications/size_changed", "ui/request-display-mode": "requestDisplayMode", "ui/open-link": "openLink", "ui/update-model-context": "updateModelContext", "ui/message": "sendMessage", "notifications/message": "notifications/log" }; const normalizeMethod = (method) => METHOD_ALIASES[method] ?? method; const JSONRPC_ERROR = { parseError: -32700, invalidRequest: -32600, methodNotFound: -32601, invalidParams: -32602, internalError: -32603 }; function isJsonRpcMessage(value) { if (!value || typeof value !== "object") return false; const v = value; return v.jsonrpc === "2.0" && typeof v.method === "string"; } function isRequest(msg) { return "id" in msg; } function isNotification(msg) { return !("id" in msg); } function createMcpAppBridge(opts) { const { frame, handlers = {}, hostInfo = DEFAULT_HOST_INFO, hostContext = {} } = opts; let disposed = false; let initializeParams = {}; const post = (msg) => { if (disposed) return; frame.sendMessage(msg); }; const respond = (id, payload) => { const res = { jsonrpc: "2.0", id, ...payload }; post(res); }; const errorResponse = (id, code, message, data) => { respond(id, { error: { code, message, ...data !== void 0 ? { data } : {} } }); }; const reportError = (error) => { if (disposed) return; invokeUserCallback("assistant-ui", "MCP App onError", handlers.onError?.bind(handlers), error); }; const handleRequest = async (req) => { try { const params = req.params; switch (normalizeMethod(req.method)) { case "ui/initialize": { initializeParams = getInitializeParams(params); const requestedProtocolVersion = initializeParams.protocolVersion ?? "0.1"; respond(req.id, { result: { protocolVersion: requestedProtocolVersion, host: hostInfo, hostInfo, hostContext, capabilities: { tools: handlers.callTool ? {} : void 0, resources: handlers.readResource || handlers.listResources ? {} : void 0, ui: { sendMessage: !!handlers.sendMessage, openLink: !!handlers.openLink, requestDisplayMode: !!handlers.requestDisplayMode, updateModelContext: !!handlers.updateModelContext } }, hostCapabilities: { ...handlers.openLink ? { openLinks: {} } : {}, ...handlers.callTool ? { serverTools: {} } : {}, ...handlers.readResource || handlers.listResources ? { serverResources: {} } : {}, ...handlers.updateModelContext ? { updateModelContext: { text: {} } } : {}, ...handlers.sendMessage ? { message: { text: {} } } : {}, ...handlers.onLog ? { logging: {} } : {} } } }); return; } case "tools/call": { if (!handlers.callTool) { errorResponse(req.id, JSONRPC_ERROR.methodNotFound, "tools/call is not supported by this host"); return; } const callParams = params ?? {}; if (typeof callParams.name !== "string") { errorResponse(req.id, JSONRPC_ERROR.invalidParams, "tools/call requires a string 'name'"); return; } if (handlers.allowedTools && !handlers.allowedTools.includes(callParams.name)) { errorResponse(req.id, JSONRPC_ERROR.invalidParams, `tool '${callParams.name}' is not allowed for this app`); return; } let callArgs; if (callParams.arguments !== void 0) { if (callParams.arguments === null || typeof callParams.arguments !== "object" || Array.isArray(callParams.arguments)) { errorResponse(req.id, JSONRPC_ERROR.invalidParams, "tools/call 'arguments' must be an object"); return; } callArgs = callParams.arguments; } const result = await handlers.callTool({ name: callParams.name, ...callArgs !== void 0 ? { arguments: callArgs } : {} }); respond(req.id, { result }); return; } case "resources/read": { if (!handlers.readResource) { errorResponse(req.id, JSONRPC_ERROR.methodNotFound, "resources/read is not supported by this host"); return; } const readParams = params ?? {}; if (typeof readParams.uri !== "string") { errorResponse(req.id, JSONRPC_ERROR.invalidParams, "resources/read requires a string 'uri'"); return; } respond(req.id, { result: await handlers.readResource({ uri: readParams.uri }) }); return; } case "resources/list": if (!handlers.listResources) { errorResponse(req.id, JSONRPC_ERROR.methodNotFound, "resources/list is not supported by this host"); return; } respond(req.id, { result: await handlers.listResources(params) ?? null }); return; case "openLink": { if (!handlers.openLink) { errorResponse(req.id, JSONRPC_ERROR.methodNotFound, "openLink is not supported by this host"); return; } const linkParams = params ?? {}; if (typeof linkParams.url !== "string") { errorResponse(req.id, JSONRPC_ERROR.invalidParams, "openLink requires a string 'url'"); return; } let linkProtocol; try { linkProtocol = new URL(linkParams.url).protocol; } catch { errorResponse(req.id, JSONRPC_ERROR.invalidParams, "openLink requires a valid URL"); return; } if (linkProtocol !== "https:" && linkProtocol !== "http:") { errorResponse(req.id, JSONRPC_ERROR.invalidParams, "openLink only accepts http(s) URLs"); return; } respond(req.id, { result: await handlers.openLink({ url: linkParams.url }) }); return; } case "sendMessage": if (!handlers.sendMessage) { errorResponse(req.id, JSONRPC_ERROR.methodNotFound, "sendMessage is not supported by this host"); return; } respond(req.id, { result: await handlers.sendMessage(params) ?? null }); return; case "updateModelContext": if (!handlers.updateModelContext) { errorResponse(req.id, JSONRPC_ERROR.methodNotFound, "updateModelContext is not supported by this host"); return; } respond(req.id, { result: await handlers.updateModelContext(params) ?? null }); return; case "requestDisplayMode": { if (!handlers.requestDisplayMode) { errorResponse(req.id, JSONRPC_ERROR.methodNotFound, "requestDisplayMode is not supported by this host"); return; } const modeParams = params ?? {}; if (!isMcpAppDisplayMode(modeParams.mode)) { errorResponse(req.id, JSONRPC_ERROR.invalidParams, "requestDisplayMode requires a valid 'mode'"); return; } respond(req.id, { result: await handlers.requestDisplayMode({ mode: modeParams.mode }) }); return; } default: errorResponse(req.id, JSONRPC_ERROR.methodNotFound, `Unknown method: ${req.method}`); } } catch (err) { const error = err instanceof Error ? err : new Error(String(err)); reportError(error); errorResponse(req.id, JSONRPC_ERROR.internalError, error.message); } }; const handleNotification = (note) => { try { switch (normalizeMethod(note.method)) { case "notifications/initialized": handlers.onInitialized?.(initializeParams); return; case "notifications/size_changed": { const p = note.params ?? {}; handlers.onSizeChange?.({ ...typeof p.width === "number" ? { width: p.width } : {}, ...typeof p.height === "number" ? { height: p.height } : {} }); return; } case "notifications/log": handlers.onLog?.(note.params); return; case "notifications/request_teardown": handlers.onRequestTeardown?.(note.params); return; case "notifications/error": { const p = note.params ?? {}; reportError(new Error(typeof p.message === "string" ? p.message : "Widget error")); return; } default: return; } } catch (err) { const error = err instanceof Error ? err : new Error(String(err)); reportError(error); } }; const onMessage = (event) => { if (disposed) return; if (!isJsonRpcMessage(event.data)) return; const msg = event.data; if (isRequest(msg)) handleRequest(msg); else if (isNotification(msg)) handleNotification(msg); }; return { onMessage, dispose: () => { disposed = true; }, notifyToolInput: (input) => { post({ jsonrpc: "2.0", method: "notifications/tools/call/input", params: { input } }); post({ jsonrpc: "2.0", method: "ui/notifications/tool-input", params: isRecord(input) ? { arguments: input } : {} }); }, notifyToolResult: (result) => { post({ jsonrpc: "2.0", method: "notifications/tools/call/result", params: { result } }); post({ jsonrpc: "2.0", method: "ui/notifications/tool-result", params: isRecord(result) ? result : { content: [{ type: "text", text: String(result) }] } }); }, notifyHostContextChanged: (ctx) => { post({ jsonrpc: "2.0", method: "notifications/host_context/changed", params: ctx }); post({ jsonrpc: "2.0", method: "ui/notifications/host-context-changed", params: ctx }); } }; } export { createMcpAppBridge }; //# sourceMappingURL=bridge.js.map