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

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 (57) 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 +88 -36
  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 +90 -38
  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.cjs +3 -0
  35. package/dist/hooks.cjs.map +1 -1
  36. package/dist/hooks.d.ts +2 -0
  37. package/dist/hooks.js +4 -1
  38. package/dist/hooks.js.map +1 -1
  39. package/dist/index.cjs +8 -0
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.ts +5 -3
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +8 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/types/index.d.ts +41 -2
  46. package/dist/types/index.d.ts.map +1 -1
  47. package/dist/types.d.ts +41 -2
  48. package/package.json +1 -1
  49. package/src/components/GridRenderer.tsx +140 -0
  50. package/src/components/UIResourceRenderer.tsx +65 -25
  51. package/src/context/MCPActionContext.tsx +350 -0
  52. package/src/context/index.ts +19 -0
  53. package/src/hooks/index.ts +4 -0
  54. package/src/hooks/useAction.ts +138 -0
  55. package/src/index.ts +14 -1
  56. package/src/types/index.ts +48 -1
  57. package/tsconfig.tsbuildinfo +1 -1
@@ -9,6 +9,9 @@ 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
 
14
17
  /**
@@ -554,43 +557,32 @@ function ComponentRenderer(props: {
554
557
  <Show when={props.component.type === 'action'}>
555
558
  <ActionRenderer component={props.component} />
556
559
  </Show>
560
+ <Show when={props.component.type === 'grid'}>
561
+ <GridRenderer component={props.component} onError={props.onError} />
562
+ </Show>
557
563
  </GenerativeUIErrorBoundary>
558
564
  )
559
565
  }
560
566
 
561
567
  /**
562
568
  * Render an action component (button or link)
569
+ * Refactored in Phase 5.0 to use useAction hook for Context-based execution
563
570
  */
564
571
  function ActionRenderer(props: { component: UIComponent }) {
565
572
  const params = props.component.params as any
566
- let dispatchAction: ((toolName: string, toolParams: any) => void) | null = null
567
-
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
- })
573
+ const { execute, isExecuting } = useAction()
584
574
 
