@seed-ship/mcp-ui-solid 1.1.0 → 1.2.1

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 (64) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +94 -3
  3. package/dist/components/FooterRenderer.cjs +75 -0
  4. package/dist/components/FooterRenderer.cjs.map +1 -0
  5. package/dist/components/FooterRenderer.js +75 -0
  6. package/dist/components/FooterRenderer.js.map +1 -0
  7. package/dist/components/GridRenderer.cjs +82 -0
  8. package/dist/components/GridRenderer.cjs.map +1 -0
  9. package/dist/components/GridRenderer.d.ts +49 -0
  10. package/dist/components/GridRenderer.d.ts.map +1 -0
  11. package/dist/components/GridRenderer.js +82 -0
  12. package/dist/components/GridRenderer.js.map +1 -0
  13. package/dist/components/UIResourceRenderer.cjs +304 -174
  14. package/dist/components/UIResourceRenderer.cjs.map +1 -1
  15. package/dist/components/UIResourceRenderer.d.ts.map +1 -1
  16. package/dist/components/UIResourceRenderer.js +306 -176
  17. package/dist/components/UIResourceRenderer.js.map +1 -1
  18. package/dist/context/MCPActionContext.cjs +149 -0
  19. package/dist/context/MCPActionContext.cjs.map +1 -0
  20. package/dist/context/MCPActionContext.d.ts +158 -0
  21. package/dist/context/MCPActionContext.d.ts.map +1 -0
  22. package/dist/context/MCPActionContext.js +149 -0
  23. package/dist/context/MCPActionContext.js.map +1 -0
  24. package/dist/context/index.d.ts +8 -0
  25. package/dist/context/index.d.ts.map +1 -0
  26. package/dist/hooks/index.d.ts +2 -0
  27. package/dist/hooks/index.d.ts.map +1 -1
  28. package/dist/hooks/useAction.cjs +49 -0
  29. package/dist/hooks/useAction.cjs.map +1 -0
  30. package/dist/hooks/useAction.d.ts +79 -0
  31. package/dist/hooks/useAction.d.ts.map +1 -0
  32. package/dist/hooks/useAction.js +49 -0
  33. package/dist/hooks/useAction.js.map +1 -0
  34. package/dist/hooks/useStreamingUI.cjs +4 -1
  35. package/dist/hooks/useStreamingUI.cjs.map +1 -1
  36. package/dist/hooks/useStreamingUI.d.ts +2 -0
  37. package/dist/hooks/useStreamingUI.d.ts.map +1 -1
  38. package/dist/hooks/useStreamingUI.js +4 -1
  39. package/dist/hooks/useStreamingUI.js.map +1 -1
  40. package/dist/hooks.cjs +3 -0
  41. package/dist/hooks.cjs.map +1 -1
  42. package/dist/hooks.d.ts +2 -0
  43. package/dist/hooks.js +4 -1
  44. package/dist/hooks.js.map +1 -1
  45. package/dist/index.cjs +8 -0
  46. package/dist/index.cjs.map +1 -1
  47. package/dist/index.d.ts +5 -3
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +8 -0
  50. package/dist/index.js.map +1 -1
  51. package/dist/types/index.d.ts +41 -2
  52. package/dist/types/index.d.ts.map +1 -1
  53. package/dist/types.d.ts +41 -2
  54. package/package.json +1 -1
  55. package/src/components/GridRenderer.tsx +140 -0
  56. package/src/components/UIResourceRenderer.tsx +144 -28
  57. package/src/context/MCPActionContext.tsx +350 -0
  58. package/src/context/index.ts +19 -0
  59. package/src/hooks/index.ts +4 -0
  60. package/src/hooks/useAction.ts +138 -0
  61. package/src/hooks/useStreamingUI.ts +7 -1
  62. package/src/index.ts +14 -1
  63. package/src/types/index.ts +48 -1
  64. package/tsconfig.tsbuildinfo +1 -1
@@ -9,8 +9,55 @@ import { isServer } from 'solid-js/web'
9
9
  import type { UIComponent, UILayout, RendererError, ComponentType } from '../types'
10
10
  import { validateComponent, DEFAULT_RESOURCE_LIMITS } from '../services/validation'
11
11
  import { GenerativeUIErrorBoundary } from './GenerativeUIErrorBoundary'
12
+ import { GridRenderer } from './GridRenderer'
13
+ import { FooterRenderer } from './FooterRenderer'
14
+ import { useAction } from '../hooks/useAction'
12
15
  import { marked } from 'marked'
