1+ /**
2+ * MCP-UI Adapter
3+ *
4+ * Provides an adapter between mcp-use high-level UIResource definitions
5+ * and the low-level @mcp-ui/server resource format.
6+ * Ref: https://mcpui.dev/guide/server/typescript/usage-examples
7+ */
8+
9+ import { createUIResource } from '@mcp-ui/server'
10+ import type { UIResourceContent , UIResourceDefinition } from '../types/resource.js'
11+
12+ /**
13+ * Content type options for UI resources
14+ */
15+ export type UIContentType =
16+ | 'externalUrl' // Default: iframe URL for serving widgets
17+ | 'rawHtml' // Direct HTML content
18+ | 'remoteDom' // Remote DOM scripting
19+
20+ /**
21+ * Encoding options for UI resources
22+ */
23+ export type UIEncoding = 'text' | 'blob'
24+
25+ /**
26+ * Framework options for Remote DOM resources
27+ */
28+ export type RemoteDomFramework = 'react' | 'webcomponents'
29+
30+ /**
31+ * Extended UI resource definition with content type support
32+ */
33+ export interface ExtendedUIResourceDefinition extends UIResourceDefinition {
34+ contentType ?: UIContentType
35+ encoding ?: UIEncoding
36+ htmlContent ?: string
37+ remoteDomScript ?: string
38+ remoteDomFramework ?: RemoteDomFramework
39+ }
40+
41+ /**
42+ * Configuration for the adapter
43+ */
44+ export interface AdapterConfig {
45+ baseUrl : string
46+ port : number | string
47+ }
48+
49+ /**
50+ * MCP-UI Adapter class
51+ */
52+ export class McpUiAdapter {
53+ private config : AdapterConfig
54+
55+ constructor ( config : AdapterConfig ) {
56+ this . config = config
57+ }
58+
59+ /**
60+ * Build the full URL for a widget
61+ */
62+ private buildWidgetUrl ( widget : string , props ?: Record < string , any > ) : string {
63+ const url = new URL (
64+ `/mcp-use/widgets/${ widget } ` ,
65+ `http://localhost:${ this . config . port } `
66+ )
67+
68+ if ( props ) {
69+ Object . entries ( props ) . forEach ( ( [ key , value ] ) => {
70+ if ( value !== undefined && value !== null ) {
71+ const stringValue = typeof value === 'object'
72+ ? JSON . stringify ( value )
73+ : String ( value )
74+ url . searchParams . set ( key , stringValue )
75+ }
76+ } )
77+ }
78+
79+ return url . toString ( )
80+ }
81+
82+ /**
83+ * Create a UIResource for an external URL (default for widgets)
84+ * @param uri - URI of the resource
85+ * @param iframeUrl - URL of the iframe
86+ * @param encoding - Encoding of the resource (text or blob (URL is Base64 encoded))
87+ * @returns UIResourceContent
88+ */
89+ createExternalUrlResource (
90+ uri : string ,
91+ iframeUrl : string ,
92+ encoding : UIEncoding = 'text'
93+ ) : UIResourceContent {
94+ return createUIResource ( {
95+ uri : uri as `ui://${string } `,
96+ content : { type : 'externalUrl' , iframeUrl } ,
97+ encoding
98+ } )
99+ }
100+
101+ /**
102+ * Create a UIResource for raw HTML content
103+ *
104+ * @param uri - URI of the resource
105+ * @param htmlString - HTML string to embed
106+ * @param encoding - Encoding of the resource
107+ * @returns UIResourceContent
108+ */
109+ createRawHtmlResource (
110+ uri : string ,
111+ htmlString : string ,
112+ encoding : UIEncoding = 'text'
113+ ) : UIResourceContent {
114+ return createUIResource ( {
115+ uri : uri as `ui://${string } `,
116+ content : { type : 'rawHtml' , htmlString } ,
117+ encoding
118+ } )
119+ }
120+
121+ /**
122+ * Create a UIResource for Remote DOM scripting
123+ */
124+ createRemoteDomResource (
125+ uri : string ,
126+ script : string ,
127+ framework : RemoteDomFramework = 'react' ,
128+ encoding : UIEncoding = 'text'
129+ ) : UIResourceContent {
130+ return createUIResource ( {
131+ uri : uri as `ui://${string } `,
132+ content : { type : 'remoteDom' , script, framework } ,
133+ encoding
134+ } )
135+ }
136+
137+ /**
138+ * Create a UIResource from our high-level definition
139+ */
140+ createWidgetUIResource (
141+ definition : ExtendedUIResourceDefinition ,
142+ props ?: Record < string , any >
143+ ) : UIResourceContent {
144+ const uri = `ui://widget/${ definition . name } `
145+ const contentType = definition . contentType || 'externalUrl'
146+ const encoding = definition . encoding || 'text'
147+
148+ switch ( contentType ) {
149+ case 'externalUrl' : {
150+ const widgetUrl = this . buildWidgetUrl ( definition . widget , props )
151+ return this . createExternalUrlResource ( uri , widgetUrl , encoding )
152+ }
153+
154+ case 'rawHtml' : {
155+ if ( ! definition . htmlContent ) {
156+ throw new Error ( `HTML content required for rawHtml type in widget ${ definition . name } ` )
157+ }
158+ return this . createRawHtmlResource ( uri , definition . htmlContent , encoding )
159+ }
160+
161+ case 'remoteDom' : {
162+ if ( ! definition . remoteDomScript ) {
163+ throw new Error ( `Remote DOM script required for remoteDom type in widget ${ definition . name } ` )
164+ }
165+ const framework = definition . remoteDomFramework || 'react'
166+ return this . createRemoteDomResource (
167+ uri ,
168+ definition . remoteDomScript ,
169+ framework ,
170+ encoding
171+ )
172+ }
173+
174+ default :
175+ throw new Error ( `Unknown content type: ${ contentType } ` )
176+ }
177+ }
178+
179+ /**
180+ * Generate HTML content for a widget (for rawHtml type)
181+ */
182+ generateWidgetHtml (
183+ definition : UIResourceDefinition ,
184+ props ?: Record < string , any >
185+ ) : string {
186+ const [ width = '100%' , height = '400px' ] = definition . size || [ ]
187+ const propsJson = props ? JSON . stringify ( props ) : '{}'
188+
189+ return `
190+ <!DOCTYPE html>
191+ <html>
192+ <head>
193+ <meta charset="UTF-8">
194+ <title>${ definition . title || definition . name } </title>
195+ <style>
196+ body {
197+ margin: 0;
198+ padding: 20px;
199+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
200+ }
201+ .widget-container {
202+ width: ${ width } ;
203+ height: ${ height } ;
204+ border: 1px solid #e0e0e0;
205+ border-radius: 8px;
206+ overflow: auto;
207+ padding: 20px;
208+ background: white;
209+ }
210+ .widget-title {
211+ font-size: 1.5em;
212+ font-weight: 600;
213+ margin-bottom: 10px;
214+ }
215+ .widget-description {
216+ color: #666;
217+ margin-bottom: 20px;
218+ }
219+ </style>
220+ </head>
221+ <body>
222+ <div class="widget-container">
223+ <div class="widget-title">${ definition . title || definition . name } </div>
224+ ${ definition . description ? `<div class="widget-description">${ definition . description } </div>` : '' }
225+ <div id="widget-root"></div>
226+ </div>
227+ <script>
228+ // Widget props passed from server
229+ window.__WIDGET_PROPS__ = ${ propsJson } ;
230+
231+ // Placeholder for widget initialization
232+ console.log('Widget ${ definition . name } loaded with props:', window.__WIDGET_PROPS__);
233+
234+ // Communication with parent window
235+ window.addEventListener('message', (event) => {
236+ console.log('Received message:', event.data);
237+ });
238+
239+ // Example tool call
240+ function callTool(toolName, params) {
241+ window.parent.postMessage({
242+ type: 'tool',
243+ payload: { toolName, params }
244+ }, '*');
245+ }
246+ </script>
247+ </body>
248+ </html>`
249+ }
250+
251+ /**
252+ * Generate a Remote DOM script for a widget
253+ */
254+ generateRemoteDomScript (
255+ definition : UIResourceDefinition ,
256+ props ?: Record < string , any >
257+ ) : string {
258+ return `
259+ // Remote DOM script for ${ definition . name }
260+ const container = document.createElement('div');
261+ container.style.padding = '20px';
262+
263+ // Create title
264+ const title = document.createElement('h2');
265+ title.textContent = '${ definition . title || definition . name } ';
266+ container.appendChild(title);
267+
268+ ${ definition . description ? `
269+ // Add description
270+ const description = document.createElement('p');
271+ description.textContent = '${ definition . description } ';
272+ description.style.color = '#666';
273+ container.appendChild(description);
274+ ` : '' }
275+
276+ // Widget props
277+ const props = ${ JSON . stringify ( props || { } ) } ;
278+
279+ // Create interactive button
280+ const button = document.createElement('ui-button');
281+ button.setAttribute('label', 'Interact with ${ definition . name } ');
282+ button.addEventListener('press', () => {
283+ window.parent.postMessage({
284+ type: 'tool',
285+ payload: {
286+ toolName: 'ui_${ definition . name } ',
287+ params: props
288+ }
289+ }, '*');
290+ });
291+ container.appendChild(button);
292+
293+ // Add custom widget logic here
294+ console.log('Remote DOM widget ${ definition . name } initialized with props:', props);
295+
296+ // Append to root
297+ root.appendChild(container);`
298+ }
299+ }
300+
301+ /**
302+ * Factory function to create an adapter instance
303+ */
304+ export function createMcpUiAdapter ( config : AdapterConfig ) : McpUiAdapter {
305+ return new McpUiAdapter ( config )
306+ }
0 commit comments