From b78a986eaec7eab02097eec6d4342f2efe2f05ab Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 10:09:33 +0200 Subject: [PATCH 01/19] update mcp-ui/server with support to apps sdk --- package.json | 2 +- packages/mcp-use/package.json | 2 +- ...mcp-ui__server@5.11.0.patch => @mcp-ui__server@5.12.0.patch} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename patches/{@mcp-ui__server@5.11.0.patch => @mcp-ui__server@5.12.0.patch} (100%) diff --git a/package.json b/package.json index e9278a44..604cff03 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "packageManager": "pnpm@10.6.1", "pnpm": { "patchedDependencies": { - "@mcp-ui/server@5.11.0": "patches/@mcp-ui__server@5.11.0.patch" + "@mcp-ui/server@5.12.0": "patches/@mcp-ui__server@5.12.0.patch" }, "overrides": { "mcp-use": "workspace:*", diff --git a/packages/mcp-use/package.json b/packages/mcp-use/package.json index 3691c9a8..841a975c 100644 --- a/packages/mcp-use/package.json +++ b/packages/mcp-use/package.json @@ -117,7 +117,7 @@ "@langchain/anthropic": "^0.3.26", "@langchain/core": "^0.3.72", "@langchain/openai": "^0.6.9", - "@mcp-ui/server": "^5.11.0", + "@mcp-ui/server": "^5.12.0", "@modelcontextprotocol/sdk": "1.20.0", "@scarf/scarf": "^1.4.0", "ai": "^4.3.19", diff --git a/patches/@mcp-ui__server@5.11.0.patch b/patches/@mcp-ui__server@5.12.0.patch similarity index 100% rename from patches/@mcp-ui__server@5.11.0.patch rename to patches/@mcp-ui__server@5.12.0.patch From 4dd4415dbeccbb7b79f2a19919ea0e27a5160518 Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 11:04:49 +0200 Subject: [PATCH 02/19] remove patch mcp-ui issue has been fixed --- package.json | 1 - patches/@mcp-ui__server@5.12.0.patch | 7 ------- pnpm-lock.yaml | 22 +++++++++++++--------- 3 files changed, 13 insertions(+), 17 deletions(-) delete mode 100644 patches/@mcp-ui__server@5.12.0.patch diff --git a/package.json b/package.json index 604cff03..86db6af0 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "packageManager": "pnpm@10.6.1", "pnpm": { "patchedDependencies": { - "@mcp-ui/server@5.12.0": "patches/@mcp-ui__server@5.12.0.patch" }, "overrides": { "mcp-use": "workspace:*", diff --git a/patches/@mcp-ui__server@5.12.0.patch b/patches/@mcp-ui__server@5.12.0.patch deleted file mode 100644 index 999ba41e..00000000 --- a/patches/@mcp-ui__server@5.12.0.patch +++ /dev/null @@ -1,7 +0,0 @@ -diff --git a/dist/index.d.ts b/dist/index.d.ts -index 3091b7219d0bdeb6202cf88862928c5029d9cb51..a48cb664e9c3db9fa58d3ea234fcc6e5f9a665fb 100644 ---- a/dist/index.d.ts -+++ b/dist/index.d.ts -@@ -1 +1 @@ --export * from './src/index' -+export * from './src/index.js' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71b5735e..9985e3d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,11 +12,6 @@ overrides: react: ^19.2.0 react-dom: ^19.2.0 -patchedDependencies: - '@mcp-ui/server@5.11.0': - hash: 5d24b4104a7d6d537b7de1dc183b27e42d9c1b3fde96a28dbeb722713ba85f11 - path: patches/@mcp-ui__server@5.11.0.patch - importers: .: @@ -122,7 +117,7 @@ importers: dependencies: '@mcp-ui/server': specifier: ^5.11.0 - version: 5.11.0(patch_hash=5d24b4104a7d6d537b7de1dc183b27e42d9c1b3fde96a28dbeb722713ba85f11) + version: 5.11.0 cors: specifier: ^2.8.5 version: 2.8.5 @@ -348,8 +343,8 @@ importers: specifier: ^0.6.9 version: 0.6.14(@langchain/core@0.3.78(@opentelemetry/api@1.9.0)(openai@5.12.2(ws@8.18.3)(zod@3.25.76)))(ws@8.18.3) '@mcp-ui/server': - specifier: ^5.11.0 - version: 5.11.0(patch_hash=5d24b4104a7d6d537b7de1dc183b27e42d9c1b3fde96a28dbeb722713ba85f11) + specifier: ^5.12.0 + version: 5.12.0 '@mcp-use/inspector': specifier: workspace:* version: link:../inspector @@ -1789,6 +1784,9 @@ packages: '@mcp-ui/server@5.11.0': resolution: {integrity: sha512-DLakzSNA17XsViC+YYQxy2ViVtdJu0SDxQeAIviguXFq58qACvDC5tMFTNn2c0vMB5qfXqz0HLZImE3hHd3C8g==} + '@mcp-ui/server@5.12.0': + resolution: {integrity: sha512-ZAAHsvzfrBgA0gkyIOjoKNTBTsD0VSJT4KXKHe+Fx/kBASctG6mrzK5gvxD/LLLliantN2UWLTKtEeI4DH4FRQ==} + '@modelcontextprotocol/sdk@1.12.1': resolution: {integrity: sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw==} engines: {node: '>=18'} @@ -7864,12 +7862,18 @@ snapshots: - preact - supports-color - '@mcp-ui/server@5.11.0(patch_hash=5d24b4104a7d6d537b7de1dc183b27e42d9c1b3fde96a28dbeb722713ba85f11)': + '@mcp-ui/server@5.11.0': dependencies: '@modelcontextprotocol/sdk': 1.12.1 transitivePeerDependencies: - supports-color + '@mcp-ui/server@5.12.0': + dependencies: + '@modelcontextprotocol/sdk': 1.20.0 + transitivePeerDependencies: + - supports-color + '@modelcontextprotocol/sdk@1.12.1': dependencies: ajv: 6.12.6 From 9a550663a3b18f2bd9252960e330becc226f0565 Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 12:24:33 +0200 Subject: [PATCH 03/19] feat: implement UI resource functionality in MCP server - Add UIResourceDefinition type and supporting interfaces for widget configuration - Implement uiResource() method that registers widgets as both tools and resources - Add widget prop handling with automatic query parameter conversion - Create modular type system with separate files for common, resource, tool, and prompt types - Support widget iframe rendering with configurable frame sizes - Add widget serving routes for static assets and HTML files - Export UIResource types from main package index for external consumption --- packages/mcp-use/index.ts | 6 + packages/mcp-use/src/server/index.ts | 17 +- packages/mcp-use/src/server/mcp-server.ts | 210 +++++++++++++++++- packages/mcp-use/src/server/types.ts | 87 +------- packages/mcp-use/src/server/types/common.ts | 29 +++ packages/mcp-use/src/server/types/index.ts | 37 +++ packages/mcp-use/src/server/types/prompt.ts | 15 ++ packages/mcp-use/src/server/types/resource.ts | 109 +++++++++ packages/mcp-use/src/server/types/tool.ts | 15 ++ 9 files changed, 428 insertions(+), 97 deletions(-) create mode 100644 packages/mcp-use/src/server/types/common.ts create mode 100644 packages/mcp-use/src/server/types/index.ts create mode 100644 packages/mcp-use/src/server/types/prompt.ts create mode 100644 packages/mcp-use/src/server/types/resource.ts create mode 100644 packages/mcp-use/src/server/types/tool.ts diff --git a/packages/mcp-use/index.ts b/packages/mcp-use/index.ts index 3ec34719..aa2da6d4 100644 --- a/packages/mcp-use/index.ts +++ b/packages/mcp-use/index.ts @@ -32,6 +32,12 @@ export type { ServerConfig, ToolDefinition, ToolHandler, + // UIResource specific types + UIResourceDefinition, + WidgetProps, + WidgetConfig, + WidgetManifest, + DiscoverWidgetsOptions, } from './src/server/types.js' // Export telemetry utilities export { setTelemetrySource, Telemetry } from './src/telemetry/index.js' diff --git a/packages/mcp-use/src/server/index.ts b/packages/mcp-use/src/server/index.ts index 52e80796..e07e6939 100644 --- a/packages/mcp-use/src/server/index.ts +++ b/packages/mcp-use/src/server/index.ts @@ -1,13 +1,6 @@ -export { - createMCPServer +export { + createMCPServer, + type McpServerInstance } from './mcp-server.js' -export type { - InputDefinition, - PromptDefinition, - PromptHandler, - ResourceDefinition, - ResourceHandler, - ServerConfig, - ToolDefinition, - ToolHandler, -} from './types.js' + +export * from './types/index.js' diff --git a/packages/mcp-use/src/server/mcp-server.ts b/packages/mcp-use/src/server/mcp-server.ts index 20b6bb50..e4970d79 100644 --- a/packages/mcp-use/src/server/mcp-server.ts +++ b/packages/mcp-use/src/server/mcp-server.ts @@ -4,13 +4,17 @@ import type { ResourceTemplateDefinition, ServerConfig, ToolDefinition, -} from './types.js' + UIResourceDefinition, + WidgetProps, + InputDefinition, +} from './types/index.js' import { McpServer as OfficialMcpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js' import { z } from 'zod' import express, { type Express } from 'express' import { existsSync, readdirSync } from 'node:fs' import { join } from 'node:path' import { requestLogger } from './logging.js' +import { createUIResource } from '@mcp-ui/server' export class McpServer { private server: OfficialMcpServer @@ -291,6 +295,210 @@ export class McpServer { 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. + * + * @param definition - Configuration for the UI widget + * @param definition.name - Unique identifier for the resource + * @param definition.widget - Widget name (matches directory in dist/resources/mcp-use/widgets) + * @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., ['800px', '600px']) + * @param definition.annotations - Resource annotations for discovery + * @returns The server instance for method chaining + * + * @example + * ```typescript + * server.uiResource({ + * 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 + * }, + * theme: { + * type: 'string', + * default: 'light' + * } + * }, + * size: ['900px', '600px'] + * }) + * ``` + */ + uiResource(definition: UIResourceDefinition): this { + // Register the tool - returns UIResource with parameters + this.tool({ + name: `ui_${definition.widget}`, + description: definition.description || `Display ${definition.widget} widget`, + inputs: this.convertPropsToInputs(definition.props), + fn: async (params) => { + // Create the UIResource with user-provided params + const uiResource = this.createWidgetUIResource( + definition.widget, + params, + definition.size + ) + + return { + content: [ + { + type: 'text', + text: `Displaying ${definition.title || definition.widget} widget` + }, + uiResource // Reuse the same UIResource + ] + } + } + }) + + // Register the resource - returns widget URL for MCP clients + this.resource({ + name: definition.name, + uri: `ui://widget/${definition.widget}`, + title: definition.title, + description: definition.description, + mimeType: 'text/uri-list', + annotations: definition.annotations, + fn: async () => { + // Build the widget URL with default props + const widgetUrl = this.buildWidgetUrl( + definition.widget, + this.applyDefaultProps(definition.props) + ) + + return { + contents: [{ + uri: `ui://widget/${definition.widget}`, + mimeType: 'text/uri-list', + text: widgetUrl + }] + } + } + }) + + 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 widget - Widget name/identifier + * @param params - Parameters to pass to the widget via URL + * @param size - Optional preferred frame size [width, height] + * @returns UIResource object compatible with MCP-UI + */ + private createWidgetUIResource( + widget: string, + params: Record, + size?: [string, string] + ): any { + const iframeUrl = this.buildWidgetUrl(widget, params) + + return createUIResource({ + uri: `ui://widget/${widget}` as any, + content: { + type: 'externalUrl', + iframeUrl + }, + encoding: 'text', + uiMetadata: size ? { + 'preferred-frame-size': size + } : undefined + }) + } + + /** + * 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 + */ + private buildWidgetUrl(widget: string, params: Record): string { + const baseUrl = `http://localhost:${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 !== undefined && 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 + */ + private convertPropsToInputs(props?: WidgetProps): InputDefinition[] { + 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 + */ + private applyDefaultProps(props?: WidgetProps): Record { + if (!props) return {} + + const defaults: Record = {} + for (const [key, prop] of Object.entries(props)) { + if (prop.default !== undefined) { + defaults[key] = prop.default + } + } + return defaults + } + /** * Mount MCP server endpoints at /mcp * diff --git a/packages/mcp-use/src/server/types.ts b/packages/mcp-use/src/server/types.ts index fe35de6d..5a66bc06 100644 --- a/packages/mcp-use/src/server/types.ts +++ b/packages/mcp-use/src/server/types.ts @@ -1,86 +1,5 @@ -import type { CallToolResult, GetPromptResult, ReadResourceResult} from '@modelcontextprotocol/sdk/types.js' -export interface ServerConfig { - name: string - version: string - description?: string -} - -export interface InputDefinition { - name: string - type: 'string' | 'number' | 'boolean' | 'object' | 'array' - description?: string - required?: boolean - default?: any -} - /** - * Annotations provide hints to clients about how to use or display resources + * Legacy types export file - maintained for backward compatibility + * New code should import from './types/index.js' instead */ -export interface ResourceAnnotations { - /** Intended audience(s) for this resource */ - audience?: ('user' | 'assistant')[] - /** Priority from 0.0 (least important) to 1.0 (most important) */ - priority?: number - /** ISO 8601 formatted timestamp of last modification */ - lastModified?: string -} - -/** - * Configuration for a resource template - */ -export interface ResourceTemplateConfig { - /** URI template with {param} placeholders (e.g., "user://{userId}/profile") */ - uriTemplate: string - /** Name of the resource */ - name?: string - /** MIME type of the resource content */ - mimeType?: string - /** Description of the resource */ - description?: string -} - -export interface ResourceTemplateDefinition { - name: string - resourceTemplate: ResourceTemplateConfig - title?: string - description?: string - annotations?: ResourceAnnotations - fn: ResourceTemplateHandler -} - -export interface ResourceDefinition { - /** Unique identifier for the resource */ - name: string - /** URI pattern for accessing the resource (e.g., 'config://app-settings') */ - uri: string - /** Resource metadata including MIME type and description */ - /** Optional title for the resource */ - title?: string - /** Optional description of the resource */ - description?: string - /** MIME type of the resource content (required) */ - mimeType: string - /** Optional annotations for the resource */ - annotations?: ResourceAnnotations - /** Async function that returns the resource content */ - fn: ResourceHandler -} - -export interface ToolDefinition { - name: string - description?: string - inputs?: InputDefinition[] - fn: ToolHandler -} - -export interface PromptDefinition { - name: string - description?: string - args?: InputDefinition[] - fn: PromptHandler -} - -export type ResourceHandler = () => Promise -export type ResourceTemplateHandler = (uri: URL, params: Record) => Promise -export type ToolHandler = (params: Record) => Promise -export type PromptHandler = (params: Record) => Promise +export * from './types/index.js' diff --git a/packages/mcp-use/src/server/types/common.ts b/packages/mcp-use/src/server/types/common.ts new file mode 100644 index 00000000..a35f1131 --- /dev/null +++ b/packages/mcp-use/src/server/types/common.ts @@ -0,0 +1,29 @@ +/** + * Common type definitions shared across different MCP components + */ + +export interface ServerConfig { + name: string + version: string + description?: string +} + +export interface InputDefinition { + name: string + type: 'string' | 'number' | 'boolean' | 'object' | 'array' + description?: string + required?: boolean + default?: any +} + +/** + * Annotations provide hints to clients about how to use or display resources + */ +export interface ResourceAnnotations { + /** Intended audience(s) for this resource */ + audience?: ('user' | 'assistant')[] + /** Priority from 0.0 (least important) to 1.0 (most important) */ + priority?: number + /** ISO 8601 formatted timestamp of last modification */ + lastModified?: string +} \ No newline at end of file diff --git a/packages/mcp-use/src/server/types/index.ts b/packages/mcp-use/src/server/types/index.ts new file mode 100644 index 00000000..c8e12c43 --- /dev/null +++ b/packages/mcp-use/src/server/types/index.ts @@ -0,0 +1,37 @@ +/** + * Centralized type exports for MCP server + */ + +// Common types +export { + ServerConfig, + InputDefinition, + ResourceAnnotations +} from './common.js' + +// Resource types including UIResource +export { + ResourceHandler, + ResourceTemplateHandler, + ResourceTemplateConfig, + ResourceTemplateDefinition, + ResourceDefinition, + // UIResource specific types + WidgetProps, + UIResourceDefinition, + WidgetConfig, + WidgetManifest, + DiscoverWidgetsOptions +} from './resource.js' + +// Tool types +export { + ToolHandler, + ToolDefinition +} from './tool.js' + +// Prompt types +export { + PromptHandler, + PromptDefinition +} from './prompt.js' \ No newline at end of file diff --git a/packages/mcp-use/src/server/types/prompt.ts b/packages/mcp-use/src/server/types/prompt.ts new file mode 100644 index 00000000..cb707af4 --- /dev/null +++ b/packages/mcp-use/src/server/types/prompt.ts @@ -0,0 +1,15 @@ +import type { GetPromptResult } from '@modelcontextprotocol/sdk/types.js' +import type { InputDefinition } from './common.js' + +export type PromptHandler = (params: Record) => Promise + +export interface PromptDefinition { + /** Unique identifier for the prompt */ + name: string + /** Description of what the prompt does */ + description?: string + /** Argument definitions */ + args?: InputDefinition[] + /** Async function that generates the prompt */ + fn: PromptHandler +} \ No newline at end of file diff --git a/packages/mcp-use/src/server/types/resource.ts b/packages/mcp-use/src/server/types/resource.ts new file mode 100644 index 00000000..e1c04c91 --- /dev/null +++ b/packages/mcp-use/src/server/types/resource.ts @@ -0,0 +1,109 @@ +import type { ReadResourceResult } from '@modelcontextprotocol/sdk/types.js' +import type { ResourceAnnotations } from './common.js' + +// Handler types +export type ResourceHandler = () => Promise +export type ResourceTemplateHandler = (uri: URL, params: Record) => Promise + +/** + * Configuration for a resource template + */ +export interface ResourceTemplateConfig { + /** URI template with {param} placeholders (e.g., "user://{userId}/profile") */ + uriTemplate: string + /** Name of the resource */ + name?: string + /** MIME type of the resource content */ + mimeType?: string + /** Description of the resource */ + description?: string +} + +export interface ResourceTemplateDefinition { + name: string + resourceTemplate: ResourceTemplateConfig + title?: string + description?: string + annotations?: ResourceAnnotations + fn: ResourceTemplateHandler +} + +export interface ResourceDefinition { + /** Unique identifier for the resource */ + name: string + /** URI pattern for accessing the resource (e.g., 'config://app-settings') */ + uri: string + /** Optional title for the resource */ + title?: string + /** Optional description of the resource */ + description?: string + /** MIME type of the resource content (required) */ + mimeType: string + /** Optional annotations for the resource */ + annotations?: ResourceAnnotations + /** Async function that returns the resource content */ + fn: ResourceHandler +} + +/** + * UIResource-specific types + */ +export interface WidgetProps { + [key: string]: { + type: 'string' | 'number' | 'boolean' | 'object' | 'array' + required?: boolean + default?: any + description?: string + } +} + +export interface UIResourceDefinition { + /** Unique identifier for the resource */ + name: string + /** Widget identifier (e.g., 'kanban-board', 'chart') */ + widget: string + /** Human-readable title */ + title?: string + /** Description of what the widget does */ + description?: string + /** Widget properties/parameters configuration */ + props?: WidgetProps + /** Preferred frame size [width, height] (e.g., ['800px', '600px']) */ + size?: [string, string] + /** Resource annotations for discovery and presentation */ + annotations?: ResourceAnnotations +} + +export interface WidgetConfig { + /** Widget directory name */ + name: string + /** Absolute path to widget directory */ + path: string + /** Widget manifest if present */ + manifest?: WidgetManifest + /** Main component file name */ + component?: string +} + +export interface WidgetManifest { + name: string + title?: string + description?: string + version?: string + props?: WidgetProps + size?: [string, string] + assets?: { + main?: string + scripts?: string[] + styles?: string[] + } +} + +export interface DiscoverWidgetsOptions { + /** Path to widgets directory (defaults to dist/resources/mcp-use/widgets) */ + path?: string + /** Automatically register widgets without manifests */ + autoRegister?: boolean + /** Filter widgets by name pattern */ + filter?: string | RegExp +} \ No newline at end of file diff --git a/packages/mcp-use/src/server/types/tool.ts b/packages/mcp-use/src/server/types/tool.ts new file mode 100644 index 00000000..509e8dea --- /dev/null +++ b/packages/mcp-use/src/server/types/tool.ts @@ -0,0 +1,15 @@ +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js' +import type { InputDefinition } from './common.js' + +export type ToolHandler = (params: Record) => Promise + +export interface ToolDefinition { + /** Unique identifier for the tool */ + name: string + /** Description of what the tool does */ + description?: string + /** Input parameter definitions */ + inputs?: InputDefinition[] + /** Async function that executes the tool */ + fn: ToolHandler +} \ No newline at end of file From ea5fb1f35b63960b77a3ce4e89dd6e9f1ae9865c Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 12:25:31 +0200 Subject: [PATCH 04/19] feat: add new uiresource template --- .../src/templates/uiresource/README.md | 376 ++++++++++++++++++ .../src/templates/uiresource/index.ts | 12 + .../src/templates/uiresource/package.json | 47 +++ .../uiresource/resources/kanban-board.tsx | 306 ++++++++++++++ .../src/templates/uiresource/src/server.ts | 211 ++++++++++ .../src/templates/uiresource/tsconfig.json | 20 + 6 files changed, 972 insertions(+) create mode 100644 packages/create-mcp-use-app/src/templates/uiresource/README.md create mode 100644 packages/create-mcp-use-app/src/templates/uiresource/index.ts create mode 100644 packages/create-mcp-use-app/src/templates/uiresource/package.json create mode 100644 packages/create-mcp-use-app/src/templates/uiresource/resources/kanban-board.tsx create mode 100644 packages/create-mcp-use-app/src/templates/uiresource/src/server.ts create mode 100644 packages/create-mcp-use-app/src/templates/uiresource/tsconfig.json diff --git a/packages/create-mcp-use-app/src/templates/uiresource/README.md b/packages/create-mcp-use-app/src/templates/uiresource/README.md new file mode 100644 index 00000000..ed483a57 --- /dev/null +++ b/packages/create-mcp-use-app/src/templates/uiresource/README.md @@ -0,0 +1,376 @@ +# UIResource MCP Server + +An MCP server with the new UIResource integration for simplified widget management and MCP-UI compatibility. + +## Features + +- **🚀 UIResource Method**: Single method to register both tools and resources +- **🎨 React Widgets**: Interactive UI components built with React +- **🔄 Automatic Registration**: Tools and resources created automatically +- **📦 Props to Parameters**: Widget props automatically become tool parameters +- **🌐 MCP-UI Compatible**: Full compatibility with MCP-UI clients +- **🛠️ TypeScript Support**: Complete type safety and IntelliSense + +## What's New: UIResource + +The `uiResource` method is a powerful new addition that simplifies widget registration: + +```typescript +// Old way: Manual registration of tool and resource +server.tool({ /* tool config */ }) +server.resource({ /* resource config */ }) + +// New way: Single method does both! +server.uiResource({ + name: 'kanban-board', + widget: 'kanban-board', + title: 'Kanban Board', + props: { + initialTasks: { type: 'array', required: false }, + theme: { type: 'string', default: 'light' } + } +}) +``` + +This automatically creates: +- **Tool**: `ui_kanban-board` - Accepts parameters and returns UIResource +- **Resource**: `ui://widget/kanban-board` - Static access with defaults + +## Getting Started + +### Development + +```bash +# Install dependencies +npm install + +# Start development server with hot reloading +npm run dev +``` + +This will start: +- MCP server on port 3000 +- Widget serving at `/mcp-use/widgets/*` +- Inspector UI at `/inspector` + +### Production + +```bash +# Build the server and widgets +npm run build + +# Run the built server +npm start +``` + +## Basic Usage + +### Simple Widget Registration + +```typescript +import { createMCPServer } from 'mcp-use' + +const server = createMCPServer('my-server', { + version: '1.0.0', + description: 'Server with UIResource widgets' +}) + +// Register a widget - creates both tool and resource +server.uiResource({ + name: 'my-widget', + widget: 'my-widget', + title: 'My Widget', + description: 'An interactive widget' +}) + +server.listen(3000) +``` + +### Widget with Props + +```typescript +server.uiResource({ + name: 'data-chart', + widget: 'chart', + title: 'Data Chart', + description: 'Interactive data visualization', + props: { + data: { + type: 'array', + description: 'Data points to display', + required: true + }, + chartType: { + type: 'string', + description: 'Type of chart (line/bar/pie)', + default: 'line' + }, + theme: { + type: 'string', + description: 'Visual theme', + default: 'light' + } + }, + size: ['800px', '400px'], // Preferred iframe size + annotations: { + audience: ['user', 'assistant'], + priority: 0.8 + } +}) +``` + +## Widget Development + +### 1. Create Your Widget Component + +```typescript +// resources/my-widget.tsx +import React, { useState, useEffect } from 'react' +import { createRoot } from 'react-dom/client' + +interface MyWidgetProps { + initialData?: any + theme?: 'light' | 'dark' +} + +const MyWidget: React.FC = ({ + initialData = [], + theme = 'light' +}) => { + const [data, setData] = useState(initialData) + + // Load props from URL query parameters + useEffect(() => { + const params = new URLSearchParams(window.location.search) + + const dataParam = params.get('initialData') + if (dataParam) { + try { + setData(JSON.parse(dataParam)) + } catch (e) { + console.error('Error parsing data:', e) + } + } + + const themeParam = params.get('theme') + if (themeParam) { + // Apply theme + } + }, []) + + return ( +
+ {/* Your widget UI */} +
+ ) +} + +// Mount the widget +const container = document.getElementById('widget-root') +if (container) { + createRoot(container).render() +} +``` + +### 2. Register with UIResource + +```typescript +// src/server.ts +server.uiResource({ + name: 'my-widget', + widget: 'my-widget', + title: 'My Custom Widget', + description: 'A custom interactive widget', + props: { + initialData: { + type: 'array', + description: 'Initial data for the widget', + required: false + }, + theme: { + type: 'string', + description: 'Widget theme', + default: 'light' + } + }, + size: ['600px', '400px'] +}) +``` + +## How It Works + +### Tool Registration +When you call `uiResource`, it automatically creates a tool: +- Name: `ui_[widget-name]` +- Accepts all props as parameters +- Returns both text description and UIResource object + +### Resource Registration +Also creates a resource: +- URI: `ui://widget/[widget-name]` +- Returns UIResource with default prop values +- Discoverable by MCP clients + +### Parameter Passing +Tool parameters are automatically: +1. Converted to URL query parameters +2. Complex objects are JSON-stringified +3. Passed to widget via iframe URL + +## Advanced Examples + +### Multiple Widgets + +```typescript +const widgets = [ + { + name: 'todo-list', + widget: 'todo-list', + title: 'Todo List', + props: { + items: { type: 'array', default: [] } + } + }, + { + name: 'calendar', + widget: 'calendar', + title: 'Calendar', + props: { + date: { type: 'string', required: false } + } + } +] + +// Register all widgets +widgets.forEach(widget => server.uiResource(widget)) +``` + +### Mixed Registration + +```typescript +// UIResource for widgets +server.uiResource({ + name: 'dashboard', + widget: 'dashboard', + title: 'Analytics Dashboard' +}) + +// Traditional tool for actions +server.tool({ + name: 'calculate', + description: 'Perform calculations', + fn: async (params) => { /* ... */ } +}) + +// Traditional resource for data +server.resource({ + name: 'config', + uri: 'config://app', + mimeType: 'application/json', + fn: async () => { /* ... */ } +}) +``` + +## API Reference + +### `server.uiResource(definition)` + +#### Parameters + +- `definition: UIResourceDefinition` + - `name: string` - Resource identifier + - `widget: string` - Widget directory name + - `title?: string` - Human-readable title + - `description?: string` - Widget description + - `props?: WidgetProps` - Widget properties configuration + - `size?: [string, string]` - Preferred iframe size + - `annotations?: ResourceAnnotations` - Discovery hints + +#### WidgetProps + +Each prop can have: +- `type: 'string' | 'number' | 'boolean' | 'object' | 'array'` +- `required?: boolean` - Whether the prop is required +- `default?: any` - Default value if not provided +- `description?: string` - Prop description + +## Testing Your Widgets + +### Via Inspector UI +1. Start the server: `npm run dev` +2. Open: `http://localhost:3000/inspector` +3. Test tools and resources + +### Direct Browser Access +Visit: `http://localhost:3000/mcp-use/widgets/[widget-name]` + +### Via MCP Client +```typescript +// Call as tool +const result = await client.callTool('ui_kanban-board', { + initialTasks: [...], + theme: 'dark' +}) + +// Access as resource +const resource = await client.readResource('ui://widget/kanban-board') +``` + +## Benefits of UIResource + +✅ **Simplified API** - One method instead of two +✅ **Automatic Wiring** - Props become tool inputs automatically +✅ **Type Safety** - Full TypeScript support +✅ **MCP-UI Compatible** - Works with all MCP-UI clients +✅ **DRY Principle** - No duplicate UIResource creation +✅ **Discoverable** - Both tools and resources are listed + +## Troubleshooting + +### Widget Not Loading +- Ensure widget exists in `dist/resources/mcp-use/widgets/` +- Check server console for errors +- Verify widget is registered with `uiResource()` + +### Props Not Passed +- Check URL parameters in browser DevTools +- Ensure prop names match exactly +- Complex objects must be JSON-stringified + +### Type Errors +- Import types: `import type { UIResourceDefinition } from 'mcp-use'` +- Ensure mcp-use is updated to latest version + +## Migration from Old Pattern + +If you have existing code using separate tool/resource: + +```typescript +// Old pattern +server.tool({ name: 'show-widget', /* ... */ }) +server.resource({ uri: 'ui://widget', /* ... */ }) + +// New pattern - replace both with: +server.uiResource({ + name: 'widget', + widget: 'widget', + // ... consolidated configuration +}) +``` + +## Future Enhancements + +Coming soon: +- Automatic widget discovery from filesystem +- Widget manifests (widget.json) +- Prop extraction from TypeScript interfaces +- Build-time optimization + +## Learn More + +- [MCP Documentation](https://modelcontextprotocol.io) +- [MCP-UI Documentation](https://github.com/idosal/mcp-ui) +- [mcp-use Documentation](https://github.com/pyroprompt/mcp-use) +- [React Documentation](https://react.dev/) + +Happy widget building! 🚀 \ No newline at end of file diff --git a/packages/create-mcp-use-app/src/templates/uiresource/index.ts b/packages/create-mcp-use-app/src/templates/uiresource/index.ts new file mode 100644 index 00000000..6f48dc04 --- /dev/null +++ b/packages/create-mcp-use-app/src/templates/uiresource/index.ts @@ -0,0 +1,12 @@ +/** + * MCP Server Entry Point + * + * This file serves as the main entry point for the MCP server application. + * It re-exports all functionality from the server implementation, allowing + * the CLI and other tools to locate and start the server. + * + * The server is automatically started when this module is imported, making + * it suitable for both direct execution and programmatic usage. + */ +export * from './src/server.js' + diff --git a/packages/create-mcp-use-app/src/templates/uiresource/package.json b/packages/create-mcp-use-app/src/templates/uiresource/package.json new file mode 100644 index 00000000..3f70f2b4 --- /dev/null +++ b/packages/create-mcp-use-app/src/templates/uiresource/package.json @@ -0,0 +1,47 @@ +{ + "name": "mcp-uiresource-server", + "type": "module", + "version": "1.0.0", + "description": "MCP server with UIResource widget integration", + "author": "", + "license": "MIT", + "keywords": [ + "mcp", + "server", + "uiresource", + "ui", + "react", + "widgets", + "ai", + "tools", + "mcp-ui" + ], + "main": "dist/index.js", + "scripts": { + "build": "mcp-use build", + "dev": "mcp-use dev", + "start": "mcp-use start" + }, + "dependencies": { + "@mcp-ui/server": "^5.11.0", + "cors": "^2.8.5", + "express": "^4.18.0", + "mcp-use": "workspace:*" + }, + "devDependencies": { + "@mcp-use/cli": "workspace:*", + "@mcp-use/inspector": "workspace:*", + "@types/cors": "^2.8.0", + "@types/express": "^4.17.0", + "@types/node": "^20.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "concurrently": "^8.0.0", + "esbuild": "^0.23.0", + "globby": "^14.0.2", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "tsx": "^4.0.0", + "typescript": "^5.0.0" + } +} \ No newline at end of file diff --git a/packages/create-mcp-use-app/src/templates/uiresource/resources/kanban-board.tsx b/packages/create-mcp-use-app/src/templates/uiresource/resources/kanban-board.tsx new file mode 100644 index 00000000..5a4454cb --- /dev/null +++ b/packages/create-mcp-use-app/src/templates/uiresource/resources/kanban-board.tsx @@ -0,0 +1,306 @@ +import React, { useEffect, useState } from 'react' +import { createRoot } from 'react-dom/client' + +interface Task { + id: string + title: string + description: string + status: 'todo' | 'in-progress' | 'done' + priority: 'low' | 'medium' | 'high' + assignee?: string +} + +interface KanbanBoardProps { + initialTasks?: Task[] +} + +const KanbanBoard: React.FC = ({ initialTasks = [] }) => { + const [tasks, setTasks] = useState(initialTasks) + const [newTask, setNewTask] = useState({ title: '', description: '', priority: 'medium' as Task['priority'] }) + + // Load tasks from URL parameters or use defaults + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search) + const tasksParam = urlParams.get('tasks') + + if (tasksParam) { + try { + const parsedTasks = JSON.parse(decodeURIComponent(tasksParam)) + setTasks(parsedTasks) + } + catch (error) { + console.error('Error parsing tasks from URL:', error) + } + } + else { + // Default tasks for demo + setTasks([ + { id: '1', title: 'Design UI mockups', description: 'Create wireframes for the new dashboard', status: 'todo', priority: 'high', assignee: 'Alice' }, + { id: '2', title: 'Implement authentication', description: 'Add login and registration functionality', status: 'in-progress', priority: 'high', assignee: 'Bob' }, + { id: '3', title: 'Write documentation', description: 'Document the API endpoints', status: 'done', priority: 'medium', assignee: 'Charlie' }, + { id: '4', title: 'Setup CI/CD', description: 'Configure automated testing and deployment', status: 'todo', priority: 'medium' }, + { id: '5', title: 'Code review', description: 'Review pull requests from the team', status: 'in-progress', priority: 'low', assignee: 'David' }, + ]) + } + }, []) + + const addTask = () => { + if (newTask.title.trim()) { + const task: Task = { + id: Date.now().toString(), + title: newTask.title, + description: newTask.description, + status: 'todo', + priority: newTask.priority, + } + setTasks([...tasks, task]) + setNewTask({ title: '', description: '', priority: 'medium' }) + } + } + + const moveTask = (taskId: string, newStatus: Task['status']) => { + setTasks(tasks.map(task => + task.id === taskId ? { ...task, status: newStatus } : task, + )) + } + + const deleteTask = (taskId: string) => { + setTasks(tasks.filter(task => task.id !== taskId)) + } + + const getTasksByStatus = (status: Task['status']) => { + return tasks.filter(task => task.status === status) + } + + const getPriorityColor = (priority: Task['priority']) => { + switch (priority) { + case 'high': return '#ff4757' + case 'medium': return '#ffa502' + case 'low': return '#2ed573' + default: return '#57606f' + } + } + + const columns = [ + { id: 'todo', title: 'To Do', color: '#57606f' }, + { id: 'in-progress', title: 'In Progress', color: '#ffa502' }, + { id: 'done', title: 'Done', color: '#2ed573' }, + ] as const + + return ( +
+
+

Kanban Board

+ + {/* Add new task form */} +
+

Add New Task

+
+ setNewTask({ ...newTask, title: e.target.value })} + style={{ + padding: '8px 12px', + border: '1px solid #ddd', + borderRadius: '4px', + flex: '1', + minWidth: '200px', + }} + /> + setNewTask({ ...newTask, description: e.target.value })} + style={{ + padding: '8px 12px', + border: '1px solid #ddd', + borderRadius: '4px', + flex: '1', + minWidth: '200px', + }} + /> + + +
+
+
+ + {/* Kanban columns */} +
+ {columns.map(column => ( +
+
+ {column.title} + + {getTasksByStatus(column.id).length} + +
+ +
+ {getTasksByStatus(column.id).map(task => ( +
{ + e.dataTransfer.setData('text/plain', task.id) + }} + onDragOver={e => e.preventDefault()} + onDrop={(e) => { + e.preventDefault() + const taskId = e.dataTransfer.getData('text/plain') + if (taskId === task.id) { + // Move to next column + const currentIndex = columns.findIndex(col => col.id === column.id) + const nextColumn = columns[currentIndex + 1] + if (nextColumn) { + moveTask(taskId, nextColumn.id) + } + } + }} + > +
+

{task.title}

+ +
+ + {task.description && ( +

+ {task.description} +

+ )} + +
+
+ {task.priority.toUpperCase()} +
+ + {task.assignee && ( + + {task.assignee} + + )} +
+
+ ))} + + {getTasksByStatus(column.id).length === 0 && ( +
+ No tasks in this column +
+ )} +
+
+ ))} +
+
+ ) +} + +// Mount the component +const container = document.getElementById('widget-root') +if (container) { + const root = createRoot(container) + root.render() +} diff --git a/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts b/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts new file mode 100644 index 00000000..86ec6f05 --- /dev/null +++ b/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts @@ -0,0 +1,211 @@ +import { createMCPServer, type UIResourceDefinition } from 'mcp-use' + +// Create an MCP server with UIResource support +const server = createMCPServer('uiresource-mcp-server', { + version: '1.0.0', + description: 'MCP server with UIResource widget integration', +}) + +const PORT = process.env.PORT || 3000 + +/** + * Main Kanban Board Widget + * + * This demonstrates the new uiResource method which automatically: + * 1. Creates a tool (ui_kanban-board) that accepts parameters + * 2. Creates a resource (ui://widget/kanban-board) for static access + * 3. Handles parameter passing via URL query strings + */ +server.uiResource({ + name: 'kanban-board', + widget: 'kanban-board', + title: 'Kanban Board', + description: 'Interactive task management board with drag-and-drop support', + props: { + initialTasks: { + type: 'array', + description: 'Initial tasks to display on the board', + required: false, + }, + theme: { + type: 'string', + description: 'Visual theme for the board (light/dark)', + required: false, + default: 'light' + }, + columns: { + type: 'array', + description: 'Column configuration for the board', + required: false, + } + }, + size: ['900px', '600px'], + annotations: { + audience: ['user', 'assistant'], + priority: 0.8 + } +}) + +// Example: Additional widget registrations +// Uncomment to add more widgets to your server + +/* +// Data visualization widget +server.uiResource({ + name: 'chart-widget', + widget: 'chart', + title: 'Data Chart', + description: 'Interactive data visualization', + props: { + data: { + type: 'array', + description: 'Chart data points', + required: true + }, + chartType: { + type: 'string', + description: 'Type of chart (line/bar/pie)', + default: 'line' + } + }, + size: ['800px', '400px'] +}) + +// Todo list widget +server.uiResource({ + name: 'todo-list', + widget: 'todo-list', + title: 'Todo List', + description: 'Simple todo list manager', + props: { + items: { + type: 'array', + description: 'Initial todo items', + default: [] + } + } +}) +*/ + +// Example: Programmatic widget registration +// You can also register widgets from an array programmatically +const additionalWidgets: UIResourceDefinition[] = [ + // Add your widget definitions here +] + +// Register all additional widgets +additionalWidgets.forEach(widget => server.uiResource(widget)) + +/** + * Traditional MCP Tool + * + * You can still add regular tools alongside UIResources. + * This example shows how to mix both approaches. + */ +server.tool({ + name: 'get-widget-info', + description: 'Get information about available UI widgets', + fn: async () => { + const widgets = [ + { + name: 'kanban-board', + tool: 'ui_kanban-board', + resource: 'ui://widget/kanban-board', + url: `http://localhost:${PORT}/mcp-use/widgets/kanban-board` + } + ] + + return { + content: [{ + type: 'text', + text: `Available UI Widgets:\n\n${widgets.map(w => + `📦 ${w.name}\n` + + ` Tool: ${w.tool}\n` + + ` Resource: ${w.resource}\n` + + ` Browser: ${w.url}` + ).join('\n\n')}\n\n` + + `Each widget can be:\n` + + `1. Called as a tool with parameters\n` + + `2. Accessed as a resource for static version\n` + + `3. Viewed directly in browser` + }] + } + } +}) + +/** + * Traditional MCP Resource + * + * Example of a non-UI resource for configuration data. + * Shows how UIResources work alongside regular resources. + */ +server.resource({ + name: 'server-config', + uri: 'config://server', + title: 'Server Configuration', + description: 'Current server configuration and status', + mimeType: 'application/json', + fn: async () => ({ + contents: [{ + uri: 'config://server', + mimeType: 'application/json', + text: JSON.stringify({ + port: PORT, + version: '1.0.0', + widgets: { + registered: ['kanban-board'], + baseUrl: `http://localhost:${PORT}/mcp-use/widgets/` + }, + endpoints: { + mcp: `http://localhost:${PORT}/mcp`, + inspector: `http://localhost:${PORT}/inspector`, + widgets: `http://localhost:${PORT}/mcp-use/widgets/` + } + }, null, 2) + }] + }) +}) + +// Start the server +server.listen(PORT) + +// Display helpful startup message +console.log(` +╔═══════════════════════════════════════════════════════════════╗ +║ 🚀 UIResource MCP Server ║ +╚═══════════════════════════════════════════════════════════════╝ + +Server is running on port ${PORT} + +📍 Endpoints: + MCP Protocol: http://localhost:${PORT}/mcp + Inspector UI: http://localhost:${PORT}/inspector + Widgets Base: http://localhost:${PORT}/mcp-use/widgets/ + +🎯 Available UIResources: + • kanban-board + Tool: ui_kanban-board (accepts props as parameters) + Resource: ui://widget/kanban-board (static with defaults) + Browser: http://localhost:${PORT}/mcp-use/widgets/kanban-board + +📝 Usage Examples: + + // Call as tool with parameters + await client.callTool('ui_kanban-board', { + initialTasks: [...], + theme: 'dark' + }) + + // Access as resource + await client.readResource('ui://widget/kanban-board') + +💡 Tip: Open the Inspector UI to test your widgets interactively! +`) + +// Handle graceful shutdown +process.on('SIGINT', () => { + console.log('\n\nShutting down server...') + process.exit(0) +}) + +export default server \ No newline at end of file diff --git a/packages/create-mcp-use-app/src/templates/uiresource/tsconfig.json b/packages/create-mcp-use-app/src/templates/uiresource/tsconfig.json new file mode 100644 index 00000000..5a2c6947 --- /dev/null +++ b/packages/create-mcp-use-app/src/templates/uiresource/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "node", + "allowJs": true, + "strict": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["index.ts", "src/**/*", "resources/**/*"], + "exclude": ["node_modules", "dist"] +} From c6a5894b0a264a0f57ee73fa8041487235ef5791 Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 12:25:47 +0200 Subject: [PATCH 05/19] feat: dynamically list available templates in create-mcp-use-app - Replace hardcoded template list with dynamic directory scanning - Add helpful tip about uiresource template for UI resources - Improve error message when template not found --- packages/create-mcp-use-app/src/index.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/create-mcp-use-app/src/index.ts b/packages/create-mcp-use-app/src/index.ts index 3f9f58d8..3ade1897 100644 --- a/packages/create-mcp-use-app/src/index.ts +++ b/packages/create-mcp-use-app/src/index.ts @@ -208,8 +208,22 @@ async function copyTemplate(projectPath: string, template: string, versions: Rec if (!existsSync(templatePath)) { console.error(`❌ Template "${template}" not found!`) - console.log('Available templates: basic, filesystem, api, ui') + + // Dynamically list available templates + const templatesDir = join(__dirname, 'templates') + if (existsSync(templatesDir)) { + const availableTemplates = readdirSync(templatesDir, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name) + .sort() + + console.log(`Available templates: ${availableTemplates.join(', ')}`) + } else { + console.log('No templates directory found') + } + console.log('💡 Tip: Use "ui" template for React components and modern UI features') + console.log('💡 Tip: Use "uiresource" template for UI resources and advanced server examples') process.exit(1) } From 5f8456e0965bb384a75963c6ea809034efef502b Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 15:16:02 +0200 Subject: [PATCH 06/19] feat: create mcp-ui-adapter + tests --- .../src/server/adapters/mcp-ui-adapter.ts | 306 ++++++++++++ packages/mcp-use/src/server/index.ts | 8 + packages/mcp-use/src/server/mcp-server.ts | 39 +- packages/mcp-use/src/server/types/index.ts | 4 + packages/mcp-use/src/server/types/resource.ts | 40 ++ packages/mcp-use/tests/mcp-ui-adapter.test.ts | 456 ++++++++++++++++++ 6 files changed, 830 insertions(+), 23 deletions(-) create mode 100644 packages/mcp-use/src/server/adapters/mcp-ui-adapter.ts create mode 100644 packages/mcp-use/tests/mcp-ui-adapter.test.ts diff --git a/packages/mcp-use/src/server/adapters/mcp-ui-adapter.ts b/packages/mcp-use/src/server/adapters/mcp-ui-adapter.ts new file mode 100644 index 00000000..749b642b --- /dev/null +++ b/packages/mcp-use/src/server/adapters/mcp-ui-adapter.ts @@ -0,0 +1,306 @@ +/** + * MCP-UI Adapter + * + * Provides an adapter between mcp-use high-level UIResource definitions + * and the low-level @mcp-ui/server resource format. + * Ref: https://mcpui.dev/guide/server/typescript/usage-examples + */ + +import { createUIResource } from '@mcp-ui/server' +import type { UIResourceContent, UIResourceDefinition } from '../types/resource.js' + +/** + * Content type options for UI resources + */ +export type UIContentType = + | 'externalUrl' // Default: iframe URL for serving widgets + | 'rawHtml' // Direct HTML content + | 'remoteDom' // Remote DOM scripting + +/** + * Encoding options for UI resources + */ +export type UIEncoding = 'text' | 'blob' + +/** + * Framework options for Remote DOM resources + */ +export type RemoteDomFramework = 'react' | 'webcomponents' + +/** + * Extended UI resource definition with content type support + */ +export interface ExtendedUIResourceDefinition extends UIResourceDefinition { + contentType?: UIContentType + encoding?: UIEncoding + htmlContent?: string + remoteDomScript?: string + remoteDomFramework?: RemoteDomFramework +} + +/** + * Configuration for the adapter + */ +export interface AdapterConfig { + baseUrl: string + port: number | string +} + +/** + * MCP-UI Adapter class + */ +export class McpUiAdapter { + private config: AdapterConfig + + constructor(config: AdapterConfig) { + this.config = config + } + + /** + * Build the full URL for a widget + */ + private buildWidgetUrl(widget: string, props?: Record): string { + const url = new URL( + `/mcp-use/widgets/${widget}`, + `http://localhost:${this.config.port}` + ) + + if (props) { + Object.entries(props).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + const stringValue = typeof value === 'object' + ? JSON.stringify(value) + : String(value) + url.searchParams.set(key, stringValue) + } + }) + } + + return url.toString() + } + + /** + * Create a UIResource for an external URL (default for widgets) + * @param uri - URI of the resource + * @param iframeUrl - URL of the iframe + * @param encoding - Encoding of the resource (text or blob (URL is Base64 encoded)) + * @returns UIResourceContent + */ + createExternalUrlResource( + uri: string, + iframeUrl: string, + encoding: UIEncoding = 'text' + ): UIResourceContent { + return createUIResource({ + uri: uri as `ui://${string}`, + content: { type: 'externalUrl', iframeUrl }, + encoding + }) + } + + /** + * Create a UIResource for raw HTML content + * + * @param uri - URI of the resource + * @param htmlString - HTML string to embed + * @param encoding - Encoding of the resource + * @returns UIResourceContent + */ + createRawHtmlResource( + uri: string, + htmlString: string, + encoding: UIEncoding = 'text' + ): UIResourceContent { + return createUIResource({ + uri: uri as `ui://${string}`, + content: { type: 'rawHtml', htmlString }, + encoding + }) + } + + /** + * Create a UIResource for Remote DOM scripting + */ + createRemoteDomResource( + uri: string, + script: string, + framework: RemoteDomFramework = 'react', + encoding: UIEncoding = 'text' + ): UIResourceContent { + return createUIResource({ + uri: uri as `ui://${string}`, + content: { type: 'remoteDom', script, framework }, + encoding + }) + } + + /** + * Create a UIResource from our high-level definition + */ + createWidgetUIResource( + definition: ExtendedUIResourceDefinition, + props?: Record + ): UIResourceContent { + const uri = `ui://widget/${definition.name}` + const contentType = definition.contentType || 'externalUrl' + const encoding = definition.encoding || 'text' + + switch (contentType) { + case 'externalUrl': { + const widgetUrl = this.buildWidgetUrl(definition.widget, props) + return this.createExternalUrlResource(uri, widgetUrl, encoding) + } + + case 'rawHtml': { + if (!definition.htmlContent) { + throw new Error(`HTML content required for rawHtml type in widget ${definition.name}`) + } + return this.createRawHtmlResource(uri, definition.htmlContent, encoding) + } + + case 'remoteDom': { + if (!definition.remoteDomScript) { + throw new Error(`Remote DOM script required for remoteDom type in widget ${definition.name}`) + } + const framework = definition.remoteDomFramework || 'react' + return this.createRemoteDomResource( + uri, + definition.remoteDomScript, + framework, + encoding + ) + } + + default: + throw new Error(`Unknown content type: ${contentType}`) + } + } + + /** + * Generate HTML content for a widget (for rawHtml type) + */ + generateWidgetHtml( + definition: UIResourceDefinition, + props?: Record + ): string { + const [width = '100%', height = '400px'] = definition.size || [] + const propsJson = props ? JSON.stringify(props) : '{}' + + return ` + + + + + ${definition.title || definition.name} + + + +
+
${definition.title || definition.name}
+ ${definition.description ? `
${definition.description}
` : ''} +
+
+ + +` + } + + /** + * Generate a Remote DOM script for a widget + */ + generateRemoteDomScript( + definition: UIResourceDefinition, + props?: Record + ): string { + return ` +// Remote DOM script for ${definition.name} +const container = document.createElement('div'); +container.style.padding = '20px'; + +// Create title +const title = document.createElement('h2'); +title.textContent = '${definition.title || definition.name}'; +container.appendChild(title); + +${definition.description ? ` +// Add description +const description = document.createElement('p'); +description.textContent = '${definition.description}'; +description.style.color = '#666'; +container.appendChild(description); +` : ''} + +// Widget props +const props = ${JSON.stringify(props || {})}; + +// Create interactive button +const button = document.createElement('ui-button'); +button.setAttribute('label', 'Interact with ${definition.name}'); +button.addEventListener('press', () => { + window.parent.postMessage({ + type: 'tool', + payload: { + toolName: 'ui_${definition.name}', + params: props + } + }, '*'); +}); +container.appendChild(button); + +// Add custom widget logic here +console.log('Remote DOM widget ${definition.name} initialized with props:', props); + +// Append to root +root.appendChild(container);` + } +} + +/** + * Factory function to create an adapter instance + */ +export function createMcpUiAdapter(config: AdapterConfig): McpUiAdapter { + return new McpUiAdapter(config) +} \ No newline at end of file diff --git a/packages/mcp-use/src/server/index.ts b/packages/mcp-use/src/server/index.ts index e07e6939..ca351147 100644 --- a/packages/mcp-use/src/server/index.ts +++ b/packages/mcp-use/src/server/index.ts @@ -4,3 +4,11 @@ export { } from './mcp-server.js' export * from './types/index.js' + +// MCP-UI adapter exports +export { + McpUiAdapter, + createMcpUiAdapter, + type ExtendedUIResourceDefinition, + type AdapterConfig +} from './adapters/mcp-ui-adapter.js' diff --git a/packages/mcp-use/src/server/mcp-server.ts b/packages/mcp-use/src/server/mcp-server.ts index e4970d79..32e963ef 100644 --- a/packages/mcp-use/src/server/mcp-server.ts +++ b/packages/mcp-use/src/server/mcp-server.ts @@ -14,7 +14,8 @@ import express, { type Express } from 'express' import { existsSync, readdirSync } from 'node:fs' import { join } from 'node:path' import { requestLogger } from './logging.js' -import { createUIResource } from '@mcp-ui/server' +import type { createMcpUiAdapter } from './adapters/mcp-ui-adapter.js' +import type { McpUiAdapter } from './adapters/mcp-ui-adapter.js' export class McpServer { private server: OfficialMcpServer @@ -23,6 +24,7 @@ export class McpServer { private mcpMounted = false private inspectorMounted = false private serverPort?: number + private uiAdapter?: McpUiAdapter /** * Creates a new MCP server instance with Express integration @@ -342,11 +344,7 @@ export class McpServer { inputs: this.convertPropsToInputs(definition.props), fn: async (params) => { // Create the UIResource with user-provided params - const uiResource = this.createWidgetUIResource( - definition.widget, - params, - definition.size - ) + const uiResource = this.createWidgetUIResource(definition, params) return { content: [ @@ -396,29 +394,24 @@ export class McpServer { * compatible clients. * * @private - * @param widget - Widget name/identifier + * @param definition - UIResource definition * @param params - Parameters to pass to the widget via URL - * @param size - Optional preferred frame size [width, height] * @returns UIResource object compatible with MCP-UI */ private createWidgetUIResource( - widget: string, - params: Record, - size?: [string, string] + definition: UIResourceDefinition, + params: Record ): any { - const iframeUrl = this.buildWidgetUrl(widget, params) + // Initialize adapter if not already created + if (!this.uiAdapter) { + this.uiAdapter = createMcpUiAdapter({ + baseUrl: `http://localhost`, + port: this.serverPort || 3001 + }) + } - return createUIResource({ - uri: `ui://widget/${widget}` as any, - content: { - type: 'externalUrl', - iframeUrl - }, - encoding: 'text', - uiMetadata: size ? { - 'preferred-frame-size': size - } : undefined - }) + // Use the adapter to create the UIResource + return this.uiAdapter.createWidgetUIResource(definition, params) } /** diff --git a/packages/mcp-use/src/server/types/index.ts b/packages/mcp-use/src/server/types/index.ts index c8e12c43..6ff5e7b3 100644 --- a/packages/mcp-use/src/server/types/index.ts +++ b/packages/mcp-use/src/server/types/index.ts @@ -17,7 +17,11 @@ export { ResourceTemplateDefinition, ResourceDefinition, // UIResource specific types + UIResourceContent, WidgetProps, + UIContentType, + UIEncoding, + RemoteDomFramework, UIResourceDefinition, WidgetConfig, WidgetManifest, diff --git a/packages/mcp-use/src/server/types/resource.ts b/packages/mcp-use/src/server/types/resource.ts index e1c04c91..bd3bfedd 100644 --- a/packages/mcp-use/src/server/types/resource.ts +++ b/packages/mcp-use/src/server/types/resource.ts @@ -1,6 +1,18 @@ import type { ReadResourceResult } from '@modelcontextprotocol/sdk/types.js' import type { ResourceAnnotations } from './common.js' +// UIResourceContent type from MCP-UI +export type UIResourceContent = { + type: 'resource' + resource: { + uri: string + mimeType: string + } & ( + | { text: string; blob?: never } + | { blob: string; text?: never } + ) +} + // Handler types export type ResourceHandler = () => Promise export type ResourceTemplateHandler = (uri: URL, params: Record) => Promise @@ -57,6 +69,24 @@ export interface WidgetProps { } } +/** + * UIResource content types + */ +export type UIContentType = + | 'externalUrl' // Default: iframe URL for serving widgets + | 'rawHtml' // Direct HTML content + | 'remoteDom' // Remote DOM scripting + +/** + * Encoding options for UI resources + */ +export type UIEncoding = 'text' | 'blob' + +/** + * Framework options for Remote DOM resources + */ +export type RemoteDomFramework = 'react' | 'webcomponents' + export interface UIResourceDefinition { /** Unique identifier for the resource */ name: string @@ -72,6 +102,16 @@ export interface UIResourceDefinition { size?: [string, string] /** Resource annotations for discovery and presentation */ annotations?: ResourceAnnotations + /** Content type for the UIResource (defaults to 'externalUrl') */ + contentType?: UIContentType + /** Encoding for the resource content (defaults to 'text') */ + encoding?: UIEncoding + /** HTML content for rawHtml content type */ + htmlContent?: string + /** Script for remoteDom content type */ + remoteDomScript?: string + /** Framework for remoteDom content type */ + remoteDomFramework?: RemoteDomFramework } export interface WidgetConfig { diff --git a/packages/mcp-use/tests/mcp-ui-adapter.test.ts b/packages/mcp-use/tests/mcp-ui-adapter.test.ts new file mode 100644 index 00000000..044482a3 --- /dev/null +++ b/packages/mcp-use/tests/mcp-ui-adapter.test.ts @@ -0,0 +1,456 @@ +/** + * Tests for MCP-UI Adapter + * + * These tests verify that the adapter correctly generates UIResource objects + * matching the @mcp-ui/server format for all content types. + */ + +import { describe, it, expect, beforeEach } from 'vitest' +import { type McpUiAdapter, createMcpUiAdapter } from '../src/server/adapters/mcp-ui-adapter.js' +import type { ExtendedUIResourceDefinition } from '../src/server/adapters/mcp-ui-adapter.js' + +describe('MCP-UI Adapter', () => { + let adapter: McpUiAdapter + + beforeEach(() => { + adapter = createMcpUiAdapter({ + baseUrl: 'http://localhost', + port: 3000 + }) + }) + + describe('External URL Resources', () => { + it('should create external URL resource with text encoding', () => { + const definition: ExtendedUIResourceDefinition = { + name: 'kanban-board', + widget: 'kanban-board', + title: 'Kanban Board', + contentType: 'externalUrl', + encoding: 'text' + } + + const resource = adapter.createWidgetUIResource(definition, { + theme: 'dark', + initialTasks: ['task1', 'task2'] + }) + + expect(resource).toEqual({ + type: 'resource', + resource: { + uri: 'ui://widget/kanban-board', + mimeType: 'text/uri-list', + text: 'http://localhost:3000/mcp-use/widgets/kanban-board?theme=dark&initialTasks=%5B%22task1%22%2C%22task2%22%5D' + } + }) + }) + + it('should create external URL resource with blob encoding', () => { + const definition: ExtendedUIResourceDefinition = { + name: 'chart-widget', + widget: 'chart', + contentType: 'externalUrl', + encoding: 'blob' + } + + const resource = adapter.createWidgetUIResource(definition, { + data: [1, 2, 3] + }) + + expect(resource).toEqual({ + type: 'resource', + resource: { + uri: 'ui://widget/chart-widget', + mimeType: 'text/uri-list', + // Base64 encoded URL + blob: expect.stringMatching(/^[A-Za-z0-9+/=]+$/) + } + }) + + // Decode and verify the blob content + const decodedUrl = Buffer.from(resource.resource.blob!, 'base64').toString() + expect(decodedUrl).toBe('http://localhost:3000/mcp-use/widgets/chart?data=%5B1%2C2%2C3%5D') + }) + + it('should handle complex object parameters', () => { + const definition: ExtendedUIResourceDefinition = { + name: 'dashboard', + widget: 'dashboard', + contentType: 'externalUrl' + } + + const resource = adapter.createWidgetUIResource(definition, { + config: { + layout: 'grid', + columns: 3, + widgets: ['chart', 'table', 'metrics'] + } + }) + + expect(resource.resource.text).toContain('config=%7B%22layout%22%3A%22grid%22') + }) + + it('should default to externalUrl with text encoding', () => { + const definition: ExtendedUIResourceDefinition = { + name: 'todo-list', + widget: 'todo-list' + // No contentType or encoding specified + } + + const resource = adapter.createWidgetUIResource(definition, {}) + + expect(resource).toEqual({ + type: 'resource', + resource: { + uri: 'ui://widget/todo-list', + mimeType: 'text/uri-list', + text: 'http://localhost:3000/mcp-use/widgets/todo-list' + } + }) + }) + }) + + describe('Raw HTML Resources', () => { + it('should create raw HTML resource with text encoding', () => { + const htmlContent = '

Hello World

' + const definition: ExtendedUIResourceDefinition = { + name: 'static-widget', + widget: 'static', + contentType: 'rawHtml', + encoding: 'text', + htmlContent + } + + const resource = adapter.createWidgetUIResource(definition, {}) + + expect(resource).toEqual({ + type: 'resource', + resource: { + uri: 'ui://widget/static-widget', + mimeType: 'text/html', + text: htmlContent + } + }) + }) + + it('should create raw HTML resource with blob encoding', () => { + const htmlContent = '

Complex HTML

' + const definition: ExtendedUIResourceDefinition = { + name: 'complex-widget', + widget: 'complex', + contentType: 'rawHtml', + encoding: 'blob', + htmlContent + } + + const resource = adapter.createWidgetUIResource(definition, {}) + + expect(resource).toEqual({ + type: 'resource', + resource: { + uri: 'ui://widget/complex-widget', + mimeType: 'text/html', + blob: Buffer.from(htmlContent).toString('base64') + } + }) + }) + + it('should throw error if HTML content is missing', () => { + const definition: ExtendedUIResourceDefinition = { + name: 'broken-widget', + widget: 'broken', + contentType: 'rawHtml' + // Missing htmlContent + } + + expect(() => adapter.createWidgetUIResource(definition, {})).toThrow( + 'HTML content required for rawHtml type in widget broken-widget' + ) + }) + + it('should generate HTML content with widget metadata', () => { + const definition: ExtendedUIResourceDefinition = { + name: 'generated-widget', + widget: 'generated', + title: 'Generated Widget', + description: 'A dynamically generated widget', + size: ['800px', '600px'] + } + + const html = adapter.generateWidgetHtml(definition, { + value: 42, + items: ['a', 'b', 'c'] + }) + + expect(html).toContain('Generated Widget') + expect(html).toContain('A dynamically generated widget') + expect(html).toContain('width: 800px') + expect(html).toContain('height: 600px') + expect(html).toContain('"value":42') + expect(html).toContain('"items":["a","b","c"]') + }) + }) + + describe('Remote DOM Resources', () => { + it('should create remote DOM resource with React framework', () => { + const script = ` + const button = document.createElement('ui-button'); + button.setAttribute('label', 'Click me'); + root.appendChild(button); + ` + const definition: ExtendedUIResourceDefinition = { + name: 'remote-button', + widget: 'button', + contentType: 'remoteDom', + encoding: 'text', + remoteDomScript: script, + remoteDomFramework: 'react' + } + + const resource = adapter.createWidgetUIResource(definition, {}) + + expect(resource).toEqual({ + type: 'resource', + resource: { + uri: 'ui://widget/remote-button', + mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; framework=react', + text: script + } + }) + }) + + it('should create remote DOM resource with webcomponents framework', () => { + const script = ` + class MyComponent extends HTMLElement { + connectedCallback() { + this.innerHTML = '

Web Component

'; + } + } + customElements.define('my-component', MyComponent); + ` + const definition: ExtendedUIResourceDefinition = { + name: 'web-component', + widget: 'component', + contentType: 'remoteDom', + remoteDomScript: script, + remoteDomFramework: 'webcomponents' + } + + const resource = adapter.createWidgetUIResource(definition, {}) + + expect(resource).toEqual({ + type: 'resource', + resource: { + uri: 'ui://widget/web-component', + mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; framework=webcomponents', + text: script + } + }) + }) + + it('should create remote DOM resource with blob encoding', () => { + const script = 'root.appendChild(document.createElement("div"));' + const definition: ExtendedUIResourceDefinition = { + name: 'blob-dom', + widget: 'blob-dom', + contentType: 'remoteDom', + encoding: 'blob', + remoteDomScript: script + } + + const resource = adapter.createWidgetUIResource(definition, {}) + + expect(resource).toEqual({ + type: 'resource', + resource: { + uri: 'ui://widget/blob-dom', + mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; framework=react', + blob: Buffer.from(script).toString('base64') + } + }) + }) + + it('should throw error if script is missing', () => { + const definition: ExtendedUIResourceDefinition = { + name: 'no-script', + widget: 'no-script', + contentType: 'remoteDom' + // Missing remoteDomScript + } + + expect(() => adapter.createWidgetUIResource(definition, {})).toThrow( + 'Remote DOM script required for remoteDom type in widget no-script' + ) + }) + + it('should generate remote DOM script with widget metadata', () => { + const definition: ExtendedUIResourceDefinition = { + name: 'interactive-widget', + widget: 'interactive', + title: 'Interactive Widget', + description: 'An interactive remote DOM widget' + } + + const script = adapter.generateRemoteDomScript(definition, { + enabled: true, + count: 5 + }) + + expect(script).toContain('Interactive Widget') + expect(script).toContain('An interactive remote DOM widget') + expect(script).toContain('"enabled":true') + expect(script).toContain('"count":5') + expect(script).toContain('ui_interactive') + expect(script).toContain('ui-button') + }) + + it('should default to React framework if not specified', () => { + const definition: ExtendedUIResourceDefinition = { + name: 'default-framework', + widget: 'default', + contentType: 'remoteDom', + remoteDomScript: 'const div = document.createElement("div");' + // No remoteDomFramework specified + } + + const resource = adapter.createWidgetUIResource(definition, {}) + + expect(resource.resource.mimeType).toContain('framework=react') + }) + }) + + describe('Direct Method Calls', () => { + it('should create external URL resource directly', () => { + const resource = adapter.createExternalUrlResource( + 'ui://dashboard/main', + 'https://my.analytics.com/dashboard/123', + 'text' + ) + + expect(resource).toEqual({ + type: 'resource', + resource: { + uri: 'ui://dashboard/main', + mimeType: 'text/uri-list', + text: 'https://my.analytics.com/dashboard/123' + } + }) + }) + + it('should create raw HTML resource directly', () => { + const resource = adapter.createRawHtmlResource( + 'ui://content/page', + '

Hello World

', + 'text' + ) + + expect(resource).toEqual({ + type: 'resource', + resource: { + uri: 'ui://content/page', + mimeType: 'text/html', + text: '

Hello World

' + } + }) + }) + + it('should create remote DOM resource directly', () => { + const resource = adapter.createRemoteDomResource( + 'ui://component/button', + 'const btn = document.createElement("button");', + 'webcomponents', + 'text' + ) + + expect(resource).toEqual({ + type: 'resource', + resource: { + uri: 'ui://component/button', + mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; framework=webcomponents', + text: 'const btn = document.createElement("button");' + } + }) + }) + }) + + describe('URL Building', () => { + it('should handle null and undefined values in parameters', () => { + const definition: ExtendedUIResourceDefinition = { + name: 'test-widget', + widget: 'test' + } + + const resource = adapter.createWidgetUIResource(definition, { + valid: 'value', + nullValue: null, + undefinedValue: undefined, + emptyString: '', + zero: 0, + falseBool: false + }) + + const url = resource.resource.text + expect(url).toContain('valid=value') + expect(url).not.toContain('nullValue') + expect(url).not.toContain('undefinedValue') + expect(url).toContain('emptyString=') + expect(url).toContain('zero=0') + expect(url).toContain('falseBool=false') + }) + + it('should JSON stringify complex objects in URL parameters', () => { + const definition: ExtendedUIResourceDefinition = { + name: 'complex-params', + widget: 'complex' + } + + const resource = adapter.createWidgetUIResource(definition, { + nested: { + array: [1, 2, { key: 'value' }], + bool: true, + number: 42 + } + }) + + const url = resource.resource.text + expect(url).toContain('nested=%7B%22array') + + // Decode and verify the parameter + expect(url).toBeDefined() + const urlObj = new URL(url!) + const nestedParam = urlObj.searchParams.get('nested') + expect(nestedParam).toBeDefined() + const parsed = JSON.parse(nestedParam!) + expect(parsed).toEqual({ + array: [1, 2, { key: 'value' }], + bool: true, + number: 42 + }) + }) + }) + + describe('Error Handling', () => { + it('should throw error for unknown content type', () => { + const definition: any = { + name: 'unknown', + widget: 'unknown', + contentType: 'invalid-type' + } + + expect(() => adapter.createWidgetUIResource(definition, {})).toThrow( + 'Unknown content type: invalid-type' + ) + }) + + it('should handle empty widget name', () => { + const definition: ExtendedUIResourceDefinition = { + name: '', + widget: '' + } + + const resource = adapter.createWidgetUIResource(definition, {}) + + expect(resource.resource.uri).toBe('ui://widget/') + expect(resource.resource.text).toBe('http://localhost:3000/mcp-use/widgets/') + }) + }) +}) \ No newline at end of file From 12493cdeacbbabc5f2f8a220ec57303e60820f44 Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 15:55:16 +0200 Subject: [PATCH 07/19] fix: refactor mcp-ui-adapter to be a pure function --- .../src/server/adapters/mcp-ui-adapter.ts | 329 +++++++++--------- packages/mcp-use/src/server/index.ts | 14 +- packages/mcp-use/src/server/mcp-server.ts | 100 +++--- packages/mcp-use/src/server/types/index.ts | 4 +- packages/mcp-use/src/server/types/resource.ts | 60 +++- packages/mcp-use/tests/mcp-ui-adapter.test.ts | 236 ++++++------- 6 files changed, 374 insertions(+), 369 deletions(-) diff --git a/packages/mcp-use/src/server/adapters/mcp-ui-adapter.ts b/packages/mcp-use/src/server/adapters/mcp-ui-adapter.ts index 749b642b..e6a5ebb4 100644 --- a/packages/mcp-use/src/server/adapters/mcp-ui-adapter.ts +++ b/packages/mcp-use/src/server/adapters/mcp-ui-adapter.ts @@ -1,193 +1,178 @@ /** - * MCP-UI Adapter + * MCP-UI Adapter Utilities + * + * Pure functions to convert mcp-use high-level UIResource definitions + * into @mcp-ui/server compatible resource objects. * - * Provides an adapter between mcp-use high-level UIResource definitions - * and the low-level @mcp-ui/server resource format. * Ref: https://mcpui.dev/guide/server/typescript/usage-examples */ import { createUIResource } from '@mcp-ui/server' -import type { UIResourceContent, UIResourceDefinition } from '../types/resource.js' +import type { + UIResourceContent, + UIResourceDefinition, + UIEncoding +} from '../types/resource.js' /** - * Content type options for UI resources + * Configuration for building widget URLs */ -export type UIContentType = - | 'externalUrl' // Default: iframe URL for serving widgets - | 'rawHtml' // Direct HTML content - | 'remoteDom' // Remote DOM scripting +export interface UrlConfig { + baseUrl: string + port: number | string +} /** - * Encoding options for UI resources + * Build the full URL for a widget including query parameters + * + * @param widget - Widget identifier + * @param props - Parameters to pass as query params + * @param config - URL configuration (baseUrl and port) + * @returns Complete widget URL with encoded parameters */ -export type UIEncoding = 'text' | 'blob' +export function buildWidgetUrl( + widget: string, + props: Record | undefined, + config: UrlConfig +): string { + const url = new URL( + `/mcp-use/widgets/${widget}`, + `${config.baseUrl}:${config.port}` + ) + + if (props) { + Object.entries(props).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + const stringValue = typeof value === 'object' + ? JSON.stringify(value) + : String(value) + url.searchParams.set(key, stringValue) + } + }) + } + + return url.toString() +} /** - * Framework options for Remote DOM resources + * Create a UIResource for an external URL (iframe) + * + * @param uri - Resource URI (must start with ui://) + * @param iframeUrl - URL to load in iframe + * @param encoding - Encoding type ('text' or 'blob') + * @returns UIResourceContent object */ -export type RemoteDomFramework = 'react' | 'webcomponents' +export function createExternalUrlResource( + uri: string, + iframeUrl: string, + encoding: UIEncoding = 'text' +): UIResourceContent { + return createUIResource({ + uri: uri as `ui://${string}`, + content: { type: 'externalUrl', iframeUrl }, + encoding + }) +} /** - * Extended UI resource definition with content type support + * Create a UIResource for raw HTML content + * + * @param uri - Resource URI (must start with ui://) + * @param htmlString - HTML content to render + * @param encoding - Encoding type ('text' or 'blob') + * @returns UIResourceContent object */ -export interface ExtendedUIResourceDefinition extends UIResourceDefinition { - contentType?: UIContentType - encoding?: UIEncoding - htmlContent?: string - remoteDomScript?: string - remoteDomFramework?: RemoteDomFramework +export function createRawHtmlResource( + uri: string, + htmlString: string, + encoding: UIEncoding = 'text' +): UIResourceContent { + return createUIResource({ + uri: uri as `ui://${string}`, + content: { type: 'rawHtml', htmlString }, + encoding + }) } /** - * Configuration for the adapter + * Create a UIResource for Remote DOM scripting + * + * @param uri - Resource URI (must start with ui://) + * @param script - JavaScript code for remote DOM manipulation + * @param framework - Framework for remote DOM ('react' or 'webcomponents') + * @param encoding - Encoding type ('text' or 'blob') + * @returns UIResourceContent object */ -export interface AdapterConfig { - baseUrl: string - port: number | string +export function createRemoteDomResource( + uri: string, + script: string, + framework: 'react' | 'webcomponents' = 'react', + encoding: UIEncoding = 'text' +): UIResourceContent { + return createUIResource({ + uri: uri as `ui://${string}`, + content: { type: 'remoteDom', script, framework }, + encoding + }) } /** - * MCP-UI Adapter class + * Create a UIResource from a high-level definition + * + * This is the main function that routes to the appropriate resource creator + * based on the discriminated union type. + * + * @param definition - UIResource definition (discriminated union) + * @param params - Runtime parameters for the widget (for externalUrl type) + * @param config - URL configuration for building widget URLs + * @returns UIResourceContent object */ -export class McpUiAdapter { - private config: AdapterConfig - - constructor(config: AdapterConfig) { - this.config = config - } - - /** - * Build the full URL for a widget - */ - private buildWidgetUrl(widget: string, props?: Record): string { - const url = new URL( - `/mcp-use/widgets/${widget}`, - `http://localhost:${this.config.port}` - ) - - if (props) { - Object.entries(props).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - const stringValue = typeof value === 'object' - ? JSON.stringify(value) - : String(value) - url.searchParams.set(key, stringValue) - } - }) +export function createUIResourceFromDefinition( + definition: UIResourceDefinition, + params: Record, + config: UrlConfig +): UIResourceContent { + const uri = `ui://widget/${definition.name}` as `ui://${string}` + const encoding = definition.encoding || 'text' + + switch (definition.type) { + case 'externalUrl': { + const widgetUrl = buildWidgetUrl(definition.widget, params, config) + return createExternalUrlResource(uri, widgetUrl, encoding) } - return url.toString() - } - - /** - * Create a UIResource for an external URL (default for widgets) - * @param uri - URI of the resource - * @param iframeUrl - URL of the iframe - * @param encoding - Encoding of the resource (text or blob (URL is Base64 encoded)) - * @returns UIResourceContent - */ - createExternalUrlResource( - uri: string, - iframeUrl: string, - encoding: UIEncoding = 'text' - ): UIResourceContent { - return createUIResource({ - uri: uri as `ui://${string}`, - content: { type: 'externalUrl', iframeUrl }, - encoding - }) - } - - /** - * Create a UIResource for raw HTML content - * - * @param uri - URI of the resource - * @param htmlString - HTML string to embed - * @param encoding - Encoding of the resource - * @returns UIResourceContent - */ - createRawHtmlResource( - uri: string, - htmlString: string, - encoding: UIEncoding = 'text' - ): UIResourceContent { - return createUIResource({ - uri: uri as `ui://${string}`, - content: { type: 'rawHtml', htmlString }, - encoding - }) - } - - /** - * Create a UIResource for Remote DOM scripting - */ - createRemoteDomResource( - uri: string, - script: string, - framework: RemoteDomFramework = 'react', - encoding: UIEncoding = 'text' - ): UIResourceContent { - return createUIResource({ - uri: uri as `ui://${string}`, - content: { type: 'remoteDom', script, framework }, - encoding - }) - } - - /** - * Create a UIResource from our high-level definition - */ - createWidgetUIResource( - definition: ExtendedUIResourceDefinition, - props?: Record - ): UIResourceContent { - const uri = `ui://widget/${definition.name}` - const contentType = definition.contentType || 'externalUrl' - const encoding = definition.encoding || 'text' - - switch (contentType) { - case 'externalUrl': { - const widgetUrl = this.buildWidgetUrl(definition.widget, props) - return this.createExternalUrlResource(uri, widgetUrl, encoding) - } - - case 'rawHtml': { - if (!definition.htmlContent) { - throw new Error(`HTML content required for rawHtml type in widget ${definition.name}`) - } - return this.createRawHtmlResource(uri, definition.htmlContent, encoding) - } + case 'rawHtml': { + return createRawHtmlResource(uri, definition.htmlContent, encoding) + } - case 'remoteDom': { - if (!definition.remoteDomScript) { - throw new Error(`Remote DOM script required for remoteDom type in widget ${definition.name}`) - } - const framework = definition.remoteDomFramework || 'react' - return this.createRemoteDomResource( - uri, - definition.remoteDomScript, - framework, - encoding - ) - } + case 'remoteDom': { + const framework = definition.framework || 'react' + return createRemoteDomResource(uri, definition.script, framework, encoding) + } - default: - throw new Error(`Unknown content type: ${contentType}`) + default: { + // TypeScript exhaustiveness check + const _exhaustive: never = definition + throw new Error(`Unknown UI resource type: ${(_exhaustive as any).type}`) } } +} - /** - * Generate HTML content for a widget (for rawHtml type) - */ - generateWidgetHtml( - definition: UIResourceDefinition, - props?: Record - ): string { - const [width = '100%', height = '400px'] = definition.size || [] - const propsJson = props ? JSON.stringify(props) : '{}' - - return ` - +/** + * Generate HTML content for a widget (utility function) + * + * @param definition - Base UI resource definition + * @param props - Widget properties to inject + * @returns Generated HTML string + */ +export function generateWidgetHtml( + definition: Pick, + props?: Record +): string { + const [width = '100%', height = '400px'] = definition.size || [] + const propsJson = props ? JSON.stringify(props) : '{}' + + return ` @@ -246,16 +231,20 @@ export class McpUiAdapter { ` - } +} - /** - * Generate a Remote DOM script for a widget - */ - generateRemoteDomScript( - definition: UIResourceDefinition, - props?: Record - ): string { - return ` +/** + * Generate a Remote DOM script for a widget (utility function) + * + * @param definition - Base UI resource definition + * @param props - Widget properties to inject + * @returns Generated JavaScript string + */ +export function generateRemoteDomScript( + definition: Pick, + props?: Record +): string { + return ` // Remote DOM script for ${definition.name} const container = document.createElement('div'); container.style.padding = '20px'; @@ -295,12 +284,4 @@ console.log('Remote DOM widget ${definition.name} initialized with props:', prop // Append to root root.appendChild(container);` - } } - -/** - * Factory function to create an adapter instance - */ -export function createMcpUiAdapter(config: AdapterConfig): McpUiAdapter { - return new McpUiAdapter(config) -} \ No newline at end of file diff --git a/packages/mcp-use/src/server/index.ts b/packages/mcp-use/src/server/index.ts index ca351147..1b136551 100644 --- a/packages/mcp-use/src/server/index.ts +++ b/packages/mcp-use/src/server/index.ts @@ -5,10 +5,14 @@ export { export * from './types/index.js' -// MCP-UI adapter exports +// MCP-UI adapter utility functions export { - McpUiAdapter, - createMcpUiAdapter, - type ExtendedUIResourceDefinition, - type AdapterConfig + buildWidgetUrl, + createExternalUrlResource, + createRawHtmlResource, + createRemoteDomResource, + createUIResourceFromDefinition, + generateWidgetHtml, + generateRemoteDomScript, + type UrlConfig } from './adapters/mcp-ui-adapter.js' diff --git a/packages/mcp-use/src/server/mcp-server.ts b/packages/mcp-use/src/server/mcp-server.ts index 32e963ef..6f034fd8 100644 --- a/packages/mcp-use/src/server/mcp-server.ts +++ b/packages/mcp-use/src/server/mcp-server.ts @@ -7,6 +7,7 @@ import type { UIResourceDefinition, WidgetProps, InputDefinition, + UIResourceContent, } from './types/index.js' import { McpServer as OfficialMcpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js' import { z } from 'zod' @@ -14,8 +15,7 @@ import express, { type Express } from 'express' import { existsSync, readdirSync } from 'node:fs' import { join } from 'node:path' import { requestLogger } from './logging.js' -import type { createMcpUiAdapter } from './adapters/mcp-ui-adapter.js' -import type { McpUiAdapter } from './adapters/mcp-ui-adapter.js' +import { createUIResourceFromDefinition, type UrlConfig } from './adapters/mcp-ui-adapter.js' export class McpServer { private server: OfficialMcpServer @@ -24,7 +24,6 @@ export class McpServer { private mcpMounted = false private inspectorMounted = false private serverPort?: number - private uiAdapter?: McpUiAdapter /** * Creates a new MCP server instance with Express integration @@ -337,10 +336,57 @@ export class McpServer { * ``` */ uiResource(definition: UIResourceDefinition): this { + // Determine tool name based on resource type + const toolName = definition.type === 'externalUrl' ? `ui_${definition.widget}` : `ui_${definition.name}` + const displayName = definition.title || definition.name + + // Determine resource URI and mimeType based on type + let resourceUri: string + let mimeType: string + + 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 + default: + throw new Error(`Unsupported UI resource type. Must be one of: externalUrl, rawHtml, remoteDom`) + } + + // Register the resource + this.resource({ + name: definition.name, + uri: resourceUri, + title: definition.title, + description: definition.description, + mimeType, + annotations: definition.annotations, + fn: async () => { + // For externalUrl type, use default props. For others, use empty params + const params = definition.type === 'externalUrl' + ? this.applyDefaultProps(definition.props) + : {} + + const uiResource = this.createWidgetUIResource(definition, params) + + return { + contents: [uiResource.resource] + } + } + }) + // Register the tool - returns UIResource with parameters this.tool({ - name: `ui_${definition.widget}`, - description: definition.description || `Display ${definition.widget} widget`, + name: toolName, + description: definition.description || `Display ${displayName}`, inputs: this.convertPropsToInputs(definition.props), fn: async (params) => { // Create the UIResource with user-provided params @@ -350,39 +396,15 @@ export class McpServer { content: [ { type: 'text', - text: `Displaying ${definition.title || definition.widget} widget` + text: `Displaying ${displayName}`, + description: `Show MCP-UI widget for ${displayName}` }, - uiResource // Reuse the same UIResource + uiResource ] } } }) - // Register the resource - returns widget URL for MCP clients - this.resource({ - name: definition.name, - uri: `ui://widget/${definition.widget}`, - title: definition.title, - description: definition.description, - mimeType: 'text/uri-list', - annotations: definition.annotations, - fn: async () => { - // Build the widget URL with default props - const widgetUrl = this.buildWidgetUrl( - definition.widget, - this.applyDefaultProps(definition.props) - ) - - return { - contents: [{ - uri: `ui://widget/${definition.widget}`, - mimeType: 'text/uri-list', - text: widgetUrl - }] - } - } - }) - return this } @@ -401,17 +423,13 @@ export class McpServer { private createWidgetUIResource( definition: UIResourceDefinition, params: Record - ): any { - // Initialize adapter if not already created - if (!this.uiAdapter) { - this.uiAdapter = createMcpUiAdapter({ - baseUrl: `http://localhost`, - port: this.serverPort || 3001 - }) + ): UIResourceContent { + const urlConfig: UrlConfig = { + baseUrl: 'http://localhost', + port: this.serverPort || 3001 } - // Use the adapter to create the UIResource - return this.uiAdapter.createWidgetUIResource(definition, params) + return createUIResourceFromDefinition(definition, params, urlConfig) } /** diff --git a/packages/mcp-use/src/server/types/index.ts b/packages/mcp-use/src/server/types/index.ts index 6ff5e7b3..18d0828f 100644 --- a/packages/mcp-use/src/server/types/index.ts +++ b/packages/mcp-use/src/server/types/index.ts @@ -19,10 +19,12 @@ export { // UIResource specific types UIResourceContent, WidgetProps, - UIContentType, UIEncoding, RemoteDomFramework, UIResourceDefinition, + ExternalUrlUIResource, + RawHtmlUIResource, + RemoteDomUIResource, WidgetConfig, WidgetManifest, DiscoverWidgetsOptions diff --git a/packages/mcp-use/src/server/types/resource.ts b/packages/mcp-use/src/server/types/resource.ts index bd3bfedd..13aaa89e 100644 --- a/packages/mcp-use/src/server/types/resource.ts +++ b/packages/mcp-use/src/server/types/resource.ts @@ -69,14 +69,6 @@ export interface WidgetProps { } } -/** - * UIResource content types - */ -export type UIContentType = - | 'externalUrl' // Default: iframe URL for serving widgets - | 'rawHtml' // Direct HTML content - | 'remoteDom' // Remote DOM scripting - /** * Encoding options for UI resources */ @@ -87,11 +79,12 @@ export type UIEncoding = 'text' | 'blob' */ export type RemoteDomFramework = 'react' | 'webcomponents' -export interface UIResourceDefinition { +/** + * Base properties shared by all UI resource types + */ +interface BaseUIResourceDefinition { /** Unique identifier for the resource */ name: string - /** Widget identifier (e.g., 'kanban-board', 'chart') */ - widget: string /** Human-readable title */ title?: string /** Description of what the widget does */ @@ -102,18 +95,47 @@ export interface UIResourceDefinition { size?: [string, string] /** Resource annotations for discovery and presentation */ annotations?: ResourceAnnotations - /** Content type for the UIResource (defaults to 'externalUrl') */ - contentType?: UIContentType /** Encoding for the resource content (defaults to 'text') */ encoding?: UIEncoding - /** HTML content for rawHtml content type */ - htmlContent?: string - /** Script for remoteDom content type */ - remoteDomScript?: string - /** Framework for remoteDom content type */ - remoteDomFramework?: RemoteDomFramework } +/** + * External URL UI resource - serves widget via iframe + */ +export interface ExternalUrlUIResource extends BaseUIResourceDefinition { + type: 'externalUrl' + /** Widget identifier (e.g., 'kanban-board', 'chart') */ + widget: string +} + +/** + * Raw HTML UI resource - direct HTML content + */ +export interface RawHtmlUIResource extends BaseUIResourceDefinition { + type: 'rawHtml' + /** HTML content to render */ + htmlContent: string +} + +/** + * Remote DOM UI resource - scripted UI components + */ +export interface RemoteDomUIResource extends BaseUIResourceDefinition { + type: 'remoteDom' + /** JavaScript code for remote DOM manipulation */ + script: string + /** Framework for remote DOM (defaults to 'react') */ + framework?: RemoteDomFramework +} + +/** + * Discriminated union of all UI resource types + */ +export type UIResourceDefinition = + | ExternalUrlUIResource + | RawHtmlUIResource + | RemoteDomUIResource + export interface WidgetConfig { /** Widget directory name */ name: string diff --git a/packages/mcp-use/tests/mcp-ui-adapter.test.ts b/packages/mcp-use/tests/mcp-ui-adapter.test.ts index 044482a3..242a2d43 100644 --- a/packages/mcp-use/tests/mcp-ui-adapter.test.ts +++ b/packages/mcp-use/tests/mcp-ui-adapter.test.ts @@ -1,38 +1,47 @@ /** * Tests for MCP-UI Adapter * - * These tests verify that the adapter correctly generates UIResource objects + * These tests verify that the adapter pure functions correctly generate UIResource objects * matching the @mcp-ui/server format for all content types. */ -import { describe, it, expect, beforeEach } from 'vitest' -import { type McpUiAdapter, createMcpUiAdapter } from '../src/server/adapters/mcp-ui-adapter.js' -import type { ExtendedUIResourceDefinition } from '../src/server/adapters/mcp-ui-adapter.js' +import { describe, it, expect } from 'vitest' +import { + buildWidgetUrl, + createExternalUrlResource, + createRawHtmlResource, + createRemoteDomResource, + createUIResourceFromDefinition, + generateWidgetHtml, + generateRemoteDomScript, + type UrlConfig +} from '../src/server/adapters/mcp-ui-adapter.js' +import type { + ExternalUrlUIResource, + RawHtmlUIResource, + RemoteDomUIResource +} from '../src/server/types/resource.js' describe('MCP-UI Adapter', () => { - let adapter: McpUiAdapter - - beforeEach(() => { - adapter = createMcpUiAdapter({ - baseUrl: 'http://localhost', - port: 3000 - }) - }) + const urlConfig: UrlConfig = { + baseUrl: 'http://localhost', + port: 3000 + } describe('External URL Resources', () => { it('should create external URL resource with text encoding', () => { - const definition: ExtendedUIResourceDefinition = { + const definition: ExternalUrlUIResource = { + type: 'externalUrl', name: 'kanban-board', widget: 'kanban-board', title: 'Kanban Board', - contentType: 'externalUrl', encoding: 'text' } - const resource = adapter.createWidgetUIResource(definition, { + const resource = createUIResourceFromDefinition(definition, { theme: 'dark', initialTasks: ['task1', 'task2'] - }) + }, urlConfig) expect(resource).toEqual({ type: 'resource', @@ -45,16 +54,16 @@ describe('MCP-UI Adapter', () => { }) it('should create external URL resource with blob encoding', () => { - const definition: ExtendedUIResourceDefinition = { + const definition: ExternalUrlUIResource = { + type: 'externalUrl', name: 'chart-widget', widget: 'chart', - contentType: 'externalUrl', encoding: 'blob' } - const resource = adapter.createWidgetUIResource(definition, { + const resource = createUIResourceFromDefinition(definition, { data: [1, 2, 3] - }) + }, urlConfig) expect(resource).toEqual({ type: 'resource', @@ -72,31 +81,32 @@ describe('MCP-UI Adapter', () => { }) it('should handle complex object parameters', () => { - const definition: ExtendedUIResourceDefinition = { + const definition: ExternalUrlUIResource = { + type: 'externalUrl', name: 'dashboard', - widget: 'dashboard', - contentType: 'externalUrl' + widget: 'dashboard' } - const resource = adapter.createWidgetUIResource(definition, { + const resource = createUIResourceFromDefinition(definition, { config: { layout: 'grid', columns: 3, widgets: ['chart', 'table', 'metrics'] } - }) + }, urlConfig) expect(resource.resource.text).toContain('config=%7B%22layout%22%3A%22grid%22') }) - it('should default to externalUrl with text encoding', () => { - const definition: ExtendedUIResourceDefinition = { + it('should default to text encoding', () => { + const definition: ExternalUrlUIResource = { + type: 'externalUrl', name: 'todo-list', widget: 'todo-list' - // No contentType or encoding specified + // No encoding specified } - const resource = adapter.createWidgetUIResource(definition, {}) + const resource = createUIResourceFromDefinition(definition, {}, urlConfig) expect(resource).toEqual({ type: 'resource', @@ -112,15 +122,14 @@ describe('MCP-UI Adapter', () => { describe('Raw HTML Resources', () => { it('should create raw HTML resource with text encoding', () => { const htmlContent = '

Hello World

' - const definition: ExtendedUIResourceDefinition = { + const definition: RawHtmlUIResource = { + type: 'rawHtml', name: 'static-widget', - widget: 'static', - contentType: 'rawHtml', - encoding: 'text', - htmlContent + htmlContent, + encoding: 'text' } - const resource = adapter.createWidgetUIResource(definition, {}) + const resource = createUIResourceFromDefinition(definition, {}, urlConfig) expect(resource).toEqual({ type: 'resource', @@ -134,15 +143,14 @@ describe('MCP-UI Adapter', () => { it('should create raw HTML resource with blob encoding', () => { const htmlContent = '

Complex HTML

' - const definition: ExtendedUIResourceDefinition = { + const definition: RawHtmlUIResource = { + type: 'rawHtml', name: 'complex-widget', - widget: 'complex', - contentType: 'rawHtml', - encoding: 'blob', - htmlContent + htmlContent, + encoding: 'blob' } - const resource = adapter.createWidgetUIResource(definition, {}) + const resource = createUIResourceFromDefinition(definition, {}, urlConfig) expect(resource).toEqual({ type: 'resource', @@ -154,29 +162,17 @@ describe('MCP-UI Adapter', () => { }) }) - it('should throw error if HTML content is missing', () => { - const definition: ExtendedUIResourceDefinition = { - name: 'broken-widget', - widget: 'broken', - contentType: 'rawHtml' - // Missing htmlContent - } - - expect(() => adapter.createWidgetUIResource(definition, {})).toThrow( - 'HTML content required for rawHtml type in widget broken-widget' - ) - }) - it('should generate HTML content with widget metadata', () => { - const definition: ExtendedUIResourceDefinition = { + const definition: RawHtmlUIResource = { + type: 'rawHtml', name: 'generated-widget', - widget: 'generated', + htmlContent: '
Test
', title: 'Generated Widget', description: 'A dynamically generated widget', size: ['800px', '600px'] } - const html = adapter.generateWidgetHtml(definition, { + const html = generateWidgetHtml(definition, { value: 42, items: ['a', 'b', 'c'] }) @@ -197,16 +193,15 @@ describe('MCP-UI Adapter', () => { button.setAttribute('label', 'Click me'); root.appendChild(button); ` - const definition: ExtendedUIResourceDefinition = { + const definition: RemoteDomUIResource = { + type: 'remoteDom', name: 'remote-button', - widget: 'button', - contentType: 'remoteDom', - encoding: 'text', - remoteDomScript: script, - remoteDomFramework: 'react' + script, + framework: 'react', + encoding: 'text' } - const resource = adapter.createWidgetUIResource(definition, {}) + const resource = createUIResourceFromDefinition(definition, {}, urlConfig) expect(resource).toEqual({ type: 'resource', @@ -227,15 +222,14 @@ describe('MCP-UI Adapter', () => { } customElements.define('my-component', MyComponent); ` - const definition: ExtendedUIResourceDefinition = { + const definition: RemoteDomUIResource = { + type: 'remoteDom', name: 'web-component', - widget: 'component', - contentType: 'remoteDom', - remoteDomScript: script, - remoteDomFramework: 'webcomponents' + script, + framework: 'webcomponents' } - const resource = adapter.createWidgetUIResource(definition, {}) + const resource = createUIResourceFromDefinition(definition, {}, urlConfig) expect(resource).toEqual({ type: 'resource', @@ -249,15 +243,14 @@ describe('MCP-UI Adapter', () => { it('should create remote DOM resource with blob encoding', () => { const script = 'root.appendChild(document.createElement("div"));' - const definition: ExtendedUIResourceDefinition = { + const definition: RemoteDomUIResource = { + type: 'remoteDom', name: 'blob-dom', - widget: 'blob-dom', - contentType: 'remoteDom', - encoding: 'blob', - remoteDomScript: script + script, + encoding: 'blob' } - const resource = adapter.createWidgetUIResource(definition, {}) + const resource = createUIResourceFromDefinition(definition, {}, urlConfig) expect(resource).toEqual({ type: 'resource', @@ -269,28 +262,16 @@ describe('MCP-UI Adapter', () => { }) }) - it('should throw error if script is missing', () => { - const definition: ExtendedUIResourceDefinition = { - name: 'no-script', - widget: 'no-script', - contentType: 'remoteDom' - // Missing remoteDomScript - } - - expect(() => adapter.createWidgetUIResource(definition, {})).toThrow( - 'Remote DOM script required for remoteDom type in widget no-script' - ) - }) - it('should generate remote DOM script with widget metadata', () => { - const definition: ExtendedUIResourceDefinition = { + const definition: RemoteDomUIResource = { + type: 'remoteDom', name: 'interactive-widget', - widget: 'interactive', + script: 'console.log("test")', title: 'Interactive Widget', description: 'An interactive remote DOM widget' } - const script = adapter.generateRemoteDomScript(definition, { + const script = generateRemoteDomScript(definition, { enabled: true, count: 5 }) @@ -299,20 +280,19 @@ describe('MCP-UI Adapter', () => { expect(script).toContain('An interactive remote DOM widget') expect(script).toContain('"enabled":true') expect(script).toContain('"count":5') - expect(script).toContain('ui_interactive') + expect(script).toContain('ui_interactive-widget') expect(script).toContain('ui-button') }) it('should default to React framework if not specified', () => { - const definition: ExtendedUIResourceDefinition = { + const definition: RemoteDomUIResource = { + type: 'remoteDom', name: 'default-framework', - widget: 'default', - contentType: 'remoteDom', - remoteDomScript: 'const div = document.createElement("div");' - // No remoteDomFramework specified + script: 'const div = document.createElement("div");' + // No framework specified } - const resource = adapter.createWidgetUIResource(definition, {}) + const resource = createUIResourceFromDefinition(definition, {}, urlConfig) expect(resource.resource.mimeType).toContain('framework=react') }) @@ -320,7 +300,7 @@ describe('MCP-UI Adapter', () => { describe('Direct Method Calls', () => { it('should create external URL resource directly', () => { - const resource = adapter.createExternalUrlResource( + const resource = createExternalUrlResource( 'ui://dashboard/main', 'https://my.analytics.com/dashboard/123', 'text' @@ -337,7 +317,7 @@ describe('MCP-UI Adapter', () => { }) it('should create raw HTML resource directly', () => { - const resource = adapter.createRawHtmlResource( + const resource = createRawHtmlResource( 'ui://content/page', '

Hello World

', 'text' @@ -354,7 +334,7 @@ describe('MCP-UI Adapter', () => { }) it('should create remote DOM resource directly', () => { - const resource = adapter.createRemoteDomResource( + const resource = createRemoteDomResource( 'ui://component/button', 'const btn = document.createElement("button");', 'webcomponents', @@ -373,22 +353,25 @@ describe('MCP-UI Adapter', () => { }) describe('URL Building', () => { - it('should handle null and undefined values in parameters', () => { - const definition: ExtendedUIResourceDefinition = { - name: 'test-widget', - widget: 'test' - } + it('should build URL with query parameters', () => { + const url = buildWidgetUrl('kanban-board', { + theme: 'dark', + count: 5 + }, urlConfig) + + expect(url).toBe('http://localhost:3000/mcp-use/widgets/kanban-board?theme=dark&count=5') + }) - const resource = adapter.createWidgetUIResource(definition, { + it('should handle null and undefined values in parameters', () => { + const url = buildWidgetUrl('test', { valid: 'value', nullValue: null, undefinedValue: undefined, emptyString: '', zero: 0, falseBool: false - }) + }, urlConfig) - const url = resource.resource.text expect(url).toContain('valid=value') expect(url).not.toContain('nullValue') expect(url).not.toContain('undefinedValue') @@ -398,24 +381,25 @@ describe('MCP-UI Adapter', () => { }) it('should JSON stringify complex objects in URL parameters', () => { - const definition: ExtendedUIResourceDefinition = { + const definition: ExternalUrlUIResource = { + type: 'externalUrl', name: 'complex-params', widget: 'complex' } - const resource = adapter.createWidgetUIResource(definition, { + const resource = createUIResourceFromDefinition(definition, { nested: { array: [1, 2, { key: 'value' }], bool: true, number: 42 } - }) + }, urlConfig) const url = resource.resource.text + expect(url).toBeDefined() expect(url).toContain('nested=%7B%22array') // Decode and verify the parameter - expect(url).toBeDefined() const urlObj = new URL(url!) const nestedParam = urlObj.searchParams.get('nested') expect(nestedParam).toBeDefined() @@ -426,31 +410,25 @@ describe('MCP-UI Adapter', () => { number: 42 }) }) - }) - describe('Error Handling', () => { - it('should throw error for unknown content type', () => { - const definition: any = { - name: 'unknown', - widget: 'unknown', - contentType: 'invalid-type' - } - - expect(() => adapter.createWidgetUIResource(definition, {})).toThrow( - 'Unknown content type: invalid-type' - ) + it('should handle empty parameters', () => { + const url = buildWidgetUrl('empty', undefined, urlConfig) + expect(url).toBe('http://localhost:3000/mcp-use/widgets/empty') }) + }) + describe('Error Handling', () => { it('should handle empty widget name', () => { - const definition: ExtendedUIResourceDefinition = { + const definition: ExternalUrlUIResource = { + type: 'externalUrl', name: '', widget: '' } - const resource = adapter.createWidgetUIResource(definition, {}) + const resource = createUIResourceFromDefinition(definition, {}, urlConfig) expect(resource.resource.uri).toBe('ui://widget/') expect(resource.resource.text).toBe('http://localhost:3000/mcp-use/widgets/') }) }) -}) \ No newline at end of file +}) From 5b9942dfbbd083a3fca0a46442db0ae187a2dfd7 Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 16:31:46 +0200 Subject: [PATCH 08/19] minor --- packages/mcp-use/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/mcp-use/index.ts b/packages/mcp-use/index.ts index aa2da6d4..e5bade4e 100644 --- a/packages/mcp-use/index.ts +++ b/packages/mcp-use/index.ts @@ -34,6 +34,9 @@ export type { ToolHandler, // UIResource specific types UIResourceDefinition, + ExternalUrlUIResource, + RawHtmlUIResource, + RemoteDomUIResource, WidgetProps, WidgetConfig, WidgetManifest, From 2e2d65e94b1d388e408e41259e73f7d70c76c109 Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 16:32:07 +0200 Subject: [PATCH 09/19] update uiresource template with all mcp-ui resource types --- .../src/templates/uiresource/src/server.ts | 367 ++++++++++++++---- 1 file changed, 293 insertions(+), 74 deletions(-) diff --git a/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts b/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts index 86ec6f05..f26b9a10 100644 --- a/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts +++ b/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts @@ -1,22 +1,33 @@ -import { createMCPServer, type UIResourceDefinition } from 'mcp-use' +import { createMCPServer } from 'mcp-use' +import type { + ExternalUrlUIResource, + RawHtmlUIResource, + RemoteDomUIResource +} from 'mcp-use' // Create an MCP server with UIResource support const server = createMCPServer('uiresource-mcp-server', { version: '1.0.0', - description: 'MCP server with UIResource widget integration', + description: 'MCP server demonstrating all UIResource types', }) const PORT = process.env.PORT || 3000 /** - * Main Kanban Board Widget + * ════════════════════════════════════════════════════════════════════ + * Type 1: External URL (Iframe Widget) + * ════════════════════════════════════════════════════════════════════ * - * This demonstrates the new uiResource method which automatically: + * Serves a widget from your local filesystem via iframe. + * Best for: Complex interactive widgets with their own assets + * + * This automatically: * 1. Creates a tool (ui_kanban-board) that accepts parameters * 2. Creates a resource (ui://widget/kanban-board) for static access - * 3. Handles parameter passing via URL query strings + * 3. Serves the widget from dist/resources/mcp-use/widgets/kanban-board/ */ server.uiResource({ + type: 'externalUrl', name: 'kanban-board', widget: 'kanban-board', title: 'Kanban Board', @@ -44,64 +55,242 @@ server.uiResource({ audience: ['user', 'assistant'], priority: 0.8 } -}) +} satisfies ExternalUrlUIResource) + +/** + * ════════════════════════════════════════════════════════════════════ + * Type 2: Raw HTML + * ════════════════════════════════════════════════════════════════════ + * + * Renders HTML content directly without an iframe. + * Best for: Simple visualizations, status displays, formatted text + * + * This creates: + * - Tool: ui_welcome-card + * - Resource: ui://widget/welcome-card + */ +server.uiResource({ + type: 'rawHtml', + name: 'welcome-card', + title: 'Welcome Message', + description: 'A welcoming card with server information', + htmlContent: ` + + + + + + + +
+

🎉 Welcome to MCP-UI

+

Your server is running and ready to serve interactive widgets!

+ +
+
+
3
+
Widget Types
+
+
+
+
Possibilities
+
+
+
+
Fast & Simple
+
+
-// Example: Additional widget registrations -// Uncomment to add more widgets to your server +

+ Server: uiresource-mcp-server v1.0.0
+ Port: ${PORT} +

+
+ + + `, + encoding: 'text', + size: ['600px', '400px'] +} satisfies RawHtmlUIResource) -/* -// Data visualization widget +/** + * ════════════════════════════════════════════════════════════════════ + * Type 3: Remote DOM (React Components) + * ════════════════════════════════════════════════════════════════════ + * + * Uses Remote DOM to render interactive components. + * Best for: Lightweight interactive UIs using MCP-UI React components + * + * This creates: + * - Tool: ui_quick-poll + * - Resource: ui://widget/quick-poll + */ server.uiResource({ - name: 'chart-widget', - widget: 'chart', - title: 'Data Chart', - description: 'Interactive data visualization', - props: { - data: { - type: 'array', - description: 'Chart data points', - required: true - }, - chartType: { - type: 'string', - description: 'Type of chart (line/bar/pie)', - default: 'line' + type: 'remoteDom', + name: 'quick-poll', + title: 'Quick Poll', + description: 'Create instant polls with interactive voting', + script: ` +// Remote DOM script for quick-poll widget +// Note: Remote DOM only supports registered MCP-UI components like ui-stack, ui-text, ui-button +// Standard HTML elements (div, h2, p, etc.) are NOT available + +// Get props (passed from tool parameters) +const props = ${JSON.stringify({ question: 'What is your favorite framework?', options: ['React', 'Vue', 'Svelte', 'Angular'] })}; + +// Create main container stack (vertical layout) +const container = document.createElement('ui-stack'); +container.setAttribute('direction', 'column'); +container.setAttribute('spacing', 'medium'); +container.setAttribute('padding', 'large'); + +// Title text +const title = document.createElement('ui-text'); +title.setAttribute('size', 'xlarge'); +title.setAttribute('weight', 'bold'); +title.textContent = '📊 Quick Poll'; +container.appendChild(title); + +// Description text +const description = document.createElement('ui-text'); +description.textContent = 'Cast your vote below!'; +container.appendChild(description); + +// Question text +const questionText = document.createElement('ui-text'); +questionText.setAttribute('size', 'large'); +questionText.setAttribute('weight', 'semibold'); +questionText.textContent = props.question || 'What is your preference?'; +container.appendChild(questionText); + +// Button stack (horizontal layout) +const buttonStack = document.createElement('ui-stack'); +buttonStack.setAttribute('direction', 'row'); +buttonStack.setAttribute('spacing', 'small'); +buttonStack.setAttribute('wrap', 'true'); + +// Create vote tracking +const votes = {}; +let feedbackText = null; + +// Create buttons for each option +const options = props.options || ['Option 1', 'Option 2', 'Option 3']; +options.forEach((option) => { + const button = document.createElement('ui-button'); + button.setAttribute('label', option); + button.setAttribute('variant', 'secondary'); + + button.addEventListener('press', () => { + // Record vote + votes[option] = (votes[option] || 0) + 1; + + // Send vote to parent (for tracking) + window.parent.postMessage({ + type: 'tool', + payload: { + toolName: 'record_vote', + params: { + question: props.question, + selected: option, + votes: votes + } + } + }, '*'); + + // Update or create feedback text + if (feedbackText) { + feedbackText.textContent = \`✓ Voted for \${option}! (Total votes: \${votes[option]})\`; + } else { + feedbackText = document.createElement('ui-text'); + feedbackText.setAttribute('emphasis', 'high'); + feedbackText.textContent = \`✓ Voted for \${option}!\`; + container.appendChild(feedbackText); } - }, - size: ['800px', '400px'] -}) + }); -// Todo list widget -server.uiResource({ - name: 'todo-list', - widget: 'todo-list', - title: 'Todo List', - description: 'Simple todo list manager', + buttonStack.appendChild(button); +}); + +container.appendChild(buttonStack); + +// Results section +const resultsTitle = document.createElement('ui-text'); +resultsTitle.setAttribute('size', 'medium'); +resultsTitle.setAttribute('weight', 'semibold'); +resultsTitle.textContent = 'Vote to see results!'; +container.appendChild(resultsTitle); + +// Append to root +root.appendChild(container); + `, + framework: 'react', + encoding: 'text', + size: ['500px', '450px'], props: { - items: { + question: { + type: 'string', + description: 'The poll question', + default: 'What is your favorite framework?' + }, + options: { type: 'array', - description: 'Initial todo items', - default: [] + description: 'Poll options', + default: ['React', 'Vue', 'Svelte'] } } -}) -*/ - -// Example: Programmatic widget registration -// You can also register widgets from an array programmatically -const additionalWidgets: UIResourceDefinition[] = [ - // Add your widget definitions here -] - -// Register all additional widgets -additionalWidgets.forEach(widget => server.uiResource(widget)) +} satisfies RemoteDomUIResource) /** - * Traditional MCP Tool + * ════════════════════════════════════════════════════════════════════ + * Traditional MCP Tools and Resources + * ════════════════════════════════════════════════════════════════════ * - * You can still add regular tools alongside UIResources. - * This example shows how to mix both approaches. + * You can mix UIResources with traditional MCP tools and resources */ + server.tool({ name: 'get-widget-info', description: 'Get information about available UI widgets', @@ -109,9 +298,22 @@ server.tool({ const widgets = [ { name: 'kanban-board', + type: 'externalUrl', tool: 'ui_kanban-board', resource: 'ui://widget/kanban-board', url: `http://localhost:${PORT}/mcp-use/widgets/kanban-board` + }, + { + name: 'welcome-card', + type: 'rawHtml', + tool: 'ui_welcome-card', + resource: 'ui://widget/welcome-card' + }, + { + name: 'quick-poll', + type: 'remoteDom', + tool: 'ui_quick-poll', + resource: 'ui://widget/quick-poll' } ] @@ -119,26 +321,20 @@ server.tool({ content: [{ type: 'text', text: `Available UI Widgets:\n\n${widgets.map(w => - `📦 ${w.name}\n` + + `📦 ${w.name} (${w.type})\n` + ` Tool: ${w.tool}\n` + ` Resource: ${w.resource}\n` + - ` Browser: ${w.url}` - ).join('\n\n')}\n\n` + - `Each widget can be:\n` + - `1. Called as a tool with parameters\n` + - `2. Accessed as a resource for static version\n` + - `3. Viewed directly in browser` + (w.url ? ` Browser: ${w.url}\n` : '') + ).join('\n')}\n` + + `\nTypes Explained:\n` + + `• externalUrl: Iframe widget from filesystem\n` + + `• rawHtml: Direct HTML rendering\n` + + `• remoteDom: React/Web Components scripting` }] } } }) -/** - * Traditional MCP Resource - * - * Example of a non-UI resource for configuration data. - * Shows how UIResources work alongside regular resources. - */ server.resource({ name: 'server-config', uri: 'config://server', @@ -153,7 +349,12 @@ server.resource({ port: PORT, version: '1.0.0', widgets: { - registered: ['kanban-board'], + total: 3, + types: { + externalUrl: ['kanban-board'], + rawHtml: ['welcome-card'], + remoteDom: ['quick-poll'] + }, baseUrl: `http://localhost:${PORT}/mcp-use/widgets/` }, endpoints: { @@ -172,7 +373,7 @@ server.listen(PORT) // Display helpful startup message console.log(` ╔═══════════════════════════════════════════════════════════════╗ -║ 🚀 UIResource MCP Server ║ +║ 🎨 UIResource MCP Server (All Types) ║ ╚═══════════════════════════════════════════════════════════════╝ Server is running on port ${PORT} @@ -182,24 +383,42 @@ Server is running on port ${PORT} Inspector UI: http://localhost:${PORT}/inspector Widgets Base: http://localhost:${PORT}/mcp-use/widgets/ -🎯 Available UIResources: +🎯 Available UIResources (3 types demonstrated): + + 1️⃣ External URL Widget (Iframe) • kanban-board - Tool: ui_kanban-board (accepts props as parameters) - Resource: ui://widget/kanban-board (static with defaults) + Tool: ui_kanban-board + Resource: ui://widget/kanban-board Browser: http://localhost:${PORT}/mcp-use/widgets/kanban-board + 2️⃣ Raw HTML Widget (Direct Rendering) + • welcome-card + Tool: ui_welcome-card + Resource: ui://widget/welcome-card + + 3️⃣ Remote DOM Widget (React Components) + • quick-poll + Tool: ui_quick-poll + Resource: ui://widget/quick-poll + 📝 Usage Examples: - // Call as tool with parameters + // External URL - Call with dynamic parameters await client.callTool('ui_kanban-board', { - initialTasks: [...], + initialTasks: [{id: 1, title: 'Task 1'}], theme: 'dark' }) - // Access as resource - await client.readResource('ui://widget/kanban-board') + // Raw HTML - Access as resource + await client.readResource('ui://widget/welcome-card') + + // Remote DOM - Interactive component + await client.callTool('ui_quick-poll', { + question: 'Favorite color?', + options: ['Red', 'Blue', 'Green'] + }) -💡 Tip: Open the Inspector UI to test your widgets interactively! +💡 Tip: Open the Inspector UI to test all widget types interactively! `) // Handle graceful shutdown @@ -208,4 +427,4 @@ process.on('SIGINT', () => { process.exit(0) }) -export default server \ No newline at end of file +export default server From 7221965cf19c42850a8333a5d1e67bc9ccf6f78a Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 18:10:12 +0200 Subject: [PATCH 10/19] add plans --- .../INSPECTOR_INTEGRATION.md | 0 CLAUDE/MCP_UI_IMPLEMENTATION_PLAN.md | 516 ++++++++++++++++++ 2 files changed, 516 insertions(+) rename INSPECTOR_INTEGRATION.md => CLAUDE/INSPECTOR_INTEGRATION.md (100%) create mode 100644 CLAUDE/MCP_UI_IMPLEMENTATION_PLAN.md diff --git a/INSPECTOR_INTEGRATION.md b/CLAUDE/INSPECTOR_INTEGRATION.md similarity index 100% rename from INSPECTOR_INTEGRATION.md rename to CLAUDE/INSPECTOR_INTEGRATION.md diff --git a/CLAUDE/MCP_UI_IMPLEMENTATION_PLAN.md b/CLAUDE/MCP_UI_IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..f14280be --- /dev/null +++ b/CLAUDE/MCP_UI_IMPLEMENTATION_PLAN.md @@ -0,0 +1,516 @@ +# MCP-UI Integration Implementation Plan + +## Overview +This document outlines the plan to implement MCP-UI integration into the mcp-use library, providing a fancy way to expose UI widgets as MCP resources with automatic discovery, prop extraction, and tool generation. + +## Current Architecture Analysis + +### Existing Components +1. **Widget Serving**: The `McpServer` class already serves widgets from `/mcp-use/widgets/*` through `setupWidgetRoutes()` method (mcp-server.ts:445-481) +2. **MCP-UI Support**: The `@mcp-ui/server` package provides `createUIResource` function with support for: + - External URLs with iframe rendering + - Raw HTML content + - Remote DOM scripts +3. **Widget Implementation**: Widgets like the kanban-board are React components that can accept props via URL query parameters +4. **Manual Integration**: Currently requires manual creation of both tools and resources for each widget + +### Key Opportunities +- **Automatic Widget Discovery**: Scan filesystem for widgets and auto-register them +- **Props Extraction**: Parse TypeScript/React component props to generate tool input schemas +- **Unified Interface**: Create a `uiResource` method that handles both tool and resource registration +- **Dynamic URL Generation**: Automatically construct iframe URLs with query parameters based on tool inputs + +## Proposed Architecture + +### Core Concepts + +#### 1. UIResource Method +A specialized method on the McpServer class that: +- Accepts widget configuration (name, path, props) +- Automatically creates both a tool and a UI resource +- Handles prop-to-query-parameter conversion +- Returns UIResource format compatible with MCP-UI + +#### 2. Widget Discovery System +- Scan `dist/resources/mcp-use/widgets/*` directories +- Parse widget manifest files or TypeScript interfaces +- Extract component props and their types +- Generate input schemas automatically + +#### 3. Automatic Tool Generation +- Create tools that return both text and UI resources +- Pass tool inputs as query parameters to widget iframes +- Support complex data types through JSON encoding + +## Implementation Phases + +### Phase 1: Core UIResource Infrastructure + +#### 1.1 Create UIResource Type Definitions +**File**: `packages/mcp-use/src/server/types.ts` + +```typescript +export interface UIResourceDefinition { + name: string + widget: string + title?: string + description?: string + props?: WidgetProps + size?: [string, string] + annotations?: ResourceAnnotations +} + +export interface WidgetProps { + [key: string]: { + type: 'string' | 'number' | 'boolean' | 'object' | 'array' + required?: boolean + default?: any + description?: string + } +} + +export interface WidgetConfig { + name: string + path: string + manifest?: WidgetManifest + component?: string +} +``` + +#### 1.2 Implement uiResource Method +**File**: `packages/mcp-use/src/server/mcp-server.ts` + +Add methods to McpServer class: +```typescript +/** + * Create a UIResource object for a widget with the given parameters + * This method is shared between tool and resource handlers to avoid duplication + */ +private createWidgetUIResource( + widget: string, + params: Record, + size?: [string, string] +): any { + const iframeUrl = this.buildWidgetUrl(widget, params) + + return createUIResource({ + uri: `ui://widget/${widget}`, + content: { + type: 'externalUrl', + iframeUrl + }, + encoding: 'text', + uiMetadata: size ? { + 'preferred-frame-size': size + } : undefined + }) +} + +/** + * Register a widget as both a tool and a resource + * The tool allows passing parameters, the resource provides static access + */ +uiResource(definition: UIResourceDefinition): this { + // Register the tool - returns UIResource with parameters + this.tool({ + name: `ui_${definition.widget}`, + description: definition.description || `Display ${definition.widget} widget`, + inputs: this.convertPropsToInputs(definition.props), + fn: async (params) => { + // Create the UIResource with user-provided params + const uiResource = this.createWidgetUIResource( + definition.widget, + params, + definition.size + ) + + return { + content: [ + { + type: 'text', + text: `Displaying ${definition.title || definition.widget} widget` + }, + uiResource // Reuse the same UIResource + ] + } + } + }) + + // Register the resource - returns UIResource with defaults + this.resource({ + name: definition.name, + uri: `ui://widget/${definition.widget}`, + title: definition.title, + description: definition.description, + mimeType: 'text/html', + annotations: definition.annotations, + fn: async () => { + // Create the UIResource with default/empty params + const uiResource = this.createWidgetUIResource( + definition.widget, + this.applyDefaultProps(definition.props), + definition.size + ) + + return { + contents: [uiResource] // Return the UIResource directly + } + } + }) + + return this +} + +/** + * Apply default values to widget props + */ +private applyDefaultProps(props?: WidgetProps): Record { + if (!props) return {} + + const defaults: Record = {} + for (const [key, prop] of Object.entries(props)) { + if (prop.default !== undefined) { + defaults[key] = prop.default + } + } + return defaults +} +``` + +### Phase 2: Widget Discovery System + +#### 2.1 Create Widget Discovery Module +**File**: `packages/mcp-use/src/server/widget-discovery.ts` + +```typescript +import { readdirSync, existsSync, readFileSync } from 'fs' +import { join } from 'path' + +export interface WidgetManifest { + name: string + title?: string + description?: string + props?: Record + size?: [string, string] +} + +export class WidgetDiscovery { + private widgetsPath: string + + constructor(widgetsPath: string) { + this.widgetsPath = widgetsPath + } + + async discoverWidgets(): Promise { + const widgets: WidgetConfig[] = [] + + if (!existsSync(this.widgetsPath)) { + return widgets + } + + const dirs = readdirSync(this.widgetsPath, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + + for (const dir of dirs) { + const widgetPath = join(this.widgetsPath, dir.name) + const manifestPath = join(widgetPath, 'widget.json') + + if (existsSync(manifestPath)) { + const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) + widgets.push({ + name: dir.name, + path: widgetPath, + manifest + }) + } else { + // Try to auto-detect from index.html or component files + widgets.push({ + name: dir.name, + path: widgetPath + }) + } + } + + return widgets + } +} +``` + +#### 2.2 Add discoverWidgets Method to McpServer +**File**: `packages/mcp-use/src/server/mcp-server.ts` + +```typescript +async discoverWidgets(options?: DiscoverWidgetsOptions): Promise { + const discovery = new WidgetDiscovery( + options?.path || join(process.cwd(), 'dist/resources/mcp-use/widgets') + ) + + const widgets = await discovery.discoverWidgets() + + for (const widget of widgets) { + if (widget.manifest) { + this.uiResource({ + name: widget.name, + widget: widget.name, + title: widget.manifest.title, + description: widget.manifest.description, + props: widget.manifest.props, + size: widget.manifest.size + }) + } else if (options?.autoRegister) { + // Register with minimal configuration + this.uiResource({ + name: widget.name, + widget: widget.name + }) + } + } +} +``` + +### Phase 3: Props and Schema Generation + +#### 3.1 Implement Prop Extraction Utilities +**File**: `packages/mcp-use/src/server/widget-props.ts` + +```typescript +import * as ts from 'typescript' + +export class PropExtractor { + extractPropsFromFile(filePath: string): WidgetProps { + const program = ts.createProgram([filePath], {}) + const sourceFile = program.getSourceFile(filePath) + + if (!sourceFile) return {} + + const props: WidgetProps = {} + + // Find interface or type definitions for props + ts.forEachChild(sourceFile, (node) => { + if (ts.isInterfaceDeclaration(node) && + node.name?.text.includes('Props')) { + node.members.forEach(member => { + if (ts.isPropertySignature(member) && member.name) { + const propName = member.name.getText() + const propType = this.getTypeString(member.type) + const isOptional = !!member.questionToken + + props[propName] = { + type: this.mapTsTypeToSchemaType(propType), + required: !isOptional + } + } + }) + } + }) + + return props + } + + private mapTsTypeToSchemaType(tsType: string): string { + switch (tsType) { + case 'string': return 'string' + case 'number': return 'number' + case 'boolean': return 'boolean' + case 'any[]': + case 'Array': return 'array' + default: return 'object' + } + } +} +``` + +#### 3.2 Create Query Parameter Builder +**File**: `packages/mcp-use/src/server/mcp-server.ts` (addition) + +```typescript +private buildWidgetUrl(widget: string, params: Record): string { + const baseUrl = `http://localhost:${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 !== undefined && value !== null) { + if (typeof value === 'object') { + queryParams.append(key, JSON.stringify(value)) + } else { + queryParams.append(key, String(value)) + } + } + } + + return `${baseUrl}?${queryParams.toString()}` +} + +private convertPropsToInputs(props?: WidgetProps): InputDefinition[] { + if (!props) return [] + + return Object.entries(props).map(([name, prop]) => ({ + name, + type: prop.type, + description: prop.description, + required: prop.required, + default: prop.default + })) +} +``` + +### Phase 4: Widget Manifest System + +#### 4.1 Define Widget Manifest Format +**File**: `widget.json` (example for kanban-board) + +```json +{ + "name": "kanban-board", + "title": "Kanban Board", + "description": "Interactive task management board with drag-and-drop", + "version": "1.0.0", + "props": { + "initialTasks": { + "type": "array", + "description": "Initial tasks to display on the board", + "required": false + }, + "columns": { + "type": "array", + "description": "Column configuration", + "required": false, + "default": [ + { "id": "todo", "title": "To Do" }, + { "id": "in-progress", "title": "In Progress" }, + { "id": "done", "title": "Done" } + ] + }, + "theme": { + "type": "string", + "description": "Visual theme (light/dark)", + "required": false, + "default": "light" + } + }, + "size": ["900px", "600px"], + "assets": { + "main": "index.html", + "scripts": ["assets/index.js"], + "styles": ["assets/style.css"] + } +} +``` + +#### 4.2 Update Build Process +**File**: `packages/mcp-use-cli/src/commands/build.ts` (conceptual) + +- Add step to scan for React/TypeScript components +- Extract prop interfaces automatically +- Generate widget.json if not present +- Bundle widgets with manifests + +### Phase 5: Integration and Testing + +#### 5.1 Update Server Template +**File**: `packages/create-mcp-use-app/src/templates/ui/src/server.ts` + +```typescript +import { createMCPServer } from 'mcp-use' + +const server = createMCPServer('ui-mcp-server', { + version: '1.0.0', + description: 'MCP server with auto-discovered UI widgets', +}) + +const PORT = process.env.PORT || 3000 + +// Manual widget registration with full control +server.uiResource({ + name: 'kanban-board', + widget: 'kanban-board', + title: 'Kanban Board', + description: 'Task management with drag-and-drop', + props: { + initialTasks: { + type: 'array', + description: 'Initial task list', + required: false + }, + theme: { + type: 'string', + description: 'Visual theme', + default: 'light' + } + }, + size: ['900px', '600px'] +}) + +// OR: Automatic discovery (alternative approach) +await server.discoverWidgets({ + path: './dist/resources/mcp-use/widgets', + autoRegister: true +}) + +server.listen(PORT) +``` + +#### 5.2 Create Example Widgets + +**Additional widgets to create**: +1. **Chart Widget** - Data visualization with configurable chart type +2. **Form Builder** - Dynamic form with field configuration +3. **Data Table** - Sortable/filterable table with pagination + +Each widget should: +- Have TypeScript prop interfaces +- Include a widget.json manifest +- Support query parameter initialization +- Demonstrate different prop types + +## Benefits of This Implementation + +### Developer Experience +- **Simplified API**: Single `uiResource` method instead of separate tool and resource definitions +- **Auto-discovery**: Widgets automatically registered from filesystem +- **Type Safety**: Props extracted from TypeScript interfaces +- **Zero Config**: Works out of the box with sensible defaults + +### Features +- **Automatic Tool Generation**: Each widget gets a corresponding tool +- **Props to Query Params**: Seamless data passing to widgets +- **Manifest System**: Declarative widget configuration +- **Asset Management**: Automatic handling of JS/CSS assets + +### Extensibility +- **Plugin Architecture**: Easy to add new widget types +- **Custom Prop Types**: Support for complex data structures +- **Framework Agnostic**: Works with React, Vue, or vanilla JS +- **Build Integration**: Hooks into existing build pipeline + +## Migration Path + +For existing implementations: +1. Keep backward compatibility with manual tool/resource registration +2. Add deprecation warnings for old patterns +3. Provide migration tool to generate manifests from existing code +4. Document migration guide with examples + +## Success Criteria + +- [ ] Widgets can be registered with a single method call +- [ ] Automatic discovery finds and registers all widgets in a directory +- [ ] Props are extracted from TypeScript interfaces +- [ ] Tool inputs are converted to widget props via query parameters +- [ ] Each widget exposes both tool and resource endpoints +- [ ] UIResources render correctly in MCP-UI compatible clients +- [ ] Documentation and examples are comprehensive + +## Next Steps + +1. Implement Phase 1 (Core Infrastructure) +2. Test with existing kanban-board widget +3. Implement Phase 2 (Discovery System) +4. Create additional example widgets +5. Write comprehensive documentation +6. Create migration guide for existing users \ No newline at end of file From 5948480f1af854128ebdd35802a08932db7bd51a Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 18:11:59 +0200 Subject: [PATCH 11/19] add test_app --- pnpm-lock.yaml | 116 ++++++++++++++++++++++++++++++++++++++++++++ pnpm-workspace.yaml | 2 + tsconfig.json | 3 +- 3 files changed, 120 insertions(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9985e3d4..ad48a354 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -171,6 +171,64 @@ importers: specifier: ^5.0.0 version: 5.9.3 + packages/create-mcp-use-app/src/templates/uiresource: + dependencies: + '@mcp-ui/server': + specifier: ^5.11.0 + version: 5.12.0 + cors: + specifier: ^2.8.5 + version: 2.8.5 + express: + specifier: ^4.18.0 + version: 4.21.2 + mcp-use: + specifier: workspace:* + version: link:../../../../mcp-use + devDependencies: + '@mcp-use/cli': + specifier: workspace:* + version: link:../../../../cli + '@mcp-use/inspector': + specifier: workspace:* + version: link:../../../../inspector + '@types/cors': + specifier: ^2.8.0 + version: 2.8.19 + '@types/express': + specifier: ^4.17.0 + version: 4.17.23 + '@types/node': + specifier: ^20.0.0 + version: 20.19.19 + '@types/react': + specifier: ^18.0.0 + version: 18.3.26 + '@types/react-dom': + specifier: ^18.0.0 + version: 18.3.7(@types/react@18.3.26) + concurrently: + specifier: ^8.0.0 + version: 8.2.2 + esbuild: + specifier: ^0.23.0 + version: 0.23.1 + globby: + specifier: ^14.0.2 + version: 14.1.0 + react: + specifier: ^19.2.0 + version: 19.2.0 + react-dom: + specifier: ^19.2.0 + version: 19.2.0(react@19.2.0) + tsx: + specifier: ^4.0.0 + version: 4.20.6 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + packages/inspector: dependencies: '@hono/node-server': @@ -511,6 +569,64 @@ importers: specifier: ^5.0.0 version: 5.9.3 + test_app: + dependencies: + '@mcp-ui/server': + specifier: ^5.11.0 + version: 5.12.0 + cors: + specifier: ^2.8.5 + version: 2.8.5 + express: + specifier: ^4.18.0 + version: 4.21.2 + mcp-use: + specifier: workspace:* + version: link:../packages/mcp-use + devDependencies: + '@mcp-use/cli': + specifier: workspace:* + version: link:../packages/cli + '@mcp-use/inspector': + specifier: workspace:* + version: link:../packages/inspector + '@types/cors': + specifier: ^2.8.0 + version: 2.8.19 + '@types/express': + specifier: ^4.17.0 + version: 4.17.23 + '@types/node': + specifier: ^20.0.0 + version: 20.19.19 + '@types/react': + specifier: ^18.0.0 + version: 18.3.26 + '@types/react-dom': + specifier: ^18.0.0 + version: 18.3.7(@types/react@18.3.26) + concurrently: + specifier: ^8.0.0 + version: 8.2.2 + esbuild: + specifier: ^0.23.0 + version: 0.23.1 + globby: + specifier: ^14.0.2 + version: 14.1.0 + react: + specifier: ^19.2.0 + version: 19.2.0 + react-dom: + specifier: ^19.2.0 + version: 19.2.0(react@19.2.0) + tsx: + specifier: ^4.0.0 + version: 4.20.6 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + packages: '@ai-sdk/provider-utils@2.2.8': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 28363ba3..04bd672c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,7 @@ packages: - 'packages/*' + - 'test_app' - 'packages/mcp-use/examples/client/react' - 'packages/mcp-use/examples/server/simple' - 'packages/create-mcp-use-app/src/templates/ui' + - 'packages/create-mcp-use-app/src/templates/uiresource' diff --git a/tsconfig.json b/tsconfig.json index e51e3b43..ea829e2f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,7 @@ { "path": "./packages/mcp-use" }, { "path": "./packages/inspector" }, { "path": "./packages/cli" }, - { "path": "./packages/create-mcp-use-app" } + { "path": "./packages/create-mcp-use-app" }, + { "path": "./test_app" } ] } From 2523e2eac022c492b15279bac9dc85e17bf556eb Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 18:14:49 +0200 Subject: [PATCH 12/19] minor --- pnpm-lock.yaml | 58 -------------------------------------------------- 1 file changed, 58 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad48a354..4f23c8f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -569,64 +569,6 @@ importers: specifier: ^5.0.0 version: 5.9.3 - test_app: - dependencies: - '@mcp-ui/server': - specifier: ^5.11.0 - version: 5.12.0 - cors: - specifier: ^2.8.5 - version: 2.8.5 - express: - specifier: ^4.18.0 - version: 4.21.2 - mcp-use: - specifier: workspace:* - version: link:../packages/mcp-use - devDependencies: - '@mcp-use/cli': - specifier: workspace:* - version: link:../packages/cli - '@mcp-use/inspector': - specifier: workspace:* - version: link:../packages/inspector - '@types/cors': - specifier: ^2.8.0 - version: 2.8.19 - '@types/express': - specifier: ^4.17.0 - version: 4.17.23 - '@types/node': - specifier: ^20.0.0 - version: 20.19.19 - '@types/react': - specifier: ^18.0.0 - version: 18.3.26 - '@types/react-dom': - specifier: ^18.0.0 - version: 18.3.7(@types/react@18.3.26) - concurrently: - specifier: ^8.0.0 - version: 8.2.2 - esbuild: - specifier: ^0.23.0 - version: 0.23.1 - globby: - specifier: ^14.0.2 - version: 14.1.0 - react: - specifier: ^19.2.0 - version: 19.2.0 - react-dom: - specifier: ^19.2.0 - version: 19.2.0(react@19.2.0) - tsx: - specifier: ^4.0.0 - version: 4.20.6 - typescript: - specifier: ^5.0.0 - version: 5.9.3 - packages: '@ai-sdk/provider-utils@2.2.8': From b7cbdb0341285e98a9b6fa045c57a4fe4e7ce795 Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 18:16:35 +0200 Subject: [PATCH 13/19] changelog --- .changeset/clever-vans-rhyme.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .changeset/clever-vans-rhyme.md diff --git a/.changeset/clever-vans-rhyme.md b/.changeset/clever-vans-rhyme.md new file mode 100644 index 00000000..966a7e15 --- /dev/null +++ b/.changeset/clever-vans-rhyme.md @@ -0,0 +1,16 @@ +--- +'create-mcp-use-app': patch +'mcp-use': patch +--- + +Add MCP-UI Resource Integration + +Add uiResource() method to McpServer for unified widget registration with MCP-UI compatibility. + +- Support three resource types: externalUrl (iframe), rawHtml (direct), remoteDom (scripted) +- Automatic tool and resource generation with ui\_ prefix and ui://widget/ URIs +- Props-to-parameters conversion with type safety +- New uiresource template with examples +- Inspector integration for UI resource rendering +- Add @mcp-ui/server dependency +- Complete test coverage From 4e6f0bf67a3f12f90dbc8b26e8db8ea386752e0a Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 18:21:56 +0200 Subject: [PATCH 14/19] update package with /server --- CLAUDE/MCP_UI_IMPLEMENTATION_PLAN.md | 2 +- packages/create-mcp-use-app/src/templates/uiresource/README.md | 2 +- .../create-mcp-use-app/src/templates/uiresource/src/server.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CLAUDE/MCP_UI_IMPLEMENTATION_PLAN.md b/CLAUDE/MCP_UI_IMPLEMENTATION_PLAN.md index f14280be..824d0416 100644 --- a/CLAUDE/MCP_UI_IMPLEMENTATION_PLAN.md +++ b/CLAUDE/MCP_UI_IMPLEMENTATION_PLAN.md @@ -416,7 +416,7 @@ private convertPropsToInputs(props?: WidgetProps): InputDefinition[] { **File**: `packages/create-mcp-use-app/src/templates/ui/src/server.ts` ```typescript -import { createMCPServer } from 'mcp-use' +import { createMCPServer } from 'mcp-use/server' const server = createMCPServer('ui-mcp-server', { version: '1.0.0', diff --git a/packages/create-mcp-use-app/src/templates/uiresource/README.md b/packages/create-mcp-use-app/src/templates/uiresource/README.md index ed483a57..5e2b2208 100644 --- a/packages/create-mcp-use-app/src/templates/uiresource/README.md +++ b/packages/create-mcp-use-app/src/templates/uiresource/README.md @@ -68,7 +68,7 @@ npm start ### Simple Widget Registration ```typescript -import { createMCPServer } from 'mcp-use' +import { createMCPServer } from 'mcp-use/server' const server = createMCPServer('my-server', { version: '1.0.0', diff --git a/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts b/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts index f26b9a10..68e88006 100644 --- a/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts +++ b/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts @@ -1,4 +1,4 @@ -import { createMCPServer } from 'mcp-use' +import { createMCPServer } from 'mcp-use/server' import type { ExternalUrlUIResource, RawHtmlUIResource, From b362e0cc3294227d21331abb35f2e1582638ca4d Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 18:22:07 +0200 Subject: [PATCH 15/19] add uiresource to readme --- packages/mcp-use/README.md | 99 +++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/packages/mcp-use/README.md b/packages/mcp-use/README.md index aeeadf4c..03ddcc44 100644 --- a/packages/mcp-use/README.md +++ b/packages/mcp-use/README.md @@ -532,7 +532,104 @@ server.listen(3000) | **♻️ Hot Reload** | Development mode with automatic reloading | | **📊 Observability** | Built-in logging and monitoring capabilities | -### Building UI Widgets +### MCP-UI Resources + +MCP-Use provides a unified `uiResource()` method for registering interactive UI widgets that are compatible with MCP-UI clients. This automatically creates both a tool (for dynamic parameters) and a resource (for static access). + +#### Quick Start + +```ts +import { createMCPServer } from 'mcp-use/server' + +const server = createMCPServer('my-server', { version: '1.0.0' }) + +// Register a widget - creates both tool and resource automatically +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', + required: false + }, + theme: { + type: 'string', + default: 'light' + } + }, + size: ['900px', '600px'] +}) + +server.listen(3000) +``` + +This automatically creates: +- **Tool**: `ui_kanban-board` - Accepts parameters and returns UIResource +- **Resource**: `ui://widget/kanban-board` - Static access with defaults + +#### Three Resource Types + +**1. External URL (Iframe)** +Serve widgets from your filesystem via iframe: + +```ts +server.uiResource({ + type: 'externalUrl', + name: 'dashboard', + widget: 'dashboard', + props: { userId: { type: 'string', required: true } } +}) +``` + +**2. Raw HTML** +Direct HTML content rendering: + +```ts +server.uiResource({ + type: 'rawHtml', + name: 'welcome-card', + htmlContent: ` + + +

Welcome!

+ + ` +}) +``` + +**3. Remote DOM** +Interactive components using MCP-UI React components: + +```ts +server.uiResource({ + type: 'remoteDom', + name: 'quick-poll', + script: ` + const button = document.createElement('ui-button'); + button.setAttribute('label', 'Vote'); + root.appendChild(button); + `, + framework: 'react' +}) +``` + +#### Get Started with Templates + +```bash +# Create a new project with UIResource examples +npx create-mcp-use-app my-app +# Select: "MCP Server with UIResource widgets" + +cd my-app +npm install +npm run dev +``` + +### Building Custom UI Widgets MCP-Use supports building custom UI widgets for your MCP tools using React: From c55ee63bb4af027aab39c599241aea11f0073265 Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 18:58:15 +0200 Subject: [PATCH 16/19] cleanup: move helpers into test file --- .../src/server/adapters/mcp-ui-adapter.ts | 127 ---------------- packages/mcp-use/src/server/index.ts | 2 - .../tests/helpers/widget-generators.ts | 137 ++++++++++++++++++ packages/mcp-use/tests/mcp-ui-adapter.test.ts | 6 +- pnpm-lock.yaml | 58 ++++++++ 5 files changed, 199 insertions(+), 131 deletions(-) create mode 100644 packages/mcp-use/tests/helpers/widget-generators.ts diff --git a/packages/mcp-use/src/server/adapters/mcp-ui-adapter.ts b/packages/mcp-use/src/server/adapters/mcp-ui-adapter.ts index e6a5ebb4..2f935569 100644 --- a/packages/mcp-use/src/server/adapters/mcp-ui-adapter.ts +++ b/packages/mcp-use/src/server/adapters/mcp-ui-adapter.ts @@ -158,130 +158,3 @@ export function createUIResourceFromDefinition( } } -/** - * Generate HTML content for a widget (utility function) - * - * @param definition - Base UI resource definition - * @param props - Widget properties to inject - * @returns Generated HTML string - */ -export function generateWidgetHtml( - definition: Pick, - props?: Record -): string { - const [width = '100%', height = '400px'] = definition.size || [] - const propsJson = props ? JSON.stringify(props) : '{}' - - return ` - - - - ${definition.title || definition.name} - - - -
-
${definition.title || definition.name}
- ${definition.description ? `
${definition.description}
` : ''} -
-
- - -` -} - -/** - * Generate a Remote DOM script for a widget (utility function) - * - * @param definition - Base UI resource definition - * @param props - Widget properties to inject - * @returns Generated JavaScript string - */ -export function generateRemoteDomScript( - definition: Pick, - props?: Record -): string { - return ` -// Remote DOM script for ${definition.name} -const container = document.createElement('div'); -container.style.padding = '20px'; - -// Create title -const title = document.createElement('h2'); -title.textContent = '${definition.title || definition.name}'; -container.appendChild(title); - -${definition.description ? ` -// Add description -const description = document.createElement('p'); -description.textContent = '${definition.description}'; -description.style.color = '#666'; -container.appendChild(description); -` : ''} - -// Widget props -const props = ${JSON.stringify(props || {})}; - -// Create interactive button -const button = document.createElement('ui-button'); -button.setAttribute('label', 'Interact with ${definition.name}'); -button.addEventListener('press', () => { - window.parent.postMessage({ - type: 'tool', - payload: { - toolName: 'ui_${definition.name}', - params: props - } - }, '*'); -}); -container.appendChild(button); - -// Add custom widget logic here -console.log('Remote DOM widget ${definition.name} initialized with props:', props); - -// Append to root -root.appendChild(container);` -} diff --git a/packages/mcp-use/src/server/index.ts b/packages/mcp-use/src/server/index.ts index 1b136551..1d7432f3 100644 --- a/packages/mcp-use/src/server/index.ts +++ b/packages/mcp-use/src/server/index.ts @@ -12,7 +12,5 @@ export { createRawHtmlResource, createRemoteDomResource, createUIResourceFromDefinition, - generateWidgetHtml, - generateRemoteDomScript, type UrlConfig } from './adapters/mcp-ui-adapter.js' diff --git a/packages/mcp-use/tests/helpers/widget-generators.ts b/packages/mcp-use/tests/helpers/widget-generators.ts new file mode 100644 index 00000000..b1baa881 --- /dev/null +++ b/packages/mcp-use/tests/helpers/widget-generators.ts @@ -0,0 +1,137 @@ +/** + * Test Helper Utilities for Widget Generation + * + * These functions generate HTML and JavaScript content for testing purposes. + * They are not part of the core UIResource creation flow. + */ + +import type { UIResourceDefinition } from '../../src/server/types/resource.js' + +/** + * Generate HTML content for a widget (utility function for tests) + * + * @param definition - Base UI resource definition + * @param props - Widget properties to inject + * @returns Generated HTML string + */ +export function generateWidgetHtml( + definition: Pick, + props?: Record +): string { + const [width = '100%', height = '400px'] = definition.size || [] + const propsJson = props ? JSON.stringify(props) : '{}' + + return ` + + + + ${definition.title || definition.name} + + + +
+
${definition.title || definition.name}
+ ${definition.description ? `
${definition.description}
` : ''} +
+
+ + +` +} + +/** + * Generate a Remote DOM script for a widget (utility function for tests) + * + * @param definition - Base UI resource definition + * @param props - Widget properties to inject + * @returns Generated JavaScript string + */ +export function generateRemoteDomScript( + definition: Pick, + props?: Record +): string { + return ` +// Remote DOM script for ${definition.name} +const container = document.createElement('div'); +container.style.padding = '20px'; + +// Create title +const title = document.createElement('h2'); +title.textContent = '${definition.title || definition.name}'; +container.appendChild(title); + +${definition.description ? ` +// Add description +const description = document.createElement('p'); +description.textContent = '${definition.description}'; +description.style.color = '#666'; +container.appendChild(description); +` : ''} + +// Widget props +const props = ${JSON.stringify(props || {})}; + +// Create interactive button +const button = document.createElement('ui-button'); +button.setAttribute('label', 'Interact with ${definition.name}'); +button.addEventListener('press', () => { + window.parent.postMessage({ + type: 'tool', + payload: { + toolName: 'ui_${definition.name}', + params: props + } + }, '*'); +}); +container.appendChild(button); + +// Add custom widget logic here +console.log('Remote DOM widget ${definition.name} initialized with props:', props); + +// Append to root +root.appendChild(container);` +} + diff --git a/packages/mcp-use/tests/mcp-ui-adapter.test.ts b/packages/mcp-use/tests/mcp-ui-adapter.test.ts index 242a2d43..7d659a1f 100644 --- a/packages/mcp-use/tests/mcp-ui-adapter.test.ts +++ b/packages/mcp-use/tests/mcp-ui-adapter.test.ts @@ -12,10 +12,12 @@ import { createRawHtmlResource, createRemoteDomResource, createUIResourceFromDefinition, - generateWidgetHtml, - generateRemoteDomScript, type UrlConfig } from '../src/server/adapters/mcp-ui-adapter.js' +import { + generateWidgetHtml, + generateRemoteDomScript +} from './helpers/widget-generators.js' import type { ExternalUrlUIResource, RawHtmlUIResource, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f23c8f2..ad48a354 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -569,6 +569,64 @@ importers: specifier: ^5.0.0 version: 5.9.3 + test_app: + dependencies: + '@mcp-ui/server': + specifier: ^5.11.0 + version: 5.12.0 + cors: + specifier: ^2.8.5 + version: 2.8.5 + express: + specifier: ^4.18.0 + version: 4.21.2 + mcp-use: + specifier: workspace:* + version: link:../packages/mcp-use + devDependencies: + '@mcp-use/cli': + specifier: workspace:* + version: link:../packages/cli + '@mcp-use/inspector': + specifier: workspace:* + version: link:../packages/inspector + '@types/cors': + specifier: ^2.8.0 + version: 2.8.19 + '@types/express': + specifier: ^4.17.0 + version: 4.17.23 + '@types/node': + specifier: ^20.0.0 + version: 20.19.19 + '@types/react': + specifier: ^18.0.0 + version: 18.3.26 + '@types/react-dom': + specifier: ^18.0.0 + version: 18.3.7(@types/react@18.3.26) + concurrently: + specifier: ^8.0.0 + version: 8.2.2 + esbuild: + specifier: ^0.23.0 + version: 0.23.1 + globby: + specifier: ^14.0.2 + version: 14.1.0 + react: + specifier: ^19.2.0 + version: 19.2.0 + react-dom: + specifier: ^19.2.0 + version: 19.2.0(react@19.2.0) + tsx: + specifier: ^4.0.0 + version: 4.20.6 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + packages: '@ai-sdk/provider-utils@2.2.8': From 51c596025803ff179cf5b8dc82cdc6cd7ffd9249 Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 19:06:15 +0200 Subject: [PATCH 17/19] remove unused --- .../src/templates/uiresource/src/server.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts b/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts index 68e88006..2e92fe22 100644 --- a/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts +++ b/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts @@ -49,11 +49,6 @@ server.uiResource({ description: 'Column configuration for the board', required: false, } - }, - size: ['900px', '600px'], - annotations: { - audience: ['user', 'assistant'], - priority: 0.8 } } satisfies ExternalUrlUIResource) From d11870f5a08e4abfca7405077d2b2a0a8f1f74ad Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 19:22:36 +0200 Subject: [PATCH 18/19] fix imports --- .../src/templates/uiresource/README.md | 2 +- .../src/templates/uiresource/src/server.ts | 2 +- .../src/templates/uiresource/tsconfig.json | 2 +- packages/mcp-use/index.ts | 22 ------------------- packages/mcp-use/src/server/index.ts | 20 +++++++++++++++++ .../tests/helpers/widget-generators.ts | 2 +- packages/mcp-use/tests/mcp-ui-adapter.test.ts | 2 +- 7 files changed, 25 insertions(+), 27 deletions(-) diff --git a/packages/create-mcp-use-app/src/templates/uiresource/README.md b/packages/create-mcp-use-app/src/templates/uiresource/README.md index 5e2b2208..dc8a92aa 100644 --- a/packages/create-mcp-use-app/src/templates/uiresource/README.md +++ b/packages/create-mcp-use-app/src/templates/uiresource/README.md @@ -338,7 +338,7 @@ const resource = await client.readResource('ui://widget/kanban-board') - Complex objects must be JSON-stringified ### Type Errors -- Import types: `import type { UIResourceDefinition } from 'mcp-use'` +- Import types: `import type { UIResourceDefinition } from 'mcp-use/server'` - Ensure mcp-use is updated to latest version ## Migration from Old Pattern diff --git a/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts b/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts index 2e92fe22..5ae629ad 100644 --- a/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts +++ b/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts @@ -3,7 +3,7 @@ import type { ExternalUrlUIResource, RawHtmlUIResource, RemoteDomUIResource -} from 'mcp-use' +} from 'mcp-use/server' // Create an MCP server with UIResource support const server = createMCPServer('uiresource-mcp-server', { diff --git a/packages/create-mcp-use-app/src/templates/uiresource/tsconfig.json b/packages/create-mcp-use-app/src/templates/uiresource/tsconfig.json index 5a2c6947..cafb2cf5 100644 --- a/packages/create-mcp-use-app/src/templates/uiresource/tsconfig.json +++ b/packages/create-mcp-use-app/src/templates/uiresource/tsconfig.json @@ -3,7 +3,7 @@ "target": "ES2022", "jsx": "react-jsx", "module": "ESNext", - "moduleResolution": "node", + "moduleResolution": "bundler", "allowJs": true, "strict": true, "declaration": true, diff --git a/packages/mcp-use/index.ts b/packages/mcp-use/index.ts index e5bade4e..b447e02d 100644 --- a/packages/mcp-use/index.ts +++ b/packages/mcp-use/index.ts @@ -20,28 +20,6 @@ export * from './src/managers/tools/index.js' // Export observability utilities export { type ObservabilityConfig, ObservabilityManager } from './src/observability/index.js' -// Export server utilities -export { createMCPServer } from './src/server/index.js' - -export type { - InputDefinition, - PromptDefinition, - PromptHandler, - ResourceDefinition, - ResourceHandler, - ServerConfig, - ToolDefinition, - ToolHandler, - // UIResource specific types - UIResourceDefinition, - ExternalUrlUIResource, - RawHtmlUIResource, - RemoteDomUIResource, - WidgetProps, - WidgetConfig, - WidgetManifest, - DiscoverWidgetsOptions, -} from './src/server/types.js' // Export telemetry utilities export { setTelemetrySource, Telemetry } from './src/telemetry/index.js' diff --git a/packages/mcp-use/src/server/index.ts b/packages/mcp-use/src/server/index.ts index 1d7432f3..db042e3b 100644 --- a/packages/mcp-use/src/server/index.ts +++ b/packages/mcp-use/src/server/index.ts @@ -14,3 +14,23 @@ export { createUIResourceFromDefinition, type UrlConfig } from './adapters/mcp-ui-adapter.js' + +export type { + InputDefinition, + PromptDefinition, + PromptHandler, + ResourceDefinition, + ResourceHandler, + ServerConfig, + ToolDefinition, + ToolHandler, + // UIResource specific types + UIResourceDefinition, + ExternalUrlUIResource, + RawHtmlUIResource, + RemoteDomUIResource, + WidgetProps, + WidgetConfig, + WidgetManifest, + DiscoverWidgetsOptions, +} from './types/index.js' \ No newline at end of file diff --git a/packages/mcp-use/tests/helpers/widget-generators.ts b/packages/mcp-use/tests/helpers/widget-generators.ts index b1baa881..8f2a4a66 100644 --- a/packages/mcp-use/tests/helpers/widget-generators.ts +++ b/packages/mcp-use/tests/helpers/widget-generators.ts @@ -5,7 +5,7 @@ * They are not part of the core UIResource creation flow. */ -import type { UIResourceDefinition } from '../../src/server/types/resource.js' +import type { UIResourceDefinition } from 'mcp-use/server' /** * Generate HTML content for a widget (utility function for tests) diff --git a/packages/mcp-use/tests/mcp-ui-adapter.test.ts b/packages/mcp-use/tests/mcp-ui-adapter.test.ts index 7d659a1f..dc985a6a 100644 --- a/packages/mcp-use/tests/mcp-ui-adapter.test.ts +++ b/packages/mcp-use/tests/mcp-ui-adapter.test.ts @@ -22,7 +22,7 @@ import type { ExternalUrlUIResource, RawHtmlUIResource, RemoteDomUIResource -} from '../src/server/types/resource.js' +} from 'mcp-use/server' describe('MCP-UI Adapter', () => { const urlConfig: UrlConfig = { From 884f86b8927b219fdf3a9961d8dbe3e0732d060c Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Tue, 14 Oct 2025 19:25:15 +0200 Subject: [PATCH 19/19] never export serrver modules --- packages/mcp-use/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/mcp-use/index.ts b/packages/mcp-use/index.ts index b447e02d..12b72569 100644 --- a/packages/mcp-use/index.ts +++ b/packages/mcp-use/index.ts @@ -1,3 +1,14 @@ +/** + * Main package exports for MCP client and MCP agent functionality + * + * This file serves as the primary entry point for consuming MCP (Model Context Protocol) + * functionality in client applications and agent implementations. It exports all necessary + * classes, utilities, and types for building MCP-based applications. + * + * @important Server functionality is exported from ./src/server/index.js - + * do NOT export server-related modules from this file. + */ + import { MCPAgent } from './src/agents/mcp_agent.js' import { RemoteAgent } from './src/agents/remote.js' import { MCPClient } from './src/client.js' @@ -41,3 +52,4 @@ export { AIMessage, BaseMessage, HumanMessage, SystemMessage, ToolMessage } from export type { StreamEvent } from '@langchain/core/tracers/log_stream' export { BaseConnector, HttpConnector, loadConfigFile, Logger, logger, MCPAgent, MCPClient, MCPSession, RemoteAgent, StdioConnector, WebSocketConnector } +