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 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..824d0416 --- /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/server' + +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 diff --git a/package.json b/package.json index e9278a44..86db6af0 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "packageManager": "pnpm@10.6.1", "pnpm": { "patchedDependencies": { - "@mcp-ui/server@5.11.0": "patches/@mcp-ui__server@5.11.0.patch" }, "overrides": { "mcp-use": "workspace:*", 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) } 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..dc8a92aa --- /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/server' + +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/server'` +- 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..5ae629ad --- /dev/null +++ b/packages/create-mcp-use-app/src/templates/uiresource/src/server.ts @@ -0,0 +1,425 @@ +import { createMCPServer } from 'mcp-use/server' +import type { + ExternalUrlUIResource, + RawHtmlUIResource, + RemoteDomUIResource +} from 'mcp-use/server' + +// Create an MCP server with UIResource support +const server = createMCPServer('uiresource-mcp-server', { + version: '1.0.0', + description: 'MCP server demonstrating all UIResource types', +}) + +const PORT = process.env.PORT || 3000 + +/** + * ════════════════════════════════════════════════════════════════════ + * Type 1: External URL (Iframe Widget) + * ════════════════════════════════════════════════════════════════════ + * + * 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. Serves the widget from dist/resources/mcp-use/widgets/kanban-board/ + */ +server.uiResource({ + type: 'externalUrl', + 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, + } + } +} 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
+
+
+ +

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

