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

Commit 12493cd

Browse files
committed
fix: refactor mcp-ui-adapter to be a pure function
1 parent 5f8456e commit 12493cd

6 files changed

Lines changed: 374 additions & 369 deletions

File tree

Lines changed: 155 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -1,193 +1,178 @@
11
/**
2-
* MCP-UI Adapter
2+
* MCP-UI Adapter Utilities
3+
*
4+
* Pure functions to convert mcp-use high-level UIResource definitions
5+
* into @mcp-ui/server compatible resource objects.
36
*
4-
* Provides an adapter between mcp-use high-level UIResource definitions
5-
* and the low-level @mcp-ui/server resource format.
67
* Ref: https://mcpui.dev/guide/server/typescript/usage-examples
78
*/
89

910
import { createUIResource } from '@mcp-ui/server'
10-
import type { UIResourceContent, UIResourceDefinition } from '../types/resource.js'
11+
import type {
12+
UIResourceContent,
13+
UIResourceDefinition,
14+
UIEncoding
15+
} from '../types/resource.js'
1116

1217
/**
13-
* Content type options for UI resources
18+
* Configuration for building widget URLs
1419
*/
15-
export type UIContentType =
16-
| 'externalUrl' // Default: iframe URL for serving widgets
17-
| 'rawHtml' // Direct HTML content
18-
| 'remoteDom' // Remote DOM scripting
20+
export interface UrlConfig {
21+
baseUrl: string
22+
port: number | string
23+
}
1924

2025
/**
21-
* Encoding options for UI resources
26+
* Build the full URL for a widget including query parameters
27+
*
28+
* @param widget - Widget identifier
29+
* @param props - Parameters to pass as query params
30+
* @param config - URL configuration (baseUrl and port)
31+
* @returns Complete widget URL with encoded parameters
2232
*/
23-
export type UIEncoding = 'text' | 'blob'
33+
export function buildWidgetUrl(
34+
widget: string,
35+
props: Record<string, any> | undefined,
36+
config: UrlConfig
37+
): string {
38+
const url = new URL(
39+
`/mcp-use/widgets/${widget}`,
40+
`${config.baseUrl}:${config.port}`
41+
)
42+
43+
if (props) {
44+
Object.entries(props).forEach(([key, value]) => {
45+
if (value !== undefined && value !== null) {
46+
const stringValue = typeof value === 'object'
47+
? JSON.stringify(value)
48+
: String(value)
49+
url.searchParams.set(key, stringValue)
50+
}
51+
})
52+
}
53+
54+
return url.toString()
55+
}
2456

2557
/**
26-
* Framework options for Remote DOM resources
58+
* Create a UIResource for an external URL (iframe)
59+
*
60+
* @param uri - Resource URI (must start with ui://)
61+
* @param iframeUrl - URL to load in iframe
62+
* @param encoding - Encoding type ('text' or 'blob')
63+
* @returns UIResourceContent object
2764
*/
28-
export type RemoteDomFramework = 'react' | 'webcomponents'
65+
export function createExternalUrlResource(
66+
uri: string,
67+
iframeUrl: string,
68+
encoding: UIEncoding = 'text'
69+
): UIResourceContent {
70+
return createUIResource({
71+
uri: uri as `ui://${string}`,
72+
content: { type: 'externalUrl', iframeUrl },
73+
encoding
74+
})
75+
}
2976

