@seed-ship/mcp-ui-solid 1.0.29 → 1.0.31

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.
Files changed (85) hide show
  1. package/dist/components/GenerativeUIErrorBoundary.cjs.map +1 -0
  2. package/dist/components/GenerativeUIErrorBoundary.js.map +1 -0
  3. package/dist/components/StreamingUIRenderer.cjs.map +1 -0
  4. package/dist/components/StreamingUIRenderer.js.map +1 -0
  5. package/dist/{mcp-ui-solid/src/components → components}/UIResourceRenderer.cjs +102 -97
  6. package/dist/components/UIResourceRenderer.cjs.map +1 -0
  7. package/dist/components/UIResourceRenderer.d.ts +0 -11
  8. package/dist/components/UIResourceRenderer.d.ts.map +1 -1
  9. package/dist/{mcp-ui-solid/src/components → components}/UIResourceRenderer.js +102 -97
  10. package/dist/components/UIResourceRenderer.js.map +1 -0
  11. package/dist/components.cjs +3 -3
  12. package/dist/components.d.ts +12 -0
  13. package/dist/components.js +3 -3
  14. package/dist/hooks/useStreamingUI.cjs.map +1 -0
  15. package/dist/hooks/useStreamingUI.js.map +1 -0
  16. package/dist/hooks.cjs +1 -1
  17. package/dist/hooks.d.ts +8 -0
  18. package/dist/hooks.js +1 -1
  19. package/dist/index.cjs +6 -6
  20. package/dist/index.js +6 -6
  21. package/dist/node_modules/.pnpm/dompurify@3.3.0/node_modules/dompurify/dist/purify.es.cjs +1006 -0
  22. package/dist/node_modules/.pnpm/dompurify@3.3.0/node_modules/dompurify/dist/purify.es.cjs.map +1 -0
  23. package/dist/node_modules/.pnpm/dompurify@3.3.0/node_modules/dompurify/dist/purify.es.js +1007 -0
  24. package/dist/node_modules/.pnpm/dompurify@3.3.0/node_modules/dompurify/dist/purify.es.js.map +1 -0
  25. package/dist/services/component-registry.cjs.map +1 -0
  26. package/dist/services/component-registry.js.map +1 -0
  27. package/dist/services/validation.cjs.map +1 -0
  28. package/dist/services/validation.js.map +1 -0
  29. package/dist/types.d.ts +265 -0
  30. package/dist/utils/logger.cjs.map +1 -0
  31. package/dist/utils/logger.js.map +1 -0
  32. package/dist/validation.cjs +1 -1
  33. package/dist/validation.js +1 -1
  34. package/package.json +20 -23
  35. package/src/components/ActionRenderer.tsx +33 -0
  36. package/src/components/ArtifactRenderer.tsx +54 -0
  37. package/src/components/CarouselRenderer.tsx +77 -0
  38. package/src/components/FooterRenderer.tsx +66 -0
  39. package/src/components/GenerativeUIErrorBoundary.tsx +259 -0
  40. package/src/components/StreamingUIRenderer.tsx +327 -0
  41. package/src/components/UIResourceRenderer.tsx +573 -0
  42. package/src/components/index.ts +14 -0
  43. package/src/hooks/index.ts +14 -0
  44. package/src/hooks/useStreamingUI.ts +447 -0
  45. package/src/index.test.ts +36 -0
  46. package/src/index.ts +70 -0
  47. package/src/services/component-registry.ts +378 -0
  48. package/src/services/index.ts +9 -0
  49. package/src/services/validation.ts +472 -0
  50. package/src/types/index.ts +320 -0
  51. package/src/types-export.ts +31 -0
  52. package/src/utils/logger.ts +74 -0
  53. package/src/validation.ts +38 -0
  54. package/src/vite-env.d.ts +11 -0
  55. package/tsconfig.json +20 -0
  56. package/tsconfig.tsbuildinfo +1 -0
  57. package/vite.config.ts +52 -0
  58. package/vite.config.ts.timestamp-1763266929437-a71eed80b91318.mjs +45 -0
  59. package/vitest.config.ts +10 -0
  60. package/dist/mcp-ui-solid/src/components/GenerativeUIErrorBoundary.cjs.map +0 -1
  61. package/dist/mcp-ui-solid/src/components/GenerativeUIErrorBoundary.js.map +0 -1
  62. package/dist/mcp-ui-solid/src/components/StreamingUIRenderer.cjs.map +0 -1
  63. package/dist/mcp-ui-solid/src/components/StreamingUIRenderer.js.map +0 -1
  64. package/dist/mcp-ui-solid/src/components/UIResourceRenderer.cjs.map +0 -1
  65. package/dist/mcp-ui-solid/src/components/UIResourceRenderer.js.map +0 -1
  66. package/dist/mcp-ui-solid/src/hooks/useStreamingUI.cjs.map +0 -1
  67. package/dist/mcp-ui-solid/src/hooks/useStreamingUI.js.map +0 -1
  68. package/dist/mcp-ui-solid/src/services/component-registry.cjs.map +0 -1
  69. package/dist/mcp-ui-solid/src/services/component-registry.js.map +0 -1
  70. package/dist/mcp-ui-solid/src/services/validation.cjs.map +0 -1
  71. package/dist/mcp-ui-solid/src/services/validation.js.map +0 -1
  72. package/dist/mcp-ui-solid/src/utils/logger.cjs.map +0 -1
  73. package/dist/mcp-ui-solid/src/utils/logger.js.map +0 -1
  74. /package/dist/{mcp-ui-solid/src/components → components}/GenerativeUIErrorBoundary.cjs +0 -0
  75. /package/dist/{mcp-ui-solid/src/components → components}/GenerativeUIErrorBoundary.js +0 -0
  76. /package/dist/{mcp-ui-solid/src/components → components}/StreamingUIRenderer.cjs +0 -0
  77. /package/dist/{mcp-ui-solid/src/components → components}/StreamingUIRenderer.js +0 -0
  78. /package/dist/{mcp-ui-solid/src/hooks → hooks}/useStreamingUI.cjs +0 -0
  79. /package/dist/{mcp-ui-solid/src/hooks → hooks}/useStreamingUI.js +0 -0
  80. /package/dist/{mcp-ui-solid/src/services → services}/component-registry.cjs +0 -0
  81. /package/dist/{mcp-ui-solid/src/services → services}/component-registry.js +0 -0
  82. /package/dist/{mcp-ui-solid/src/services → services}/validation.cjs +0 -0
  83. /package/dist/{mcp-ui-solid/src/services → services}/validation.js +0 -0
  84. /package/dist/{mcp-ui-solid/src/utils → utils}/logger.cjs +0 -0
  85. /package/dist/{mcp-ui-solid/src/utils → utils}/logger.js +0 -0