+
+ + + `, + encoding: 'text', + size: ['600px', '400px'] +} satisfies RawHtmlUIResource) + +/** + * ════════════════════════════════════════════════════════════════════ + * 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({ + 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); + } + }); + + 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: { + question: { + type: 'string', + description: 'The poll question', + default: 'What is your favorite framework?' + }, + options: { + type: 'array', + description: 'Poll options', + default: ['React', 'Vue', 'Svelte'] + } + } +} satisfies RemoteDomUIResource) + +/** + * ════════════════════════════════════════════════════════════════════ + * Traditional MCP Tools and Resources + * ════════════════════════════════════════════════════════════════════ + * + * You can mix UIResources with traditional MCP tools and resources + */ + +server.tool({ + name: 'get-widget-info', + description: 'Get information about available UI widgets', + fn: async () => { + 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' + } + ] + + return { + content: [{ + type: 'text', + text: `Available UI Widgets:\n\n${widgets.map(w => + `📦 ${w.name} (${w.type})\n` + + ` Tool: ${w.tool}\n` + + ` Resource: ${w.resource}\n` + + (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` + }] + } + } +}) + +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: { + total: 3, + types: { + externalUrl: ['kanban-board'], + rawHtml: ['welcome-card'], + remoteDom: ['quick-poll'] + }, + 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 (All Types) ║ +╚═══════════════════════════════════════════════════════════════╝ + +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 (3 types demonstrated): + + 1️⃣ External URL Widget (Iframe) + • kanban-board + 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: + + // External URL - Call with dynamic parameters + await client.callTool('ui_kanban-board', { + initialTasks: [{id: 1, title: 'Task 1'}], + theme: 'dark' + }) + + // 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 all widget types interactively! +`) + +// Handle graceful shutdown +process.on('SIGINT', () => { + console.log('\n\nShutting down server...') + process.exit(0) +}) + +export default 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 new file mode 100644 index 00000000..cafb2cf5 --- /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": "bundler", + "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"] +} 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: 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 } + diff --git a/packages/mcp-use/package.json b/packages/mcp-use/package.json index d7cca0f2..62f31271 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/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..2f935569 --- /dev/null +++ b/packages/mcp-use/src/server/adapters/mcp-ui-adapter.ts @@ -0,0 +1,160 @@ +/** + * MCP-UI Adapter Utilities + * + * Pure functions to convert mcp-use high-level UIResource definitions + * into @mcp-ui/server compatible resource objects. + * + * Ref: https://mcpui.dev/guide/server/typescript/usage-examples + */ + +import { createUIResource } from '@mcp-ui/server' +import type { + UIResourceContent, + UIResourceDefinition, + UIEncoding +} from '../types/resource.js' + +/** + * Configuration for building widget URLs + */ +export interface UrlConfig { + baseUrl: string + port: number | string +} + +/** + * 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 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() +} + +/** + * 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 function 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 - Resource URI (must start with ui://) + * @param htmlString - HTML content to render + * @param encoding - Encoding type ('text' or 'blob') + * @returns UIResourceContent object + */ +export function 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 + * + * @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 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 + }) +} + +/** + * 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 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) + } + + case 'rawHtml': { + return createRawHtmlResource(uri, definition.htmlContent, encoding) + } + + case 'remoteDom': { + const framework = definition.framework || 'react' + return createRemoteDomResource(uri, definition.script, framework, encoding) + } + + default: { + // TypeScript exhaustiveness check + const _exhaustive: never = definition + throw new Error(`Unknown UI resource type: ${(_exhaustive as any).type}`) + } + } +} + diff --git a/packages/mcp-use/src/server/index.ts b/packages/mcp-use/src/server/index.ts index 52e80796..db042e3b 100644 --- a/packages/mcp-use/src/server/index.ts +++ b/packages/mcp-use/src/server/index.ts @@ -1,6 +1,20 @@ -export { - createMCPServer +export { + createMCPServer, + type McpServerInstance } from './mcp-server.js' + +export * from './types/index.js' + +// MCP-UI adapter utility functions +export { + buildWidgetUrl, + createExternalUrlResource, + createRawHtmlResource, + createRemoteDomResource, + createUIResourceFromDefinition, + type UrlConfig +} from './adapters/mcp-ui-adapter.js' + export type { InputDefinition, PromptDefinition, @@ -10,4 +24,13 @@ export type { ServerConfig, ToolDefinition, ToolHandler, -} from './types.js' + // 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/src/server/mcp-server.ts b/packages/mcp-use/src/server/mcp-server.ts index 7ef56dd9..055afb3f 100644 --- a/packages/mcp-use/src/server/mcp-server.ts +++ b/packages/mcp-use/src/server/mcp-server.ts @@ -4,13 +4,18 @@ import type { ResourceTemplateDefinition, ServerConfig, ToolDefinition, -} from './types.js' + UIResourceDefinition, + WidgetProps, + InputDefinition, + UIResourceContent, +} 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 { createUIResourceFromDefinition, type UrlConfig } from './adapters/mcp-ui-adapter.js' export class McpServer { private server: OfficialMcpServer @@ -291,6 +296,220 @@ 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 { + // 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: toolName, + description: definition.description || `Display ${displayName}`, + inputs: this.convertPropsToInputs(definition.props), + fn: async (params) => { + // Create the UIResource with user-provided params + const uiResource = this.createWidgetUIResource(definition, params) + + return { + content: [ + { + type: 'text', + text: `Displaying ${displayName}`, + description: `Show MCP-UI widget for ${displayName}` + }, + uiResource + ] + } + } + }) + + return this + } + + /** + * Create a UIResource object for a widget with the given parameters + * + * This method is shared between tool and resource handlers to avoid duplication. + * It creates a consistent UIResource structure that can be rendered by MCP-UI + * compatible clients. + * + * @private + * @param definition - UIResource definition + * @param params - Parameters to pass to the widget via URL + * @returns UIResource object compatible with MCP-UI + */ + private createWidgetUIResource( + definition: UIResourceDefinition, + params: Record + ): UIResourceContent { + const urlConfig: UrlConfig = { + baseUrl: 'http://localhost', + port: this.serverPort || 3001 + } + + return createUIResourceFromDefinition(definition, params, urlConfig) + } + + /** + * Build a complete URL for a widget including query parameters + * + * Constructs the full URL to access a widget's iframe, encoding any provided + * parameters as query string parameters. Complex objects are JSON-stringified + * for transmission. + * + * @private + * @param widget - Widget name/identifier + * @param params - Parameters to encode in the URL + * @returns Complete URL with encoded parameters + */ + 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..18d0828f --- /dev/null +++ b/packages/mcp-use/src/server/types/index.ts @@ -0,0 +1,43 @@ +/** + * 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 + UIResourceContent, + WidgetProps, + UIEncoding, + RemoteDomFramework, + UIResourceDefinition, + ExternalUrlUIResource, + RawHtmlUIResource, + RemoteDomUIResource, + 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..13aaa89e --- /dev/null +++ b/packages/mcp-use/src/server/types/resource.ts @@ -0,0 +1,171 @@ +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 + +/** + * 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 + } +} + +/** + * Encoding options for UI resources + */ +export type UIEncoding = 'text' | 'blob' + +/** + * Framework options for Remote DOM resources + */ +export type RemoteDomFramework = 'react' | 'webcomponents' + +/** + * Base properties shared by all UI resource types + */ +interface BaseUIResourceDefinition { + /** Unique identifier for the resource */ + name: 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 + /** Encoding for the resource content (defaults to 'text') */ + encoding?: UIEncoding +} + +/** + * 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 + /** 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 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..8f2a4a66 --- /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 'mcp-use/server' + +/** + * 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 new file mode 100644 index 00000000..dc985a6a --- /dev/null +++ b/packages/mcp-use/tests/mcp-ui-adapter.test.ts @@ -0,0 +1,436 @@ +/** + * Tests for MCP-UI Adapter + * + * 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 } from 'vitest' +import { + buildWidgetUrl, + createExternalUrlResource, + createRawHtmlResource, + createRemoteDomResource, + createUIResourceFromDefinition, + type UrlConfig +} from '../src/server/adapters/mcp-ui-adapter.js' +import { + generateWidgetHtml, + generateRemoteDomScript +} from './helpers/widget-generators.js' +import type { + ExternalUrlUIResource, + RawHtmlUIResource, + RemoteDomUIResource +} from 'mcp-use/server' + +describe('MCP-UI Adapter', () => { + const urlConfig: UrlConfig = { + baseUrl: 'http://localhost', + port: 3000 + } + + describe('External URL Resources', () => { + it('should create external URL resource with text encoding', () => { + const definition: ExternalUrlUIResource = { + type: 'externalUrl', + name: 'kanban-board', + widget: 'kanban-board', + title: 'Kanban Board', + encoding: 'text' + } + + const resource = createUIResourceFromDefinition(definition, { + theme: 'dark', + initialTasks: ['task1', 'task2'] + }, urlConfig) + + 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: ExternalUrlUIResource = { + type: 'externalUrl', + name: 'chart-widget', + widget: 'chart', + encoding: 'blob' + } + + const resource = createUIResourceFromDefinition(definition, { + data: [1, 2, 3] + }, urlConfig) + + 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: ExternalUrlUIResource = { + type: 'externalUrl', + name: 'dashboard', + widget: 'dashboard' + } + + 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 text encoding', () => { + const definition: ExternalUrlUIResource = { + type: 'externalUrl', + name: 'todo-list', + widget: 'todo-list' + // No encoding specified + } + + const resource = createUIResourceFromDefinition(definition, {}, urlConfig) + + 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: RawHtmlUIResource = { + type: 'rawHtml', + name: 'static-widget', + htmlContent, + encoding: 'text' + } + + const resource = createUIResourceFromDefinition(definition, {}, urlConfig) + + 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: RawHtmlUIResource = { + type: 'rawHtml', + name: 'complex-widget', + htmlContent, + encoding: 'blob' + } + + const resource = createUIResourceFromDefinition(definition, {}, urlConfig) + + expect(resource).toEqual({ + type: 'resource', + resource: { + uri: 'ui://widget/complex-widget', + mimeType: 'text/html', + blob: Buffer.from(htmlContent).toString('base64') + } + }) + }) + + it('should generate HTML content with widget metadata', () => { + const definition: RawHtmlUIResource = { + type: 'rawHtml', + name: 'generated-widget', + htmlContent: '
Test
', + title: 'Generated Widget', + description: 'A dynamically generated widget', + size: ['800px', '600px'] + } + + const html = 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: RemoteDomUIResource = { + type: 'remoteDom', + name: 'remote-button', + script, + framework: 'react', + encoding: 'text' + } + + const resource = createUIResourceFromDefinition(definition, {}, urlConfig) + + 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: RemoteDomUIResource = { + type: 'remoteDom', + name: 'web-component', + script, + framework: 'webcomponents' + } + + const resource = createUIResourceFromDefinition(definition, {}, urlConfig) + + 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: RemoteDomUIResource = { + type: 'remoteDom', + name: 'blob-dom', + script, + encoding: 'blob' + } + + const resource = createUIResourceFromDefinition(definition, {}, urlConfig) + + 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 generate remote DOM script with widget metadata', () => { + const definition: RemoteDomUIResource = { + type: 'remoteDom', + name: 'interactive-widget', + script: 'console.log("test")', + title: 'Interactive Widget', + description: 'An interactive remote DOM widget' + } + + const script = 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-widget') + expect(script).toContain('ui-button') + }) + + it('should default to React framework if not specified', () => { + const definition: RemoteDomUIResource = { + type: 'remoteDom', + name: 'default-framework', + script: 'const div = document.createElement("div");' + // No framework specified + } + + const resource = createUIResourceFromDefinition(definition, {}, urlConfig) + + expect(resource.resource.mimeType).toContain('framework=react') + }) + }) + + describe('Direct Method Calls', () => { + it('should create external URL resource directly', () => { + const resource = 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 = 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 = 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 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') + }) + + 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) + + 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: ExternalUrlUIResource = { + type: 'externalUrl', + name: 'complex-params', + widget: 'complex' + } + + 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 + 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 + }) + }) + + 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: ExternalUrlUIResource = { + type: 'externalUrl', + name: '', + widget: '' + } + + const resource = createUIResourceFromDefinition(definition, {}, urlConfig) + + expect(resource.resource.uri).toBe('ui://widget/') + expect(resource.resource.text).toBe('http://localhost:3000/mcp-use/widgets/') + }) + }) +}) diff --git a/patches/@mcp-ui__server@5.11.0.patch b/patches/@mcp-ui__server@5.11.0.patch deleted file mode 100644 index 999ba41e..00000000 --- a/patches/@mcp-ui__server@5.11.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 2c82159d..ad48a354 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,65 @@ 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 + 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/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 @@ -348,8 +401,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 @@ -520,10 +573,7 @@ importers: dependencies: '@mcp-ui/server': specifier: ^5.11.0 - version: 5.11.0(patch_hash=5d24b4104a7d6d537b7de1dc183b27e42d9c1b3fde96a28dbeb722713ba85f11) - '@modelcontextprotocol/sdk': - specifier: ^1.0.0 - version: 1.20.0 + version: 5.12.0 cors: specifier: ^2.8.5 version: 2.8.5 @@ -533,9 +583,6 @@ importers: mcp-use: specifier: workspace:* version: link:../packages/mcp-use - zod: - specifier: ^3.22.0 - version: 3.25.76 devDependencies: '@mcp-use/cli': specifier: workspace:* @@ -1853,6 +1900,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'} @@ -7928,12 +7978,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 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cd64b37d..04bd672c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +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' - - 'test_app' + - '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" } ] }