@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.
- package/CHANGELOG.md +41 -0
- package/README.md +94 -3
- package/dist/components/FooterRenderer.cjs +75 -0
- package/dist/components/FooterRenderer.cjs.map +1 -0
- package/dist/components/FooterRenderer.js +75 -0
- package/dist/components/FooterRenderer.js.map +1 -0
- package/dist/components/GridRenderer.cjs +82 -0
- package/dist/components/GridRenderer.cjs.map +1 -0
- package/dist/components/GridRenderer.d.ts +49 -0
- package/dist/components/GridRenderer.d.ts.map +1 -0
- package/dist/components/GridRenderer.js +82 -0
- package/dist/components/GridRenderer.js.map +1 -0
- package/dist/components/UIResourceRenderer.cjs +304 -174
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +306 -176
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/dist/context/MCPActionContext.cjs +149 -0
- package/dist/context/MCPActionContext.cjs.map +1 -0
- package/dist/context/MCPActionContext.d.ts +158 -0
- package/dist/context/MCPActionContext.d.ts.map +1 -0
- package/dist/context/MCPActionContext.js +149 -0
- package/dist/context/MCPActionContext.js.map +1 -0
- package/dist/context/index.d.ts +8 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useAction.cjs +49 -0
- package/dist/hooks/useAction.cjs.map +1 -0
- package/dist/hooks/useAction.d.ts +79 -0
- package/dist/hooks/useAction.d.ts.map +1 -0
- package/dist/hooks/useAction.js +49 -0
- package/dist/hooks/useAction.js.map +1 -0
- package/dist/hooks/useStreamingUI.cjs +4 -1
- package/dist/hooks/useStreamingUI.cjs.map +1 -1
- package/dist/hooks/useStreamingUI.d.ts +2 -0
- package/dist/hooks/useStreamingUI.d.ts.map +1 -1
- package/dist/hooks/useStreamingUI.js +4 -1
- package/dist/hooks/useStreamingUI.js.map +1 -1
- package/dist/hooks.cjs +3 -0
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.js +4 -1
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +41 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types.d.ts +41 -2
- package/package.json +1 -1
- package/src/components/GridRenderer.tsx +140 -0
- package/src/components/UIResourceRenderer.tsx +144 -28
- package/src/context/MCPActionContext.tsx +350 -0
- package/src/context/index.ts +19 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useAction.ts +138 -0
- package/src/hooks/useStreamingUI.ts +7 -1
- package/src/index.ts +14 -1
- package/src/types/index.ts +48 -1
- 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
|
-
|
|
649
|
+
const { execute, isExecuting } = useAction()
|
|
567
650
|
|
|
568
|
-
//
|
|
569
|
-
|
|
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
|
-
|
|
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={
|
|
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
|
-
${
|
|
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.
|
|
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'
|
package/src/hooks/index.ts
CHANGED