Skip to content
This repository was archived by the owner on May 21, 2026. It is now read-only.

Commit 9a55066

Browse files
committed
feat: implement UI resource functionality in MCP server
- Add UIResourceDefinition type and supporting interfaces for widget configuration - Implement uiResource() method that registers widgets as both tools and resources - Add widget prop handling with automatic query parameter conversion - Create modular type system with separate files for common, resource, tool, and prompt types - Support widget iframe rendering with configurable frame sizes - Add widget serving routes for static assets and HTML files - Export UIResource types from main package index for external consumption
1 parent 4dd4415 commit 9a55066

9 files changed

Lines changed: 428 additions & 97 deletions

File tree

packages/mcp-use/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ export type {
3232
ServerConfig,
3333
ToolDefinition,
3434
ToolHandler,
35+
// UIResource specific types
36+
UIResourceDefinition,
37+
WidgetProps,
38+
WidgetConfig,
39+
WidgetManifest,
40+
DiscoverWidgetsOptions,
3541
} from './src/server/types.js'
3642
// Export telemetry utilities
3743
export { setTelemetrySource, Telemetry } from './src/telemetry/index.js'
Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
1-
export {
2-
createMCPServer
1+
export {
2+
createMCPServer,
3+
type McpServerInstance
34
} from './mcp-server.js'
4-
export type {
5-
InputDefinition,
6-
PromptDefinition,
7-
PromptHandler,
8-
ResourceDefinition,
9-
ResourceHandler,
10-
ServerConfig,
11-
ToolDefinition,
12-
ToolHandler,
13-
} from './types.js'
5+
6+
export * from './types/index.js'

packages/mcp-use/src/server/mcp-server.ts

Lines changed: 209 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ import type {
44
ResourceTemplateDefinition,
55
ServerConfig,
66
ToolDefinition,
7-
} from './types.js'
7+
UIResourceDefinition,
8+
WidgetProps,
9+
InputDefinition,
10+
} from './types/index.js'
811
import { McpServer as OfficialMcpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
912
import { z } from 'zod'
1013
import express, { type Express } from 'express'
1114
import { existsSync, readdirSync } from 'node:fs'
1215
import { join } from 'node:path'
1316
import { requestLogger } from './logging.js'
17+
import { createUIResource } from '@mcp-ui/server'
1418

1519
export class McpServer {
1620
private server: OfficialMcpServer
@@ -291,6 +295,210 @@ export class McpServer {
291295
return this
292296
}
293297

298+
/**
299+
* Register a UI widget as both a tool and a resource
300+
*
301+
* Creates a unified interface for MCP-UI compatible widgets that can be accessed
302+
* either as tools (with parameters) or as resources (static access). The tool
303+
* allows dynamic parameter passing while the resource provides discoverable access.
304+
*
305+
* @param definition - Configuration for the UI widget
306+
* @param definition.name - Unique identifier for the resource
307+
* @param definition.widget - Widget name (matches directory in dist/resources/mcp-use/widgets)
308+
* @param definition.title - Human-readable title for the widget
309+
* @param definition.description - Description of the widget's functionality
310+
* @param definition.props - Widget properties configuration with types and defaults
311+
* @param definition.size - Preferred iframe size [width, height] (e.g., ['800px', '600px'])
312+
* @param definition.annotations - Resource annotations for discovery
313+
* @returns The server instance for method chaining
314+
*
315+
* @example
316+
* ```typescript
317+
* server.uiResource({
318+
* name: 'kanban-board',
319+
* widget: 'kanban-board',
320+
* title: 'Kanban Board',
321+
* description: 'Interactive task management board',
322+
* props: {
323+
* initialTasks: {
324+
* type: 'array',
325+
* description: 'Initial tasks to display',
326+
* required: false
327+
* },
328+
* theme: {
329+
* type: 'string',
330+
* default: 'light'
331+
* }
332+
* },
333+
* size: ['900px', '600px']
334+
* })
335+
* ```
336+
*/
337+
uiResource(definition: UIResourceDefinition): this {
338+
// Register the tool - returns UIResource with parameters
339+
this.tool({
340+
name: `ui_${definition.widget}`,
341+
description: definition.description || `Display ${definition.widget} widget`,
342+
inputs: this.convertPropsToInputs(definition.props),
343+
fn: async (params) => {
344+
// Create the UIResource with user-provided params
345+
const uiResource = this.createWidgetUIResource(
346+
definition.widget,
347+
params,
348+
definition.size
349+
)
350+
351+
return {
352+
content: [
353+
{
354+
type: 'text',
355+
text: `Displaying ${definition.title || definition.widget} widget`
356+
},
357+
uiResource // Reuse the same UIResource
358+
]
359+
}
360+
}
361+
})
362+
363+
// Register the resource - returns widget URL for MCP clients
364+
this.resource({
365+
name: definition.name,
366+
uri: `ui://widget/${definition.widget}`,
367+
title: definition.title,
368+
description: definition.description,
369+
mimeType: 'text/uri-list',
370+
annotations: definition.annotations,
371+
fn: async () => {
372+
// Build the widget URL with default props
373+
const widgetUrl = this.buildWidgetUrl(
374+
definition.widget,
375+
this.applyDefaultProps(definition.props)
376+
)
377+
378+
return {
379+
contents: [{
380+
uri: `ui://widget/${definition.widget}`,
381+
mimeType: 'text/uri-list',
382+
text: widgetUrl
383+
}]
384+
}
385+
}
386+
})
387+
388+
return this
389+
}
390+
391+
/**
392+
* Create a UIResource object for a widget with the given parameters
393+
*
394+
* This method is shared between tool and resource handlers to avoid duplication.
395+
* It creates a consistent UIResource structure that can be rendered by MCP-UI
396+
* compatible clients.
397+
*
398+
* @private
399+
* @param widget - Widget name/identifier
400+
* @param params - Parameters to pass to the widget via URL
401+
* @param size - Optional preferred frame size [width, height]
402+
* @returns UIResource object compatible with MCP-UI
403+
*/
404+
private createWidgetUIResource(
405+
widget: string,
406+
params: Record<string, any>,
407+
size?: [string, string]
408+
): any {
409+
const iframeUrl = this.buildWidgetUrl(widget, params)
410+
411+
return createUIResource({
412+
uri: `ui://widget/${widget}` as any,
413+
content: {
414+
type: 'externalUrl',
415+
iframeUrl
416+
},
417+
encoding: 'text',
418+
uiMetadata: size ? {
419+
'preferred-frame-size': size
420+
} : undefined
421+
})
422+
}
423+
424+
/**
425+
* Build a complete URL for a widget including query parameters
426+
*
427+
* Constructs the full URL to access a widget's iframe, encoding any provided
428+
* parameters as query string parameters. Complex objects are JSON-stringified
429+
* for transmission.
430+
*
431+
* @private
432+
* @param widget - Widget name/identifier
433+
* @param params - Parameters to encode in the URL
434+
* @returns Complete URL with encoded parameters
435+
*/
436+
private buildWidgetUrl(widget: string, params: Record<string, any>): string {
437+
const baseUrl = `http://localhost:${this.serverPort}/mcp-use/widgets/${widget}`
438+
439+
if (Object.keys(params).length === 0) {
440+
return baseUrl
441+
}
442+
443+
const queryParams = new URLSearchParams()
444+
445+
for (const [key, value] of Object.entries(params)) {
446+
if (value !== undefined && value !== null) {
447+
if (typeof value === 'object') {
448+
queryParams.append(key, JSON.stringify(value))
449+
} else {
450+
queryParams.append(key, String(value))
451+
}
452+
}
453+
}
454+
455+
return `${baseUrl}?${queryParams.toString()}`
456+
}
457+
458+
/**
459+
* Convert widget props definition to tool input schema
460+
*
461+
* Transforms the widget props configuration into the format expected by
462+
* the tool registration system, mapping types and handling defaults.
463+
*
464+
* @private
465+
* @param props - Widget props configuration
466+
* @returns Array of InputDefinition objects for tool registration
467+
*/
468+
private convertPropsToInputs(props?: WidgetProps): InputDefinition[] {
469+
if (!props) return []
470+
471+
return Object.entries(props).map(([name, prop]) => ({
472+
name,
473+
type: prop.type,
474+
description: prop.description,
475+
required: prop.required,
476+
default: prop.default
477+
}))
478+
}
479+
480+
/**
481+
* Apply default values to widget props
482+
*
483+
* Extracts default values from the props configuration to use when
484+
* the resource is accessed without parameters.
485+
*
486+
* @private
487+
* @param props - Widget props configuration
488+
* @returns Object with default values for each prop
489+
*/
490+
private applyDefaultProps(props?: WidgetProps): Record<string, any> {
491+
if (!props) return {}
492+
493+
const defaults: Record<string, any> = {}
494+
for (const [key, prop] of Object.entries(props)) {
495+
if (prop.default !== undefined) {
496+
defaults[key] = prop.default
497+
}
498+
}
499+
return defaults
500+
}
501+
294502
/**
295503
* Mount MCP server endpoints at /mcp
296504
*
Lines changed: 3 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,5 @@
1-
import type { CallToolResult, GetPromptResult, ReadResourceResult} from '@modelcontextprotocol/sdk/types.js'
2-
export interface ServerConfig {
3-
name: string
4-
version: string
5-
description?: string
6-
}
7-
8-
export interface InputDefinition {
9-
name: string
10-
type: 'string' | 'number' | 'boolean' | 'object' | 'array'
11-
description?: string
12-
required?: boolean
13-
default?: any
14-
}
15-
161
/**
17-
* Annotations provide hints to clients about how to use or display resources
2+
* Legacy types export file - maintained for backward compatibility
3+
* New code should import from './types/index.js' instead
184
*/
19-
export interface ResourceAnnotations {
20-
/** Intended audience(s) for this resource */
21-
audience?: ('user' | 'assistant')[]
22-
/** Priority from 0.0 (least important) to 1.0 (most important) */
23-
priority?: number
24-
/** ISO 8601 formatted timestamp of last modification */
25-
lastModified?: string
26-
}
27-
28-
/**
29-
* Configuration for a resource template
30-
*/
31-
export interface ResourceTemplateConfig {
32-
/** URI template with {param} placeholders (e.g., "user://{userId}/profile") */
33-
uriTemplate: string
34-
/** Name of the resource */
35-
name?: string
36-
/** MIME type of the resource content */
37-
mimeType?: string
38-
/** Description of the resource */
39-
description?: string
40-
}
41-
42-
export interface ResourceTemplateDefinition {
43-
name: string
44-
resourceTemplate: ResourceTemplateConfig
45-
title?: string
46-
description?: string
47-
annotations?: ResourceAnnotations
48-
fn: ResourceTemplateHandler
49-
}
50-
51-
export interface ResourceDefinition {
52-
/** Unique identifier for the resource */
53-
name: string
54-
/** URI pattern for accessing the resource (e.g., 'config://app-settings') */
55-
uri: string
56-
/** Resource metadata including MIME type and description */
57-
/** Optional title for the resource */
58-
title?: string
59-
/** Optional description of the resource */
60-
description?: string
61-
/** MIME type of the resource content (required) */
62-
mimeType: string
63-
/** Optional annotations for the resource */
64-
annotations?: ResourceAnnotations
65-
/** Async function that returns the resource content */
66-
fn: ResourceHandler
67-
}
68-
69-
export interface ToolDefinition {
70-
name: string
71-
description?: string
72-
inputs?: InputDefinition[]
73-
fn: ToolHandler
74-
}
75-
76-
export interface PromptDefinition {
77-
name: string
78-
description?: string
79-
args?: InputDefinition[]
80-
fn: PromptHandler
81-
}
82-
83-
export type ResourceHandler = () => Promise<ReadResourceResult>
84-
export type ResourceTemplateHandler = (uri: URL, params: Record<string, any>) => Promise<ReadResourceResult>
85-
export type ToolHandler = (params: Record<string, any>) => Promise<CallToolResult>
86-
export type PromptHandler = (params: Record<string, any>) => Promise<GetPromptResult>
5+
export * from './types/index.js'
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Common type definitions shared across different MCP components
3+
*/
4+
5+
export interface ServerConfig {
6+
name: string
7+
version: string
8+
description?: string
9+
}
10+
11+
export interface InputDefinition {
12+
name: string
13+
type: 'string' | 'number' | 'boolean' | 'object' | 'array'
14+
description?: string
15+
required?: boolean
16+
default?: any
17+
}
18+
19+
/**
20+
* Annotations provide hints to clients about how to use or display resources
21+
*/
22+
export interface ResourceAnnotations {
23+
/** Intended audience(s) for this resource */
24+
audience?: ('user' | 'assistant')[]
25+
/** Priority from 0.0 (least important) to 1.0 (most important) */
26+
priority?: number
27+
/** ISO 8601 formatted timestamp of last modification */
28+
lastModified?: string
29+
}

0 commit comments

Comments
 (0)