create-mcp-use-app 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +8 -1
- package/dist/templates/uiresource/README.md +376 -0
- package/dist/templates/uiresource/index.ts +12 -0
- package/dist/templates/uiresource/package.json +47 -0
- package/dist/templates/uiresource/resources/kanban-board.tsx +306 -0
- package/dist/templates/uiresource/src/server.ts +425 -0
- package/dist/templates/uiresource/tsconfig.json +20 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -155,8 +155,15 @@ async function copyTemplate(projectPath, template, versions, isDevelopment = fal
|
|
|
155
155
|
const templatePath = join(__dirname, "templates", template);
|
|
156
156
|
if (!existsSync(templatePath)) {
|
|
157
157
|
console.error(`\u274C Template "${template}" not found!`);
|
|
158
|
-
|
|
158
|
+
const templatesDir = join(__dirname, "templates");
|
|
159
|
+
if (existsSync(templatesDir)) {
|
|
160
|
+
const availableTemplates = readdirSync(templatesDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name).sort();
|
|
161
|
+
console.log(`Available templates: ${availableTemplates.join(", ")}`);
|
|
162
|
+
} else {
|
|
163
|
+
console.log("No templates directory found");
|
|
164
|
+
}
|
|
159
165
|
console.log('\u{1F4A1} Tip: Use "ui" template for React components and modern UI features');
|
|
166
|
+
console.log('\u{1F4A1} Tip: Use "uiresource" template for UI resources and advanced server examples');
|
|
160
167
|
process.exit(1);
|
|
161
168
|
}
|
|
162
169
|
copyDirectoryWithProcessing(templatePath, projectPath, versions, isDevelopment);
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# UIResource MCP Server
|
|
2
|
+
|
|
3
|
+
An MCP server with the new UIResource integration for simplified widget management and MCP-UI compatibility.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **🚀 UIResource Method**: Single method to register both tools and resources
|
|
8
|
+
- **🎨 React Widgets**: Interactive UI components built with React
|
|
9
|
+
- **🔄 Automatic Registration**: Tools and resources created automatically
|
|
10
|
+
- **📦 Props to Parameters**: Widget props automatically become tool parameters
|
|
11
|
+
- **🌐 MCP-UI Compatible**: Full compatibility with MCP-UI clients
|
|
12
|
+
- **🛠️ TypeScript Support**: Complete type safety and IntelliSense
|
|
13
|
+
|
|
14
|
+
## What's New: UIResource
|
|
15
|
+
|
|
16
|
+
The `uiResource` method is a powerful new addition that simplifies widget registration:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// Old way: Manual registration of tool and resource
|
|
20
|
+
server.tool({ /* tool config */ })
|
|
21
|
+
server.resource({ /* resource config */ })
|
|
22
|
+
|
|
23
|
+
// New way: Single method does both!
|
|
24
|
+
server.uiResource({
|
|
25
|
+
name: 'kanban-board',
|
|
26
|
+
widget: 'kanban-board',
|
|
27
|
+
title: 'Kanban Board',
|
|
28
|
+
props: {
|
|
29
|
+
initialTasks: { type: 'array', required: false },
|
|
30
|
+
theme: { type: 'string', default: 'light' }
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This automatically creates:
|
|
36
|
+
- **Tool**: `ui_kanban-board` - Accepts parameters and returns UIResource
|
|
37
|
+
- **Resource**: `ui://widget/kanban-board` - Static access with defaults
|
|
38
|
+
|
|
39
|
+
## Getting Started
|
|
40
|
+
|
|
41
|
+
### Development
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Install dependencies
|
|
45
|
+
npm install
|
|
46
|
+
|
|
47
|
+
# Start development server with hot reloading
|
|
48
|
+
npm run dev
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This will start:
|
|
52
|
+
- MCP server on port 3000
|
|
53
|
+
- Widget serving at `/mcp-use/widgets/*`
|
|
54
|
+
- Inspector UI at `/inspector`
|
|
55
|
+
|
|
56
|
+
### Production
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Build the server and widgets
|
|
60
|
+
npm run build
|
|
61
|
+
|
|
62
|
+
# Run the built server
|
|
63
|
+
npm start
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Basic Usage
|
|
67
|
+
|
|
68
|
+
### Simple Widget Registration
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { createMCPServer } from 'mcp-use/server'
|
|
72
|
+
|
|
73
|
+
const server = createMCPServer('my-server', {
|
|
74
|
+
version: '1.0.0',
|
|
75
|
+
description: 'Server with UIResource widgets'
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Register a widget - creates both tool and resource
|
|
79
|
+
server.uiResource({
|
|
80
|
+
name: 'my-widget',
|
|
81
|
+
widget: 'my-widget',
|
|
82
|
+
title: 'My Widget',
|
|
83
|
+
description: 'An interactive widget'
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
server.listen(3000)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Widget with Props
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
server.uiResource({
|
|
93
|
+
name: 'data-chart',
|
|
94
|
+
widget: 'chart',
|
|
95
|
+
title: 'Data Chart',
|
|
96
|
+
description: 'Interactive data visualization',
|
|
97
|
+
props: {
|
|
98
|
+
data: {
|
|
99
|
+
type: 'array',
|
|
100
|
+
description: 'Data points to display',
|
|
101
|
+
required: true
|
|
102
|
+
},
|
|
103
|
+
chartType: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: 'Type of chart (line/bar/pie)',
|
|
106
|
+
default: 'line'
|
|
107
|
+
},
|
|
108
|
+
theme: {
|
|
109
|
+
type: 'string',
|
|
110
|
+
description: 'Visual theme',
|
|
111
|
+
default: 'light'
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
size: ['800px', '400px'], // Preferred iframe size
|
|
115
|
+
annotations: {
|
|
116
|
+
audience: ['user', 'assistant'],
|
|
117
|
+
priority: 0.8
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Widget Development
|
|
123
|
+
|
|
124
|
+
### 1. Create Your Widget Component
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// resources/my-widget.tsx
|
|
128
|
+
import React, { useState, useEffect } from 'react'
|
|
129
|
+
import { createRoot } from 'react-dom/client'
|
|
130
|
+
|
|
131
|
+
interface MyWidgetProps {
|
|
132
|
+
initialData?: any
|
|
133
|
+
theme?: 'light' | 'dark'
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const MyWidget: React.FC<MyWidgetProps> = ({
|
|
137
|
+
initialData = [],
|
|
138
|
+
theme = 'light'
|
|
139
|
+
}) => {
|
|
140
|
+
const [data, setData] = useState(initialData)
|
|
141
|
+
|
|
142
|
+
// Load props from URL query parameters
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const params = new URLSearchParams(window.location.search)
|
|
145
|
+
|
|
146
|
+
const dataParam = params.get('initialData')
|
|
147
|
+
if (dataParam) {
|
|
148
|
+
try {
|
|
149
|
+
setData(JSON.parse(dataParam))
|
|
150
|
+
} catch (e) {
|
|
151
|
+
console.error('Error parsing data:', e)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const themeParam = params.get('theme')
|
|
156
|
+
if (themeParam) {
|
|
157
|
+
// Apply theme
|
|
158
|
+
}
|
|
159
|
+
}, [])
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div className={`widget theme-${theme}`}>
|
|
163
|
+
{/* Your widget UI */}
|
|
164
|
+
</div>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Mount the widget
|
|
169
|
+
const container = document.getElementById('widget-root')
|
|
170
|
+
if (container) {
|
|
171
|
+
createRoot(container).render(<MyWidget />)
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 2. Register with UIResource
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// src/server.ts
|
|
179
|
+
server.uiResource({
|
|
180
|
+
name: 'my-widget',
|
|
181
|
+
widget: 'my-widget',
|
|
182
|
+
title: 'My Custom Widget',
|
|
183
|
+
description: 'A custom interactive widget',
|
|
184
|
+
props: {
|
|
185
|
+
initialData: {
|
|
186
|
+
type: 'array',
|
|
187
|
+
description: 'Initial data for the widget',
|
|
188
|
+
required: false
|
|
189
|
+
},
|
|
190
|
+
theme: {
|
|
191
|
+
type: 'string',
|
|
192
|
+
description: 'Widget theme',
|
|
193
|
+
default: 'light'
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
size: ['600px', '400px']
|
|
197
|
+
})
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## How It Works
|
|
201
|
+
|
|
202
|
+
### Tool Registration
|
|
203
|
+
When you call `uiResource`, it automatically creates a tool:
|
|
204
|
+
- Name: `ui_[widget-name]`
|
|
205
|
+
- Accepts all props as parameters
|
|
206
|
+
- Returns both text description and UIResource object
|
|
207
|
+
|
|
208
|
+
### Resource Registration
|
|
209
|
+
Also creates a resource:
|
|
210
|
+
- URI: `ui://widget/[widget-name]`
|
|
211
|
+
- Returns UIResource with default prop values
|
|
212
|
+
- Discoverable by MCP clients
|
|
213
|
+
|
|
214
|
+
### Parameter Passing
|
|
215
|
+
Tool parameters are automatically:
|
|
216
|
+
1. Converted to URL query parameters
|
|
217
|
+
2. Complex objects are JSON-stringified
|
|
218
|
+
3. Passed to widget via iframe URL
|
|
219
|
+
|
|
220
|
+
## Advanced Examples
|
|
221
|
+
|
|
222
|
+
### Multiple Widgets
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
const widgets = [
|
|
226
|
+
{
|
|
227
|
+
name: 'todo-list',
|
|
228
|
+
widget: 'todo-list',
|
|
229
|
+
title: 'Todo List',
|
|
230
|
+
props: {
|
|
231
|
+
items: { type: 'array', default: [] }
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'calendar',
|
|
236
|
+
widget: 'calendar',
|
|
237
|
+
title: 'Calendar',
|
|
238
|
+
props: {
|
|
239
|
+
date: { type: 'string', required: false }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
// Register all widgets
|
|
245
|
+
widgets.forEach(widget => server.uiResource(widget))
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Mixed Registration
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
// UIResource for widgets
|
|
252
|
+
server.uiResource({
|
|
253
|
+
name: 'dashboard',
|
|
254
|
+
widget: 'dashboard',
|
|
255
|
+
title: 'Analytics Dashboard'
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
// Traditional tool for actions
|
|
259
|
+
server.tool({
|
|
260
|
+
name: 'calculate',
|
|
261
|
+
description: 'Perform calculations',
|
|
262
|
+
fn: async (params) => { /* ... */ }
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
// Traditional resource for data
|
|
266
|
+
server.resource({
|
|
267
|
+
name: 'config',
|
|
268
|
+
uri: 'config://app',
|
|
269
|
+
mimeType: 'application/json',
|
|
270
|
+
fn: async () => { /* ... */ }
|
|
271
|
+
})
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## API Reference
|
|
275
|
+
|
|
276
|
+
### `server.uiResource(definition)`
|
|
277
|
+
|
|
278
|
+
#### Parameters
|
|
279
|
+
|
|
280
|
+
- `definition: UIResourceDefinition`
|
|
281
|
+
- `name: string` - Resource identifier
|
|
282
|
+
- `widget: string` - Widget directory name
|
|
283
|
+
- `title?: string` - Human-readable title
|
|
284
|
+
- `description?: string` - Widget description
|
|
285
|
+
- `props?: WidgetProps` - Widget properties configuration
|
|
286
|
+
- `size?: [string, string]` - Preferred iframe size
|
|
287
|
+
- `annotations?: ResourceAnnotations` - Discovery hints
|
|
288
|
+
|
|
289
|
+
#### WidgetProps
|
|
290
|
+
|
|
291
|
+
Each prop can have:
|
|
292
|
+
- `type: 'string' | 'number' | 'boolean' | 'object' | 'array'`
|
|
293
|
+
- `required?: boolean` - Whether the prop is required
|
|
294
|
+
- `default?: any` - Default value if not provided
|
|
295
|
+
- `description?: string` - Prop description
|
|
296
|
+
|
|
297
|
+
## Testing Your Widgets
|
|
298
|
+
|
|
299
|
+
### Via Inspector UI
|
|
300
|
+
1. Start the server: `npm run dev`
|
|
301
|
+
2. Open: `http://localhost:3000/inspector`
|
|
302
|
+
3. Test tools and resources
|
|
303
|
+
|
|
304
|
+
### Direct Browser Access
|
|
305
|
+
Visit: `http://localhost:3000/mcp-use/widgets/[widget-name]`
|
|
306
|
+
|
|
307
|
+
### Via MCP Client
|
|
308
|
+
```typescript
|
|
309
|
+
// Call as tool
|
|
310
|
+
const result = await client.callTool('ui_kanban-board', {
|
|
311
|
+
initialTasks: [...],
|
|
312
|
+
theme: 'dark'
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
// Access as resource
|
|
316
|
+
const resource = await client.readResource('ui://widget/kanban-board')
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Benefits of UIResource
|
|
320
|
+
|
|
321
|
+
✅ **Simplified API** - One method instead of two
|
|
322
|
+
✅ **Automatic Wiring** - Props become tool inputs automatically
|
|
323
|
+
✅ **Type Safety** - Full TypeScript support
|
|
324
|
+
✅ **MCP-UI Compatible** - Works with all MCP-UI clients
|
|
325
|
+
✅ **DRY Principle** - No duplicate UIResource creation
|
|
326
|
+
✅ **Discoverable** - Both tools and resources are listed
|
|
327
|
+
|
|
328
|
+
## Troubleshooting
|
|
329
|
+
|
|
330
|
+
### Widget Not Loading
|
|
331
|
+
- Ensure widget exists in `dist/resources/mcp-use/widgets/`
|
|
332
|
+
- Check server console for errors
|
|
333
|
+
- Verify widget is registered with `uiResource()`
|
|
334
|
+
|
|
335
|
+
### Props Not Passed
|
|
336
|
+
- Check URL parameters in browser DevTools
|
|
337
|
+
- Ensure prop names match exactly
|
|
338
|
+
- Complex objects must be JSON-stringified
|
|
339
|
+
|
|
340
|
+
### Type Errors
|
|
341
|
+
- Import types: `import type { UIResourceDefinition } from 'mcp-use/server'`
|
|
342
|
+
- Ensure mcp-use is updated to latest version
|
|
343
|
+
|
|
344
|
+
## Migration from Old Pattern
|
|
345
|
+
|
|
346
|
+
If you have existing code using separate tool/resource:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
// Old pattern
|
|
350
|
+
server.tool({ name: 'show-widget', /* ... */ })
|
|
351
|
+
server.resource({ uri: 'ui://widget', /* ... */ })
|
|
352
|
+
|
|
353
|
+
// New pattern - replace both with:
|
|
354
|
+
server.uiResource({
|
|
355
|
+
name: 'widget',
|
|
356
|
+
widget: 'widget',
|
|
357
|
+
// ... consolidated configuration
|
|
358
|
+
})
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Future Enhancements
|
|
362
|
+
|
|
363
|
+
Coming soon:
|
|
364
|
+
- Automatic widget discovery from filesystem
|
|
365
|
+
- Widget manifests (widget.json)
|
|
366
|
+
- Prop extraction from TypeScript interfaces
|
|
367
|
+
- Build-time optimization
|
|
368
|
+
|
|
369
|
+
## Learn More
|
|
370
|
+
|
|
371
|
+
- [MCP Documentation](https://modelcontextprotocol.io)
|
|
372
|
+
- [MCP-UI Documentation](https://github.com/idosal/mcp-ui)
|
|
373
|
+
- [mcp-use Documentation](https://github.com/pyroprompt/mcp-use)
|
|
374
|
+
- [React Documentation](https://react.dev/)
|
|
375
|
+
|
|
376
|
+
Happy widget building! 🚀
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server Entry Point
|
|
3
|
+
*
|
|
4
|
+
* This file serves as the main entry point for the MCP server application.
|
|
5
|
+
* It re-exports all functionality from the server implementation, allowing
|
|
6
|
+
* the CLI and other tools to locate and start the server.
|
|
7
|
+
*
|
|
8
|
+
* The server is automatically started when this module is imported, making
|
|
9
|
+
* it suitable for both direct execution and programmatic usage.
|
|
10
|
+
*/
|
|
11
|
+
export * from './src/server.js'
|
|
12
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-uiresource-server",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "MCP server with UIResource widget integration",
|
|
6
|
+
"author": "",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"mcp",
|
|
10
|
+
"server",
|
|
11
|
+
"uiresource",
|
|
12
|
+
"ui",
|
|
13
|
+
"react",
|
|
14
|
+
"widgets",
|
|
15
|
+
"ai",
|
|
16
|
+
"tools",
|
|
17
|
+
"mcp-ui"
|
|
18
|
+
],
|
|
19
|
+
"main": "dist/index.js",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "mcp-use build",
|
|
22
|
+
"dev": "mcp-use dev",
|
|
23
|
+
"start": "mcp-use start"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@mcp-ui/server": "^5.11.0",
|
|
27
|
+
"cors": "^2.8.5",
|
|
28
|
+
"express": "^4.18.0",
|
|
29
|
+
"mcp-use": "workspace:*"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@mcp-use/cli": "workspace:*",
|
|
33
|
+
"@mcp-use/inspector": "workspace:*",
|
|
34
|
+
"@types/cors": "^2.8.0",
|
|
35
|
+
"@types/express": "^4.17.0",
|
|
36
|
+
"@types/node": "^20.0.0",
|
|
37
|
+
"@types/react": "^18.0.0",
|
|
38
|
+
"@types/react-dom": "^18.0.0",
|
|
39
|
+
"concurrently": "^8.0.0",
|
|
40
|
+
"esbuild": "^0.23.0",
|
|
41
|
+
"globby": "^14.0.2",
|
|
42
|
+
"react": "^18.0.0",
|
|
43
|
+
"react-dom": "^18.0.0",
|
|
44
|
+
"tsx": "^4.0.0",
|
|
45
|
+
"typescript": "^5.0.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
|
|
4
|
+
interface Task {
|
|
5
|
+
id: string
|
|
6
|
+
title: string
|
|
7
|
+
description: string
|
|
8
|
+
status: 'todo' | 'in-progress' | 'done'
|
|
9
|
+
priority: 'low' | 'medium' | 'high'
|
|
10
|
+
assignee?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface KanbanBoardProps {
|
|
14
|
+
initialTasks?: Task[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const KanbanBoard: React.FC<KanbanBoardProps> = ({ initialTasks = [] }) => {
|
|
18
|
+
const [tasks, setTasks] = useState<Task[]>(initialTasks)
|
|
19
|
+
const [newTask, setNewTask] = useState({ title: '', description: '', priority: 'medium' as Task['priority'] })
|
|
20
|
+
|
|
21
|
+
// Load tasks from URL parameters or use defaults
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const urlParams = new URLSearchParams(window.location.search)
|
|
24
|
+
const tasksParam = urlParams.get('tasks')
|
|
25
|
+
|
|
26
|
+
if (tasksParam) {
|
|
27
|
+
try {
|
|
28
|
+
const parsedTasks = JSON.parse(decodeURIComponent(tasksParam))
|
|
29
|
+
setTasks(parsedTasks)
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error('Error parsing tasks from URL:', error)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// Default tasks for demo
|
|
37
|
+
setTasks([
|
|
38
|
+
{ id: '1', title: 'Design UI mockups', description: 'Create wireframes for the new dashboard', status: 'todo', priority: 'high', assignee: 'Alice' },
|
|
39
|
+
{ id: '2', title: 'Implement authentication', description: 'Add login and registration functionality', status: 'in-progress', priority: 'high', assignee: 'Bob' },
|
|
40
|
+
{ id: '3', title: 'Write documentation', description: 'Document the API endpoints', status: 'done', priority: 'medium', assignee: 'Charlie' },
|
|
41
|
+
{ id: '4', title: 'Setup CI/CD', description: 'Configure automated testing and deployment', status: 'todo', priority: 'medium' },
|
|
42
|
+
{ id: '5', title: 'Code review', description: 'Review pull requests from the team', status: 'in-progress', priority: 'low', assignee: 'David' },
|
|
43
|
+
])
|
|
44
|
+
}
|
|
45
|
+
}, [])
|
|
46
|
+
|
|
47
|
+
const addTask = () => {
|
|
48
|
+
if (newTask.title.trim()) {
|
|
49
|
+
const task: Task = {
|
|
50
|
+
id: Date.now().toString(),
|
|
51
|
+
title: newTask.title,
|
|
52
|
+
description: newTask.description,
|
|
53
|
+
status: 'todo',
|
|
54
|
+
priority: newTask.priority,
|
|
55
|
+
}
|
|
56
|
+
setTasks([...tasks, task])
|
|
57
|
+
setNewTask({ title: '', description: '', priority: 'medium' })
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const moveTask = (taskId: string, newStatus: Task['status']) => {
|
|
62
|
+
setTasks(tasks.map(task =>
|
|
63
|
+
task.id === taskId ? { ...task, status: newStatus } : task,
|
|
64
|
+
))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const deleteTask = (taskId: string) => {
|
|
68
|
+
setTasks(tasks.filter(task => task.id !== taskId))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const getTasksByStatus = (status: Task['status']) => {
|
|
72
|
+
return tasks.filter(task => task.status === status)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const getPriorityColor = (priority: Task['priority']) => {
|
|
76
|
+
switch (priority) {
|
|
77
|
+
case 'high': return '#ff4757'
|
|
78
|
+
case 'medium': return '#ffa502'
|
|
79
|
+
case 'low': return '#2ed573'
|
|
80
|
+
default: return '#57606f'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const columns = [
|
|
85
|
+
{ id: 'todo', title: 'To Do', color: '#57606f' },
|
|
86
|
+
{ id: 'in-progress', title: 'In Progress', color: '#ffa502' },
|
|
87
|
+
{ id: 'done', title: 'Done', color: '#2ed573' },
|
|
88
|
+
] as const
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div style={{ padding: '20px' }}>
|
|
92
|
+
<div style={{ marginBottom: '30px' }}>
|
|
93
|
+
<h1 style={{ margin: '0 0 20px 0', color: '#2c3e50' }}>Kanban Board</h1>
|
|
94
|
+
|
|
95
|
+
{/* Add new task form */}
|
|
96
|
+
<div style={{
|
|
97
|
+
background: 'white',
|
|
98
|
+
padding: '20px',
|
|
99
|
+
borderRadius: '8px',
|
|
100
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
101
|
+
marginBottom: '20px',
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
<h3 style={{ margin: '0 0 15px 0' }}>Add New Task</h3>
|
|
105
|
+
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
|
|
106
|
+
<input
|
|
107
|
+
type="text"
|
|
108
|
+
placeholder="Task title"
|
|
109
|
+
value={newTask.title}
|
|
110
|
+
onChange={e => setNewTask({ ...newTask, title: e.target.value })}
|
|
111
|
+
style={{
|
|
112
|
+
padding: '8px 12px',
|
|
113
|
+
border: '1px solid #ddd',
|
|
114
|
+
borderRadius: '4px',
|
|
115
|
+
flex: '1',
|
|
116
|
+
minWidth: '200px',
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
<input
|
|
120
|
+
type="text"
|
|
121
|
+
placeholder="Description"
|
|
122
|
+
value={newTask.description}
|
|
123
|
+
onChange={e => setNewTask({ ...newTask, description: e.target.value })}
|
|
124
|
+
style={{
|
|
125
|
+
padding: '8px 12px',
|
|
126
|
+
border: '1px solid #ddd',
|
|
127
|
+
borderRadius: '4px',
|
|
128
|
+
flex: '1',
|
|
129
|
+
minWidth: '200px',
|
|
130
|
+
}}
|
|
131
|
+
/>
|
|
132
|
+
<select
|
|
133
|
+
title="Priority"
|
|
134
|
+
value={newTask.priority}
|
|
135
|
+
onChange={e => setNewTask({ ...newTask, priority: e.target.value as Task['priority'] })}
|
|
136
|
+
style={{
|
|
137
|
+
padding: '8px 12px',
|
|
138
|
+
border: '1px solid #ddd',
|
|
139
|
+
borderRadius: '4px',
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
<option value="low">Low Priority</option>
|
|
143
|
+
<option value="medium">Medium Priority</option>
|
|
144
|
+
<option value="high">High Priority</option>
|
|
145
|
+
</select>
|
|
146
|
+
<button
|
|
147
|
+
onClick={addTask}
|
|
148
|
+
style={{
|
|
149
|
+
padding: '8px 16px',
|
|
150
|
+
background: '#3498db',
|
|
151
|
+
color: 'white',
|
|
152
|
+
border: 'none',
|
|
153
|
+
borderRadius: '4px',
|
|
154
|
+
cursor: 'pointer',
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
Add Task
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* Kanban columns */}
|
|
164
|
+
<div style={{ display: 'flex', gap: '20px', overflowX: 'auto' }}>
|
|
165
|
+
{columns.map(column => (
|
|
166
|
+
<div
|
|
167
|
+
key={column.id}
|
|
168
|
+
style={{
|
|
169
|
+
flex: '1',
|
|
170
|
+
minWidth: '300px',
|
|
171
|
+
background: 'white',
|
|
172
|
+
borderRadius: '8px',
|
|
173
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
174
|
+
overflow: 'hidden',
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
177
|
+
<div
|
|
178
|
+
style={{
|
|
179
|
+
background: column.color,
|
|
180
|
+
color: 'white',
|
|
181
|
+
padding: '15px 20px',
|
|
182
|
+
fontWeight: 'bold',
|
|
183
|
+
display: 'flex',
|
|
184
|
+
justifyContent: 'space-between',
|
|
185
|
+
alignItems: 'center',
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
<span>{column.title}</span>
|
|
189
|
+
<span style={{
|
|
190
|
+
background: 'rgba(255,255,255,0.2)',
|
|
191
|
+
padding: '4px 8px',
|
|
192
|
+
borderRadius: '12px',
|
|
193
|
+
fontSize: '12px',
|
|
194
|
+
}}
|
|
195
|
+
>
|
|
196
|
+
{getTasksByStatus(column.id).length}
|
|
197
|
+
</span>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div style={{ padding: '20px', minHeight: '400px' }}>
|
|
201
|
+
{getTasksByStatus(column.id).map(task => (
|
|
202
|
+
<div
|
|
203
|
+
key={task.id}
|
|
204
|
+
style={{
|
|
205
|
+
background: '#f8f9fa',
|
|
206
|
+
border: '1px solid #e9ecef',
|
|
207
|
+
borderRadius: '6px',
|
|
208
|
+
padding: '15px',
|
|
209
|
+
marginBottom: '10px',
|
|
210
|
+
cursor: 'grab',
|
|
211
|
+
}}
|
|
212
|
+
draggable
|
|
213
|
+
onDragStart={(e) => {
|
|
214
|
+
e.dataTransfer.setData('text/plain', task.id)
|
|
215
|
+
}}
|
|
216
|
+
onDragOver={e => e.preventDefault()}
|
|
217
|
+
onDrop={(e) => {
|
|
218
|
+
e.preventDefault()
|
|
219
|
+
const taskId = e.dataTransfer.getData('text/plain')
|
|
220
|
+
if (taskId === task.id) {
|
|
221
|
+
// Move to next column
|
|
222
|
+
const currentIndex = columns.findIndex(col => col.id === column.id)
|
|
223
|
+
const nextColumn = columns[currentIndex + 1]
|
|
224
|
+
if (nextColumn) {
|
|
225
|
+
moveTask(taskId, nextColumn.id)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '8px' }}>
|
|
231
|
+
<h4 style={{ margin: '0', color: '#2c3e50' }}>{task.title}</h4>
|
|
232
|
+
<button
|
|
233
|
+
onClick={() => deleteTask(task.id)}
|
|
234
|
+
style={{
|
|
235
|
+
background: 'none',
|
|
236
|
+
border: 'none',
|
|
237
|
+
color: '#e74c3c',
|
|
238
|
+
cursor: 'pointer',
|
|
239
|
+
fontSize: '16px',
|
|
240
|
+
}}
|
|
241
|
+
>
|
|
242
|
+
×
|
|
243
|
+
</button>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
{task.description && (
|
|
247
|
+
<p style={{ margin: '0 0 10px 0', color: '#6c757d', fontSize: '14px' }}>
|
|
248
|
+
{task.description}
|
|
249
|
+
</p>
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
253
|
+
<div
|
|
254
|
+
style={{
|
|
255
|
+
background: getPriorityColor(task.priority),
|
|
256
|
+
color: 'white',
|
|
257
|
+
padding: '2px 8px',
|
|
258
|
+
borderRadius: '12px',
|
|
259
|
+
fontSize: '12px',
|
|
260
|
+
fontWeight: 'bold',
|
|
261
|
+
}}
|
|
262
|
+
>
|
|
263
|
+
{task.priority.toUpperCase()}
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
{task.assignee && (
|
|
267
|
+
<span style={{
|
|
268
|
+
background: '#e9ecef',
|
|
269
|
+
padding: '2px 8px',
|
|
270
|
+
borderRadius: '12px',
|
|
271
|
+
fontSize: '12px',
|
|
272
|
+
color: '#495057',
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
{task.assignee}
|
|
276
|
+
</span>
|
|
277
|
+
)}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
))}
|
|
281
|
+
|
|
282
|
+
{getTasksByStatus(column.id).length === 0 && (
|
|
283
|
+
<div style={{
|
|
284
|
+
textAlign: 'center',
|
|
285
|
+
color: '#6c757d',
|
|
286
|
+
padding: '40px 20px',
|
|
287
|
+
fontStyle: 'italic',
|
|
288
|
+
}}
|
|
289
|
+
>
|
|
290
|
+
No tasks in this column
|
|
291
|
+
</div>
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
))}
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Mount the component
|
|
302
|
+
const container = document.getElementById('widget-root')
|
|
303
|
+
if (container) {
|
|
304
|
+
const root = createRoot(container)
|
|
305
|
+
root.render(<KanbanBoard />)
|
|
306
|
+
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { createMCPServer } from 'mcp-use/server'
|
|
2
|
+
import type {
|
|
3
|
+
ExternalUrlUIResource,
|
|
4
|
+
RawHtmlUIResource,
|
|
5
|
+
RemoteDomUIResource
|
|
6
|
+
} from 'mcp-use/server'
|
|
7
|
+
|
|
8
|
+
// Create an MCP server with UIResource support
|
|
9
|
+
const server = createMCPServer('uiresource-mcp-server', {
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
description: 'MCP server demonstrating all UIResource types',
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const PORT = process.env.PORT || 3000
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ════════════════════════════════════════════════════════════════════
|
|
18
|
+
* Type 1: External URL (Iframe Widget)
|
|
19
|
+
* ════════════════════════════════════════════════════════════════════
|
|
20
|
+
*
|
|
21
|
+
* Serves a widget from your local filesystem via iframe.
|
|
22
|
+
* Best for: Complex interactive widgets with their own assets
|
|
23
|
+
*
|
|
24
|
+
* This automatically:
|
|
25
|
+
* 1. Creates a tool (ui_kanban-board) that accepts parameters
|
|
26
|
+
* 2. Creates a resource (ui://widget/kanban-board) for static access
|
|
27
|
+
* 3. Serves the widget from dist/resources/mcp-use/widgets/kanban-board/
|
|
28
|
+
*/
|
|
29
|
+
server.uiResource({
|
|
30
|
+
type: 'externalUrl',
|
|
31
|
+
name: 'kanban-board',
|
|
32
|
+
widget: 'kanban-board',
|
|
33
|
+
title: 'Kanban Board',
|
|
34
|
+
description: 'Interactive task management board with drag-and-drop support',
|
|
35
|
+
props: {
|
|
36
|
+
initialTasks: {
|
|
37
|
+
type: 'array',
|
|
38
|
+
description: 'Initial tasks to display on the board',
|
|
39
|
+
required: false,
|
|
40
|
+
},
|
|
41
|
+
theme: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'Visual theme for the board (light/dark)',
|
|
44
|
+
required: false,
|
|
45
|
+
default: 'light'
|
|
46
|
+
},
|
|
47
|
+
columns: {
|
|
48
|
+
type: 'array',
|
|
49
|
+
description: 'Column configuration for the board',
|
|
50
|
+
required: false,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} satisfies ExternalUrlUIResource)
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* ════════════════════════════════════════════════════════════════════
|
|
57
|
+
* Type 2: Raw HTML
|
|
58
|
+
* ════════════════════════════════════════════════════════════════════
|
|
59
|
+
*
|
|
60
|
+
* Renders HTML content directly without an iframe.
|
|
61
|
+
* Best for: Simple visualizations, status displays, formatted text
|
|
62
|
+
*
|
|
63
|
+
* This creates:
|
|
64
|
+
* - Tool: ui_welcome-card
|
|
65
|
+
* - Resource: ui://widget/welcome-card
|
|
66
|
+
*/
|
|
67
|
+
server.uiResource({
|
|
68
|
+
type: 'rawHtml',
|
|
69
|
+
name: 'welcome-card',
|
|
70
|
+
title: 'Welcome Message',
|
|
71
|
+
description: 'A welcoming card with server information',
|
|
72
|
+
htmlContent: `
|
|
73
|
+
<!DOCTYPE html>
|
|
74
|
+
<html>
|
|
75
|
+
<head>
|
|
76
|
+
<meta charset="UTF-8">
|
|
77
|
+
<style>
|
|
78
|
+
body {
|
|
79
|
+
margin: 0;
|
|
80
|
+
padding: 20px;
|
|
81
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
82
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
83
|
+
color: white;
|
|
84
|
+
}
|
|
85
|
+
.card {
|
|
86
|
+
background: rgba(255, 255, 255, 0.1);
|
|
87
|
+
backdrop-filter: blur(10px);
|
|
88
|
+
border-radius: 16px;
|
|
89
|
+
padding: 30px;
|
|
90
|
+
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
|
|
91
|
+
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
92
|
+
}
|
|
93
|
+
h1 {
|
|
94
|
+
margin: 0 0 10px 0;
|
|
95
|
+
font-size: 2em;
|
|
96
|
+
}
|
|
97
|
+
p {
|
|
98
|
+
margin: 10px 0;
|
|
99
|
+
opacity: 0.9;
|
|
100
|
+
}
|
|
101
|
+
.stats {
|
|
102
|
+
display: flex;
|
|
103
|
+
gap: 20px;
|
|
104
|
+
margin-top: 20px;
|
|
105
|
+
}
|
|
106
|
+
.stat {
|
|
107
|
+
background: rgba(255, 255, 255, 0.1);
|
|
108
|
+
padding: 15px;
|
|
109
|
+
border-radius: 8px;
|
|
110
|
+
flex: 1;
|
|
111
|
+
}
|
|
112
|
+
.stat-value {
|
|
113
|
+
font-size: 1.5em;
|
|
114
|
+
font-weight: bold;
|
|
115
|
+
}
|
|
116
|
+
.stat-label {
|
|
117
|
+
font-size: 0.9em;
|
|
118
|
+
opacity: 0.8;
|
|
119
|
+
}
|
|
120
|
+
</style>
|
|
121
|
+
</head>
|
|
122
|
+
<body>
|
|
123
|
+
<div class="card">
|
|
124
|
+
<h1>🎉 Welcome to MCP-UI</h1>
|
|
125
|
+
<p>Your server is running and ready to serve interactive widgets!</p>
|
|
126
|
+
|
|
127
|
+
<div class="stats">
|
|
128
|
+
<div class="stat">
|
|
129
|
+
<div class="stat-value">3</div>
|
|
130
|
+
<div class="stat-label">Widget Types</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="stat">
|
|
133
|
+
<div class="stat-value">∞</div>
|
|
134
|
+
<div class="stat-label">Possibilities</div>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="stat">
|
|
137
|
+
<div class="stat-value">⚡</div>
|
|
138
|
+
<div class="stat-label">Fast & Simple</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<p style="margin-top: 20px; font-size: 0.9em;">
|
|
143
|
+
Server: <strong>uiresource-mcp-server v1.0.0</strong><br>
|
|
144
|
+
Port: <strong>${PORT}</strong>
|
|
145
|
+
</p>
|
|
146
|
+
</div>
|
|
147
|
+
</body>
|
|
148
|
+
</html>
|
|
149
|
+
`,
|
|
150
|
+
encoding: 'text',
|
|
151
|
+
size: ['600px', '400px']
|
|
152
|
+
} satisfies RawHtmlUIResource)
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* ════════════════════════════════════════════════════════════════════
|
|
156
|
+
* Type 3: Remote DOM (React Components)
|
|
157
|
+
* ════════════════════════════════════════════════════════════════════
|
|
158
|
+
*
|
|
159
|
+
* Uses Remote DOM to render interactive components.
|
|
160
|
+
* Best for: Lightweight interactive UIs using MCP-UI React components
|
|
161
|
+
*
|
|
162
|
+
* This creates:
|
|
163
|
+
* - Tool: ui_quick-poll
|
|
164
|
+
* - Resource: ui://widget/quick-poll
|
|
165
|
+
*/
|
|
166
|
+
server.uiResource({
|
|
167
|
+
type: 'remoteDom',
|
|
168
|
+
name: 'quick-poll',
|
|
169
|
+
title: 'Quick Poll',
|
|
170
|
+
description: 'Create instant polls with interactive voting',
|
|
171
|
+
script: `
|
|
172
|
+
// Remote DOM script for quick-poll widget
|
|
173
|
+
// Note: Remote DOM only supports registered MCP-UI components like ui-stack, ui-text, ui-button
|
|
174
|
+
// Standard HTML elements (div, h2, p, etc.) are NOT available
|
|
175
|
+
|
|
176
|
+
// Get props (passed from tool parameters)
|
|
177
|
+
const props = ${JSON.stringify({ question: 'What is your favorite framework?', options: ['React', 'Vue', 'Svelte', 'Angular'] })};
|
|
178
|
+
|
|
179
|
+
// Create main container stack (vertical layout)
|
|
180
|
+
const container = document.createElement('ui-stack');
|
|
181
|
+
container.setAttribute('direction', 'column');
|
|
182
|
+
container.setAttribute('spacing', 'medium');
|
|
183
|
+
container.setAttribute('padding', 'large');
|
|
184
|
+
|
|
185
|
+
// Title text
|
|
186
|
+
const title = document.createElement('ui-text');
|
|
187
|
+
title.setAttribute('size', 'xlarge');
|
|
188
|
+
title.setAttribute('weight', 'bold');
|
|
189
|
+
title.textContent = '📊 Quick Poll';
|
|
190
|
+
container.appendChild(title);
|
|
191
|
+
|
|
192
|
+
// Description text
|
|
193
|
+
const description = document.createElement('ui-text');
|
|
194
|
+
description.textContent = 'Cast your vote below!';
|
|
195
|
+
container.appendChild(description);
|
|
196
|
+
|
|
197
|
+
// Question text
|
|
198
|
+
const questionText = document.createElement('ui-text');
|
|
199
|
+
questionText.setAttribute('size', 'large');
|
|
200
|
+
questionText.setAttribute('weight', 'semibold');
|
|
201
|
+
questionText.textContent = props.question || 'What is your preference?';
|
|
202
|
+
container.appendChild(questionText);
|
|
203
|
+
|
|
204
|
+
// Button stack (horizontal layout)
|
|
205
|
+
const buttonStack = document.createElement('ui-stack');
|
|
206
|
+
buttonStack.setAttribute('direction', 'row');
|
|
207
|
+
buttonStack.setAttribute('spacing', 'small');
|
|
208
|
+
buttonStack.setAttribute('wrap', 'true');
|
|
209
|
+
|
|
210
|
+
// Create vote tracking
|
|
211
|
+
const votes = {};
|
|
212
|
+
let feedbackText = null;
|
|
213
|
+
|
|
214
|
+
// Create buttons for each option
|
|
215
|
+
const options = props.options || ['Option 1', 'Option 2', 'Option 3'];
|
|
216
|
+
options.forEach((option) => {
|
|
217
|
+
const button = document.createElement('ui-button');
|
|
218
|
+
button.setAttribute('label', option);
|
|
219
|
+
button.setAttribute('variant', 'secondary');
|
|
220
|
+
|
|
221
|
+
button.addEventListener('press', () => {
|
|
222
|
+
// Record vote
|
|
223
|
+
votes[option] = (votes[option] || 0) + 1;
|
|
224
|
+
|
|
225
|
+
// Send vote to parent (for tracking)
|
|
226
|
+
window.parent.postMessage({
|
|
227
|
+
type: 'tool',
|
|
228
|
+
payload: {
|
|
229
|
+
toolName: 'record_vote',
|
|
230
|
+
params: {
|
|
231
|
+
question: props.question,
|
|
232
|
+
selected: option,
|
|
233
|
+
votes: votes
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}, '*');
|
|
237
|
+
|
|
238
|
+
// Update or create feedback text
|
|
239
|
+
if (feedbackText) {
|
|
240
|
+
feedbackText.textContent = \`✓ Voted for \${option}! (Total votes: \${votes[option]})\`;
|
|
241
|
+
} else {
|
|
242
|
+
feedbackText = document.createElement('ui-text');
|
|
243
|
+
feedbackText.setAttribute('emphasis', 'high');
|
|
244
|
+
feedbackText.textContent = \`✓ Voted for \${option}!\`;
|
|
245
|
+
container.appendChild(feedbackText);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
buttonStack.appendChild(button);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
container.appendChild(buttonStack);
|
|
253
|
+
|
|
254
|
+
// Results section
|
|
255
|
+
const resultsTitle = document.createElement('ui-text');
|
|
256
|
+
resultsTitle.setAttribute('size', 'medium');
|
|
257
|
+
resultsTitle.setAttribute('weight', 'semibold');
|
|
258
|
+
resultsTitle.textContent = 'Vote to see results!';
|
|
259
|
+
container.appendChild(resultsTitle);
|
|
260
|
+
|
|
261
|
+
// Append to root
|
|
262
|
+
root.appendChild(container);
|
|
263
|
+
`,
|
|
264
|
+
framework: 'react',
|
|
265
|
+
encoding: 'text',
|
|
266
|
+
size: ['500px', '450px'],
|
|
267
|
+
props: {
|
|
268
|
+
question: {
|
|
269
|
+
type: 'string',
|
|
270
|
+
description: 'The poll question',
|
|
271
|
+
default: 'What is your favorite framework?'
|
|
272
|
+
},
|
|
273
|
+
options: {
|
|
274
|
+
type: 'array',
|
|
275
|
+
description: 'Poll options',
|
|
276
|
+
default: ['React', 'Vue', 'Svelte']
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} satisfies RemoteDomUIResource)
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* ════════════════════════════════════════════════════════════════════
|
|
283
|
+
* Traditional MCP Tools and Resources
|
|
284
|
+
* ════════════════════════════════════════════════════════════════════
|
|
285
|
+
*
|
|
286
|
+
* You can mix UIResources with traditional MCP tools and resources
|
|
287
|
+
*/
|
|
288
|
+
|
|
289
|
+
server.tool({
|
|
290
|
+
name: 'get-widget-info',
|
|
291
|
+
description: 'Get information about available UI widgets',
|
|
292
|
+
fn: async () => {
|
|
293
|
+
const widgets = [
|
|
294
|
+
{
|
|
295
|
+
name: 'kanban-board',
|
|
296
|
+
type: 'externalUrl',
|
|
297
|
+
tool: 'ui_kanban-board',
|
|
298
|
+
resource: 'ui://widget/kanban-board',
|
|
299
|
+
url: `http://localhost:${PORT}/mcp-use/widgets/kanban-board`
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: 'welcome-card',
|
|
303
|
+
type: 'rawHtml',
|
|
304
|
+
tool: 'ui_welcome-card',
|
|
305
|
+
resource: 'ui://widget/welcome-card'
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: 'quick-poll',
|
|
309
|
+
type: 'remoteDom',
|
|
310
|
+
tool: 'ui_quick-poll',
|
|
311
|
+
resource: 'ui://widget/quick-poll'
|
|
312
|
+
}
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
content: [{
|
|
317
|
+
type: 'text',
|
|
318
|
+
text: `Available UI Widgets:\n\n${widgets.map(w =>
|
|
319
|
+
`📦 ${w.name} (${w.type})\n` +
|
|
320
|
+
` Tool: ${w.tool}\n` +
|
|
321
|
+
` Resource: ${w.resource}\n` +
|
|
322
|
+
(w.url ? ` Browser: ${w.url}\n` : '')
|
|
323
|
+
).join('\n')}\n` +
|
|
324
|
+
`\nTypes Explained:\n` +
|
|
325
|
+
`• externalUrl: Iframe widget from filesystem\n` +
|
|
326
|
+
`• rawHtml: Direct HTML rendering\n` +
|
|
327
|
+
`• remoteDom: React/Web Components scripting`
|
|
328
|
+
}]
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
server.resource({
|
|
334
|
+
name: 'server-config',
|
|
335
|
+
uri: 'config://server',
|
|
336
|
+
title: 'Server Configuration',
|
|
337
|
+
description: 'Current server configuration and status',
|
|
338
|
+
mimeType: 'application/json',
|
|
339
|
+
fn: async () => ({
|
|
340
|
+
contents: [{
|
|
341
|
+
uri: 'config://server',
|
|
342
|
+
mimeType: 'application/json',
|
|
343
|
+
text: JSON.stringify({
|
|
344
|
+
port: PORT,
|
|
345
|
+
version: '1.0.0',
|
|
346
|
+
widgets: {
|
|
347
|
+
total: 3,
|
|
348
|
+
types: {
|
|
349
|
+
externalUrl: ['kanban-board'],
|
|
350
|
+
rawHtml: ['welcome-card'],
|
|
351
|
+
remoteDom: ['quick-poll']
|
|
352
|
+
},
|
|
353
|
+
baseUrl: `http://localhost:${PORT}/mcp-use/widgets/`
|
|
354
|
+
},
|
|
355
|
+
endpoints: {
|
|
356
|
+
mcp: `http://localhost:${PORT}/mcp`,
|
|
357
|
+
inspector: `http://localhost:${PORT}/inspector`,
|
|
358
|
+
widgets: `http://localhost:${PORT}/mcp-use/widgets/`
|
|
359
|
+
}
|
|
360
|
+
}, null, 2)
|
|
361
|
+
}]
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
// Start the server
|
|
366
|
+
server.listen(PORT)
|
|
367
|
+
|
|
368
|
+
// Display helpful startup message
|
|
369
|
+
console.log(`
|
|
370
|
+
╔═══════════════════════════════════════════════════════════════╗
|
|
371
|
+
║ 🎨 UIResource MCP Server (All Types) ║
|
|
372
|
+
╚═══════════════════════════════════════════════════════════════╝
|
|
373
|
+
|
|
374
|
+
Server is running on port ${PORT}
|
|
375
|
+
|
|
376
|
+
📍 Endpoints:
|
|
377
|
+
MCP Protocol: http://localhost:${PORT}/mcp
|
|
378
|
+
Inspector UI: http://localhost:${PORT}/inspector
|
|
379
|
+
Widgets Base: http://localhost:${PORT}/mcp-use/widgets/
|
|
380
|
+
|
|
381
|
+
🎯 Available UIResources (3 types demonstrated):
|
|
382
|
+
|
|
383
|
+
1️⃣ External URL Widget (Iframe)
|
|
384
|
+
• kanban-board
|
|
385
|
+
Tool: ui_kanban-board
|
|
386
|
+
Resource: ui://widget/kanban-board
|
|
387
|
+
Browser: http://localhost:${PORT}/mcp-use/widgets/kanban-board
|
|
388
|
+
|
|
389
|
+
2️⃣ Raw HTML Widget (Direct Rendering)
|
|
390
|
+
• welcome-card
|
|
391
|
+
Tool: ui_welcome-card
|
|
392
|
+
Resource: ui://widget/welcome-card
|
|
393
|
+
|
|
394
|
+
3️⃣ Remote DOM Widget (React Components)
|
|
395
|
+
• quick-poll
|
|
396
|
+
Tool: ui_quick-poll
|
|
397
|
+
Resource: ui://widget/quick-poll
|
|
398
|
+
|
|
399
|
+
📝 Usage Examples:
|
|
400
|
+
|
|
401
|
+
// External URL - Call with dynamic parameters
|
|
402
|
+
await client.callTool('ui_kanban-board', {
|
|
403
|
+
initialTasks: [{id: 1, title: 'Task 1'}],
|
|
404
|
+
theme: 'dark'
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
// Raw HTML - Access as resource
|
|
408
|
+
await client.readResource('ui://widget/welcome-card')
|
|
409
|
+
|
|
410
|
+
// Remote DOM - Interactive component
|
|
411
|
+
await client.callTool('ui_quick-poll', {
|
|
412
|
+
question: 'Favorite color?',
|
|
413
|
+
options: ['Red', 'Blue', 'Green']
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
💡 Tip: Open the Inspector UI to test all widget types interactively!
|
|
417
|
+
`)
|
|
418
|
+
|
|
419
|
+
// Handle graceful shutdown
|
|
420
|
+
process.on('SIGINT', () => {
|
|
421
|
+
console.log('\n\nShutting down server...')
|
|
422
|
+
process.exit(0)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
export default server
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"jsx": "react-jsx",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"allowJs": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
"sourceMap": true,
|
|
13
|
+
"allowSyntheticDefaultImports": true,
|
|
14
|
+
"esModuleInterop": true,
|
|
15
|
+
"forceConsistentCasingInFileNames": true,
|
|
16
|
+
"skipLibCheck": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["index.ts", "src/**/*", "resources/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|