@@ -0,0 +1,573 @@
1
+ /**
2
+ * UI Resource Renderer Component
3
+ * Phase 0: Foundation with iframe sandbox and composite grid support
4
+ */
5
+
6
+ import DOMPurify from 'dompurify'
7
+ import { Component, createSignal, onMount, Show, For, createMemo } from 'solid-js'
8
+ import { isServer } from 'solid-js/web'
9
+ import type { UIComponent, UILayout, RendererError, ComponentType } from '../types'
10
+ import { validateComponent, DEFAULT_RESOURCE_LIMITS } from '../services/validation'
11
+ import { GenerativeUIErrorBoundary } from './GenerativeUIErrorBoundary'
12
+ import { marked } from 'marked'
13
+
14
+ /**
15
+ * Props for UIResourceRenderer
16
+ */
17
+ export interface UIResourceRendererProps {
18
+ /**
19
+ * Single component or full layout to render
20
+ */
21
+ content: UIComponent | UILayout
22
+
23
+ /**
24
+ * Lazy loading (default: true)
25
+ */
26
+ lazyLoad?: boolean
27
+
28
+ /**
29
+ * Error callback
30
+ */
31
+ onError?: (error: RendererError) => void
32
+
33
+ /**
34
+ * Custom CSS class
35
+ */
36
+ class?: string
37
+ }
38
+
39
+ /**
40
+ * Render a single chart component in a sandboxed iframe
41
+ */
42
+ function ChartRenderer(props: {
43
+ component: UIComponent
44
+ onError?: (error: RendererError) => void
45
+ }) {
46
+ const [iframeUrl, setIframeUrl] = createSignal<string>()
47
+ const [isLoading, setIsLoading] = createSignal(true)
48
+ const [error, setError] = createSignal<string>()
49
+
50
+ onMount(() => {
51
+ const chartParams = props.component.params as any
52
+
53
+ // Build Quickchart URL
54
+ const chartConfig = {
55
+ type: chartParams.type,
56
+ data: chartParams.data,
57
+ options: {
58
+ ...chartParams.options,
59
+ responsive: true,
60
+ maintainAspectRatio: false,
61
+ },
62
+ }
63
+
64
+ // Encode chart configuration for Quickchart API
65
+ const configStr = encodeURIComponent(JSON.stringify(chartConfig))
66
+ const url = `https://quickchart.io/chart?c=${configStr}&width=500&height=300&devicePixelRatio=2`
67
+
68
+ // Validate domain (should always pass for quickchart.io)
69
+ setIframeUrl(url)
70
+ setIsLoading(false)
71
+ })
72
+
73
+ return (
74
+ <div class="relative w-full h-full min-h-[300px] bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
75
+ <Show when={isLoading()}>
76
+ <div class="absolute inset-0 flex items-center justify-center">
77
+ <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
78
+ </div>
79
+ </Show>
80
+
81
+ <Show when={error()}>
82
+ <div class="absolute inset-0 flex items-center justify-center p-4">
83
+ <div class="text-center">
84
+ <p class="text-red-600 dark:text-red-400 text-sm font-medium">Chart Error</p>
85
+ <p class="text-gray-600 dark:text-gray-400 text-xs mt-1">{error()}</p>
86
+ </div>
87
+ </div>
88
+ </Show>
89
+
90
+ <Show when={iframeUrl() && !error()}>
91
+ <div class="w-full h-full p-4">
92
+ <Show when={(props.component.params as any).title}>
93
+ <h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-3">
94
+ {(props.component.params as any).title}
95
+ </h3>
96
+ </Show>
97
+ <div class="w-full h-full">
98
+ <img
99
+ src={iframeUrl()}
100
+ alt="Chart visualization"
101
+ class="w-full h-auto max-h-[300px] object-contain"
102
+ onError={() => {
103
+ setError('Failed to load chart')
104
+ props.onError?.({
105
+ type: 'render',
106
+ message: 'Chart rendering failed',
107
+ componentId: props.component.id,
108
+ })
109
+ }}
110
+ />
111
+ </div>
112
+ </div>
113
+ </Show>
114
+ </div>
115
+ )
116
+ }
117
+
118
+ /**
119
+ * Render a table component
120
+ */
121
+ function TableRenderer(props: {
122
+ component: UIComponent
123
+ onError?: (error: RendererError) => void
124
+ }) {
125
+ const tableParams = props.component.params as any
126
+
127
+ return (
128
+ <div class="w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
129
+ <div class="p-4">
130
+ <Show when={tableParams.title}>
131
+ <h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-3">
132
+ {tableParams.title}
133
+ </h3>
134
+ </Show>
135
+
136
+ <div class="overflow-x-auto">
137
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 border-separate border-spacing-0">
138
+ <thead class="bg-gray-50 dark:bg-gray-900/50">
139
+ <tr>
140
+ <For each={tableParams.columns}>
141
+ {(column: any) => (
142
+ <th
143
+ scope="col"
144
+ class="px-6 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider border-b border-gray-200 dark:border-gray-700 first:pl-6 last:pr-6"
145
+ style={column.width ? { width: column.width } : {}}
146
+ >
147
+ {column.label}
148
+ </th>
149
+ )}
150
+ </For>
151
+ </tr>
152
+ </thead>
153
+ <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
154
+ <For each={tableParams.rows.slice(0, DEFAULT_RESOURCE_LIMITS.maxTableRows)}>
155
+ {(row: any, i) => (
156
+ <tr class={`hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors ${i() % 2 === 0 ? 'bg-white dark:bg-gray-800' : 'bg-gray-50/30 dark:bg-gray-800/50'}`}>
157
+ <For each={tableParams.columns}>
158
+ {(column: any) => (
159
+ <td class="px-6 py-4 text-sm text-gray-700 dark:text-gray-200 whitespace-normal break-words leading-relaxed first:pl-6 last:pr-6">
160
+ <div
161
+ innerHTML={
162
+ typeof row[column.key] === 'string' && (row[column.key].includes('[') || row[column.key].includes('**') || row[column.key].includes('`'))
163
+ ? DOMPurify.sanitize(marked.parse(row[column.key], { async: false }) as string, { ADD_ATTR: ['target', 'rel'] })
164
+ : (row[column.key] || '-')
165
+ }
166
+ />
167
+ </td>
168
+ )}
169
+ </For>
170
+ </tr>
171
+ )}
172
+ </For>
173
+ </tbody>
174
+ </table>
175
+ </div>
176
+
177
+ <Show when={tableParams.pagination}>
178
+ <div class="mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
179
+ <span>
180
+ Showing {tableParams.pagination.currentPage * tableParams.pagination.pageSize + 1} -{' '}
181
+ {Math.min(
182
+ (tableParams.pagination.currentPage + 1) * tableParams.pagination.pageSize,
183
+ tableParams.pagination.totalRows
184
+ )}{' '}
185
+ of {tableParams.pagination.totalRows}
186
+ </span>
187
+ </div>
188
+ </Show>
189
+ </div>
190
+ </div>
191
+ )
192
+ }
193
+
194
+ /**
195
+ * Render a metric card component
196
+ */
197
+ function MetricRenderer(props: { component: UIComponent }) {
198
+ const metricParams = props.component.params as any
199
+
200
+ return (
201
+ <div class="w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4">
202
+ <div class="flex flex-col h-full justify-between">
203
+ <div>
204
+ <p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
205
+ {metricParams.title}
206
+ </p>
207
+ <div class="mt-2 flex items-baseline">
208
+ <p class="text-2xl font-semibold text-gray-900 dark:text-white">{metricParams.value}</p>
209
+ <Show when={metricParams.unit}>
210
+ <span class="ml-2 text-sm font-medium text-gray-500 dark:text-gray-400">
211
+ {metricParams.unit}
212
+ </span>
213
+ </Show>
214
+ </div>
215
+ </div>
216
+
217
+ <Show when={metricParams.trend}>
218
+ <div class="mt-3 flex items-center">
219
+ <span
220
+ class={`text-sm font-medium ${metricParams.trend.direction === 'up'
221
+ ? 'text-green-600 dark:text-green-400'
222
+ : metricParams.trend.direction === 'down'
223
+ ? 'text-red-600 dark:text-red-400'
224
+ : 'text-gray-600 dark:text-gray-400'
225
+ }`}
226
+ >
227
+ {metricParams.trend.direction === 'up'
228
+ ? '�'
229
+ : metricParams.trend.direction === 'down'
230
+ ? '�'
231
+ : '�'}{' '}
232
+ {Math.abs(metricParams.trend.value)}%
233
+ </span>
234
+ </div>
235
+ </Show>
236
+
237
+ <Show when={metricParams.subtitle}>
238
+ <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">{metricParams.subtitle}</p>
239
+ </Show>
240
+ </div>
241
+ </div>
242
+ )
243
+ }
244
+
245
+ /**
246
+ * Render a text component (with optional markdown)
247
+ */
248
+ function TextRenderer(props: { component: UIComponent }) {
249
+ const textParams = props.component.params as any
250
+
251
+ // Convert markdown to HTML if markdown flag is true
252
+ const htmlContent = createMemo(() => {
253
+ if (textParams.markdown) {
254
+ return marked.parse(textParams.content, { async: false }) as string
255
+ }
256
+ return textParams.content
257
+ })
258
+
259
+ return (
260
+ <div class="w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4">
261
+ <div
262
+ class={`prose prose-sm dark:prose-invert max-w-none ${textParams.className || ''}`}
263
+ innerHTML={htmlContent()}
264
+ />
265
+ </div>
266
+ )
267
+ }
268
+
269
+ /**
270
+ * Render an iframe component
271
+ */
272
+ function IframeRenderer(props: { component: UIComponent }) {
273
+ const params = props.component.params as any
274
+ return (
275
+ <div class="w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col">
276
+ <Show when={params.title}>
277
+ <div class="px-4 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
278
+ <h3 class="text-sm font-semibold text-gray-900 dark:text-white">{params.title}</h3>
279
+ </div>
280
+ </Show>
281
+ <iframe
282
+ src={params.url}
283
+ title={params.title || 'Embedded content'}
284
+ class="w-full border-0 flex-1"
285
+ style={`height: ${params.height || '400px'}; min-height: 300px;`}
286
+ sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
287
+ loading="lazy"
288
+ />
289
+ </div>
290
+ )
291
+ }
292
+
293
+ /**
294
+ * Render an image component
295
+ */
296
+ function ImageRenderer(props: { component: UIComponent }) {
297
+ const params = props.component.params as any
298
+ return (
299
+ <div class="w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col">
300
+ <div class="flex-1 flex items-center justify-center p-4 bg-gray-50 dark:bg-gray-900 min-h-[200px]">
301
+ <a href={params.url} target="_blank" rel="noopener noreferrer" class="cursor-zoom-in">
302
+ <img
303
+ src={params.url}
304
+ alt={params.alt || 'Image'}
305
+ class="max-w-full max-h-[500px] object-contain rounded shadow-sm hover:opacity-95 transition-opacity"
306
+ loading="lazy"
307
+ />
308
+ </a>
309
+ </div>
310
+ <Show when={params.caption}>
311
+ <div class="p-3 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
312
+ <p class="text-sm text-gray-600 dark:text-gray-400 text-center">{params.caption}</p>
313
+ </div>
314
+ </Show>
315
+ </div>
316
+ )
317
+ }
318
+
319
+ /**
320
+ * Render a link component
321
+ */
322
+ /**
323
+ * Render a link component
324
+ */
325
+ function LinkRenderer(props: { component: UIComponent }) {
326
+ const params = props.component.params as any
327
+ return (
328
+ <a
329
+ href={params.url}
330
+ target="_blank"
331
+ rel="noopener noreferrer"
332
+ class="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors group h-full"
333
+ onClick={(e) => e.stopPropagation()}
334
+ >
335
+ <div class="p-2 bg-blue-50 dark:bg-blue-900/30 rounded-full text-blue-600 dark:text-blue-400 group-hover:bg-blue-100 dark:group-hover:bg-blue-900/50 shrink-0 transition-colors">
336
+ <svg
337
+ xmlns="http://www.w3.org/2000/svg"
338
+ class="w-5 h-5"
339
+ viewBox="0 0 24 24"
340
+ fill="none"
341
+ stroke="currentColor"
342
+ stroke-width="2"
343
+ stroke-linecap="round"
344
+ stroke-linejoin="round"
345
+ >
346
+ <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
347
+ <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
348
+ </svg>
349
+ </div>
350
+ <div class="flex-1 min-w-0">
351
+ <h4 class="text-sm font-medium text-gray-900 dark:text-white truncate group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
352
+ {params.label || params.url}
353
+ </h4>
354
+ <Show when={params.description}>
355
+ <p class="text-xs text-gray-500 dark:text-gray-400 truncate">{params.description}</p>
356
+ </Show>
357
+ </div>
358
+ <svg
359
+ xmlns="http://www.w3.org/2000/svg"
360
+ class="w-4 h-4 text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300 shrink-0 transition-colors"
361
+ viewBox="0 0 24 24"
362
+ fill="none"
363
+ stroke="currentColor"
364
+ stroke-width="2"
365
+ stroke-linecap="round"
366
+ stroke-linejoin="round"
367
+ >
368
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
369
+ <polyline points="15 3 21 3 21 9" />
370
+ <line x1="10" y1="14" x2="21" y2="3" />
371
+ </svg>
372
+ </a>
373
+ )
374
+ }
375
+
376
+ /**
377
+ * Render a single component with error boundary
378
+ */
379
+ function ComponentRenderer(props: {
380
+ component: UIComponent
381
+ onError?: (error: RendererError) => void
382
+ }) {
383
+ // Validate component before rendering
384
+ const validation = validateComponent(props.component)
385
+ if (!validation.valid) {
386
+ props.onError?.({
387
+ type: 'validation',
388
+ message: 'Component validation failed',
389
+ componentId: props.component.id,
390
+ details: validation.errors,
391
+ })
392
+
393
+ return (
394
+ <div class="w-full h-full bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
395
+ <p class="text-sm font-medium text-red-900 dark:text-red-100">Validation Error</p>
396
+ <p class="text-xs text-red-700 dark:text-red-300 mt-1">
397
+ {validation.errors?.[0]?.message || 'Unknown validation error'}
398
+ </p>
399
+ </div>
400
+ )
401
+ }
402
+
403
+ // Render based on component type with enhanced error boundary
404
+ return (
405
+ <GenerativeUIErrorBoundary
406
+ componentId={props.component.id}
407
+ componentType={props.component.type}
408
+ onError={props.onError}
409
+ allowRetry={true}
410
+ >
411
+ <Show when={props.component.type === 'chart'}>
412
+ <ChartRenderer component={props.component} onError={props.onError} />
413
+ </Show>
414
+ <Show when={props.component.type === 'table'}>
415
+ <TableRenderer component={props.component} onError={props.onError} />
416
+ </Show>
417
+ <Show when={props.component.type === 'metric'}>
418
+ <MetricRenderer component={props.component} />
419
+ </Show>
420
+ <Show when={props.component.type === 'text'}>
421
+ <TextRenderer component={props.component} />
422
+ </Show>
423
+ <Show when={props.component.type === 'iframe'}>
424
+ <IframeRenderer component={props.component} />
425
+ </Show>
426
+ <Show when={props.component.type === 'image'}>
427
+ <ImageRenderer component={props.component} />
428
+ </Show>
429
+ <Show when={props.component.type === 'link'}>
430
+ <LinkRenderer component={props.component} />
431
+ </Show>
432
+ <Show when={props.component.type === 'action'}>
433
+ <ActionRenderer component={props.component} />
434
+ </Show>
435
+ </GenerativeUIErrorBoundary>
436
+ )
437
+ }
438
+
439
+ /**
440
+ * Render an action component (button or link)
441
+ */
442
+ function ActionRenderer(props: { component: UIComponent }) {
443
+ const params = props.component.params as any
444
+
445
+ // Handle click to execute tool via window event
446
+ const handleClick = (e: MouseEvent) => {
447
+ if (params.action === 'tool-call' && params.toolName) {
448
+ e.preventDefault()
449
+ // Client-only: CustomEvent and window are not available in SSR
450
+ if (!isServer && typeof window !== 'undefined') {
451
+ const event = new CustomEvent('mcp-action', {
452
+ detail: {
453
+ toolName: params.toolName,
454
+ params: params.params || {},
455
+ },
456
+ bubbles: true,
457
+ })
458
+ window.dispatchEvent(event)
459
+ }
460
+ }
461
+ }
462
+
463
+ if (params.type === 'link' || params.action === 'link') {
464
+ return (
465
+ <a
466
+ href={params.url || '#'}
467
+ target={params.url ? '_blank' : undefined}
468
+ rel="noopener noreferrer"
469
+ class={`inline-flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors
470
+ ${params.variant === 'primary' ? 'bg-blue-600 text-white hover:bg-blue-700' :
471
+ params.variant === 'outline' ? 'border border-gray-300 text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800' :
472
+ 'text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300'}`}
473
+ onClick={handleClick}
474
+ >
475
+ <Show when={params.icon}>
476
+ <span>{params.icon}</span>
477
+ </Show>
478
+ {params.label}
479
+ </a>
480
+ )
481
+ }
482
+
483
+ return (
484
+ <button
485
+ type={params.action === 'submit' ? 'submit' : 'button'}
486
+ disabled={params.disabled}
487
+ class={`inline-flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500
488
+ ${params.variant === 'primary' ? 'bg-blue-600 text-white hover:bg-blue-700 shadow-sm' :
489
+ params.variant === 'secondary' ? 'bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600' :
490
+ params.variant === 'outline' ? 'border border-gray-300 text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800' :
491
+ params.variant === 'danger' ? 'bg-red-600 text-white hover:bg-red-700' :
492
+ 'bg-transparent text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'}
493
+ ${params.disabled ? 'opacity-50 cursor-not-allowed' : ''}
494
+ ${params.size === 'sm' ? 'px-3 py-1.5 text-xs' : params.size === 'lg' ? 'px-6 py-3 text-base' : ''}`}
495
+ onClick={handleClick}
496
+ >
497
+ <Show when={params.icon}>
498
+ <span>{params.icon}</span>
499
+ </Show>
500
+ {params.label}
501
+ </button>
502
+ )
503
+ }
504
+
505
+ /**
506
+ * Main UIResourceRenderer component
507
+ */
508
+ export const UIResourceRenderer: Component<UIResourceRendererProps> = (props) => {
509
+ const layout = () => {
510
+ // ✅ PHASE 3.3 FIX: Check if content is a UIComponent (non-composite) vs UILayout (composite)
511
+ // UILayout has type='composite', UIComponent has type='chart'|'table'|'metric'|'text'
512
+ if ('type' in props.content && (props.content as any).type !== 'composite') {
513
+ return {
514
+ id: 'single-component',
515
+ components: [props.content as UIComponent],
516
+ grid: {
517
+ columns: 12,
518
+ gap: '1rem',
519
+ },
520
+ } as UILayout
521
+ }
522
+ return props.content as UILayout
523
+ }
524
+
525
+ // Grid position to CSS Grid styles
526
+ const getGridStyles = (component: UIComponent) => {
527
+ // ✅ PHASE 3 FIX: Defensive check for position field
528
+ if (!component.position) {
529
+ console.error('[UIResourceRenderer] Component missing position field:', component)
530
+ return {
531
+ 'grid-column': '1 / span 12',
532
+ 'grid-row': 'auto',
533
+ }
534
+ }
535
+
536
+ const { colStart, colSpan, rowStart, rowSpan = 1 } = component.position
537
+
538
+ return {
539
+ 'grid-column': `${colStart} / span ${colSpan}`,
540
+ 'grid-row': rowStart ? `${rowStart} / span ${rowSpan}` : 'auto',
541
+ }
542
+ }
543
+
544
+ // Convert grid styles to CSS string to avoid setStyleProperty
545
+ const gridContainerStyle = () =>
546
+ `grid-template-columns: repeat(${layout().grid.columns}, 1fr); gap: ${layout().grid.gap}`
547
+
548
+ // Convert component grid styles to CSS string
549
+ const getGridStyleString = (component: UIComponent) => {
550
+ // ✅ PHASE 3 FIX: Defensive check for position field
551
+ if (!component.position) {
552
+ console.error('[UIResourceRenderer] Component missing position field:', component)
553
+ return 'grid-column: 1 / span 12; grid-row: auto' // Default to full width
554
+ }
555
+
556
+ const { colStart, colSpan, rowStart, rowSpan = 1 } = component.position
557
+ return `grid-column: ${colStart} / span ${colSpan}; grid-row: ${rowStart ? `${rowStart} / span ${rowSpan}` : 'auto'}`
558
+ }
559
+
560
+ return (
561
+ <div class={`w-full ${props.class || ''}`}>
562
+ <div class="grid gap-4" style={gridContainerStyle()}>
563
+ <For each={layout().components}>
564
+ {(component) => (
565
+ <div style={getGridStyleString(component)}>
566
+ <ComponentRenderer component={component} onError={props.onError} />
567
+ </div>
568
+ )}
569
+ </For>
570
+ </div>
571
+ </div>
572
+ )
573
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * MCP UI Solid - Components
3
+ *
4
+ * SolidJS components for rendering MCP-generated UI resources
5
+ */
6
+
7
+ export { UIResourceRenderer } from './UIResourceRenderer'
8
+ export type { UIResourceRendererProps } from './UIResourceRenderer'
9
+
10
+ export { StreamingUIRenderer } from './StreamingUIRenderer'
11
+ export type { StreamingUIRendererProps } from './StreamingUIRenderer'
12
+
13
+ export { GenerativeUIErrorBoundary } from './GenerativeUIErrorBoundary'
14
+ export type { GenerativeUIErrorBoundaryProps } from './GenerativeUIErrorBoundary'
@@ -0,0 +1,14 @@
1
+ /**
2
+ * MCP UI Solid - Hooks
3
+ *
4
+ * SolidJS hooks for managing UI resource state and streaming
5
+ */
6
+
7
+ export { useStreamingUI } from './useStreamingUI'
8
+ export type {
9
+ UseStreamingUIOptions,
10
+ StreamingUIState,
11
+ StreamProgress,
12
+ StreamError,
13
+ CompleteMetadata,
14
+ } from './useStreamingUI'