3077
/**
31-
* Extended UI resource definition with content type support
78+
* Create a UIResource for raw HTML content
79+
*
80+
* @param uri - Resource URI (must start with ui://)
81+
* @param htmlString - HTML content to render
82+
* @param encoding - Encoding type ('text' or 'blob')
83+
* @returns UIResourceContent object
3284
*/
33-
export interface ExtendedUIResourceDefinition extends UIResourceDefinition {
34-
contentType?: UIContentType
35-
encoding?: UIEncoding
36-
htmlContent?: string
37-
remoteDomScript?: string
38-
remoteDomFramework?: RemoteDomFramework
85+
export function createRawHtmlResource(
86+
uri: string,
87+
htmlString: string,
88+
encoding: UIEncoding = 'text'
89+
): UIResourceContent {
90+
return createUIResource({
91+
uri: uri as `ui://${string}`,
92+
content: { type: 'rawHtml', htmlString },
93+
encoding
94+
})
3995
}
4096

4197
/**
42-
* Configuration for the adapter
98+
* Create a UIResource for Remote DOM scripting
99+
*
100+
* @param uri - Resource URI (must start with ui://)
101+
* @param script - JavaScript code for remote DOM manipulation
102+
* @param framework - Framework for remote DOM ('react' or 'webcomponents')
103+
* @param encoding - Encoding type ('text' or 'blob')
104+
* @returns UIResourceContent object
43105
*/
44-
export interface AdapterConfig {
45-
baseUrl: string
46-
port: number | string
106+
export function createRemoteDomResource(
107+
uri: string,
108+
script: string,
109+
framework: 'react' | 'webcomponents' = 'react',
110+
encoding: UIEncoding = 'text'
111+
): UIResourceContent {
112+
return createUIResource({
113+
uri: uri as `ui://${string}`,
114+
content: { type: 'remoteDom', script, framework },
115+
encoding
116+
})
47117
}
48118

49119
/**
50-
* MCP-UI Adapter class
120+
* Create a UIResource from a high-level definition
121+
*
122+
* This is the main function that routes to the appropriate resource creator
123+
* based on the discriminated union type.
124+
*
125+
* @param definition - UIResource definition (discriminated union)
126+
* @param params - Runtime parameters for the widget (for externalUrl type)
127+
* @param config - URL configuration for building widget URLs
128+
* @returns UIResourceContent object
51129
*/
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-
})
130+
export function createUIResourceFromDefinition(
131+
definition: UIResourceDefinition,
132+
params: Record<string, any>,
133+
config: UrlConfig
134+
): UIResourceContent {
135+
const uri = `ui://widget/${definition.name}` as `ui://${string}`
136+
const encoding = definition.encoding || 'text'
137+
138+
switch (definition.type) {
139+
case 'externalUrl': {
140+
const widgetUrl = buildWidgetUrl(definition.widget, params, config)
141+
return createExternalUrlResource(uri, widgetUrl, encoding)
77142
}
78143

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-
}
144+
case 'rawHtml': {
145+
return createRawHtmlResource(uri, definition.htmlContent, encoding)
146+
}
160147

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-
}
148+
case 'remoteDom': {
149+
const framework = definition.framework || 'react'
150+
return createRemoteDomResource(uri, definition.script, framework, encoding)
151+
}
173152

174-
default:
175-
throw new Error(`Unknown content type: ${contentType}`)
153+
default: {
154+
// TypeScript exhaustiveness check
155+
const _exhaustive: never = definition
156+
throw new Error(`Unknown UI resource type: ${(_exhaustive as any).type}`)
176157
}
177158
}
159+
}
178160

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>
161+
/**
162+
* Generate HTML content for a widget (utility function)
163+
*
164+
* @param definition - Base UI resource definition
165+
* @param props - Widget properties to inject
166+
* @returns Generated HTML string
167+
*/
168+
export function generateWidgetHtml(
169+
definition: Pick<UIResourceDefinition, 'name' | 'title' | 'description' | 'size'>,
170+
props?: Record<string, any>
171+
): string {
172+
const [width = '100%', height = '400px'] = definition.size || []
173+
const propsJson = props ? JSON.stringify(props) : '{}'
174+
175+
return `<!DOCTYPE html>
191176
<html>
192177
<head>
193178
<meta charset="UTF-8">
@@ -246,16 +231,20 @@ export class McpUiAdapter {
246231
</script>
247232
</body>
248233
</html>`
249-
}
234+
}
250235

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 `
236+
/**
237+
* Generate a Remote DOM script for a widget (utility function)
238+
*
239+
* @param definition - Base UI resource definition
240+
* @param props - Widget properties to inject
241+
* @returns Generated JavaScript string
242+
*/
243+
export function generateRemoteDomScript(
244+
definition: Pick<UIResourceDefinition, 'name' | 'title' | 'description'>,
245+
props?: Record<string, any>
246+
): string {
247+
return `
259248
// Remote DOM script for ${definition.name}
260249
const container = document.createElement('div');
261250
container.style.padding = '20px';
@@ -295,12 +284,4 @@ console.log('Remote DOM widget ${definition.name} initialized with props:', prop
295284
296285
// Append to root
297286
root.appendChild(container);`
298-
}
299287
}
300-
301-
/**
302-
* Factory function to create an adapter instance
303-
*/
304-
export function createMcpUiAdapter(config: AdapterConfig): McpUiAdapter {
305-
return new McpUiAdapter(config)
306-
}

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ export {
55

66
export * from './types/index.js'
77

8-
// MCP-UI adapter exports
8+
// MCP-UI adapter utility functions
99
export {
10-
McpUiAdapter,
11-
createMcpUiAdapter,
12-
type ExtendedUIResourceDefinition,
13-
type AdapterConfig
10+
buildWidgetUrl,
11+
createExternalUrlResource,
12+
createRawHtmlResource,
13+
createRemoteDomResource,
14+
createUIResourceFromDefinition,
15+
generateWidgetHtml,
16+
generateRemoteDomScript,
17+
type UrlConfig
1418
} from './adapters/mcp-ui-adapter.js'

0 commit comments

Comments
 (0)