585
- // Handle click to execute tool via window event
586
- const handleClick = (e: MouseEvent) => {
575
+ // Handle click to execute tool via Context (falls back to CustomEvent if no provider)
576
+ const handleClick = async (e: MouseEvent) => {
587
577
  if (params.action === 'tool-call' && params.toolName) {
588
578
  e.preventDefault()
589
- // SSR-safe: Only call if dispatcher was initialized client-side
590
- dispatchAction?.(params.toolName, params.params || {})
579
+ await execute(params.toolName, params.params || {})
591
580
  }
592
581
  }
593
582
 
583
+ // Determine if button should be disabled (explicit disable or currently executing)
584
+ const isDisabled = () => params.disabled || (params.action === 'tool-call' && isExecuting())
585
+
594
586
  if (params.type === 'link' || params.action === 'link') {
595
587
  return (
596
588
  <a
@@ -614,18 +606,21 @@ function ActionRenderer(props: { component: UIComponent }) {
614
606
  return (
615
607
  <button
616
608
  type={params.action === 'submit' ? 'submit' : 'button'}
617
- disabled={params.disabled}
609
+ disabled={isDisabled()}
618
610
  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
611
  ${params.variant === 'primary' ? 'bg-blue-600 text-white hover:bg-blue-700 shadow-sm' :
620
612
  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
613
  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
614
  params.variant === 'danger' ? 'bg-red-600 text-white hover:bg-red-700' :
623
615
  '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' : ''}
616
+ ${isDisabled() ? 'opacity-50 cursor-not-allowed' : ''}
625
617
  ${params.size === 'sm' ? 'px-3 py-1.5 text-xs' : params.size === 'lg' ? 'px-6 py-3 text-base' : ''}`}
626
618
  onClick={handleClick}
627
619
  >
628
- <Show when={params.icon}>
620
+ <Show when={isExecuting() && params.action === 'tool-call'}>
621
+ <span class="animate-spin h-4 w-4 border-2 border-current border-t-transparent rounded-full" />
622
+ </Show>
623
+ <Show when={params.icon && !(isExecuting() && params.action === 'tool-call')}>
629
624
  <span>{params.icon}</span>
630
625
  </Show>
631
626
  {params.label}
@@ -668,6 +663,46 @@ export const UIResourceRenderer: Component<UIResourceRendererProps> = (props) =>
668
663
  return `grid-column: ${colStart} / span ${colSpan}; grid-row: ${rowStart ? `${rowStart} / span ${rowSpan}` : 'auto'}`
669
664
  }
670
665
 
666
+ // Auto-footer logic (Phase 5.0)
667
+ // Automatically inject footer when metadata is present and no explicit footer exists
668
+ const shouldShowAutoFooter = createMemo(() => {
669
+ const layoutData = layout()
670
+
671
+ // Don't show if explicitly hidden
672
+ if (layoutData.metadata?.hideFooter) {
673
+ return false
674
+ }
675
+
676
+ // Don't show if no metadata (nothing to display)
677
+ if (!layoutData.metadata) {
678
+ return false
679
+ }
680
+
681
+ // Don't show if explicit footer component exists
682
+ const hasExplicitFooter = layoutData.components.some((c) => c.type === 'footer')
683
+ if (hasExplicitFooter) {
684
+ return false
685
+ }
686
+
687
+ // Show auto-footer if metadata has relevant info
688
+ return !!(
689
+ layoutData.metadata.executionTime ||
690
+ layoutData.metadata.sourceCount ||
691
+ layoutData.metadata.llmModel
692
+ )
693
+ })
694
+
695
+ // Build auto-footer params from metadata
696
+ const autoFooterParams = createMemo(() => {
697
+ const layoutData = layout()
698
+ return {
699
+ poweredBy: 'Deposium',
700
+ executionTime: layoutData.metadata?.executionTime,
701
+ model: layoutData.metadata?.llmModel,
702
+ sourceCount: layoutData.metadata?.sourceCount,
703
+ }
704
+ })
705
+
671
706
  const layoutData = layout()
672
707
 
673
708
  return (
@@ -681,6 +716,11 @@ export const UIResourceRenderer: Component<UIResourceRendererProps> = (props) =>
681
716
  )}
682
717
  </For>
683
718
  </div>
719
+
720
+ {/* Auto-injected footer (Phase 5.0) */}
721
+ <Show when={shouldShowAutoFooter()}>
722
+ <FooterRenderer params={autoFooterParams()} />
723
+ </Show>
684
724
  </div>
685
725
  )
686
726
  }
@@ -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'
@@ -0,0 +1,138 @@
1
+ /**
2
+ * useAction - Hook for executing MCP actions from components
3
+ * Phase 5.0: Quick Wins - Simplified API for ActionRenderer and custom components
4
+ */
5
+
6
+ import { createSignal, Accessor } from 'solid-js'
7
+ import { useMCPActionSafe, ActionRequest, ActionResult } from '../context/MCPActionContext'
8
+
9
+ /**
10
+ * Return type for useAction hook
11
+ */
12
+ export interface UseActionReturn {
13
+ /**
14
+ * Execute a tool action
15
+ */
16
+ execute: (toolName: string, params?: Record<string, any>) => Promise<ActionResult>
17
+
18
+ /**
19
+ * Full execute with ActionRequest
20
+ */
21
+ executeAction: (request: ActionRequest) => Promise<ActionResult>
22
+
23
+ /**
24
+ * Whether an action is currently executing
25
+ */
26
+ isExecuting: Accessor<boolean>
27
+
28
+ /**
29
+ * Last result from action execution
30
+ */
31
+ lastResult: Accessor<ActionResult | undefined>
32
+
33
+ /**
34
+ * Last error (if any)
35
+ */
36
+ lastError: Accessor<string | undefined>
37
+ }
38
+
39
+ /**
40
+ * Hook for executing MCP actions
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * function MyButton() {
45
+ * const { execute, isExecuting, lastError } = useAction()
46
+ *
47
+ * const handleClick = async () => {
48
+ * const result = await execute('search.hub', { query: 'test' })
49
+ * if (result.success) {
50
+ * console.log('Data:', result.data)
51
+ * }
52
+ * }
53
+ *
54
+ * return (
55
+ * <button onClick={handleClick} disabled={isExecuting()}>
56
+ * {isExecuting() ? 'Loading...' : 'Search'}
57
+ * </button>
58
+ * )
59
+ * }
60
+ * ```
61
+ */
62
+ export function useAction(): UseActionReturn {
63
+ const context = useMCPActionSafe()
64
+ const [lastError, setLastError] = createSignal<string>()
65
+
66
+ const execute = async (toolName: string, params?: Record<string, any>): Promise<ActionResult> => {
67
+ setLastError(undefined)
68
+
69
+ const result = await context.executeAction({
70
+ toolName,
71
+ params,
72
+ })
73
+
74
+ if (!result.success && result.error) {
75
+ setLastError(result.error)
76
+ }
77
+
78
+ return result
79
+ }
80
+
81
+ const executeAction = async (request: ActionRequest): Promise<ActionResult> => {
82
+ setLastError(undefined)
83
+
84
+ const result = await context.executeAction(request)
85
+
86
+ if (!result.success && result.error) {
87
+ setLastError(result.error)
88
+ }
89
+
90
+ return result
91
+ }
92
+
93
+ return {
94
+ execute,
95
+ executeAction,
96
+ isExecuting: context.isExecuting,
97
+ lastResult: context.lastResult,
98
+ lastError,
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Hook for binding action to a specific tool
104
+ *
105
+ * @example
106
+ * ```tsx
107
+ * function SearchButton() {
108
+ * const { execute, isExecuting } = useToolAction('search.hub')
109
+ *
110
+ * return (
111
+ * <button onClick={() => execute({ query: 'test' })} disabled={isExecuting()}>
112
+ * Search
113
+ * </button>
114
+ * )
115
+ * }
116
+ * ```
117
+ */
118
+ export function useToolAction(toolName: string): {
119
+ execute: (params?: Record<string, any>) => Promise<ActionResult>
120
+ isExecuting: Accessor<boolean>
121
+ lastResult: Accessor<ActionResult | undefined>
122
+ lastError: Accessor<string | undefined>
123
+ } {
124
+ const { execute: baseExecute, isExecuting, lastResult, lastError } = useAction()
125
+
126
+ const execute = async (params?: Record<string, any>): Promise<ActionResult> => {
127
+ return baseExecute(toolName, params)
128
+ }
129
+
130
+ return {
131
+ execute,
132
+ isExecuting,
133
+ lastResult,
134
+ lastError,
135
+ }
136
+ }
137
+
138
+ export type { ActionRequest, ActionResult }
package/src/index.ts CHANGED
@@ -38,7 +38,7 @@ export type {
38
38
  } from './components'
39
39
 
40
40
  // Hooks
41
- export { useStreamingUI } from './hooks'
41
+ export { useStreamingUI, useAction, useToolAction } from './hooks'
42
42
 
43
43
  export type {
44
44
  UseStreamingUIOptions,
@@ -46,8 +46,19 @@ export type {
46
46
  StreamProgress,
47
47
  StreamError,
48
48
  CompleteMetadata,
49
+ UseActionReturn,
49
50
  } from './hooks'
50
51
 
52
+ // Context (Phase 5.0)
53
+ export { MCPActionProvider, MCPActionContext, useMCPAction, useMCPActionSafe } from './context'
54
+
55
+ export type {
56
+ MCPActionContextValue,
57
+ MCPActionProviderProps,
58
+ ActionRequest,
59
+ ActionResult,
60
+ } from './context'
61
+
51
62
  // Types
52
63
  export type {
53
64
  UIComponent,
@@ -59,6 +70,8 @@ export type {
59
70
  TableComponentParams,
60
71
  MetricComponentParams,
61
72
  TextComponentParams,
73
+ ActionComponentParams,
74
+ GridComponentParams,
62
75
  } from './types'
63
76
 
64
77
  // Services