@seed-ship/mcp-ui-solid 6.6.0 → 6.7.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.
- package/CHANGELOG.md +62 -0
- package/dist/adapters/index.d.ts +2 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/macro-run.cjs +226 -0
- package/dist/adapters/macro-run.cjs.map +1 -0
- package/dist/adapters/macro-run.d.ts +65 -0
- package/dist/adapters/macro-run.d.ts.map +1 -0
- package/dist/adapters/macro-run.js +226 -0
- package/dist/adapters/macro-run.js.map +1 -0
- package/dist/adapters.cjs +3 -0
- package/dist/adapters.cjs.map +1 -1
- package/dist/adapters.d.cts +2 -1
- package/dist/adapters.d.ts +2 -1
- package/dist/adapters.js +4 -1
- package/dist/adapters.js.map +1 -1
- package/dist/components/ActionGroupRenderer.cjs +12 -3
- package/dist/components/ActionGroupRenderer.cjs.map +1 -1
- package/dist/components/ActionGroupRenderer.d.ts.map +1 -1
- package/dist/components/ActionGroupRenderer.js +12 -3
- package/dist/components/ActionGroupRenderer.js.map +1 -1
- package/dist/components/UIResourceRenderer.cjs +22 -15
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +22 -15
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/dist/context/MCPActionContext.cjs +4 -1
- package/dist/context/MCPActionContext.cjs.map +1 -1
- package/dist/context/MCPActionContext.d.ts +13 -1
- package/dist/context/MCPActionContext.d.ts.map +1 -1
- package/dist/context/MCPActionContext.js +4 -1
- package/dist/context/MCPActionContext.js.map +1 -1
- package/dist/mcp-ui-spec/dist/schemas.cjs +250 -1
- package/dist/mcp-ui-spec/dist/schemas.cjs.map +1 -1
- package/dist/mcp-ui-spec/dist/schemas.js +251 -2
- package/dist/mcp-ui-spec/dist/schemas.js.map +1 -1
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.cjs +2 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.cjs.map +1 -1
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js +2 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js.map +1 -1
- package/package.json +2 -2
- package/src/adapters/index.ts +4 -5
- package/src/adapters/macro-run.test.ts +293 -0
- package/src/adapters/macro-run.ts +362 -0
- package/src/components/ActionGroupRenderer.test.tsx +1 -0
- package/src/components/ActionGroupRenderer.tsx +19 -4
- package/src/components/ActionSubmit.test.tsx +188 -0
- package/src/components/UIResourceRenderer.tsx +19 -6
- package/src/context/MCPActionContext.tsx +17 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -26,7 +26,12 @@ const ActionButton: Component<{
|
|
|
26
26
|
action: ActionComponentParams
|
|
27
27
|
index: number
|
|
28
28
|
}> = (props) => {
|
|
29
|
-
const { execute, isExecuting } = useAction()
|
|
29
|
+
const { execute, executeAction, isExecuting } = useAction()
|
|
30
|
+
|
|
31
|
+
// tool-call and submit both go through the host executor — they show a
|
|
32
|
+
// loading state and gate disabled while running. link does neither.
|
|
33
|
+
const isExecutable = () =>
|
|
34
|
+
props.action.action === 'tool-call' || props.action.action === 'submit'
|
|
30
35
|
|
|
31
36
|
const handleClick = async (e: MouseEvent) => {
|
|
32
37
|
if (props.action.disabled) return
|
|
@@ -34,13 +39,23 @@ const ActionButton: Component<{
|
|
|
34
39
|
if (props.action.action === 'tool-call' && props.action.toolName) {
|
|
35
40
|
e.preventDefault()
|
|
36
41
|
await execute(props.action.toolName, props.action.params || {})
|
|
42
|
+
} else if (props.action.action === 'submit') {
|
|
43
|
+
// submit is NOT a tool call — route it through the executor with the
|
|
44
|
+
// `action: 'submit'` kind preserved so the host can POST to
|
|
45
|
+
// params.submit_url etc. Works without a surrounding <form>.
|
|
46
|
+
e.preventDefault()
|
|
47
|
+
await executeAction({
|
|
48
|
+
action: 'submit',
|
|
49
|
+
toolName: props.action.toolName || 'submit',
|
|
50
|
+
params: props.action.params || {},
|
|
51
|
+
})
|
|
37
52
|
} else if (props.action.action === 'link' && props.action.url) {
|
|
38
53
|
window.open(props.action.url, '_blank', 'noopener,noreferrer')
|
|
39
54
|
}
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
const isDisabled = () =>
|
|
43
|
-
props.action.disabled || (
|
|
58
|
+
props.action.disabled || (isExecutable() && isExecuting())
|
|
44
59
|
|
|
45
60
|
const variantClass = () => {
|
|
46
61
|
switch (props.action.variant) {
|
|
@@ -98,10 +113,10 @@ const ActionButton: Component<{
|
|
|
98
113
|
isDisabled() ? 'opacity-50 cursor-not-allowed' : ''
|
|
99
114
|
}`}
|
|
100
115
|
>
|
|
101
|
-
<Show when={isExecuting() &&
|
|
116
|
+
<Show when={isExecuting() && isExecutable()}>
|
|
102
117
|
<span class="animate-spin h-4 w-4 border-2 border-current border-t-transparent rounded-full" />
|
|
103
118
|
</Show>
|
|
104
|
-
<Show when={props.action.icon && !(isExecuting() &&
|
|
119
|
+
<Show when={props.action.icon && !(isExecuting() && isExecutable())}>
|
|
105
120
|
<span class="text-current">{props.action.icon}</span>
|
|
106
121
|
</Show>
|
|
107
122
|
{props.action.label}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v6.6.1 — `action: 'submit'` reaches the host executor outside a <form>.
|
|
3
|
+
*
|
|
4
|
+
* Before v6.6.1, `submit` actions were inert : `ActionGroupRenderer` only
|
|
5
|
+
* branched on `tool-call` / `link`, and the standalone `action` renderer
|
|
6
|
+
* emitted a native `type="submit"` button that did nothing outside a real
|
|
7
|
+
* `<form>`. This file pins the fix — a full integration test through the
|
|
8
|
+
* real `useAction` → `MCPActionContext` → host `executor` path (no mocks).
|
|
9
|
+
*
|
|
10
|
+
* Covers both render surfaces :
|
|
11
|
+
* - `<ActionGroupRenderer>` (action-group)
|
|
12
|
+
* - `<UIResourceRenderer content={{ type: 'action', ... }}>` (standalone)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
16
|
+
import { render, screen, fireEvent, waitFor, cleanup } from '@solidjs/testing-library'
|
|
17
|
+
import { ActionGroupRenderer } from './ActionGroupRenderer'
|
|
18
|
+
import { UIResourceRenderer } from './UIResourceRenderer'
|
|
19
|
+
import { MCPActionProvider } from '../context/MCPActionContext'
|
|
20
|
+
import type { ActionRequest, ActionResult } from '../context/MCPActionContext'
|
|
21
|
+
import type { UIComponent } from '../types'
|
|
22
|
+
|
|
23
|
+
// The connector "feedback format" payload from the bug report.
|
|
24
|
+
const SUBMIT_PARAMS = {
|
|
25
|
+
submit_url: '/api/connector-render-feedback',
|
|
26
|
+
feedback_kind: 'presentation',
|
|
27
|
+
connector_id: 'clinicaltrials',
|
|
28
|
+
tool_name: 'clinicaltrials_search',
|
|
29
|
+
render_kind: 'clinical_trial_search',
|
|
30
|
+
preferred_layout_options: ['table', 'cards', 'bar'],
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeExecutor() {
|
|
34
|
+
const calls: ActionRequest[] = []
|
|
35
|
+
const executor = vi.fn(async (req: ActionRequest): Promise<ActionResult> => {
|
|
36
|
+
calls.push(req)
|
|
37
|
+
return { success: true, timestamp: new Date().toISOString(), toolName: req.toolName }
|
|
38
|
+
})
|
|
39
|
+
return { executor, calls }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('action: submit reaches the host executor (v6.6.1)', () => {
|
|
43
|
+
beforeEach(() => cleanup())
|
|
44
|
+
|
|
45
|
+
it('ActionGroupRenderer routes a submit action to the executor', async () => {
|
|
46
|
+
const { executor, calls } = makeExecutor()
|
|
47
|
+
const component: UIComponent = {
|
|
48
|
+
id: 'ag',
|
|
49
|
+
type: 'action-group',
|
|
50
|
+
position: { colStart: 1, colSpan: 12 },
|
|
51
|
+
params: {
|
|
52
|
+
actions: [
|
|
53
|
+
{ label: 'Feedback format', action: 'submit', params: SUBMIT_PARAMS },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
} as UIComponent
|
|
57
|
+
|
|
58
|
+
render(() => (
|
|
59
|
+
<MCPActionProvider executor={executor}>
|
|
60
|
+
<ActionGroupRenderer component={component} />
|
|
61
|
+
</MCPActionProvider>
|
|
62
|
+
))
|
|
63
|
+
|
|
64
|
+
fireEvent.click(screen.getByRole('button', { name: 'Feedback format' }))
|
|
65
|
+
|
|
66
|
+
await waitFor(() => expect(executor).toHaveBeenCalledTimes(1))
|
|
67
|
+
const req = calls[0]
|
|
68
|
+
// The action KIND is preserved — host can tell it apart from a tool call.
|
|
69
|
+
expect(req.action).toBe('submit')
|
|
70
|
+
// The full params payload survives intact.
|
|
71
|
+
expect(req.params).toEqual(SUBMIT_PARAMS)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('a submit action is NOT executed as a tool call', async () => {
|
|
75
|
+
const { executor, calls } = makeExecutor()
|
|
76
|
+
const component: UIComponent = {
|
|
77
|
+
id: 'ag',
|
|
78
|
+
type: 'action-group',
|
|
79
|
+
position: { colStart: 1, colSpan: 12 },
|
|
80
|
+
params: {
|
|
81
|
+
actions: [{ label: 'Send', action: 'submit', params: SUBMIT_PARAMS }],
|
|
82
|
+
},
|
|
83
|
+
} as UIComponent
|
|
84
|
+
|
|
85
|
+
render(() => (
|
|
86
|
+
<MCPActionProvider executor={executor}>
|
|
87
|
+
<ActionGroupRenderer component={component} />
|
|
88
|
+
</MCPActionProvider>
|
|
89
|
+
))
|
|
90
|
+
fireEvent.click(screen.getByRole('button', { name: 'Send' }))
|
|
91
|
+
|
|
92
|
+
await waitFor(() => expect(executor).toHaveBeenCalledTimes(1))
|
|
93
|
+
// action is 'submit', never silently coerced to 'tool-call'
|
|
94
|
+
expect(calls[0].action).not.toBe('tool-call')
|
|
95
|
+
expect(calls[0].action).toBe('submit')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('standalone action component (UIResourceRenderer) routes submit to the executor', async () => {
|
|
99
|
+
const { executor, calls } = makeExecutor()
|
|
100
|
+
const component: UIComponent = {
|
|
101
|
+
id: 'act',
|
|
102
|
+
type: 'action',
|
|
103
|
+
position: { colStart: 1, colSpan: 12 },
|
|
104
|
+
params: { label: 'Feedback format', action: 'submit', params: SUBMIT_PARAMS },
|
|
105
|
+
} as UIComponent
|
|
106
|
+
|
|
107
|
+
render(() => (
|
|
108
|
+
<MCPActionProvider executor={executor}>
|
|
109
|
+
<UIResourceRenderer content={component} />
|
|
110
|
+
</MCPActionProvider>
|
|
111
|
+
))
|
|
112
|
+
|
|
113
|
+
fireEvent.click(screen.getByRole('button', { name: 'Feedback format' }))
|
|
114
|
+
|
|
115
|
+
await waitFor(() => expect(executor).toHaveBeenCalledTimes(1))
|
|
116
|
+
expect(calls[0].action).toBe('submit')
|
|
117
|
+
expect(calls[0].params).toEqual(SUBMIT_PARAMS)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('standalone submit button is type="button" — not a native form submit', () => {
|
|
121
|
+
const component: UIComponent = {
|
|
122
|
+
id: 'act',
|
|
123
|
+
type: 'action',
|
|
124
|
+
position: { colStart: 1, colSpan: 12 },
|
|
125
|
+
params: { label: 'Send', action: 'submit', params: {} },
|
|
126
|
+
} as UIComponent
|
|
127
|
+
|
|
128
|
+
const { container } = render(() => (
|
|
129
|
+
<MCPActionProvider executor={makeExecutor().executor}>
|
|
130
|
+
<UIResourceRenderer content={component} />
|
|
131
|
+
</MCPActionProvider>
|
|
132
|
+
))
|
|
133
|
+
const btn = container.querySelector('button')
|
|
134
|
+
// Must NOT rely on a surrounding <form> — JS-handled, type=button.
|
|
135
|
+
expect(btn?.getAttribute('type')).toBe('button')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('tool-call actions still work unchanged', async () => {
|
|
139
|
+
const { executor, calls } = makeExecutor()
|
|
140
|
+
const component: UIComponent = {
|
|
141
|
+
id: 'ag',
|
|
142
|
+
type: 'action-group',
|
|
143
|
+
position: { colStart: 1, colSpan: 12 },
|
|
144
|
+
params: {
|
|
145
|
+
actions: [
|
|
146
|
+
{ label: 'Run', action: 'tool-call', toolName: 'do_thing', params: { a: 1 } },
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
} as UIComponent
|
|
150
|
+
|
|
151
|
+
render(() => (
|
|
152
|
+
<MCPActionProvider executor={executor}>
|
|
153
|
+
<ActionGroupRenderer component={component} />
|
|
154
|
+
</MCPActionProvider>
|
|
155
|
+
))
|
|
156
|
+
fireEvent.click(screen.getByRole('button', { name: 'Run' }))
|
|
157
|
+
|
|
158
|
+
await waitFor(() => expect(executor).toHaveBeenCalledTimes(1))
|
|
159
|
+
expect(calls[0].toolName).toBe('do_thing')
|
|
160
|
+
expect(calls[0].params).toEqual({ a: 1 })
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('a disabled submit action does not call the executor', async () => {
|
|
164
|
+
const { executor } = makeExecutor()
|
|
165
|
+
const component: UIComponent = {
|
|
166
|
+
id: 'ag',
|
|
167
|
+
type: 'action-group',
|
|
168
|
+
position: { colStart: 1, colSpan: 12 },
|
|
169
|
+
params: {
|
|
170
|
+
actions: [
|
|
171
|
+
{ label: 'Send', action: 'submit', params: SUBMIT_PARAMS, disabled: true },
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
} as UIComponent
|
|
175
|
+
|
|
176
|
+
render(() => (
|
|
177
|
+
<MCPActionProvider executor={executor}>
|
|
178
|
+
<ActionGroupRenderer component={component} />
|
|
179
|
+
</MCPActionProvider>
|
|
180
|
+
))
|
|
181
|
+
const btn = screen.getByRole('button', { name: 'Send' })
|
|
182
|
+
expect(btn.hasAttribute('disabled')).toBe(true)
|
|
183
|
+
fireEvent.click(btn)
|
|
184
|
+
// Give any async handler a tick — nothing should fire.
|
|
185
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
186
|
+
expect(executor).not.toHaveBeenCalled()
|
|
187
|
+
})
|
|
188
|
+
})
|
|
@@ -1524,9 +1524,13 @@ function ComponentRenderer(props: {
|
|
|
1524
1524
|
*/
|
|
1525
1525
|
function ActionRenderer(props: { component: UIComponent }) {
|
|
1526
1526
|
const params = props.component.params as any
|
|
1527
|
-
const { execute, isExecuting } = useAction()
|
|
1527
|
+
const { execute, executeAction, isExecuting } = useAction()
|
|
1528
1528
|
const telemetry = useTelemetry()
|
|
1529
1529
|
|
|
1530
|
+
// tool-call and submit both run through the host executor — loading +
|
|
1531
|
+
// disabled state apply to both. link does neither.
|
|
1532
|
+
const isExecutable = () => params.action === 'tool-call' || params.action === 'submit'
|
|
1533
|
+
|
|
1530
1534
|
// Telemetry: action:dispatched on click (B.5 — v5.6.0). Fires for every
|
|
1531
1535
|
// click attempt (tool-call or link), regardless of execute success.
|
|
1532
1536
|
// Privacy: actionName is `toolName` (tool-call) or the action kind
|
|
@@ -1549,11 +1553,20 @@ function ActionRenderer(props: { component: UIComponent }) {
|
|
|
1549
1553
|
if (params.action === 'tool-call' && params.toolName) {
|
|
1550
1554
|
e.preventDefault()
|
|
1551
1555
|
await execute(params.toolName, params.params || {})
|
|
1556
|
+
} else if (params.action === 'submit') {
|
|
1557
|
+
// submit is NOT a tool call — route through the executor with the
|
|
1558
|
+
// `action: 'submit'` kind preserved. Works outside any <form>.
|
|
1559
|
+
e.preventDefault()
|
|
1560
|
+
await executeAction({
|
|
1561
|
+
action: 'submit',
|
|
1562
|
+
toolName: params.toolName || 'submit',
|
|
1563
|
+
params: params.params || {},
|
|
1564
|
+
})
|
|
1552
1565
|
}
|
|
1553
1566
|
}
|
|
1554
1567
|
|
|
1555
1568
|
// Determine if button should be disabled (explicit disable or currently executing)
|
|
1556
|
-
const isDisabled = () => params.disabled || (
|
|
1569
|
+
const isDisabled = () => params.disabled || (isExecutable() && isExecuting())
|
|
1557
1570
|
|
|
1558
1571
|
if (params.type === 'link' || params.action === 'link') {
|
|
1559
1572
|
return (
|
|
@@ -1579,9 +1592,9 @@ function ActionRenderer(props: { component: UIComponent }) {
|
|
|
1579
1592
|
|
|
1580
1593
|
return (
|
|
1581
1594
|
<button
|
|
1582
|
-
type=
|
|
1595
|
+
type="button"
|
|
1583
1596
|
disabled={isDisabled()}
|
|
1584
|
-
aria-busy={isExecuting() &&
|
|
1597
|
+
aria-busy={isExecuting() && isExecutable()}
|
|
1585
1598
|
aria-label={params.ariaLabel || params.label}
|
|
1586
1599
|
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
|
|
1587
1600
|
${params.variant === 'primary' ? 'bg-blue-600 text-white hover:bg-blue-700 shadow-sm' :
|
|
@@ -1594,10 +1607,10 @@ function ActionRenderer(props: { component: UIComponent }) {
|
|
|
1594
1607
|
${params.className || ''}`}
|
|
1595
1608
|
onClick={handleClick}
|
|
1596
1609
|
>
|
|
1597
|
-
<Show when={isExecuting() &&
|
|
1610
|
+
<Show when={isExecuting() && isExecutable()}>
|
|
1598
1611
|
<span class="animate-spin h-4 w-4 border-2 border-current border-t-transparent rounded-full" aria-hidden="true" />
|
|
1599
1612
|
</Show>
|
|
1600
|
-
<Show when={params.icon && !(isExecuting() &&
|
|
1613
|
+
<Show when={params.icon && !(isExecuting() && isExecutable())}>
|
|
1601
1614
|
<span aria-hidden="true">{params.icon}</span>
|
|
1602
1615
|
</Show>
|
|
1603
1616
|
{params.label}
|
|
@@ -10,7 +10,9 @@ import { createContext, createSignal, useContext, ParentComponent, Accessor } fr
|
|
|
10
10
|
*/
|
|
11
11
|
export interface ActionRequest {
|
|
12
12
|
/**
|
|
13
|
-
* MCP tool name to execute
|
|
13
|
+
* MCP tool name to execute. For a `submit` action with no associated
|
|
14
|
+
* tool, renderers pass the sentinel `'submit'` — branch on `action`,
|
|
15
|
+
* not on `toolName`, to tell a submit apart from a tool call.
|
|
14
16
|
*/
|
|
15
17
|
toolName: string
|
|
16
18
|
|
|
@@ -28,6 +30,17 @@ export interface ActionRequest {
|
|
|
28
30
|
* Optional macro ID for template execution
|
|
29
31
|
*/
|
|
30
32
|
macroId?: string
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Action kind (v6.6.1). Lets a host `executor` tell a tool call apart
|
|
36
|
+
* from a form-style `submit`. Absent ⇒ treat as `'tool-call'` (backward
|
|
37
|
+
* compatible — every pre-v6.6.1 request omits it).
|
|
38
|
+
*
|
|
39
|
+
* A `submit` action carries its payload in `params` (e.g. `submit_url`,
|
|
40
|
+
* `connector_id`, `feedback_value`) and **must NOT** be executed as a
|
|
41
|
+
* tool call — the host routes it (e.g. POST to `params.submit_url`).
|
|
42
|
+
*/
|
|
43
|
+
action?: 'tool-call' | 'submit' | 'link'
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
/**
|
|
@@ -149,6 +162,9 @@ const defaultExecutor = async (request: ActionRequest): Promise<ActionResult> =>
|
|
|
149
162
|
params: request.params || {},
|
|
150
163
|
spaceIds: request.spaceIds,
|
|
151
164
|
macroId: request.macroId,
|
|
165
|
+
// v6.6.1 — action kind so a window-level listener can route a
|
|
166
|
+
// `submit` (POST to params.submit_url) vs a tool call.
|
|
167
|
+
action: request.action ?? 'tool-call',
|
|
152
168
|
},
|
|
153
169
|
bubbles: true,
|
|
154
170
|
})
|