13
16
 
17
+ /**
18
+ * Copy button component with visual feedback
19
+ */
20
+ function CopyButton(props: { getText: () => string; title?: string; position?: 'top-right' | 'bottom-right' }) {
21
+ const [copied, setCopied] = createSignal(false)
22
+
23
+ const handleCopy = async () => {
24
+ try {
25
+ await navigator.clipboard.writeText(props.getText())
26
+ setCopied(true)
27
+ setTimeout(() => setCopied(false), 2000)
28
+ } catch (err) {
29
+ console.error('Failed to copy:', err)
30
+ }
31
+ }
32
+
33
+ const positionClasses = () => {
34
+ return props.position === 'bottom-right'
35
+ ? 'absolute -right-2 -bottom-3'
36
+ : 'absolute right-2 top-2'
37
+ }
38
+
39
+ return (
40
+ <button
41
+ onClick={handleCopy}
42
+ class={`${positionClasses()} opacity-60 hover:opacity-100 px-2 py-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-all shadow-sm z-10`}
43
+ title={props.title || 'Copy'}
44
+ >
45
+ <Show
46
+ when={!copied()}
47
+ fallback={
48
+ <svg class="w-3 h-3 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
49
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
50
+ </svg>
51
+ }
52
+ >
53
+ <svg class="w-3 h-3 text-gray-500 dark:text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
54
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
55
+ </svg>
56
+ </Show>
57
+ </button>
58
+ )
59
+ }
60
+
14
61
  /**
15
62
  * Props for UIResourceRenderer
16
63
  */
