@seed-ship/mcp-ui-solid 6.5.0 → 6.6.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 +161 -0
- package/README.md +37 -0
- package/dist/adapters/connector.cjs +112 -0
- package/dist/adapters/connector.cjs.map +1 -0
- package/dist/adapters/connector.d.ts +71 -0
- package/dist/adapters/connector.d.ts.map +1 -0
- package/dist/adapters/connector.js +112 -0
- package/dist/adapters/connector.js.map +1 -0
- package/dist/adapters/index.d.ts +18 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters.cjs +6 -0
- package/dist/adapters.cjs.map +1 -0
- package/dist/adapters.d.cts +18 -0
- package/dist/adapters.d.ts +18 -0
- package/dist/adapters.js +6 -0
- package/dist/adapters.js.map +1 -0
- 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/ExpandableWrapper.cjs +24 -6
- package/dist/components/ExpandableWrapper.cjs.map +1 -1
- package/dist/components/ExpandableWrapper.d.ts.map +1 -1
- package/dist/components/ExpandableWrapper.js +24 -6
- package/dist/components/ExpandableWrapper.js.map +1 -1
- package/dist/components/FeedbackInline.cjs +6 -2
- package/dist/components/FeedbackInline.cjs.map +1 -1
- package/dist/components/FeedbackInline.d.ts +2 -2
- package/dist/components/FeedbackInline.d.ts.map +1 -1
- package/dist/components/FeedbackInline.js +7 -3
- package/dist/components/FeedbackInline.js.map +1 -1
- package/dist/components/PresentationFeedback.cjs +207 -0
- package/dist/components/PresentationFeedback.cjs.map +1 -0
- package/dist/components/PresentationFeedback.d.ts +113 -0
- package/dist/components/PresentationFeedback.d.ts.map +1 -0
- package/dist/components/PresentationFeedback.js +207 -0
- package/dist/components/PresentationFeedback.js.map +1 -0
- package/dist/components/StreamingUIRenderer.cjs +82 -195
- package/dist/components/StreamingUIRenderer.cjs.map +1 -1
- package/dist/components/StreamingUIRenderer.d.ts +25 -5
- package/dist/components/StreamingUIRenderer.d.ts.map +1 -1
- package/dist/components/StreamingUIRenderer.js +84 -197
- package/dist/components/StreamingUIRenderer.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/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components.cjs +3 -0
- package/dist/components.cjs.map +1 -1
- package/dist/components.d.cts +2 -0
- package/dist/components.d.ts +2 -0
- package/dist/components.js +3 -0
- package/dist/components.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/context/MCPUIStringsContext.cjs +38 -0
- package/dist/context/MCPUIStringsContext.cjs.map +1 -0
- package/dist/context/MCPUIStringsContext.d.ts +95 -0
- package/dist/context/MCPUIStringsContext.d.ts.map +1 -0
- package/dist/context/MCPUIStringsContext.js +38 -0
- package/dist/context/MCPUIStringsContext.js.map +1 -0
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-ui-spec/dist/schemas.cjs +103 -0
- package/dist/mcp-ui-spec/dist/schemas.cjs.map +1 -1
- package/dist/mcp-ui-spec/dist/schemas.js +103 -0
- package/dist/mcp-ui-spec/dist/schemas.js.map +1 -1
- package/docs/briefs/ROADMAP-opendata-macro-mcpui.md +912 -0
- package/package.json +17 -5
- package/src/adapters/connector.test.ts +165 -0
- package/src/adapters/connector.ts +234 -0
- package/src/adapters/index.ts +24 -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/ExpandableWrapper.test.tsx +5 -2
- package/src/components/ExpandableWrapper.tsx +8 -6
- package/src/components/FeedbackInline.test.tsx +6 -3
- package/src/components/FeedbackInline.tsx +8 -6
- package/src/components/PresentationFeedback.test.tsx +163 -0
- package/src/components/PresentationFeedback.tsx +326 -0
- package/src/components/StreamingUIRenderer.parity.test.tsx +158 -0
- package/src/components/StreamingUIRenderer.tsx +42 -166
- package/src/components/UIResourceRenderer.tsx +19 -6
- package/src/components/index.ts +10 -0
- package/src/context/MCPActionContext.tsx +17 -1
- package/src/context/MCPUIStringsContext.test.tsx +116 -0
- package/src/context/MCPUIStringsContext.tsx +128 -0
- package/src/index.ts +27 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vite.config.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seed-ship/mcp-ui-solid",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.6.1",
|
|
4
4
|
"description": "SolidJS components for rendering MCP-generated UI resources",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -73,6 +73,17 @@
|
|
|
73
73
|
"default": "./dist/types-export.cjs"
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
|
+
"./adapters": {
|
|
77
|
+
"solid": "./src/adapters/index.ts",
|
|
78
|
+
"import": {
|
|
79
|
+
"types": "./dist/adapters.d.ts",
|
|
80
|
+
"default": "./dist/adapters.js"
|
|
81
|
+
},
|
|
82
|
+
"require": {
|
|
83
|
+
"types": "./dist/adapters.d.cts",
|
|
84
|
+
"default": "./dist/adapters.cjs"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
76
87
|
"./plugins/duckdb": {
|
|
77
88
|
"solid": "./src/plugins/duckdb.ts",
|
|
78
89
|
"import": {
|
|
@@ -86,12 +97,13 @@
|
|
|
86
97
|
},
|
|
87
98
|
"./src/*": "./src/*"
|
|
88
99
|
},
|
|
100
|
+
"_size-limit-note": "size-limit is a regression guardrail, not a hard cap — it flags accidental bundle bloat. It measures the pre-built dist/index.js and sums the FULL graph reachable from each entry, including lazy import() chunks (leaflet, @antv/g6, chart.js) a consumer fetches on demand, NOT at import time — so the figures are worst-case totals, not eager load cost. Since v6.6.0 the 'Streaming renderer' entry delegates to UIResourceRenderer (Gap 1 parity), so its graph equals the full renderer set; its budget is set to 1 MB (generous headroom — the real eager cost is far lower). The 'Hooks only' and 'Full bundle' limits are pre-existing and unchanged by v6.6.0. size-limit is informational only — it does not gate CI.",
|
|
89
101
|
"size-limit": [
|
|
90
102
|
{
|
|
91
103
|
"name": "Streaming renderer",
|
|
92
104
|
"path": "dist/index.js",
|
|
93
105
|
"import": "{ StreamingUIRenderer }",
|
|
94
|
-
"limit": "
|
|
106
|
+
"limit": "1 MB"
|
|
95
107
|
},
|
|
96
108
|
{
|
|
97
109
|
"name": "Hooks only",
|
|
@@ -143,7 +155,7 @@
|
|
|
143
155
|
}
|
|
144
156
|
},
|
|
145
157
|
"dependencies": {
|
|
146
|
-
"@seed-ship/mcp-ui-spec": "^5.0
|
|
158
|
+
"@seed-ship/mcp-ui-spec": "^5.1.0",
|
|
147
159
|
"@types/dompurify": "^3.0.5",
|
|
148
160
|
"dompurify": "^3.4.1",
|
|
149
161
|
"marked": "^16.3.0",
|
|
@@ -195,8 +207,8 @@
|
|
|
195
207
|
"scripts": {
|
|
196
208
|
"build": "vite build && pnpm build:types",
|
|
197
209
|
"build:types": "tsc --declaration --emitDeclarationOnly --outDir dist --composite false && pnpm build:types:copy",
|
|
198
|
-
"build:types:copy": "cp dist/components/index.d.ts dist/components.d.ts 2>/dev/null || true && cp dist/hooks/index.d.ts dist/hooks.d.ts 2>/dev/null || true && cp dist/types/index.d.ts dist/types.d.ts 2>/dev/null || true && pnpm build:types:cts",
|
|
199
|
-
"build:types:cts": "cp dist/index.d.ts dist/index.d.cts && cp dist/components.d.ts dist/components.d.cts 2>/dev/null || true && cp dist/hooks.d.ts dist/hooks.d.cts 2>/dev/null || true && cp dist/types.d.ts dist/types.d.cts 2>/dev/null || true && cp dist/validation.d.ts dist/validation.d.cts 2>/dev/null || true && cp dist/types-export.d.ts dist/types-export.d.cts 2>/dev/null || true",
|
|
210
|
+
"build:types:copy": "cp dist/components/index.d.ts dist/components.d.ts 2>/dev/null || true && cp dist/hooks/index.d.ts dist/hooks.d.ts 2>/dev/null || true && cp dist/types/index.d.ts dist/types.d.ts 2>/dev/null || true && cp dist/adapters/index.d.ts dist/adapters.d.ts 2>/dev/null || true && pnpm build:types:cts",
|
|
211
|
+
"build:types:cts": "cp dist/index.d.ts dist/index.d.cts && cp dist/components.d.ts dist/components.d.cts 2>/dev/null || true && cp dist/hooks.d.ts dist/hooks.d.cts 2>/dev/null || true && cp dist/types.d.ts dist/types.d.cts 2>/dev/null || true && cp dist/validation.d.ts dist/validation.d.cts 2>/dev/null || true && cp dist/types-export.d.ts dist/types-export.d.cts 2>/dev/null || true && cp dist/adapters.d.ts dist/adapters.d.cts 2>/dev/null || true",
|
|
200
212
|
"dev": "vite build --watch",
|
|
201
213
|
"test": "vitest run",
|
|
202
214
|
"test:watch": "vitest",
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v6.6.0 — connector adapters (D5 / D6 of ROADMAP-opendata-macro-mcpui).
|
|
3
|
+
*
|
|
4
|
+
* Coverage:
|
|
5
|
+
* 1. connectorActionsToActionGroup wraps actions into an action-group
|
|
6
|
+
* 2. connectorResultToUILayout assembles primary + supplemental + actions
|
|
7
|
+
* 3. primary that is itself a layout has its components spread in
|
|
8
|
+
* 4. unknown schemaVersion + usable envelope → degraded-but-rendered (R2)
|
|
9
|
+
* 5. unreadable payload → explicit error layout, never throws (R2)
|
|
10
|
+
* 6. purity — same input yields a deep-equal output
|
|
11
|
+
* 7. every shipped spec fixture assembles without a degraded layout
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from 'vitest'
|
|
15
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
16
|
+
import { join } from 'node:path'
|
|
17
|
+
import {
|
|
18
|
+
connectorResultToUILayout,
|
|
19
|
+
connectorActionsToActionGroup,
|
|
20
|
+
} from './connector'
|
|
21
|
+
import { CONNECTOR_DYNAMIC_RESULT_V1 } from '@seed-ship/mcp-ui-spec'
|
|
22
|
+
|
|
23
|
+
const VALID = {
|
|
24
|
+
schemaVersion: CONNECTOR_DYNAMIC_RESULT_V1,
|
|
25
|
+
connectorId: 'datagouv',
|
|
26
|
+
toolName: 'datagouv.search',
|
|
27
|
+
query: 'immobilier toulouse',
|
|
28
|
+
queryHash: 'abc123',
|
|
29
|
+
primary: {
|
|
30
|
+
id: 'tbl',
|
|
31
|
+
type: 'table',
|
|
32
|
+
position: { colStart: 1, colSpan: 12 },
|
|
33
|
+
params: { columns: [{ key: 'a', label: 'A' }], rows: [] },
|
|
34
|
+
},
|
|
35
|
+
supplemental: [
|
|
36
|
+
{ id: 'mtr', type: 'metric', position: { colStart: 1, colSpan: 4 }, params: { title: 'M', value: 1 } },
|
|
37
|
+
],
|
|
38
|
+
actions: [
|
|
39
|
+
{ label: 'Compare', action: 'tool-call', toolName: 'datagouv.search', params: { query: 'x' } },
|
|
40
|
+
],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('connectorActionsToActionGroup (v6.6.0)', () => {
|
|
44
|
+
it('wraps actions into an action-group UIComponent', () => {
|
|
45
|
+
const ag = connectorActionsToActionGroup(VALID.actions as never)
|
|
46
|
+
expect(ag.type).toBe('action-group')
|
|
47
|
+
expect((ag.params as { actions: unknown[] }).actions).toHaveLength(1)
|
|
48
|
+
expect(ag.position).toEqual({ colStart: 1, colSpan: 12 })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('honors id / title / layout / gap options', () => {
|
|
52
|
+
const ag = connectorActionsToActionGroup(VALID.actions as never, {
|
|
53
|
+
id: 'my-actions',
|
|
54
|
+
title: 'Suite',
|
|
55
|
+
layout: 'end',
|
|
56
|
+
gap: 'sm',
|
|
57
|
+
})
|
|
58
|
+
expect(ag.id).toBe('my-actions')
|
|
59
|
+
const params = ag.params as { title: string; layout: string; gap: string }
|
|
60
|
+
expect(params.title).toBe('Suite')
|
|
61
|
+
expect(params.layout).toBe('end')
|
|
62
|
+
expect(params.gap).toBe('sm')
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('connectorResultToUILayout (v6.6.0)', () => {
|
|
67
|
+
it('assembles primary + supplemental + actions into one UILayout', () => {
|
|
68
|
+
const layout = connectorResultToUILayout(VALID)
|
|
69
|
+
// primary (1) + supplemental (1) + action-group (1)
|
|
70
|
+
expect(layout.components).toHaveLength(3)
|
|
71
|
+
expect(layout.components[0].id).toBe('tbl')
|
|
72
|
+
expect(layout.components[1].id).toBe('mtr')
|
|
73
|
+
expect(layout.components[2].type).toBe('action-group')
|
|
74
|
+
expect(layout.grid.columns).toBe(12)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('derives a stable layout id from connectorId + queryHash', () => {
|
|
78
|
+
const layout = connectorResultToUILayout(VALID)
|
|
79
|
+
expect(layout.id).toBe('connector-datagouv-abc123')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('omits the action-group when there are no actions', () => {
|
|
83
|
+
const { actions, ...noActions } = VALID
|
|
84
|
+
void actions
|
|
85
|
+
const layout = connectorResultToUILayout(noActions)
|
|
86
|
+
expect(layout.components.every((c) => c.type !== 'action-group')).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('spreads the components of a primary that is itself a layout', () => {
|
|
90
|
+
const withLayoutPrimary = {
|
|
91
|
+
...VALID,
|
|
92
|
+
primary: {
|
|
93
|
+
id: 'inner',
|
|
94
|
+
components: [
|
|
95
|
+
{ id: 'p1', type: 'metric', position: { colStart: 1, colSpan: 6 }, params: {} },
|
|
96
|
+
{ id: 'p2', type: 'metric', position: { colStart: 7, colSpan: 6 }, params: {} },
|
|
97
|
+
],
|
|
98
|
+
grid: { columns: 12, gap: '1rem' },
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
const layout = connectorResultToUILayout(withLayoutPrimary)
|
|
102
|
+
const ids = layout.components.map((c) => c.id)
|
|
103
|
+
expect(ids).toContain('p1')
|
|
104
|
+
expect(ids).toContain('p2')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('fills a missing grid position with full-width', () => {
|
|
108
|
+
const noPos = {
|
|
109
|
+
...VALID,
|
|
110
|
+
supplemental: undefined,
|
|
111
|
+
actions: undefined,
|
|
112
|
+
primary: { id: 'np', type: 'text', params: { content: 'x' } },
|
|
113
|
+
}
|
|
114
|
+
const layout = connectorResultToUILayout(noPos)
|
|
115
|
+
expect(layout.components[0].position).toEqual({ colStart: 1, colSpan: 12 })
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// ── R2 — unknown schemaVersion, never throw ───────────────
|
|
119
|
+
it('renders a degraded-but-usable layout for an unknown schemaVersion', () => {
|
|
120
|
+
const futureVersion = { ...VALID, schemaVersion: 'connector-dynamic-result/v2' }
|
|
121
|
+
const layout = connectorResultToUILayout(futureVersion)
|
|
122
|
+
expect(layout.id).toMatch(/^connector-degraded/)
|
|
123
|
+
// First component is the version warning, the real components follow.
|
|
124
|
+
expect(layout.components[0].id).toBe('connector-version-warning')
|
|
125
|
+
expect(layout.components.some((c) => c.id === 'tbl')).toBe(true)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('renders an explicit error layout for an unreadable payload — never throws', () => {
|
|
129
|
+
for (const bad of [null, undefined, 42, 'nope', {}, { foo: 'bar' }]) {
|
|
130
|
+
let layout!: ReturnType<typeof connectorResultToUILayout>
|
|
131
|
+
expect(() => {
|
|
132
|
+
layout = connectorResultToUILayout(bad)
|
|
133
|
+
}).not.toThrow()
|
|
134
|
+
expect(layout.id).toBe('connector-degraded')
|
|
135
|
+
expect(layout.components[0].id).toBe('connector-degraded-notice')
|
|
136
|
+
expect(layout.components[0].type).toBe('text')
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('is pure — same input yields a deep-equal output', () => {
|
|
141
|
+
expect(connectorResultToUILayout(VALID)).toEqual(connectorResultToUILayout(VALID))
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('connector adapters — spec fixtures', () => {
|
|
146
|
+
const FIXTURE_DIR = join(
|
|
147
|
+
__dirname,
|
|
148
|
+
'..',
|
|
149
|
+
'..',
|
|
150
|
+
'..',
|
|
151
|
+
'mcp-ui-spec',
|
|
152
|
+
'examples',
|
|
153
|
+
'connector'
|
|
154
|
+
)
|
|
155
|
+
const fixtures = readdirSync(FIXTURE_DIR).filter((f) => f.endsWith('.json'))
|
|
156
|
+
|
|
157
|
+
for (const file of fixtures) {
|
|
158
|
+
it(`fixture ${file} assembles into a non-degraded layout`, () => {
|
|
159
|
+
const raw = JSON.parse(readFileSync(join(FIXTURE_DIR, file), 'utf8'))
|
|
160
|
+
const layout = connectorResultToUILayout(raw)
|
|
161
|
+
expect(layout.id).not.toMatch(/^connector-degraded/)
|
|
162
|
+
expect(layout.components.length).toBeGreaterThan(0)
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
})
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connector adapters — `ConnectorDynamicResultV1` → MCP-UI render structures.
|
|
3
|
+
*
|
|
4
|
+
* @since v6.6.0 (D5 / D6 of ROADMAP-opendata-macro-mcpui)
|
|
5
|
+
*
|
|
6
|
+
* ## Opt-in, pure
|
|
7
|
+
*
|
|
8
|
+
* These adapters are published under the dedicated subpath
|
|
9
|
+
* `@seed-ship/mcp-ui-solid/adapters` — they are NEVER imported by the core
|
|
10
|
+
* renderer path, so a consumer that does not emit connector results pays
|
|
11
|
+
* nothing for them.
|
|
12
|
+
*
|
|
13
|
+
* Every adapter here is a **pure function** (D5) : same input → same
|
|
14
|
+
* output, no `fetch`, no `localStorage`, no global state, no clock, no
|
|
15
|
+
* randomness. This is what lets a host re-run an adapter deterministically
|
|
16
|
+
* after presentation feedback (D1) and replay fixtures in tests.
|
|
17
|
+
*
|
|
18
|
+
* ## Unknown schema version — never throw (R2)
|
|
19
|
+
*
|
|
20
|
+
* `connectorResultToUILayout()` never throws on the runtime render path.
|
|
21
|
+
* A payload it cannot read becomes an explicit degraded `UILayout` (a
|
|
22
|
+
* visible notice), never a silent disappearance and never an exception.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { UIComponent, UILayout, GridPosition } from '../types'
|
|
26
|
+
import {
|
|
27
|
+
ConnectorDynamicResultV1Schema,
|
|
28
|
+
CONNECTOR_DYNAMIC_RESULT_V1,
|
|
29
|
+
type ConnectorAction,
|
|
30
|
+
} from '@seed-ship/mcp-ui-spec'
|
|
31
|
+
import { z } from 'zod'
|
|
32
|
+
|
|
33
|
+
// ─────────────────────────────────────────────────────────────
|
|
34
|
+
// connectorActionsToActionGroup
|
|
35
|
+
// ─────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
export interface ConnectorActionsToActionGroupOptions {
|
|
38
|
+
/** Component id. Default `'connector-actions'`. */
|
|
39
|
+
id?: string
|
|
40
|
+
/** Optional heading shown above the buttons. */
|
|
41
|
+
title?: string
|
|
42
|
+
/** Button row layout. Default `'horizontal'`. */
|
|
43
|
+
layout?: 'horizontal' | 'vertical' | 'space-between' | 'end' | 'center'
|
|
44
|
+
/** Gap between buttons. Default `'md'`. */
|
|
45
|
+
gap?: 'none' | 'sm' | 'md' | 'lg'
|
|
46
|
+
/** Grid position. Default full-width. */
|
|
47
|
+
position?: GridPosition
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Wraps a connector's `actions` into an `action-group` `UIComponent`.
|
|
52
|
+
*
|
|
53
|
+
* `ConnectorAction` is the exact `action-group` action shape, so this is a
|
|
54
|
+
* thin, pure envelope — no transformation of the actions themselves.
|
|
55
|
+
*/
|
|
56
|
+
export function connectorActionsToActionGroup(
|
|
57
|
+
actions: ConnectorAction[],
|
|
58
|
+
options: ConnectorActionsToActionGroupOptions = {}
|
|
59
|
+
): UIComponent {
|
|
60
|
+
return {
|
|
61
|
+
id: options.id ?? 'connector-actions',
|
|
62
|
+
type: 'action-group',
|
|
63
|
+
position: options.position ?? { colStart: 1, colSpan: 12 },
|
|
64
|
+
params: {
|
|
65
|
+
actions,
|
|
66
|
+
...(options.title ? { title: options.title } : {}),
|
|
67
|
+
layout: options.layout ?? 'horizontal',
|
|
68
|
+
gap: options.gap ?? 'md',
|
|
69
|
+
},
|
|
70
|
+
} as UIComponent
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─────────────────────────────────────────────────────────────
|
|
74
|
+
// connectorResultToUILayout
|
|
75
|
+
// ─────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
export interface ConnectorResultToUILayoutOptions {
|
|
78
|
+
/** Layout id. Default derived from `connectorId` + `queryHash` / `toolName`. */
|
|
79
|
+
id?: string
|
|
80
|
+
/** Heading for the actions `action-group`. */
|
|
81
|
+
actionsTitle?: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Lenient mirror of `ConnectorDynamicResultV1Schema` — `schemaVersion`
|
|
86
|
+
* relaxed to any string. Used to tell apart "unknown version but otherwise
|
|
87
|
+
* a usable envelope" (→ render with a warning, R2) from "truly unreadable"
|
|
88
|
+
* (→ explicit error state).
|
|
89
|
+
*/
|
|
90
|
+
const LenientResultSchema = z.object({
|
|
91
|
+
schemaVersion: z.string(),
|
|
92
|
+
connectorId: z.string().min(1),
|
|
93
|
+
toolName: z.string().min(1),
|
|
94
|
+
query: z.string(),
|
|
95
|
+
queryHash: z.string().optional(),
|
|
96
|
+
intent: z.string().optional(),
|
|
97
|
+
primary: z.record(z.unknown()),
|
|
98
|
+
supplemental: z.array(z.record(z.unknown())).optional(),
|
|
99
|
+
actions: z.array(z.record(z.unknown())).optional(),
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const DEFAULT_GRID = { columns: 12, gap: '1rem' } as const
|
|
103
|
+
|
|
104
|
+
function withPosition(component: UIComponent): UIComponent {
|
|
105
|
+
if (component && component.position) return component
|
|
106
|
+
return { ...component, position: { colStart: 1, colSpan: 12 } }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A component is a UILayout when it carries a `components` array. */
|
|
110
|
+
function isLayoutShape(value: Record<string, unknown>): boolean {
|
|
111
|
+
return Array.isArray((value as { components?: unknown }).components)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function degradedTextComponent(id: string, message: string): UIComponent {
|
|
115
|
+
return {
|
|
116
|
+
id,
|
|
117
|
+
type: 'text',
|
|
118
|
+
position: { colStart: 1, colSpan: 12 },
|
|
119
|
+
params: { markdown: true, content: message },
|
|
120
|
+
} as UIComponent
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Assembles a `ConnectorDynamicResultV1` into a single `UILayout` :
|
|
125
|
+
* `primary` + `supplemental[]` + (`actions` → an `action-group`).
|
|
126
|
+
*
|
|
127
|
+
* - When `primary` is itself a layout, its components are spread in.
|
|
128
|
+
* - Raw data is never sacrificed — every supplied component is kept.
|
|
129
|
+
* - Pure : no side effects, deterministic.
|
|
130
|
+
*
|
|
131
|
+
* ### Degraded behavior (R2)
|
|
132
|
+
*
|
|
133
|
+
* - Valid v1 payload → normal assembled layout.
|
|
134
|
+
* - Unknown `schemaVersion` but an otherwise-usable envelope → the
|
|
135
|
+
* components are still rendered, prefixed with a visible warning notice.
|
|
136
|
+
* - Unreadable payload → an explicit error `UILayout` (a single `text`
|
|
137
|
+
* notice). Never throws, never returns an empty layout silently.
|
|
138
|
+
*
|
|
139
|
+
* A degraded layout always has an `id` starting with `connector-degraded`,
|
|
140
|
+
* so a host can detect it (e.g. for telemetry) without inspecting content.
|
|
141
|
+
*/
|
|
142
|
+
export function connectorResultToUILayout(
|
|
143
|
+
result: unknown,
|
|
144
|
+
options: ConnectorResultToUILayoutOptions = {}
|
|
145
|
+
): UILayout {
|
|
146
|
+
const strict = ConnectorDynamicResultV1Schema.safeParse(result)
|
|
147
|
+
|
|
148
|
+
// ── Tier 3 : unreadable ───────────────────────────────────
|
|
149
|
+
if (!strict.success) {
|
|
150
|
+
const lenient = LenientResultSchema.safeParse(result)
|
|
151
|
+
if (!lenient.success) {
|
|
152
|
+
const version =
|
|
153
|
+
result && typeof result === 'object'
|
|
154
|
+
? (result as { schemaVersion?: unknown }).schemaVersion
|
|
155
|
+
: undefined
|
|
156
|
+
return {
|
|
157
|
+
id: 'connector-degraded',
|
|
158
|
+
components: [
|
|
159
|
+
degradedTextComponent(
|
|
160
|
+
'connector-degraded-notice',
|
|
161
|
+
`### Résultat non rendu\n\nLe résultat du connecteur n'a pas pu être interprété${
|
|
162
|
+
typeof version === 'string' ? ` (schéma : \`${version}\`)` : ''
|
|
163
|
+
}. Cet état explicite remplace une disparition silencieuse du rendu.`
|
|
164
|
+
),
|
|
165
|
+
],
|
|
166
|
+
grid: { ...DEFAULT_GRID },
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// ── Tier 2 : usable envelope, unknown version ───────────
|
|
170
|
+
const r = lenient.data
|
|
171
|
+
const components: UIComponent[] = [
|
|
172
|
+
degradedTextComponent(
|
|
173
|
+
'connector-version-warning',
|
|
174
|
+
`> ⚠ Schéma connecteur non reconnu (\`${r.schemaVersion}\`, attendu \`${CONNECTOR_DYNAMIC_RESULT_V1}\`). Le rendu ci-dessous est en mode dégradé.`
|
|
175
|
+
),
|
|
176
|
+
...collectComponents(r),
|
|
177
|
+
]
|
|
178
|
+
return {
|
|
179
|
+
id: options.id ?? `connector-degraded-${r.connectorId}`,
|
|
180
|
+
components: components.map(withPosition),
|
|
181
|
+
grid: { ...DEFAULT_GRID },
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Tier 1 : valid v1 ─────────────────────────────────────
|
|
186
|
+
const r = strict.data
|
|
187
|
+
const components = collectComponents(r, options.actionsTitle).map(withPosition)
|
|
188
|
+
return {
|
|
189
|
+
id: options.id ?? layoutId(r.connectorId, r.queryHash ?? r.toolName),
|
|
190
|
+
components,
|
|
191
|
+
grid: { ...DEFAULT_GRID },
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Flattens `primary` + `supplemental` + `actions` into a component list.
|
|
197
|
+
* Shared by the valid and degraded-but-usable paths.
|
|
198
|
+
*/
|
|
199
|
+
function collectComponents(
|
|
200
|
+
r: {
|
|
201
|
+
primary: Record<string, unknown>
|
|
202
|
+
supplemental?: Record<string, unknown>[]
|
|
203
|
+
actions?: unknown[]
|
|
204
|
+
},
|
|
205
|
+
actionsTitle?: string
|
|
206
|
+
): UIComponent[] {
|
|
207
|
+
const components: UIComponent[] = []
|
|
208
|
+
|
|
209
|
+
if (isLayoutShape(r.primary)) {
|
|
210
|
+
const inner = (r.primary as { components?: UIComponent[] }).components ?? []
|
|
211
|
+
components.push(...inner)
|
|
212
|
+
} else {
|
|
213
|
+
components.push(r.primary as unknown as UIComponent)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (r.supplemental) {
|
|
217
|
+
components.push(...(r.supplemental as unknown as UIComponent[]))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (Array.isArray(r.actions) && r.actions.length > 0) {
|
|
221
|
+
components.push(
|
|
222
|
+
connectorActionsToActionGroup(r.actions as ConnectorAction[], {
|
|
223
|
+
id: 'connector-actions',
|
|
224
|
+
title: actionsTitle,
|
|
225
|
+
})
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return components
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function layoutId(connectorId: string, suffix: string): string {
|
|
233
|
+
return `connector-${connectorId}-${suffix}`
|
|
234
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @seed-ship/mcp-ui-solid/adapters
|
|
3
|
+
*
|
|
4
|
+
* Opt-in, pure adapters that turn connector / macro contracts into MCP-UI
|
|
5
|
+
* render structures. Published as a dedicated subpath so the core renderer
|
|
6
|
+
* path never depends on them.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { connectorResultToUILayout } from '@seed-ship/mcp-ui-solid/adapters'
|
|
11
|
+
*
|
|
12
|
+
* const layout = connectorResultToUILayout(connectorResult)
|
|
13
|
+
* // → <UIResourceRenderer content={layout} />
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
connectorResultToUILayout,
|
|
19
|
+
connectorActionsToActionGroup,
|
|
20
|
+
} from './connector'
|
|
21
|
+
export type {
|
|
22
|
+
ConnectorResultToUILayoutOptions,
|
|
23
|
+
ConnectorActionsToActionGroupOptions,
|
|
24
|
+
} from './connector'
|
|
@@ -12,6 +12,7 @@ import type { UIComponent, ActionGroupParams, ActionComponentParams } from '../t
|
|
|
12
12
|
vi.mock('../hooks/useAction', () => ({
|
|
13
13
|
useAction: () => ({
|
|
14
14
|
execute: vi.fn().mockResolvedValue({ success: true }),
|
|
15
|
+
executeAction: vi.fn().mockResolvedValue({ success: true }),
|
|
15
16
|
isExecuting: () => false,
|
|
16
17
|
lastResult: () => undefined,
|
|
17
18
|
lastError: () => undefined,
|
|
@@ -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}
|