@@ -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'
811import { McpServer as OfficialMcpServer , ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
912import { z } from 'zod'
1013import express , { type Express } from 'express'
1114import { existsSync , readdirSync } from 'node:fs'
1215import { join } from 'node:path'
1316import { requestLogger } from './logging.js'
17+ import { createUIResource } from '@mcp-ui/server'
1418
1519export 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 *
0 commit comments