@@ -197,8 +244,25 @@ function TableRenderer(props: {
197
244
  }) {
198
245
  const tableParams = props.component.params as any
199
246
 
247
+ // Generate copyable text from table data (TSV format for spreadsheet compatibility)
248
+ const getTableText = () => {
249
+ const columns = tableParams.columns || []
250
+ const rows = tableParams.rows || []
251
+ const header = columns.map((c: any) => c.label).join('\t')
252
+ const dataRows = rows.map((row: any) =>
253
+ columns.map((c: any) => {
254
+ const value = row[c.key]
255
+ if (value === null || value === undefined) return ''
256
+ if (typeof value === 'object') return value.name || value.label || JSON.stringify(value)
257
+ return String(value)
258
+ }).join('\t')
259
+ ).join('\n')
260
+ return `${header}\n${dataRows}`
261
+ }
262
+
200
263
  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 overflow-hidden">
264
+ <div class="relative w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden group">
265
+ <CopyButton getText={getTableText} title="Copy table data" position="top-right" />
202
266
  <div class="p-4">
203
267
  <Show when={tableParams.title}>
204
268
  <h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-3">
@@ -264,8 +328,17 @@ function TableRenderer(props: {
264
328
  function MetricRenderer(props: { component: UIComponent }) {
265
329
  const metricParams = props.component.params as any
266
330
 
331
+ // Generate copyable text for metric
332
+ const getMetricText = () => {
333
+ const title = metricParams.title || metricParams.label || ''
334
+ const value = metricParams.value
335
+ const unit = metricParams.unit || ''
336
+ return `${title}: ${value}${unit ? ' ' + unit : ''}`
337
+ }
338
+
267
339
  return (
268
- <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">
340
+ <div class="relative w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 group">
341
+ <CopyButton getText={getMetricText} title="Copy metric" position="top-right" />
269
342
  <div class="flex flex-col h-full justify-between">
270
343
  <div>
271
344
  <p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
@@ -352,12 +425,18 @@ function TextRenderer(props: { component: UIComponent }) {
352
425
  return textParams.content
353
426
  })
354
427
 
428
+ // Get plain text content for copying (strip markdown/HTML)
429
+ const getTextContent = () => {
430
+ return textParams.content || ''
431
+ }
432
+
355
433
  // Render as image component if we extracted image data
356
434
  return (
357
435
  <Show
358
436
  when={imageData()}
359
437
  fallback={
360
- <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">
438
+ <div class="relative w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 group">
439
+ <CopyButton getText={getTextContent} title="Copy text" position="top-right" />
361
440
  <div
362
441
  class={`prose prose-sm dark:prose-invert max-w-none ${textParams.className || ''}`}
363
442
  innerHTML={htmlContent()}
@@ -554,43 +633,32 @@ function ComponentRenderer(props: {
554
633
  <Show when={props.component.type === 'action'}>
555
634
  <ActionRenderer component={props.component} />
556
635
  </Show>
636
+ <Show when={props.component.type === 'grid'}>
637
+ <GridRenderer component={props.component} onError={props.onError} />
638
+ </Show>
557
639
  </GenerativeUIErrorBoundary>
558
640
  )
559
641
  }
560
642
 
561
643
  /**
562
644
  * Render an action component (button or link)
645
+ * Refactored in Phase 5.0 to use useAction hook for Context-based execution
563
646
  */
564
647
  function ActionRenderer(props: { component: UIComponent }) {
565
648
  const params = props.component.params as any
566
- let dispatchAction: ((toolName: string, toolParams: any) => void) | null = null
649
+ const { execute, isExecuting } = useAction()
567
650
 
568
- // Initialize CustomEvent dispatcher only on client-side
569
- // Use createEffect instead of onMount for SSR compatibility
570
- createEffect(() => {
571
- if (typeof window !== 'undefined') {
572
- dispatchAction = (toolName: string, toolParams: any) => {
573
- const event = new CustomEvent('mcp-action', {
574
- detail: {
575
- toolName,
576
- params: toolParams,
577
- },
578
- bubbles: true,
579
- })
580
- window.dispatchEvent(event)
581
- }
582
- }
583
- })
584
-
585
- // Handle click to execute tool via window event
586
- const handleClick = (e: MouseEvent) => {
651
+ // Handle click to execute tool via Context (falls back to CustomEvent if no provider)
652
+ const handleClick = async (e: MouseEvent) => {
587
653
  if (params.action === 'tool-call' && params.toolName) {
588
654
  e.preventDefault()
589
- // SSR-safe: Only call if dispatcher was initialized client-side
590
- dispatchAction?.(params.toolName, params.params || {})
655
+ await execute(params.toolName, params.params || {})
591
656
  }
592
657
  }
593
658
 
659
+ // Determine if button should be disabled (explicit disable or currently executing)
660
+ const isDisabled = () => params.disabled || (params.action === 'tool-call' && isExecuting())
661
+
594
662
  if (params.type === 'link' || params.action === 'link') {
595
663
  return (
596
664
  <a
@@ -614,18 +682,21 @@ function ActionRenderer(props: { component: UIComponent }) {
614
682
  return (
615
683
  <button
616
684
  type={params.action === 'submit' ? 'submit' : 'button'}
617
- disabled={params.disabled}
685
+ disabled={isDisabled()}
618
686
  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
619
687
  ${params.variant === 'primary' ? 'bg-blue-600 text-white hover:bg-blue-700 shadow-sm' :
620
688
  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' :
621
689
  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' :
622
690
  params.variant === 'danger' ? 'bg-red-600 text-white hover:bg-red-700' :
623
691
  'bg-transparent text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'}
624
- ${params.disabled ? 'opacity-50 cursor-not-allowed' : ''}
692
+ ${isDisabled() ? 'opacity-50 cursor-not-allowed' : ''}
625
693
  ${params.size === 'sm' ? 'px-3 py-1.5 text-xs' : params.size === 'lg' ? 'px-6 py-3 text-base' : ''}`}
626
694
  onClick={handleClick}
627
695
  >
628
- <Show when={params.icon}>
696
+ <Show when={isExecuting() && params.action === 'tool-call'}>
697
+ <span class="animate-spin h-4 w-4 border-2 border-current border-t-transparent rounded-full" />
698
+ </Show>
699
+ <Show when={params.icon && !(isExecuting() && params.action === 'tool-call')}>
629
700
  <span>{params.icon}</span>
630
701
  </Show>
631
702
  {params.label}
@@ -668,6 +739,46 @@ export const UIResourceRenderer: Component<UIResourceRendererProps> = (props) =>
668
739
  return `grid-column: ${colStart} / span ${colSpan}; grid-row: ${rowStart ? `${rowStart} / span ${rowSpan}` : 'auto'}`
669
740
  }
670
741
 
742
+ // Auto-footer logic (Phase 5.0)
743
+ // Automatically inject footer when metadata is present and no explicit footer exists
744
+ const shouldShowAutoFooter = createMemo(() => {
745
+ const layoutData = layout()
746
+
747
+ // Don't show if explicitly hidden
748
+ if (layoutData.metadata?.hideFooter) {
749
+ return false
750
+ }
751
+
752
+ // Don't show if no metadata (nothing to display)
753
+ if (!layoutData.metadata) {
754
+ return false
755
+ }
756
+
757
+ // Don't show if explicit footer component exists
758
+ const hasExplicitFooter = layoutData.components.some((c) => c.type === 'footer')
759
+ if (hasExplicitFooter) {
760
+ return false
761
+ }
762
+
763
+ // Show auto-footer if metadata has relevant info
764
+ return !!(
765
+ layoutData.metadata.executionTime ||
766
+ layoutData.metadata.sourceCount ||
767
+ layoutData.metadata.llmModel
768
+ )
769
+ })
770
+
771
+ // Build auto-footer params from metadata
772
+ const autoFooterParams = createMemo(() => {
773
+ const layoutData = layout()
774
+ return {
775
+ poweredBy: 'Deposium',
776
+ executionTime: layoutData.metadata?.executionTime,
777
+ model: layoutData.metadata?.llmModel,
778
+ sourceCount: layoutData.metadata?.sourceCount,
779
+ }
780
+ })
781
+
671
782
  const layoutData = layout()
672
783
 
673
784
  return (
@@ -681,6 +792,11 @@ export const UIResourceRenderer: Component<UIResourceRendererProps> = (props) =>
681
792
  )}
682
793
  </For>
683
794
  </div>
795
+
796
+ {/* Auto-injected footer (Phase 5.0) */}
797
+ <Show when={shouldShowAutoFooter()}>
798
+ <FooterRenderer params={autoFooterParams()} />
799
+ </Show>
684
800
  </div>
685
801
  )
686
802
  }
@@ -0,0 +1,350 @@
1
+ /**
2
+ * MCPActionContext - Context provider for MCP action execution
3
+ * Phase 5.0: Quick Wins - Replaces CustomEvent with typed context for Mastra integration
4
+ */
5
+
6
+ import { createContext, createSignal, useContext, ParentComponent, Accessor } from 'solid-js'
7
+
8
+ /**
9
+ * Action request payload
10
+ */
11
+ export interface ActionRequest {
12
+ /**
13
+ * MCP tool name to execute
14
+ */
15
+ toolName: string
16
+
17
+ /**
18
+ * Tool parameters
19
+ */
20
+ params?: Record<string, any>
21
+
22
+ /**
23
+ * Optional space IDs for multi-space context
24
+ */
25
+ spaceIds?: string[]
26
+
27
+ /**
28
+ * Optional macro ID for template execution
29
+ */
30
+ macroId?: string
31
+ }
32
+
33
+ /**
34
+ * Action result from execution
35
+ */
36
+ export interface ActionResult {
37
+ /**
38
+ * Whether the action was successful
39
+ */
40
+ success: boolean
41
+
42
+ /**
43
+ * Result data (if successful)
44
+ */
45
+ data?: any
46
+
47
+ /**
48
+ * Error message (if failed)
49
+ */
50
+ error?: string
51
+
52
+ /**
53
+ * Execution timestamp
54
+ */
55
+ timestamp: string
56
+
57
+ /**
58
+ * Tool that was executed
59
+ */
60
+ toolName: string
61
+ }
62
+
63
+ /**
64
+ * Context value interface
65
+ */
66
+ export interface MCPActionContextValue {
67
+ /**
68
+ * Execute an MCP action
69
+ */
70
+ executeAction: (request: ActionRequest) => Promise<ActionResult>
71
+
72
+ /**
73
+ * Currently available tools (from MCP server)
74
+ */
75
+ availableTools: Accessor<string[]>
76
+
77
+ /**
78
+ * Space IDs in current context
79
+ */
80
+ spaceIds: Accessor<string[]>
81
+
82
+ /**
83
+ * Current macro ID (if executing within a template)
84
+ */
85
+ macroId: Accessor<string | undefined>
86
+
87
+ /**
88
+ * Whether an action is currently executing
89
+ */
90
+ isExecuting: Accessor<boolean>
91
+
92
+ /**
93
+ * Last action result
94
+ */
95
+ lastResult: Accessor<ActionResult | undefined>
96
+ }
97
+
98
+ /**
99
+ * Props for MCPActionProvider
100
+ */
101
+ export interface MCPActionProviderProps {
102
+ /**
103
+ * Space IDs for multi-space queries
104
+ */
105
+ spaceIds?: string[]
106
+
107
+ /**
108
+ * Macro ID when executing within a template
109
+ */
110
+ macroId?: string
111
+
112
+ /**
113
+ * Available MCP tools
114
+ */
115
+ availableTools?: string[]
116
+
117
+ /**
118
+ * Callback for action execution (for audit logging)
119
+ */
120
+ onAction?: (request: ActionRequest, result: ActionResult) => void
121
+
122
+ /**
123
+ * Callback for webhook events (n8n, Zapier integration)
124
+ */
125
+ onWebhook?: (event: { type: string; payload: any }) => void
126
+
127
+ /**
128
+ * Custom action executor (override default)
129
+ */
130
+ executor?: (request: ActionRequest) => Promise<ActionResult>
131
+ }
132
+
133
+ // Create the context with undefined default
134
+ const MCPActionContext = createContext<MCPActionContextValue>()
135
+
136
+ /**
137
+ * Default action executor using CustomEvent fallback
138
+ * This maintains backward compatibility while allowing Context-based usage
139
+ */
140
+ const defaultExecutor = async (request: ActionRequest): Promise<ActionResult> => {
141
+ return new Promise((resolve) => {
142
+ const timestamp = new Date().toISOString()
143
+
144
+ // Dispatch CustomEvent for backward compatibility with existing listeners
145
+ if (typeof window !== 'undefined') {
146
+ const event = new CustomEvent('mcp-action', {
147
+ detail: {
148
+ toolName: request.toolName,
149
+ params: request.params || {},
150
+ spaceIds: request.spaceIds,
151
+ macroId: request.macroId,
152
+ },
153
+ bubbles: true,
154
+ })
155
+
156
+ // Listen for response event
157
+ const responseHandler = (e: Event) => {
158
+ const customEvent = e as CustomEvent
159
+ window.removeEventListener('mcp-action-response', responseHandler)
160
+ resolve({
161
+ success: customEvent.detail?.success ?? true,
162
+ data: customEvent.detail?.data,
163
+ error: customEvent.detail?.error,
164
+ timestamp,
165
+ toolName: request.toolName,
166
+ })
167
+ }
168
+
169
+ window.addEventListener('mcp-action-response', responseHandler)
170
+ window.dispatchEvent(event)
171
+
172
+ // Timeout fallback - resolve as success if no response in 100ms
173
+ // (indicates no listener, action was dispatched)
174
+ setTimeout(() => {
175
+ window.removeEventListener('mcp-action-response', responseHandler)
176
+ resolve({
177
+ success: true,
178
+ data: { dispatched: true },
179
+ timestamp,
180
+ toolName: request.toolName,
181
+ })
182
+ }, 100)
183
+ } else {
184
+ // Server-side: return immediately
185
+ resolve({
186
+ success: false,
187
+ error: 'Actions not available server-side',
188
+ timestamp,
189
+ toolName: request.toolName,
190
+ })
191
+ }
192
+ })
193
+ }
194
+
195
+ /**
196
+ * MCPActionProvider - Provides action execution context to child components
197
+ *
198
+ * @example
199
+ * ```tsx
200
+ * <MCPActionProvider
201
+ * spaceIds={['space-123']}
202
+ * macroId="sales_overview"
203
+ * onAction={(req, res) => audit(req, res)}
204
+ * >
205
+ * <UIResourceRenderer layout={layout} />
206
+ * </MCPActionProvider>
207
+ * ```
208
+ */
209
+ export const MCPActionProvider: ParentComponent<MCPActionProviderProps> = (props) => {
210
+ const [isExecuting, setIsExecuting] = createSignal(false)
211
+ const [lastResult, setLastResult] = createSignal<ActionResult>()
212
+ const [spaceIds, setSpaceIds] = createSignal<string[]>(props.spaceIds || [])
213
+ const [macroId, setMacroId] = createSignal<string | undefined>(props.macroId)
214
+ const [availableTools, setAvailableTools] = createSignal<string[]>(props.availableTools || [])
215
+
216
+ // Update signals when props change
217
+ // Note: This is a simple approach; for more complex scenarios, consider createEffect
218
+
219
+ const executeAction = async (request: ActionRequest): Promise<ActionResult> => {
220
+ setIsExecuting(true)
221
+
222
+ try {
223
+ // Enrich request with context
224
+ const enrichedRequest: ActionRequest = {
225
+ ...request,
226
+ spaceIds: request.spaceIds || spaceIds(),
227
+ macroId: request.macroId || macroId(),
228
+ }
229
+
230
+ // Execute using custom executor or default
231
+ const executor = props.executor || defaultExecutor
232
+ const result = await executor(enrichedRequest)
233
+
234
+ setLastResult(result)
235
+
236
+ // Call audit callback if provided
237
+ props.onAction?.(enrichedRequest, result)
238
+
239
+ // Trigger webhook if provided and action was successful
240
+ if (result.success && props.onWebhook) {
241
+ props.onWebhook({
242
+ type: 'action-completed',
243
+ payload: {
244
+ request: enrichedRequest,
245
+ result,
246
+ },
247
+ })
248
+ }
249
+
250
+ return result
251
+ } catch (error) {
252
+ const errorResult: ActionResult = {
253
+ success: false,
254
+ error: error instanceof Error ? error.message : 'Unknown error',
255
+ timestamp: new Date().toISOString(),
256
+ toolName: request.toolName,
257
+ }
258
+
259
+ setLastResult(errorResult)
260
+ props.onAction?.(request, errorResult)
261
+
262
+ return errorResult
263
+ } finally {
264
+ setIsExecuting(false)
265
+ }
266
+ }
267
+
268
+ const contextValue: MCPActionContextValue = {
269
+ executeAction,
270
+ availableTools,
271
+ spaceIds,
272
+ macroId,
273
+ isExecuting,
274
+ lastResult,
275
+ }
276
+
277
+ return (
278
+ <MCPActionContext.Provider value={contextValue}>
279
+ {props.children}
280
+ </MCPActionContext.Provider>
281
+ )
282
+ }
283
+
284
+ /**
285
+ * Hook to access MCP action context
286
+ * Throws if used outside of MCPActionProvider
287
+ *
288
+ * @example
289
+ * ```tsx
290
+ * const { executeAction, isExecuting } = useMCPAction()
291
+ *
292
+ * const handleClick = async () => {
293
+ * const result = await executeAction({
294
+ * toolName: 'search.hub',
295
+ * params: { query: 'revenue Q4' },
296
+ * })
297
+ * }
298
+ * ```
299
+ */
300
+ export function useMCPAction(): MCPActionContextValue {
301
+ const context = useContext(MCPActionContext)
302
+ if (!context) {
303
+ throw new Error('useMCPAction must be used within an MCPActionProvider')
304
+ }
305
+ return context
306
+ }
307
+
308
+ /**
309
+ * Hook to access MCP action context with fallback for components
310
+ * outside of provider (uses CustomEvent fallback)
311
+ *
312
+ * @example
313
+ * ```tsx
314
+ * const { executeAction, isExecuting } = useMCPActionSafe()
315
+ * // Works even without MCPActionProvider
316
+ * ```
317
+ */
318
+ export function useMCPActionSafe(): MCPActionContextValue {
319
+ const context = useContext(MCPActionContext)
320
+
321
+ if (context) {
322
+ return context
323
+ }
324
+
325
+ // Fallback implementation for components outside provider
326
+ const [isExecuting, setIsExecuting] = createSignal(false)
327
+ const [lastResult, setLastResult] = createSignal<ActionResult>()
328
+
329
+ const executeAction = async (request: ActionRequest): Promise<ActionResult> => {
330
+ setIsExecuting(true)
331
+ try {
332
+ const result = await defaultExecutor(request)
333
+ setLastResult(result)
334
+ return result
335
+ } finally {
336
+ setIsExecuting(false)
337
+ }
338
+ }
339
+
340
+ return {
341
+ executeAction,
342
+ availableTools: () => [],
343
+ spaceIds: () => [],
344
+ macroId: () => undefined,
345
+ isExecuting,
346
+ lastResult,
347
+ }
348
+ }
349
+
350
+ export { MCPActionContext }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * MCP UI Solid - Context Providers
3
+ *
4
+ * Context providers for action execution and state management
5
+ */
6
+
7
+ export {
8
+ MCPActionProvider,
9
+ MCPActionContext,
10
+ useMCPAction,
11
+ useMCPActionSafe,
12
+ } from './MCPActionContext'
13
+
14
+ export type {
15
+ MCPActionContextValue,
16
+ MCPActionProviderProps,
17
+ ActionRequest,
18
+ ActionResult,
19
+ } from './MCPActionContext'
@@ -12,3 +12,7 @@ export type {
12
12
  StreamError,
13
13
  CompleteMetadata,
14
14
  } from './useStreamingUI'
15
+
16
+ // Action hooks (Phase 5.0)
17
+ export { useAction, useToolAction } from './useAction'
18
+ export type { UseActionReturn, ActionRequest, ActionResult } from './useAction'