UNPKG

mcp-use

Version:

Opinionated MCP Framework for TypeScript (@modelcontextprotocol/sdk compatible) - Build MCP Agents and Clients + MCP Servers with support for MCP-UI.

1,404 lines (1,397 loc) 57.9 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/server/index.ts var server_exports = {}; __export(server_exports, { buildWidgetUrl: () => buildWidgetUrl, createExternalUrlResource: () => createExternalUrlResource, createMCPServer: () => createMCPServer, createRawHtmlResource: () => createRawHtmlResource, createRemoteDomResource: () => createRemoteDomResource, createUIResourceFromDefinition: () => createUIResourceFromDefinition }); module.exports = __toCommonJS(server_exports); // src/server/mcp-server.ts var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js"); var import_zod = require("zod"); var import_express = __toESM(require("express"), 1); var import_cors = __toESM(require("cors"), 1); var import_node_fs = require("fs"); var import_node_path = require("path"); var import_node_fs2 = require("fs"); // src/server/logging.ts function requestLogger(req, res, next) { const timestamp = (/* @__PURE__ */ new Date()).toISOString().substring(11, 23); const method = req.method; const url = req.url; const originalEnd = res.end.bind(res); res.end = function(chunk, encoding, cb) { const statusCode = res.statusCode; let statusColor = ""; if (statusCode >= 200 && statusCode < 300) { statusColor = "\x1B[32m"; } else if (statusCode >= 300 && statusCode < 400) { statusColor = "\x1B[33m"; } else if (statusCode >= 400 && statusCode < 500) { statusColor = "\x1B[31m"; } else if (statusCode >= 500) { statusColor = "\x1B[35m"; } let logMessage = `[${timestamp}] ${method} \x1B[1m${url}\x1B[0m`; if (method === "POST" && url === "/mcp" && req.body?.method) { logMessage += ` \x1B[1m[${req.body.method}]\x1B[0m`; } logMessage += ` ${statusColor}${statusCode}\x1B[0m`; console.log(logMessage); return originalEnd(chunk, encoding, cb); }; next(); } __name(requestLogger, "requestLogger"); // src/server/adapters/mcp-ui-adapter.ts var import_server = require("@mcp-ui/server"); function buildWidgetUrl(widget, props, config) { const url = new URL( `/mcp-use/widgets/${widget}`, `${config.baseUrl}:${config.port}` ); if (props && Object.keys(props).length > 0) { url.searchParams.set("props", JSON.stringify(props)); } return url.toString(); } __name(buildWidgetUrl, "buildWidgetUrl"); function createExternalUrlResource(uri, iframeUrl, encoding = "text", adapters, metadata) { return (0, import_server.createUIResource)({ uri, content: { type: "externalUrl", iframeUrl }, encoding, adapters, metadata }); } __name(createExternalUrlResource, "createExternalUrlResource"); function createRawHtmlResource(uri, htmlString, encoding = "text", adapters, metadata) { return (0, import_server.createUIResource)({ uri, content: { type: "rawHtml", htmlString }, encoding, adapters, metadata }); } __name(createRawHtmlResource, "createRawHtmlResource"); function createRemoteDomResource(uri, script, framework = "react", encoding = "text", adapters, metadata) { return (0, import_server.createUIResource)({ uri, content: { type: "remoteDom", script, framework }, encoding, adapters, metadata }); } __name(createRemoteDomResource, "createRemoteDomResource"); function createAppsSdkResource(uri, htmlTemplate, metadata) { const resource = { uri, mimeType: "text/html+skybridge", text: htmlTemplate }; if (metadata && Object.keys(metadata).length > 0) { resource._meta = metadata; } return { type: "resource", resource }; } __name(createAppsSdkResource, "createAppsSdkResource"); function createUIResourceFromDefinition(definition, params, config) { const uri = definition.type === "appsSdk" ? `ui://widget/${definition.name}.html` : `ui://widget/${definition.name}`; const encoding = definition.encoding || "text"; switch (definition.type) { case "externalUrl": { const widgetUrl = buildWidgetUrl(definition.widget, params, config); return createExternalUrlResource( uri, widgetUrl, encoding, definition.adapters, definition.appsSdkMetadata ); } case "rawHtml": { return createRawHtmlResource( uri, definition.htmlContent, encoding, definition.adapters, definition.appsSdkMetadata ); } case "remoteDom": { const framework = definition.framework || "react"; return createRemoteDomResource( uri, definition.script, framework, encoding, definition.adapters, definition.appsSdkMetadata ); } case "appsSdk": { return createAppsSdkResource( uri, definition.htmlTemplate, definition.appsSdkMetadata ); } default: { const _exhaustive = definition; throw new Error(`Unknown UI resource type: ${_exhaustive.type}`); } } } __name(createUIResourceFromDefinition, "createUIResourceFromDefinition"); // src/server/mcp-server.ts var import_vite = require("vite"); var TMP_MCP_USE_DIR = ".mcp-use"; var McpServer = class { static { __name(this, "McpServer"); } server; config; app; mcpMounted = false; inspectorMounted = false; serverPort; serverHost; serverBaseUrl; /** * Creates a new MCP server instance with Express integration * * Initializes the server with the provided configuration, sets up CORS headers, * configures widget serving routes, and creates a proxy that allows direct * access to Express methods while preserving MCP server functionality. * * @param config - Server configuration including name, version, and description * @returns A proxied McpServer instance that supports both MCP and Express methods */ constructor(config) { this.config = config; this.serverHost = config.host || "localhost"; this.serverBaseUrl = config.baseUrl; this.server = new import_mcp.McpServer({ name: config.name, version: config.version }); this.app = (0, import_express.default)(); this.app.use(import_express.default.json()); this.app.use( (0, import_cors.default)({ origin: "*", methods: ["GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"], allowedHeaders: [ "Content-Type", "Accept", "Authorization", "mcp-protocol-version", "mcp-session-id", "X-Proxy-Token", "X-Target-URL" ] }) ); this.app.use(requestLogger); return new Proxy(this, { get(target, prop) { if (prop in target) { return target[prop]; } const value = target.app[prop]; return typeof value === "function" ? value.bind(target.app) : value; } }); } /** * Define a static resource that can be accessed by clients * * Registers a resource with the MCP server that clients can access via HTTP. * Resources are static content like files, data, or pre-computed results that * can be retrieved by clients without requiring parameters. * * @param resourceDefinition - Configuration object containing resource metadata and handler function * @param resourceDefinition.name - Unique identifier for the resource * @param resourceDefinition.uri - URI pattern for accessing the resource * @param resourceDefinition.title - Optional human-readable title for the resource * @param resourceDefinition.description - Optional description of the resource * @param resourceDefinition.mimeType - MIME type of the resource content * @param resourceDefinition.annotations - Optional annotations (audience, priority, lastModified) * @param resourceDefinition.readCallback - Async callback function that returns the resource content * @returns The server instance for method chaining * * @example * ```typescript * server.resource({ * name: 'config', * uri: 'config://app-settings', * title: 'Application Settings', * mimeType: 'application/json', * description: 'Current application configuration', * annotations: { * audience: ['user'], * priority: 0.8 * }, * readCallback: async () => ({ * contents: [{ * uri: 'config://app-settings', * mimeType: 'application/json', * text: JSON.stringify({ theme: 'dark', language: 'en' }) * }] * }) * }) * ``` */ resource(resourceDefinition) { this.server.registerResource( resourceDefinition.name, resourceDefinition.uri, { name: resourceDefinition.name, title: resourceDefinition.title, description: resourceDefinition.description, mimeType: resourceDefinition.mimeType, annotations: resourceDefinition.annotations, _meta: resourceDefinition._meta }, async () => { return await resourceDefinition.readCallback(); } ); return this; } /** * Define a dynamic resource template with parameters * * Registers a parameterized resource template with the MCP server. Templates use URI * patterns with placeholders that can be filled in at request time, allowing dynamic * resource generation based on parameters. * * @param resourceTemplateDefinition - Configuration object for the resource template * @param resourceTemplateDefinition.name - Unique identifier for the template * @param resourceTemplateDefinition.resourceTemplate - ResourceTemplate object with uriTemplate and metadata * @param resourceTemplateDefinition.readCallback - Async callback function that generates resource content from URI and params * @returns The server instance for method chaining * * @example * ```typescript * server.resourceTemplate({ * name: 'user-profile', * resourceTemplate: { * uriTemplate: 'user://{userId}/profile', * name: 'User Profile', * mimeType: 'application/json' * }, * readCallback: async (uri, params) => ({ * contents: [{ * uri: uri.toString(), * mimeType: 'application/json', * text: JSON.stringify({ userId: params.userId, name: 'John Doe' }) * }] * }) * }) * ``` */ resourceTemplate(resourceTemplateDefinition) { const template = new import_mcp.ResourceTemplate( resourceTemplateDefinition.resourceTemplate.uriTemplate, { list: void 0, // Optional: callback to list all matching resources complete: void 0 // Optional: callback for auto-completion } ); const metadata = {}; if (resourceTemplateDefinition.resourceTemplate.name) { metadata.name = resourceTemplateDefinition.resourceTemplate.name; } if (resourceTemplateDefinition.title) { metadata.title = resourceTemplateDefinition.title; } if (resourceTemplateDefinition.description || resourceTemplateDefinition.resourceTemplate.description) { metadata.description = resourceTemplateDefinition.description || resourceTemplateDefinition.resourceTemplate.description; } if (resourceTemplateDefinition.resourceTemplate.mimeType) { metadata.mimeType = resourceTemplateDefinition.resourceTemplate.mimeType; } if (resourceTemplateDefinition.annotations) { metadata.annotations = resourceTemplateDefinition.annotations; } this.server.registerResource( resourceTemplateDefinition.name, template, metadata, async (uri) => { const params = this.parseTemplateUri( resourceTemplateDefinition.resourceTemplate.uriTemplate, uri.toString() ); return await resourceTemplateDefinition.readCallback(uri, params); } ); return this; } /** * Define a tool that can be called by clients * * Registers a tool with the MCP server that clients can invoke with parameters. * Tools are functions that perform actions, computations, or operations and * return results. They accept structured input parameters and return structured output. * * Supports Apps SDK metadata for ChatGPT integration via the _meta field. * * @param toolDefinition - Configuration object containing tool metadata and handler function * @param toolDefinition.name - Unique identifier for the tool * @param toolDefinition.description - Human-readable description of what the tool does * @param toolDefinition.inputs - Array of input parameter definitions with types and validation * @param toolDefinition.cb - Async callback function that executes the tool logic with provided parameters * @param toolDefinition._meta - Optional metadata for the tool (e.g. Apps SDK metadata) * @returns The server instance for method chaining * * @example * ```typescript * server.tool({ * name: 'calculate', * description: 'Performs mathematical calculations', * inputs: [ * { name: 'expression', type: 'string', required: true }, * { name: 'precision', type: 'number', required: false } * ], * cb: async ({ expression, precision = 2 }) => { * const result = eval(expression) * return { result: Number(result.toFixed(precision)) } * }, * _meta: { * 'openai/outputTemplate': 'ui://widgets/calculator', * 'openai/toolInvocation/invoking': 'Calculating...', * 'openai/toolInvocation/invoked': 'Calculation complete' * } * }) * ``` */ tool(toolDefinition) { const inputSchema = this.createParamsSchema(toolDefinition.inputs || []); this.server.registerTool( toolDefinition.name, { title: toolDefinition.title, description: toolDefinition.description ?? "", inputSchema, annotations: toolDefinition.annotations, _meta: toolDefinition._meta }, async (params) => { return await toolDefinition.cb(params); } ); return this; } /** * Define a prompt template * * Registers a prompt template with the MCP server that clients can use to generate * structured prompts for AI models. Prompt templates accept parameters and return * formatted text that can be used as input to language models or other AI systems. * * @param promptDefinition - Configuration object containing prompt metadata and handler function * @param promptDefinition.name - Unique identifier for the prompt template * @param promptDefinition.description - Human-readable description of the prompt's purpose * @param promptDefinition.args - Array of argument definitions with types and validation * @param promptDefinition.cb - Async callback function that generates the prompt from provided arguments * @returns The server instance for method chaining * * @example * ```typescript * server.prompt({ * name: 'code-review', * description: 'Generates a code review prompt', * args: [ * { name: 'language', type: 'string', required: true }, * { name: 'focus', type: 'string', required: false } * ], * cb: async ({ language, focus = 'general' }) => { * return { * messages: [{ * role: 'user', * content: `Please review this ${language} code with focus on ${focus}...` * }] * } * } * }) * ``` */ prompt(promptDefinition) { const argsSchema = this.createParamsSchema(promptDefinition.args || []); this.server.registerPrompt( promptDefinition.name, { title: promptDefinition.title, description: promptDefinition.description ?? "", argsSchema }, async (params) => { return await promptDefinition.cb(params); } ); return this; } /** * Register a UI widget as both a tool and a resource * * Creates a unified interface for MCP-UI compatible widgets that can be accessed * either as tools (with parameters) or as resources (static access). The tool * allows dynamic parameter passing while the resource provides discoverable access. * * Supports multiple UI resource types: * - externalUrl: Legacy MCP-UI iframe-based widgets * - rawHtml: Legacy MCP-UI raw HTML content * - remoteDom: Legacy MCP-UI Remote DOM scripting * - appsSdk: OpenAI Apps SDK compatible widgets (text/html+skybridge) * * @param widgetNameOrDefinition - Widget name (string) for auto-loading schema, or full configuration object * @param definition.name - Unique identifier for the resource * @param definition.type - Type of UI resource (externalUrl, rawHtml, remoteDom, appsSdk) * @param definition.title - Human-readable title for the widget * @param definition.description - Description of the widget's functionality * @param definition.props - Widget properties configuration with types and defaults * @param definition.size - Preferred iframe size [width, height] (e.g., ['900px', '600px']) * @param definition.annotations - Resource annotations for discovery * @param definition.appsSdkMetadata - Apps SDK specific metadata (CSP, widget description, etc.) * @returns The server instance for method chaining * * @example * ```typescript * // Simple usage - auto-loads from generated schema * server.uiResource('display-weather') * * // Legacy MCP-UI widget * server.uiResource({ * type: 'externalUrl', * name: 'kanban-board', * widget: 'kanban-board', * title: 'Kanban Board', * description: 'Interactive task management board', * props: { * initialTasks: { * type: 'array', * description: 'Initial tasks to display', * required: false * } * }, * size: ['900px', '600px'] * }) * * // Apps SDK widget * server.uiResource({ * type: 'appsSdk', * name: 'kanban-board', * title: 'Kanban Board', * description: 'Interactive task management board', * htmlTemplate: ` * <div id="kanban-root"></div> * <style>${kanbanCSS}</style> * <script type="module">${kanbanJS}</script> * `, * appsSdkMetadata: { * 'openai/widgetDescription': 'Displays an interactive kanban board', * 'openai/widgetCSP': { * connect_domains: [], * resource_domains: ['https://cdn.example.com'] * } * } * }) * ``` */ uiResource(definition) { const displayName = definition.title || definition.name; let resourceUri; let mimeType; switch (definition.type) { case "externalUrl": resourceUri = `ui://widget/${definition.widget}`; mimeType = "text/uri-list"; break; case "rawHtml": resourceUri = `ui://widget/${definition.name}`; mimeType = "text/html"; break; case "remoteDom": resourceUri = `ui://widget/${definition.name}`; mimeType = "application/vnd.mcp-ui.remote-dom+javascript"; break; case "appsSdk": resourceUri = `ui://widget/${definition.name}.html`; mimeType = "text/html+skybridge"; break; default: throw new Error( `Unsupported UI resource type. Must be one of: externalUrl, rawHtml, remoteDom, appsSdk` ); } this.resource({ name: definition.name, uri: resourceUri, title: definition.title, description: definition.description, mimeType, _meta: definition._meta, annotations: definition.annotations, readCallback: /* @__PURE__ */ __name(async () => { const params = definition.type === "externalUrl" ? this.applyDefaultProps(definition.props) : {}; const uiResource = this.createWidgetUIResource(definition, params); return { contents: [uiResource.resource] }; }, "readCallback") }); if (definition.type === "appsSdk") { this.resourceTemplate({ name: `${definition.name}-dynamic`, resourceTemplate: { uriTemplate: `ui://widget/${definition.name}-{id}.html`, name: definition.title || definition.name, description: definition.description, mimeType }, _meta: definition._meta, title: definition.title, description: definition.description, annotations: definition.annotations, readCallback: /* @__PURE__ */ __name(async (uri, params) => { const uiResource = this.createWidgetUIResource(definition, {}); return { contents: [uiResource.resource] }; }, "readCallback") }); } const toolMetadata = definition._meta || {}; if (definition.type === "appsSdk" && definition.appsSdkMetadata) { toolMetadata["openai/outputTemplate"] = resourceUri; const toolMetadataFields = [ "openai/toolInvocation/invoking", "openai/toolInvocation/invoked", "openai/widgetAccessible", "openai/resultCanProduceWidget" ]; for (const field of toolMetadataFields) { if (definition.appsSdkMetadata[field] !== void 0) { toolMetadata[field] = definition.appsSdkMetadata[field]; } } } this.tool({ name: definition.name, title: definition.title, description: definition.description, inputs: this.convertPropsToInputs(definition.props), _meta: Object.keys(toolMetadata).length > 0 ? toolMetadata : void 0, cb: /* @__PURE__ */ __name(async (params) => { const uiResource = this.createWidgetUIResource(definition, params); if (definition.type === "appsSdk") { const randomId = Math.random().toString(36).substring(2, 15); const uniqueUri = `ui://widget/${definition.name}-${randomId}.html`; const uniqueToolMetadata = { ...toolMetadata, "openai/outputTemplate": uniqueUri }; return { _meta: uniqueToolMetadata, content: [ { type: "text", text: `Displaying ${displayName}` } ], // structuredContent will be injected as window.openai.toolOutput by Apps SDK structuredContent: params }; } return { content: [ { type: "text", text: `Displaying ${displayName}`, description: `Show MCP-UI widget for ${displayName}` }, uiResource ] }; }, "cb") }); return this; } /** * Create a UIResource object for a widget with the given parameters * * This method is shared between tool and resource handlers to avoid duplication. * It creates a consistent UIResource structure that can be rendered by MCP-UI * compatible clients. * * @private * @param definition - UIResource definition * @param params - Parameters to pass to the widget via URL * @returns UIResource object compatible with MCP-UI */ createWidgetUIResource(definition, params) { let configBaseUrl = `http://${this.serverHost}`; let configPort = this.serverPort || 3001; if (this.serverBaseUrl) { try { const url = new URL(this.serverBaseUrl); configBaseUrl = `${url.protocol}//${url.hostname}`; configPort = url.port || (url.protocol === "https:" ? 443 : 80); } catch (e) { console.warn("Failed to parse baseUrl, falling back to host:port", e); } } const urlConfig = { baseUrl: configBaseUrl, port: configPort }; return createUIResourceFromDefinition(definition, params, urlConfig); } /** * Build a complete URL for a widget including query parameters * * Constructs the full URL to access a widget's iframe, encoding any provided * parameters as query string parameters. Complex objects are JSON-stringified * for transmission. * * @private * @param widget - Widget name/identifier * @param params - Parameters to encode in the URL * @returns Complete URL with encoded parameters */ buildWidgetUrl(widget, params) { const baseUrl = `http://${this.serverHost}:${this.serverPort}/mcp-use/widgets/${widget}`; if (Object.keys(params).length === 0) { return baseUrl; } const queryParams = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value !== void 0 && value !== null) { if (typeof value === "object") { queryParams.append(key, JSON.stringify(value)); } else { queryParams.append(key, String(value)); } } } return `${baseUrl}?${queryParams.toString()}`; } /** * Convert widget props definition to tool input schema * * Transforms the widget props configuration into the format expected by * the tool registration system, mapping types and handling defaults. * * @private * @param props - Widget props configuration * @returns Array of InputDefinition objects for tool registration */ convertPropsToInputs(props) { if (!props) return []; return Object.entries(props).map(([name, prop]) => ({ name, type: prop.type, description: prop.description, required: prop.required, default: prop.default })); } /** * Apply default values to widget props * * Extracts default values from the props configuration to use when * the resource is accessed without parameters. * * @private * @param props - Widget props configuration * @returns Object with default values for each prop */ applyDefaultProps(props) { if (!props) return {}; const defaults = {}; for (const [key, prop] of Object.entries(props)) { if (prop.default !== void 0) { defaults[key] = prop.default; } } return defaults; } /** * Check if server is running in production mode * * @private * @returns true if in production mode, false otherwise */ isProductionMode() { return process.env.NODE_ENV === "production"; } /** * Read build manifest file * * @private * @returns Build manifest or null if not found */ readBuildManifest() { try { const manifestPath = (0, import_node_path.join)( process.cwd(), "dist", ".mcp-use-manifest.json" ); const content = (0, import_node_fs2.readFileSync)(manifestPath, "utf8"); return JSON.parse(content); } catch { return null; } } /** * Mount widget files - automatically chooses between dev and production mode * * In development mode: creates Vite dev servers with HMR support * In production mode: serves pre-built static widgets * * @param options - Configuration options * @param options.baseRoute - Base route for widgets (defaults to '/mcp-use/widgets') * @param options.resourcesDir - Directory containing widget files (defaults to 'resources') * @returns Promise that resolves when all widgets are mounted */ async mountWidgets(options) { if (this.isProductionMode()) { await this.mountWidgetsProduction(options); } else { await this.mountWidgetsDev(options); } } /** * Mount individual widget files from resources/ directory in development mode * * Scans the resources/ directory for .tsx/.ts widget files and creates individual * Vite dev servers for each widget with HMR support. Each widget is served at its * own route: /mcp-use/widgets/{widget-name} * * @private * @param options - Configuration options * @param options.baseRoute - Base route for widgets (defaults to '/mcp-use/widgets') * @param options.resourcesDir - Directory containing widget files (defaults to 'resources') * @returns Promise that resolves when all widgets are mounted */ async mountWidgetsDev(options) { const { promises: fs } = await import("fs"); const baseRoute = options?.baseRoute || "/mcp-use/widgets"; const resourcesDir = options?.resourcesDir || "resources"; const srcDir = (0, import_node_path.join)(process.cwd(), resourcesDir); try { await fs.access(srcDir); } catch (error) { console.log( `[WIDGETS] No ${resourcesDir}/ directory found - skipping widget serving` ); return; } let entries = []; try { const files = await fs.readdir(srcDir); entries = files.filter((f) => f.endsWith(".tsx") || f.endsWith(".ts")).map((f) => (0, import_node_path.join)(srcDir, f)); } catch (error) { console.log(`[WIDGETS] No widgets found in ${resourcesDir}/ directory`); return; } if (entries.length === 0) { console.log(`[WIDGETS] No widgets found in ${resourcesDir}/ directory`); return; } const tempDir = (0, import_node_path.join)(process.cwd(), TMP_MCP_USE_DIR); await fs.mkdir(tempDir, { recursive: true }).catch(() => { }); const react = (await import("@vitejs/plugin-react")).default; const tailwindcss = (await import("@tailwindcss/vite")).default; console.log(react, tailwindcss); const widgets = entries.map((entry) => { const baseName = entry.split("/").pop()?.replace(/\.tsx?$/, "") || "widget"; const widgetName = baseName; return { name: widgetName, description: `Widget: ${widgetName}`, entry }; }); for (const widget of widgets) { const widgetTempDir = (0, import_node_path.join)(tempDir, widget.name); await fs.mkdir(widgetTempDir, { recursive: true }); const resourcesPath = (0, import_node_path.join)(process.cwd(), resourcesDir); const { relative } = await import("path"); const relativeResourcesPath = relative( widgetTempDir, resourcesPath ).replace(/\\/g, "/"); const cssContent = `@import "tailwindcss"; /* Configure Tailwind to scan the resources directory */ @source "${relativeResourcesPath}"; `; await fs.writeFile((0, import_node_path.join)(widgetTempDir, "styles.css"), cssContent, "utf8"); const entryContent = `import React from 'react' import { createRoot } from 'react-dom/client' import './styles.css' import Component from '${widget.entry}' const container = document.getElementById('widget-root') if (container && Component) { const root = createRoot(container) root.render(<Component />) } `; const htmlContent = `<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>${widget.name} Widget</title> </head> <body> <div id="widget-root"></div> <script type="module" src="${baseRoute}/${widget.name}/entry.tsx"></script> </body> </html>`; await fs.writeFile( (0, import_node_path.join)(widgetTempDir, "entry.tsx"), entryContent, "utf8" ); await fs.writeFile( (0, import_node_path.join)(widgetTempDir, "index.html"), htmlContent, "utf8" ); } const serverOrigin = this.serverBaseUrl || `http://${this.serverHost}:${this.serverPort}`; console.log( `[WIDGETS] Serving ${entries.length} widget(s) with shared Vite dev server and HMR` ); const viteServer = await (0, import_vite.createServer)({ root: tempDir, base: baseRoute + "/", plugins: [tailwindcss(), react()], resolve: { alias: { "@": (0, import_node_path.join)(process.cwd(), resourcesDir) } }, server: { middlewareMode: true, origin: serverOrigin } }); this.app.use(baseRoute, (req, res, next) => { const urlPath = req.url || ""; const [pathname, queryString] = urlPath.split("?"); const widgetMatch = pathname.match(/^\/([^/]+)/); if (widgetMatch) { const widgetName = widgetMatch[1]; const widget = widgets.find((w) => w.name === widgetName); if (widget) { if (pathname === `/${widgetName}` || pathname === `/${widgetName}/`) { req.url = `/${widgetName}/index.html${queryString ? "?" + queryString : ""}`; } } } next(); }); this.app.use(baseRoute, viteServer.middlewares); widgets.forEach((widget) => { console.log( `[WIDGET] ${widget.name} mounted at ${baseRoute}/${widget.name}` ); }); for (const widget of widgets) { const type = "appsSdk"; let metadata = {}; let props = {}; let description = widget.description; try { const mod = await viteServer.ssrLoadModule(widget.entry); if (mod.widgetMetadata) { metadata = mod.widgetMetadata; description = metadata.description || widget.description; if (metadata.inputs) { try { props = metadata.inputs.shape || {}; } catch (error) { console.warn( `[WIDGET] Failed to extract props schema for ${widget.name}:`, error ); } } } } catch (error) { console.warn( `[WIDGET] Failed to load metadata for ${widget.name}:`, error ); } console.log("[WIDGET dev] Metadata:", metadata); let html = ""; try { html = (0, import_node_fs2.readFileSync)((0, import_node_path.join)(tempDir, widget.name, "index.html"), "utf8"); const mcpUrl = process.env.MCP_URL || "/"; if (mcpUrl && html) { const htmlWithoutComments = html.replace(/<!--[\s\S]*?-->/g, ""); const baseTagRegex = /<base\s+[^>]*\/?>/i; if (baseTagRegex.test(htmlWithoutComments)) { const actualBaseTagMatch = html.match(/<base\s+[^>]*\/?>/i); if (actualBaseTagMatch) { html = html.replace( actualBaseTagMatch[0], `<base href="${mcpUrl}" />` ); } } else { const headTagRegex = /<head[^>]*>/i; if (headTagRegex.test(html)) { html = html.replace( headTagRegex, (match) => `${match} <base href="${mcpUrl}" />` ); } } } html = html.replace( /src="\/mcp-use\/widgets\/([^"]+)"/g, `src="${this.serverBaseUrl}/mcp-use/widgets/$1"` ); html = html.replace( /href="\/mcp-use\/widgets\/([^"]+)"/g, `href="${this.serverBaseUrl}/mcp-use/widgets/$1"` ); html = html.replace( /<head[^>]*>/i, `<head> <script>window.__getFile = (filename) => { return "${this.serverBaseUrl}/mcp-use/widgets/${widget.name}/"+filename }</script>` ); } catch (error) { console.error( `Failed to read html template for widget ${widget.name}`, error ); } this.uiResource({ name: widget.name, title: metadata.title || widget.name, description, type, props, _meta: { "mcp-use/widget": { name: widget.name, title: metadata.title || widget.name, description, type, props, html, dev: true }, ...metadata._meta || {} }, htmlTemplate: html, appsSdkMetadata: { "openai/widgetDescription": description, "openai/toolInvocation/invoking": `Loading ${widget.name}...`, "openai/toolInvocation/invoked": `${widget.name} ready`, "openai/widgetAccessible": true, "openai/resultCanProduceWidget": true, ...metadata.appsSdkMetadata || {}, "openai/widgetCSP": { connect_domains: [ // always also add the base url of the server ...this.serverBaseUrl ? [this.serverBaseUrl] : [], ...metadata.appsSdkMetadata?.["openai/widgetCSP"]?.connect_domains || [] ], resource_domains: [ "https://*.oaistatic.com", "https://*.oaiusercontent.com", // always also add the base url of the server ...this.serverBaseUrl ? [this.serverBaseUrl] : [], ...metadata.appsSdkMetadata?.["openai/widgetCSP"]?.resource_domains || [] ] } } }); } } /** * Mount pre-built widgets from dist/resources/widgets/ directory in production mode * * Serves static widget bundles that were built using the build command. * Sets up Express routes to serve the HTML and asset files, then registers * tools and resources for each widget. * * @private * @param options - Configuration options * @param options.baseRoute - Base route for widgets (defaults to '/mcp-use/widgets') * @returns Promise that resolves when all widgets are mounted */ async mountWidgetsProduction(options) { const baseRoute = options?.baseRoute || "/mcp-use/widgets"; const widgetsDir = (0, import_node_path.join)(process.cwd(), "dist", "resources", "widgets"); if (!(0, import_node_fs.existsSync)(widgetsDir)) { console.log( "[WIDGETS] No dist/resources/widgets/ directory found - skipping widget serving" ); return; } this.setupWidgetRoutes(); const widgets = (0, import_node_fs.readdirSync)(widgetsDir).filter((name) => { const widgetPath = (0, import_node_path.join)(widgetsDir, name); const indexPath = (0, import_node_path.join)(widgetPath, "index.html"); return (0, import_node_fs.existsSync)(indexPath); }); if (widgets.length === 0) { console.log( "[WIDGETS] No built widgets found in dist/resources/widgets/" ); return; } console.log( `[WIDGETS] Serving ${widgets.length} pre-built widget(s) from dist/resources/widgets/` ); for (const widgetName of widgets) { const widgetPath = (0, import_node_path.join)(widgetsDir, widgetName); const indexPath = (0, import_node_path.join)(widgetPath, "index.html"); const metadataPath = (0, import_node_path.join)(widgetPath, "metadata.json"); let html = ""; try { html = (0, import_node_fs2.readFileSync)(indexPath, "utf8"); const mcpUrl = process.env.MCP_URL || "/"; if (mcpUrl && html) { const htmlWithoutComments = html.replace(/<!--[\s\S]*?-->/g, ""); const baseTagRegex = /<base\s+[^>]*\/?>/i; if (baseTagRegex.test(htmlWithoutComments)) { const actualBaseTagMatch = html.match(/<base\s+[^>]*\/?>/i); if (actualBaseTagMatch) { html = html.replace( actualBaseTagMatch[0], `<base href="${mcpUrl}" />` ); } } else { const headTagRegex = /<head[^>]*>/i; if (headTagRegex.test(html)) { html = html.replace( headTagRegex, (match) => `${match} <base href="${mcpUrl}" />` ); } } html = html.replace( /src="\/mcp-use\/widgets\/([^"]+)"/g, `src="${this.serverBaseUrl}/mcp-use/widgets/$1"` ); html = html.replace( /href="\/mcp-use\/widgets\/([^"]+)"/g, `href="${this.serverBaseUrl}/mcp-use/widgets/$1"` ); html = html.replace( /<head[^>]*>/i, `<head> <script>window.__getFile = (filename) => { return "${this.serverBaseUrl}/mcp-use/widgets/${widgetName}/"+filename }</script>` ); } } catch (error) { console.error( `[WIDGET] Failed to read ${widgetName}/index.html:`, error ); continue; } let metadata = {}; let props = {}; let description = `Widget: ${widgetName}`; try { const metadataContent = (0, import_node_fs2.readFileSync)(metadataPath, "utf8"); metadata = JSON.parse(metadataContent); if (metadata.description) { description = metadata.description; } if (metadata.inputs) { props = metadata.inputs; } } catch (error) { console.log( `[WIDGET] No metadata found for ${widgetName}, using defaults` ); } this.uiResource({ name: widgetName, title: metadata.title || widgetName, description, type: "appsSdk", props, _meta: { "mcp-use/widget": { name: widgetName, description, type: "appsSdk", props, html, dev: false }, ...metadata._meta || {} }, htmlTemplate: html, appsSdkMetadata: { "openai/widgetDescription": description, "openai/toolInvocation/invoking": `Loading ${widgetName}...`, "openai/toolInvocation/invoked": `${widgetName} ready`, "openai/widgetAccessible": true, "openai/resultCanProduceWidget": true, ...metadata.appsSdkMetadata || {}, "openai/widgetCSP": { connect_domains: [ // always also add the base url of the server ...this.serverBaseUrl ? [this.serverBaseUrl] : [], ...metadata.appsSdkMetadata?.["openai/widgetCSP"]?.connect_domains || [] ], resource_domains: [ "https://*.oaistatic.com", "https://*.oaiusercontent.com", // always also add the base url of the server ...this.serverBaseUrl ? [this.serverBaseUrl] : [], ...metadata.appsSdkMetadata?.["openai/widgetCSP"]?.resource_domains || [] ] } } }); console.log( `[WIDGET] ${widgetName} mounted at ${baseRoute}/${widgetName}` ); } } /** * Mount MCP server endpoints at /mcp * * Sets up the HTTP transport layer for the MCP server, creating endpoints for * Server-Sent Events (SSE) streaming, POST message handling, and DELETE session cleanup. * Each request gets its own transport instance to prevent state conflicts between * concurrent client connections. * * This method is called automatically when the server starts listening and ensures * that MCP clients can communicate with the server over HTTP. * * @private * @returns Promise that resolves when MCP endpoints are successfully mounted * * @example * Endpoints created: * - GET /mcp - SSE streaming endpoint for real-time communication * - POST /mcp - Message handling endpoint for MCP protocol messages * - DELETE /mcp - Session cleanup endpoint */ async mountMcp() { if (this.mcpMounted) return; const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js"); const endpoint = "/mcp"; this.app.post(endpoint, import_express.default.json(), async (req, res) => { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0, enableJsonResponse: true }); res.on("close", () => { transport.close(); }); await this.server.connect(transport); await transport.handleRequest(req, res, req.body); }); this.app.get(endpoint, async (req, res) => { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0, enableJsonResponse: true }); res.on("close", () => { transport.close(); }); await this.server.connect(transport); await transport.handleRequest(req, res); }); this.app.delete(endpoint, async (req, res) => { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0, enableJsonResponse: true }); res.on("close", () => { transport.close(); }); await this.server.connect(transport); await transport.handleRequest(req, res); }); this.mcpMounted = true; console.log(`[MCP] Server mounted at ${endpoint}`); } /** * Start the Express server with MCP endpoints * * Initiates the server startup process by mounting MCP endpoints, configuring * the inspector UI (if available), and starting the Express server to listen * for incoming connections. This is the main entry point for running the server. * * The server will be accessible at the specified port with MCP endpoints at /mcp * and inspector UI at /inspector (if the inspector package is installed). * * @param port - Port number to listen on (defaults to 3001 if not specified) * @returns Promise that resolves when the server is successfully listening * * @example * ```typescript * await server.listen(8080) * // Server now running at http://localhost:8080 (or configured host) * // MCP endpoints: http://localhost:8080/mcp * // Inspector UI: http://localhost:8080/inspector * ``` */ async listen(port) { this.serverPort = port || (process.env.PORT ? parseInt(process.env.PORT, 10) : 3001); if (process.env.HOST) { this.serverHost = process.env.HOST; } await this.mountWidgets({ baseRoute: "/mcp-use/widgets", resourcesDir: "resources" }); await this.mountMcp(); this.mountInspector(); this.app.listen(this.serverPort, () => { console.log( `[SERVER] Listening on http://${this.serverHost}:${this.serverPort}` ); console.log( `[MCP] Endpoints: http://${this.serverHost}:${this.serverPort}/mcp` ); }); } /** * Mount MCP Inspector UI at /inspector * * Dynamically loads and mounts the MCP Inspector UI package if available, providing * a web-based interface for testing and debugging MCP servers. The inspector * automatically connects to the local MCP server endpoints. * * This method gracefully handles cases where the inspector package is not installed, * allowing the server to function without the inspector in production environments. * * @private * @returns void * * @example * If @mcp-use/inspector is installed: * - Inspector UI available at http://localhost:PORT/inspector * - Automatically connects to http://localhost:PORT/mcp * * If not installed: * - Server continues to function normally * - No inspector UI available */ mountInspector() { if (this.inspectorMounted) return; if (this.isProductionMode()) { const manifest = this.readBuildManifest(); if (!manifest?.includeInspector) { console.log( "[INSPECTOR] Skipped in production (use --with-inspector flag during build)" ); return; } } import("@mcp-use/inspector").then(({ mountInspector }) => { mountInspector(this.app); this.inspectorMounted = true; console.log( `[INSPECTOR] UI available at http://${this.serverHost}:${this.serverPort}/inspector` ); }).catch(() => { }); } /** * Setup default widget serving routes * * Configures Express routes to serve MCP UI widgets and their static assets. * Widgets are served from the dist/resources/widgets directory and can * be accessed via HTTP endpoints for embedding in web applications. * * Routes created: * - GET /mcp-use/widgets/:widget - Serves widget's index.html * - GET /mcp-use/widgets/:widget/assets/* - Serves widget-specific assets * - GET /mcp-use/widgets/assets/* - Fallback asset serving with auto-discovery * * @private * @returns void * * @example * Widget routes: * - http://localhost:3001/mcp-use/widgets/kanban-board * - http://localhost:3001/mcp-use/widgets/todo-list/assets/style.css * - http://localhost:3001/mcp-use/widgets/assets/script.js (auto-discovered) */ setupWidgetRoutes() { this.app.get("/mcp-use/widgets/:widget/assets/*", (req, res, next) => { const widget = req.params.widget; const assetFile = req.params[0]; const assetPath = (0, import_node_path.join)( process.cwd(), "dist", "resources", "widgets", widget, "assets", assetFile ); res.sendFile(assetPath, (err) => err ? next() : void 0); }); this.app.get("/mcp-use/widgets/assets/*", (req, res, next) => { const assetFile = req.params[0]; const widgetsDir = (0, import_node_path.join)(process.cwd(), "dist", "resources", "widgets"); try { con