@seed-ship/mcp-ui-solid 5.2.0 → 5.3.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 +43 -0
- package/dist/components/ElicitationForm.cjs +51 -0
- package/dist/components/ElicitationForm.cjs.map +1 -0
- package/dist/components/ElicitationForm.d.ts +68 -0
- package/dist/components/ElicitationForm.d.ts.map +1 -0
- package/dist/components/ElicitationForm.js +51 -0
- package/dist/components/ElicitationForm.js.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components.cjs +2 -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 +2 -0
- package/dist/components.js.map +1 -1
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/stores/server-capabilities-store.cjs +61 -0
- package/dist/stores/server-capabilities-store.cjs.map +1 -0
- package/dist/stores/server-capabilities-store.d.ts +172 -0
- package/dist/stores/server-capabilities-store.d.ts.map +1 -0
- package/dist/stores/server-capabilities-store.js +61 -0
- package/dist/stores/server-capabilities-store.js.map +1 -0
- package/docs/recipes/elicitation-pseudo-spec-adapter.md +171 -0
- package/docs/recipes/feedback-inline-wiring.md +142 -0
- package/package.json +1 -1
- package/src/components/ElicitationForm.test.tsx +197 -0
- package/src/components/ElicitationForm.tsx +126 -0
- package/src/components/index.ts +4 -0
- package/src/index.ts +16 -0
- package/src/stores/server-capabilities-store.test.tsx +206 -0
- package/src/stores/server-capabilities-store.tsx +215 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Capabilities Store — reactive snapshot of the MCP `initialize`
|
|
3
|
+
* response echoed by the server.
|
|
4
|
+
*
|
|
5
|
+
* @experimental
|
|
6
|
+
* @since v5.3.0
|
|
7
|
+
*
|
|
8
|
+
* mcp-ui doesn't speak MCP protocol directly — the consumer's transport
|
|
9
|
+
* layer (stdio child process, HTTP/SSE client, ...) parses the
|
|
10
|
+
* `initialize` JSON-RPC response and pushes the relevant fields into this
|
|
11
|
+
* store via `setServerCapabilities(info)`. Components then read
|
|
12
|
+
* reactively via `useServerCapabilities()` to gate behavior :
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* const { capabilities } = useServerCapabilities()
|
|
16
|
+
* <Show when={capabilities()?.tools?.listChanged}>
|
|
17
|
+
* <ToolListSubscriber />
|
|
18
|
+
* </Show>
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* ## Two consumption modes (mirrors `scratchpad-store`)
|
|
22
|
+
*
|
|
23
|
+
* 1. **Singleton mode (default)** — `setServerCapabilities(info)` mutates
|
|
24
|
+
* the module-level singleton. `useServerCapabilities()` reads from it.
|
|
25
|
+
* Use for single-MCP-server consumers (the common case).
|
|
26
|
+
*
|
|
27
|
+
* 2. **Multi-instance mode** — wrap a subtree in
|
|
28
|
+
* `<ServerCapabilitiesProvider>` to scope a separate handle. Pass
|
|
29
|
+
* `store={createServerCapabilitiesStore()}` explicitly when you need to
|
|
30
|
+
* drive it from a non-reactive scope (e.g. a transport adapter living
|
|
31
|
+
* at the app root).
|
|
32
|
+
*
|
|
33
|
+
* ## Note on `elicitation`
|
|
34
|
+
*
|
|
35
|
+
* Per MCP spec 2025-06-18, `elicitation` is a **CLIENT** capability, not
|
|
36
|
+
* a server one. Servers do not declare it. If you need to gate
|
|
37
|
+
* `<ElicitationForm>` rendering on whether the connected client *itself*
|
|
38
|
+
* supports elicitation — that's a separate concern (your own state, set
|
|
39
|
+
* by your transport layer based on its own configuration).
|
|
40
|
+
*/
|
|
41
|
+
import { type ParentComponent } from 'solid-js';
|
|
42
|
+
/**
|
|
43
|
+
* Server capabilities object as advertised in the MCP `initialize` response.
|
|
44
|
+
* Mirrors the spec 2025-06-18 `ServerCapabilities` shape with permissive
|
|
45
|
+
* `experimental` for forward compatibility.
|
|
46
|
+
*/
|
|
47
|
+
export interface ServerCapabilities {
|
|
48
|
+
experimental?: Record<string, unknown>;
|
|
49
|
+
logging?: Record<string, never>;
|
|
50
|
+
tools?: {
|
|
51
|
+
listChanged?: boolean;
|
|
52
|
+
};
|
|
53
|
+
prompts?: {
|
|
54
|
+
listChanged?: boolean;
|
|
55
|
+
};
|
|
56
|
+
resources?: {
|
|
57
|
+
listChanged?: boolean;
|
|
58
|
+
subscribe?: boolean;
|
|
59
|
+
};
|
|
60
|
+
completions?: Record<string, never>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Subset of the MCP `initialize` response relevant to the UI layer.
|
|
64
|
+
* Consumers may extend this via the `experimental` field.
|
|
65
|
+
*/
|
|
66
|
+
export interface ServerInitializeInfo {
|
|
67
|
+
protocolVersion: string;
|
|
68
|
+
serverInfo: {
|
|
69
|
+
name: string;
|
|
70
|
+
version: string;
|
|
71
|
+
title?: string;
|
|
72
|
+
[key: string]: unknown;
|
|
73
|
+
};
|
|
74
|
+
capabilities: ServerCapabilities;
|
|
75
|
+
instructions?: string;
|
|
76
|
+
}
|
|
77
|
+
export interface ServerCapabilitiesStoreHandle {
|
|
78
|
+
/** Push a fresh `initialize` snapshot into the store, or clear with `null`. */
|
|
79
|
+
set: (info: ServerInitializeInfo | null) => void;
|
|
80
|
+
/** Reactive accessor for the full info (null when no initialize received). */
|
|
81
|
+
info: () => ServerInitializeInfo | null;
|
|
82
|
+
/** Reactive accessor for just the `capabilities` field. */
|
|
83
|
+
capabilities: () => ServerCapabilities | null;
|
|
84
|
+
/** Reactive accessor for just the `serverInfo` field. */
|
|
85
|
+
serverInfo: () => ServerInitializeInfo['serverInfo'] | null;
|
|
86
|
+
/** Reactive accessor for the protocol version string. */
|
|
87
|
+
protocolVersion: () => string | null;
|
|
88
|
+
/**
|
|
89
|
+
* Helper : returns true if the server advertised the named capability key
|
|
90
|
+
* with a truthy value (i.e. the key is present, even as an empty object).
|
|
91
|
+
*/
|
|
92
|
+
hasCapability: (key: keyof ServerCapabilities) => boolean;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Create an isolated server-capabilities store instance.
|
|
96
|
+
*
|
|
97
|
+
* Use this when you need to track multiple MCP servers in parallel (rare),
|
|
98
|
+
* or to drive the store from a non-reactive transport adapter. Pair with
|
|
99
|
+
* `<ServerCapabilitiesProvider store={...}>` to scope a SolidJS subtree.
|
|
100
|
+
*
|
|
101
|
+
* @experimental
|
|
102
|
+
* @since v5.3.0
|
|
103
|
+
*/
|
|
104
|
+
export declare function createServerCapabilitiesStore(): ServerCapabilitiesStoreHandle;
|
|
105
|
+
/**
|
|
106
|
+
* Push the parsed MCP `initialize` response into the module-level singleton
|
|
107
|
+
* store. Pass `null` to clear (e.g. on disconnect / server change).
|
|
108
|
+
*
|
|
109
|
+
* @experimental
|
|
110
|
+
* @since v5.3.0
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* // In your transport adapter, after receiving the initialize response :
|
|
114
|
+
* setServerCapabilities({
|
|
115
|
+
* protocolVersion: response.result.protocolVersion,
|
|
116
|
+
* serverInfo: response.result.serverInfo,
|
|
117
|
+
* capabilities: response.result.capabilities,
|
|
118
|
+
* instructions: response.result.instructions,
|
|
119
|
+
* })
|
|
120
|
+
*/
|
|
121
|
+
export declare function setServerCapabilities(info: ServerInitializeInfo | null): void;
|
|
122
|
+
/**
|
|
123
|
+
* Context for a scoped server-capabilities store. Populated by
|
|
124
|
+
* `<ServerCapabilitiesProvider>`. Read by `useServerCapabilities()` with
|
|
125
|
+
* automatic fallback to the module-level singleton when absent.
|
|
126
|
+
*
|
|
127
|
+
* @experimental
|
|
128
|
+
* @since v5.3.0
|
|
129
|
+
*/
|
|
130
|
+
export declare const ServerCapabilitiesContext: import("solid-js").Context<ServerCapabilitiesStoreHandle | undefined>;
|
|
131
|
+
/**
|
|
132
|
+
* Provide a scoped `ServerCapabilitiesStoreHandle` to a SolidJS subtree.
|
|
133
|
+
* Children calling `useServerCapabilities()` bind to this store instead of
|
|
134
|
+
* the module singleton.
|
|
135
|
+
*
|
|
136
|
+
* If no `store` prop is passed, a fresh store is created for the provider's
|
|
137
|
+
* lifetime. Pass `store` explicitly when you need the handle outside the
|
|
138
|
+
* tree (e.g. in a transport adapter living at the app root).
|
|
139
|
+
*
|
|
140
|
+
* @experimental
|
|
141
|
+
* @since v5.3.0
|
|
142
|
+
*/
|
|
143
|
+
export declare const ServerCapabilitiesProvider: ParentComponent<{
|
|
144
|
+
store?: ServerCapabilitiesStoreHandle;
|
|
145
|
+
}>;
|
|
146
|
+
/**
|
|
147
|
+
* Hook for components — reads the server capabilities reactively.
|
|
148
|
+
*
|
|
149
|
+
* If called inside a `<ServerCapabilitiesProvider>`, reads the scoped
|
|
150
|
+
* handle; otherwise falls back to the module singleton.
|
|
151
|
+
*
|
|
152
|
+
* @experimental
|
|
153
|
+
* @since v5.3.0
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* const { capabilities, serverInfo, hasCapability } = useServerCapabilities()
|
|
157
|
+
*
|
|
158
|
+
* <Show when={capabilities()}>
|
|
159
|
+
* <p>Connected to {serverInfo()?.name} v{serverInfo()?.version}</p>
|
|
160
|
+
* <Show when={hasCapability('tools')}>
|
|
161
|
+
* <ToolPalette />
|
|
162
|
+
* </Show>
|
|
163
|
+
* </Show>
|
|
164
|
+
*/
|
|
165
|
+
export declare function useServerCapabilities(): {
|
|
166
|
+
info: () => ServerInitializeInfo | null;
|
|
167
|
+
capabilities: () => ServerCapabilities | null;
|
|
168
|
+
serverInfo: () => ServerInitializeInfo['serverInfo'] | null;
|
|
169
|
+
protocolVersion: () => string | null;
|
|
170
|
+
hasCapability: (key: keyof ServerCapabilities) => boolean;
|
|
171
|
+
};
|
|
172
|
+
//# sourceMappingURL=server-capabilities-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-capabilities-store.d.ts","sourceRoot":"","sources":["../../src/stores/server-capabilities-store.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,EAA6B,KAAK,eAAe,EAAY,MAAM,UAAU,CAAA;AAKpF;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IAC/B,KAAK,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IACjC,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IACnC,SAAS,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;CACpC;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,eAAe,EAAE,MAAM,CAAA;IACvB,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAA;IACrF,YAAY,EAAE,kBAAkB,CAAA;IAChC,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAID,MAAM,WAAW,6BAA6B;IAC5C,+EAA+E;IAC/E,GAAG,EAAE,CAAC,IAAI,EAAE,oBAAoB,GAAG,IAAI,KAAK,IAAI,CAAA;IAChD,8EAA8E;IAC9E,IAAI,EAAE,MAAM,oBAAoB,GAAG,IAAI,CAAA;IACvC,2DAA2D;IAC3D,YAAY,EAAE,MAAM,kBAAkB,GAAG,IAAI,CAAA;IAC7C,yDAAyD;IACzD,UAAU,EAAE,MAAM,oBAAoB,CAAC,YAAY,CAAC,GAAG,IAAI,CAAA;IAC3D,yDAAyD;IACzD,eAAe,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IACpC;;;OAGG;IACH,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,kBAAkB,KAAK,OAAO,CAAA;CAC1D;AAID;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,IAAI,6BAA6B,CAW7E;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,oBAAoB,GAAG,IAAI,GAAG,IAAI,CAE7E;AAID;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,uEAErC,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,EAAE,eAAe,CAAC;IACvD,KAAK,CAAC,EAAE,6BAA6B,CAAA;CACtC,CAOA,CAAA;AAID;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,IAAI;IACvC,IAAI,EAAE,MAAM,oBAAoB,GAAG,IAAI,CAAA;IACvC,YAAY,EAAE,MAAM,kBAAkB,GAAG,IAAI,CAAA;IAC7C,UAAU,EAAE,MAAM,oBAAoB,CAAC,YAAY,CAAC,GAAG,IAAI,CAAA;IAC3D,eAAe,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IACpC,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,kBAAkB,KAAK,OAAO,CAAA;CAC1D,CAUA"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createComponent } from "solid-js/web";
|
|
2
|
+
import { createContext, useContext } from "solid-js";
|
|
3
|
+
import { createStore } from "solid-js/store";
|
|
4
|
+
function createServerCapabilitiesStore() {
|
|
5
|
+
const [state, setState] = createStore({
|
|
6
|
+
info: null
|
|
7
|
+
});
|
|
8
|
+
return {
|
|
9
|
+
set: (info) => setState("info", info),
|
|
10
|
+
info: () => state.info,
|
|
11
|
+
capabilities: () => {
|
|
12
|
+
var _a;
|
|
13
|
+
return ((_a = state.info) == null ? void 0 : _a.capabilities) ?? null;
|
|
14
|
+
},
|
|
15
|
+
serverInfo: () => {
|
|
16
|
+
var _a;
|
|
17
|
+
return ((_a = state.info) == null ? void 0 : _a.serverInfo) ?? null;
|
|
18
|
+
},
|
|
19
|
+
protocolVersion: () => {
|
|
20
|
+
var _a;
|
|
21
|
+
return ((_a = state.info) == null ? void 0 : _a.protocolVersion) ?? null;
|
|
22
|
+
},
|
|
23
|
+
hasCapability: (key) => {
|
|
24
|
+
var _a, _b;
|
|
25
|
+
return Boolean((_b = (_a = state.info) == null ? void 0 : _a.capabilities) == null ? void 0 : _b[key]);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const defaultStore = createServerCapabilitiesStore();
|
|
30
|
+
function setServerCapabilities(info) {
|
|
31
|
+
defaultStore.set(info);
|
|
32
|
+
}
|
|
33
|
+
const ServerCapabilitiesContext = createContext(void 0);
|
|
34
|
+
const ServerCapabilitiesProvider = (props) => {
|
|
35
|
+
const store = props.store ?? createServerCapabilitiesStore();
|
|
36
|
+
return createComponent(ServerCapabilitiesContext.Provider, {
|
|
37
|
+
value: store,
|
|
38
|
+
get children() {
|
|
39
|
+
return props.children;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
function useServerCapabilities() {
|
|
44
|
+
const scoped = useContext(ServerCapabilitiesContext);
|
|
45
|
+
const handle = scoped ?? defaultStore;
|
|
46
|
+
return {
|
|
47
|
+
info: handle.info,
|
|
48
|
+
capabilities: handle.capabilities,
|
|
49
|
+
serverInfo: handle.serverInfo,
|
|
50
|
+
protocolVersion: handle.protocolVersion,
|
|
51
|
+
hasCapability: handle.hasCapability
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
ServerCapabilitiesContext,
|
|
56
|
+
ServerCapabilitiesProvider,
|
|
57
|
+
createServerCapabilitiesStore,
|
|
58
|
+
setServerCapabilities,
|
|
59
|
+
useServerCapabilities
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=server-capabilities-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-capabilities-store.js","sources":["../../src/stores/server-capabilities-store.tsx"],"sourcesContent":["/**\n * Server Capabilities Store — reactive snapshot of the MCP `initialize`\n * response echoed by the server.\n *\n * @experimental\n * @since v5.3.0\n *\n * mcp-ui doesn't speak MCP protocol directly — the consumer's transport\n * layer (stdio child process, HTTP/SSE client, ...) parses the\n * `initialize` JSON-RPC response and pushes the relevant fields into this\n * store via `setServerCapabilities(info)`. Components then read\n * reactively via `useServerCapabilities()` to gate behavior :\n *\n * ```tsx\n * const { capabilities } = useServerCapabilities()\n * <Show when={capabilities()?.tools?.listChanged}>\n * <ToolListSubscriber />\n * </Show>\n * ```\n *\n * ## Two consumption modes (mirrors `scratchpad-store`)\n *\n * 1. **Singleton mode (default)** — `setServerCapabilities(info)` mutates\n * the module-level singleton. `useServerCapabilities()` reads from it.\n * Use for single-MCP-server consumers (the common case).\n *\n * 2. **Multi-instance mode** — wrap a subtree in\n * `<ServerCapabilitiesProvider>` to scope a separate handle. Pass\n * `store={createServerCapabilitiesStore()}` explicitly when you need to\n * drive it from a non-reactive scope (e.g. a transport adapter living\n * at the app root).\n *\n * ## Note on `elicitation`\n *\n * Per MCP spec 2025-06-18, `elicitation` is a **CLIENT** capability, not\n * a server one. Servers do not declare it. If you need to gate\n * `<ElicitationForm>` rendering on whether the connected client *itself*\n * supports elicitation — that's a separate concern (your own state, set\n * by your transport layer based on its own configuration).\n */\n\nimport { createContext, useContext, type ParentComponent, type JSX } from 'solid-js'\nimport { createStore } from 'solid-js/store'\n\n// ─── Types ────────────────────────────────────────────────────\n\n/**\n * Server capabilities object as advertised in the MCP `initialize` response.\n * Mirrors the spec 2025-06-18 `ServerCapabilities` shape with permissive\n * `experimental` for forward compatibility.\n */\nexport interface ServerCapabilities {\n experimental?: Record<string, unknown>\n logging?: Record<string, never>\n tools?: { listChanged?: boolean }\n prompts?: { listChanged?: boolean }\n resources?: { listChanged?: boolean; subscribe?: boolean }\n completions?: Record<string, never>\n}\n\n/**\n * Subset of the MCP `initialize` response relevant to the UI layer.\n * Consumers may extend this via the `experimental` field.\n */\nexport interface ServerInitializeInfo {\n protocolVersion: string\n serverInfo: { name: string; version: string; title?: string; [key: string]: unknown }\n capabilities: ServerCapabilities\n instructions?: string\n}\n\n// ─── Handle ───────────────────────────────────────────────────\n\nexport interface ServerCapabilitiesStoreHandle {\n /** Push a fresh `initialize` snapshot into the store, or clear with `null`. */\n set: (info: ServerInitializeInfo | null) => void\n /** Reactive accessor for the full info (null when no initialize received). */\n info: () => ServerInitializeInfo | null\n /** Reactive accessor for just the `capabilities` field. */\n capabilities: () => ServerCapabilities | null\n /** Reactive accessor for just the `serverInfo` field. */\n serverInfo: () => ServerInitializeInfo['serverInfo'] | null\n /** Reactive accessor for the protocol version string. */\n protocolVersion: () => string | null\n /**\n * Helper : returns true if the server advertised the named capability key\n * with a truthy value (i.e. the key is present, even as an empty object).\n */\n hasCapability: (key: keyof ServerCapabilities) => boolean\n}\n\n// ─── Factory ──────────────────────────────────────────────────\n\n/**\n * Create an isolated server-capabilities store instance.\n *\n * Use this when you need to track multiple MCP servers in parallel (rare),\n * or to drive the store from a non-reactive transport adapter. Pair with\n * `<ServerCapabilitiesProvider store={...}>` to scope a SolidJS subtree.\n *\n * @experimental\n * @since v5.3.0\n */\nexport function createServerCapabilitiesStore(): ServerCapabilitiesStoreHandle {\n const [state, setState] = createStore<{ info: ServerInitializeInfo | null }>({ info: null })\n\n return {\n set: (info) => setState('info', info),\n info: () => state.info,\n capabilities: () => state.info?.capabilities ?? null,\n serverInfo: () => state.info?.serverInfo ?? null,\n protocolVersion: () => state.info?.protocolVersion ?? null,\n hasCapability: (key) => Boolean(state.info?.capabilities?.[key]),\n }\n}\n\n// ─── Module-level singleton ───────────────────────────────────\n\nconst defaultStore: ServerCapabilitiesStoreHandle = createServerCapabilitiesStore()\n\n/**\n * Push the parsed MCP `initialize` response into the module-level singleton\n * store. Pass `null` to clear (e.g. on disconnect / server change).\n *\n * @experimental\n * @since v5.3.0\n *\n * @example\n * // In your transport adapter, after receiving the initialize response :\n * setServerCapabilities({\n * protocolVersion: response.result.protocolVersion,\n * serverInfo: response.result.serverInfo,\n * capabilities: response.result.capabilities,\n * instructions: response.result.instructions,\n * })\n */\nexport function setServerCapabilities(info: ServerInitializeInfo | null): void {\n defaultStore.set(info)\n}\n\n// ─── Context ──────────────────────────────────────────────────\n\n/**\n * Context for a scoped server-capabilities store. Populated by\n * `<ServerCapabilitiesProvider>`. Read by `useServerCapabilities()` with\n * automatic fallback to the module-level singleton when absent.\n *\n * @experimental\n * @since v5.3.0\n */\nexport const ServerCapabilitiesContext = createContext<ServerCapabilitiesStoreHandle | undefined>(\n undefined\n)\n\n/**\n * Provide a scoped `ServerCapabilitiesStoreHandle` to a SolidJS subtree.\n * Children calling `useServerCapabilities()` bind to this store instead of\n * the module singleton.\n *\n * If no `store` prop is passed, a fresh store is created for the provider's\n * lifetime. Pass `store` explicitly when you need the handle outside the\n * tree (e.g. in a transport adapter living at the app root).\n *\n * @experimental\n * @since v5.3.0\n */\nexport const ServerCapabilitiesProvider: ParentComponent<{\n store?: ServerCapabilitiesStoreHandle\n}> = (props): JSX.Element => {\n const store = props.store ?? createServerCapabilitiesStore()\n return (\n <ServerCapabilitiesContext.Provider value={store}>\n {props.children}\n </ServerCapabilitiesContext.Provider>\n )\n}\n\n// ─── Reactive hook ────────────────────────────────────────────\n\n/**\n * Hook for components — reads the server capabilities reactively.\n *\n * If called inside a `<ServerCapabilitiesProvider>`, reads the scoped\n * handle; otherwise falls back to the module singleton.\n *\n * @experimental\n * @since v5.3.0\n *\n * @example\n * const { capabilities, serverInfo, hasCapability } = useServerCapabilities()\n *\n * <Show when={capabilities()}>\n * <p>Connected to {serverInfo()?.name} v{serverInfo()?.version}</p>\n * <Show when={hasCapability('tools')}>\n * <ToolPalette />\n * </Show>\n * </Show>\n */\nexport function useServerCapabilities(): {\n info: () => ServerInitializeInfo | null\n capabilities: () => ServerCapabilities | null\n serverInfo: () => ServerInitializeInfo['serverInfo'] | null\n protocolVersion: () => string | null\n hasCapability: (key: keyof ServerCapabilities) => boolean\n} {\n const scoped = useContext(ServerCapabilitiesContext)\n const handle = scoped ?? defaultStore\n return {\n info: handle.info,\n capabilities: handle.capabilities,\n serverInfo: handle.serverInfo,\n protocolVersion: handle.protocolVersion,\n hasCapability: handle.hasCapability,\n }\n}\n"],"names":["createServerCapabilitiesStore","state","setState","createStore","info","set","capabilities","serverInfo","protocolVersion","hasCapability","key","Boolean","defaultStore","setServerCapabilities","ServerCapabilitiesContext","createContext","undefined","ServerCapabilitiesProvider","props","store","_$createComponent","Provider","value","children","useServerCapabilities","scoped","useContext","handle"],"mappings":";;;AAuGO,SAASA,gCAA+D;AAC7E,QAAM,CAACC,OAAOC,QAAQ,IAAIC,YAAmD;AAAA,IAAEC,MAAM;AAAA,EAAA,CAAM;AAE3F,SAAO;AAAA,IACLC,KAAMD,CAAAA,SAASF,SAAS,QAAQE,IAAI;AAAA,IACpCA,MAAMA,MAAMH,MAAMG;AAAAA,IAClBE,cAAcA,MAAAA;;AAAML,0BAAMG,SAANH,mBAAYK,iBAAgB;AAAA;AAAA,IAChDC,YAAYA,MAAAA;;AAAMN,0BAAMG,SAANH,mBAAYM,eAAc;AAAA;AAAA,IAC5CC,iBAAiBA,MAAAA;;AAAMP,0BAAMG,SAANH,mBAAYO,oBAAmB;AAAA;AAAA,IACtDC,eAAgBC,CAAAA;;AAAQC,sBAAQV,iBAAMG,SAANH,mBAAYK,iBAAZL,mBAA2BS,IAAI;AAAA;AAAA,EAAA;AAEnE;AAIA,MAAME,eAA8CZ,8BAAAA;AAkB7C,SAASa,sBAAsBT,MAAyC;AAC7EQ,eAAaP,IAAID,IAAI;AACvB;AAYO,MAAMU,4BAA4BC,cACvCC,MACF;AAcO,MAAMC,6BAERA,CAACC,UAAuB;AAC3B,QAAMC,QAAQD,MAAMC,SAASnB,8BAAAA;AAC7B,SAAAoB,gBACGN,0BAA0BO,UAAQ;AAAA,IAACC,OAAOH;AAAAA,IAAK,IAAAI,WAAA;AAAA,aAC7CL,MAAMK;AAAAA,IAAQ;AAAA,EAAA,CAAA;AAGrB;AAuBO,SAASC,wBAMd;AACA,QAAMC,SAASC,WAAWZ,yBAAyB;AACnD,QAAMa,SAASF,UAAUb;AACzB,SAAO;AAAA,IACLR,MAAMuB,OAAOvB;AAAAA,IACbE,cAAcqB,OAAOrB;AAAAA,IACrBC,YAAYoB,OAAOpB;AAAAA,IACnBC,iBAAiBmB,OAAOnB;AAAAA,IACxBC,eAAekB,OAAOlB;AAAAA,EAAAA;AAE1B;"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Recipe — Pseudo-elicit → spec elicit adapter
|
|
2
|
+
|
|
3
|
+
> **Audience** : consumer chat apps that talk to an MCP server still emitting
|
|
4
|
+
> a *legacy* "pseudo-elicit" payload inline with `tools/call` results, but
|
|
5
|
+
> wanting to use mcp-ui's spec-correct `<ChatPrompt>` / `<ElicitationForm>`
|
|
6
|
+
> (MCP 2025-06-18).
|
|
7
|
+
>
|
|
8
|
+
> **Where this code lives** : in YOUR consumer app, not in mcp-ui. mcp-ui
|
|
9
|
+
> stays tool- and server-agnostic by design — it ships the spec helper
|
|
10
|
+
> (`elicitationToPromptConfig`, `<ElicitationForm>`) but does NOT bake in
|
|
11
|
+
> any vendor-specific wire shape.
|
|
12
|
+
|
|
13
|
+
## Why this exists
|
|
14
|
+
|
|
15
|
+
The MCP spec 2025-06-18 defines elicitation as a server→client JSON-RPC
|
|
16
|
+
*request* (`elicitation/create`) carrying `{ message, requestedSchema }`,
|
|
17
|
+
with `requestedSchema` shaped as a JSON Schema object.
|
|
18
|
+
|
|
19
|
+
Some servers ship a different convention — they return an `elicitation`
|
|
20
|
+
**object inline** in the result of a `tools/call`, with a flat `fields[]`
|
|
21
|
+
array instead of a JSON Schema. Example (deposium_MCPs as of 2026-04) :
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"jsonrpc": "2.0",
|
|
26
|
+
"id": 1,
|
|
27
|
+
"result": {
|
|
28
|
+
"elicitation": {
|
|
29
|
+
"type": "form",
|
|
30
|
+
"title": "Required Parameters Missing",
|
|
31
|
+
"description": "Please provide the following required parameters:",
|
|
32
|
+
"fields": [
|
|
33
|
+
{ "name": "tenant_id", "type": "string", "label": "Tenant ID", "required": true, "default": "<uuid>" },
|
|
34
|
+
{ "name": "space_id", "type": "string", "label": "Space ID", "required": true, "default": "default" }
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Until that server migrates to spec elicitation, the chat app can adapt the
|
|
42
|
+
shape on the fly, then drive `<ElicitationForm>` (or
|
|
43
|
+
`elicitationToPromptConfig`) as if everything were spec.
|
|
44
|
+
|
|
45
|
+
## The adapter (drop-in TypeScript)
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import type { ElicitationEvent, ElicitationPropertySchema } from '@seed-ship/mcp-ui-solid'
|
|
49
|
+
|
|
50
|
+
interface PseudoElicit {
|
|
51
|
+
type: 'form'
|
|
52
|
+
title: string
|
|
53
|
+
description?: string
|
|
54
|
+
fields: Array<{
|
|
55
|
+
name: string
|
|
56
|
+
type: 'string' | 'number' | 'boolean'
|
|
57
|
+
label?: string
|
|
58
|
+
description?: string
|
|
59
|
+
required?: boolean
|
|
60
|
+
default?: unknown
|
|
61
|
+
enum?: Array<string | number>
|
|
62
|
+
}>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Convert a pseudo-elicit payload (legacy inline form spec) to a spec-shaped
|
|
67
|
+
* MCP `ElicitationEvent`. Returns `null` if the input does not look like a
|
|
68
|
+
* pseudo-elicit — the caller can then handle the tools/call result normally.
|
|
69
|
+
*/
|
|
70
|
+
export function pseudoElicitToSpec(toolResult: unknown): ElicitationEvent | null {
|
|
71
|
+
const pseudo = (toolResult as { elicitation?: PseudoElicit })?.elicitation
|
|
72
|
+
if (!pseudo || pseudo.type !== 'form' || !Array.isArray(pseudo.fields)) {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const properties: Record<string, ElicitationPropertySchema> = {}
|
|
77
|
+
const required: string[] = []
|
|
78
|
+
|
|
79
|
+
for (const field of pseudo.fields) {
|
|
80
|
+
const schema: ElicitationPropertySchema = {
|
|
81
|
+
type: mapType(field.type),
|
|
82
|
+
...(field.label !== undefined && { title: field.label }),
|
|
83
|
+
...(field.description !== undefined && { description: field.description }),
|
|
84
|
+
...(field.default !== undefined && { default: field.default }),
|
|
85
|
+
...(field.enum && { enum: field.enum }),
|
|
86
|
+
}
|
|
87
|
+
properties[field.name] = schema
|
|
88
|
+
if (field.required) required.push(field.name)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
message: [pseudo.title, pseudo.description].filter(Boolean).join(' — '),
|
|
93
|
+
requestedSchema: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties,
|
|
96
|
+
...(required.length > 0 && { required }),
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function mapType(t: string): ElicitationPropertySchema['type'] {
|
|
102
|
+
if (t === 'number' || t === 'boolean') return t
|
|
103
|
+
return 'string' // safe fallback for unknown legacy types
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Wiring it into the chat app
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { bus } from './your-bus-instance'
|
|
111
|
+
import { ElicitationForm } from '@seed-ship/mcp-ui-solid'
|
|
112
|
+
import { pseudoElicitToSpec } from './adapters/pseudo-elicit'
|
|
113
|
+
|
|
114
|
+
async function callTool(name: string, args: Record<string, unknown>) {
|
|
115
|
+
const response = await mcpClient.callTool(name, args)
|
|
116
|
+
|
|
117
|
+
// 1. Check for pseudo-elicit BEFORE treating result as a normal tool output.
|
|
118
|
+
const elicit = pseudoElicitToSpec(response.result)
|
|
119
|
+
if (elicit) {
|
|
120
|
+
showElicitationDialog(elicit, async (content) => {
|
|
121
|
+
// 2. Re-invoke the tool with the collected args merged in.
|
|
122
|
+
return callTool(name, { ...args, ...content })
|
|
123
|
+
})
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 3. Normal tool output path.
|
|
128
|
+
handleToolResult(response.result)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function showElicitationDialog(
|
|
132
|
+
event: ElicitationEvent,
|
|
133
|
+
onAccept: (content: Record<string, unknown>) => Promise<void>
|
|
134
|
+
) {
|
|
135
|
+
// Mount <ElicitationForm> in your modal layer, or pipe through the bus :
|
|
136
|
+
bus.events.emit('onElicitation', { streamKey: 'main', elicitation: event })
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
If you also want `<ElicitationForm>` to render directly :
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
<Show when={pendingElicit()}>
|
|
144
|
+
<ElicitationForm
|
|
145
|
+
event={pendingElicit()!}
|
|
146
|
+
onAccept={async (content) => {
|
|
147
|
+
setPendingElicit(null)
|
|
148
|
+
await retryToolCall(content)
|
|
149
|
+
}}
|
|
150
|
+
onCancel={() => setPendingElicit(null)}
|
|
151
|
+
/>
|
|
152
|
+
</Show>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Going both ways (spec + pseudo)
|
|
156
|
+
|
|
157
|
+
Once your server migrates to real `elicitation/create` (server→client
|
|
158
|
+
JSON-RPC request over a bidirectional transport), keep the adapter
|
|
159
|
+
in place — it's harmless on a normal `tools/call` result (returns
|
|
160
|
+
`null`) and lets you support both wire shapes for the duration of the
|
|
161
|
+
rollout.
|
|
162
|
+
|
|
163
|
+
For the spec path, your transport adapter handles the JSON-RPC request
|
|
164
|
+
directly and emits the same `onElicitation` event with a payload that
|
|
165
|
+
already matches `ElicitationEvent` — no adapter call needed.
|
|
166
|
+
|
|
167
|
+
## Reference
|
|
168
|
+
|
|
169
|
+
- MCP spec : https://spec.modelcontextprotocol.io/specification/2025-06-18/client/elicitation/
|
|
170
|
+
- mcp-ui types : `ElicitationEvent`, `ElicitationRequestedSchema`, `ElicitationPropertySchema` (all exported from `@seed-ship/mcp-ui-solid`)
|
|
171
|
+
- mcp-ui helpers : `elicitationToPromptConfig` (services/chat-bus), `<ElicitationForm>` (components)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Recipe — Wire `<FeedbackInline>` to a feedback HTTP endpoint
|
|
2
|
+
|
|
3
|
+
> **Audience** : consumer apps that ship `<FeedbackInline>` (per-message
|
|
4
|
+
> thumbs-up/down) and want to persist ratings to a backend.
|
|
5
|
+
>
|
|
6
|
+
> mcp-ui's `<FeedbackInline>` is intentionally endpoint-agnostic — it
|
|
7
|
+
> calls `onSubmit(rating, context)` and the consumer owns the HTTP / store
|
|
8
|
+
> wiring. This recipe shows the most common pattern, using the Deposium
|
|
9
|
+
> `POST /api/feedback` endpoint as a concrete example.
|
|
10
|
+
|
|
11
|
+
## What `<FeedbackInline>` gives you
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
<FeedbackInline
|
|
15
|
+
messageHash={msg.hash}
|
|
16
|
+
context={{ intent: msg.intent, confidenceBand: msg.band }}
|
|
17
|
+
onSubmit={(rating, context) => persistFeedback(rating, context)}
|
|
18
|
+
/>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The component :
|
|
22
|
+
- Renders two buttons (positive / negative).
|
|
23
|
+
- Flips to "submitted" optimistically on click — UI does NOT revert on network error (best-effort design).
|
|
24
|
+
- Calls `onSubmit('positive' | 'negative', context?)` exactly once.
|
|
25
|
+
|
|
26
|
+
`rating` already matches the shape Deposium expects. Mapping is direct.
|
|
27
|
+
|
|
28
|
+
## Endpoint reference (Deposium)
|
|
29
|
+
|
|
30
|
+
`POST /api/feedback` — no auth required, behind the chat-stream / standard
|
|
31
|
+
middleware chain.
|
|
32
|
+
|
|
33
|
+
### Request body
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
interface FeedbackRequest {
|
|
37
|
+
message_hash: string // REQUIRED — message ID being rated
|
|
38
|
+
rating: 'positive' | 'negative' | 'partial'
|
|
39
|
+
confidence_band?: 'high' | 'medium' | 'low' // optional, free-form string
|
|
40
|
+
intent?: string // optional, e.g. 'search_query'
|
|
41
|
+
space_ids?: string[] | string | null
|
|
42
|
+
comment?: string
|
|
43
|
+
tenant_id?: string
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Response
|
|
48
|
+
|
|
49
|
+
| Status | Body |
|
|
50
|
+
|---|---|
|
|
51
|
+
| 200 | `{ ok: true, id: 'fb_<timestamp>_<rand4>' }` |
|
|
52
|
+
| 400 | `{ error: 'rating must be one of: positive, negative, partial' }` |
|
|
53
|
+
|
|
54
|
+
### Side effects (worth knowing)
|
|
55
|
+
|
|
56
|
+
- `INSERT` into `logs.feedback` (PostgreSQL) — drives dashboard analytics.
|
|
57
|
+
- `'positive' | 'negative'` ratings also update
|
|
58
|
+
`logs.intent_classifications.feedback_success`. `'partial'` does **not**
|
|
59
|
+
propagate (intentional — neither true nor false).
|
|
60
|
+
|
|
61
|
+
## Wiring (the recipe)
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { FeedbackInline, type FeedbackInlineContext } from '@seed-ship/mcp-ui-solid'
|
|
65
|
+
|
|
66
|
+
function persistFeedback(
|
|
67
|
+
messageHash: string,
|
|
68
|
+
rating: 'positive' | 'negative',
|
|
69
|
+
ctx?: FeedbackInlineContext
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
return fetch('/api/feedback', {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
message_hash: messageHash,
|
|
76
|
+
rating, // 'positive' | 'negative' — matches endpoint as-is
|
|
77
|
+
...(ctx?.intent && { intent: ctx.intent }),
|
|
78
|
+
...(ctx?.confidenceBand && { confidence_band: ctx.confidenceBand }),
|
|
79
|
+
...(ctx?.tenantId && { tenant_id: ctx.tenantId }),
|
|
80
|
+
...(ctx?.spaceIds && { space_ids: ctx.spaceIds }),
|
|
81
|
+
...(ctx?.comment && { comment: ctx.comment }),
|
|
82
|
+
}),
|
|
83
|
+
}).then(async (res) => {
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
console.warn('[feedback] persist failed', res.status, await res.text())
|
|
86
|
+
}
|
|
87
|
+
}).catch((err) => {
|
|
88
|
+
// Silent failure — UI is already in the optimistic "submitted" state.
|
|
89
|
+
console.warn('[feedback] network error', err)
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function MessageRow(props: { msg: ChatMessage }) {
|
|
94
|
+
return (
|
|
95
|
+
<div class="message-row">
|
|
96
|
+
<p>{props.msg.text}</p>
|
|
97
|
+
<FeedbackInline
|
|
98
|
+
messageHash={props.msg.hash}
|
|
99
|
+
context={{
|
|
100
|
+
intent: props.msg.intent,
|
|
101
|
+
confidenceBand: props.msg.confidenceBand,
|
|
102
|
+
}}
|
|
103
|
+
onSubmit={(rating, ctx) => persistFeedback(props.msg.hash, rating, ctx)}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Variations
|
|
111
|
+
|
|
112
|
+
### "Partial" rating
|
|
113
|
+
|
|
114
|
+
`<FeedbackInline>` emits only `'positive'` / `'negative'`. If you need a
|
|
115
|
+
third state (`'partial'`), build a separate UI (e.g. a star rating or a
|
|
116
|
+
3-button row) and call the endpoint directly with `rating: 'partial'`.
|
|
117
|
+
|
|
118
|
+
### Free-text comment
|
|
119
|
+
|
|
120
|
+
Add a textarea below `<FeedbackInline>` that opens after the rating click.
|
|
121
|
+
Send a follow-up `POST /api/feedback` with the same `message_hash` and a
|
|
122
|
+
`comment` field — the endpoint accepts multiple records per message.
|
|
123
|
+
|
|
124
|
+
### Optimistic vs strict semantics
|
|
125
|
+
|
|
126
|
+
Default behavior is best-effort (UI never reverts). If you need stricter
|
|
127
|
+
semantics — offline retry queue, edit-rating UX — wrap `<FeedbackInline>`
|
|
128
|
+
in your own component and own the state externally instead of relying on
|
|
129
|
+
the component's internal flip.
|
|
130
|
+
|
|
131
|
+
## Where this code lives
|
|
132
|
+
|
|
133
|
+
In your consumer app. mcp-ui ships `<FeedbackInline>` and the `onSubmit`
|
|
134
|
+
contract; the HTTP wiring (URL, auth, retry policy, schema mapping) is
|
|
135
|
+
the consumer's responsibility by design — same pattern as
|
|
136
|
+
`pseudo-elicit-spec-adapter`.
|
|
137
|
+
|
|
138
|
+
## Reference
|
|
139
|
+
|
|
140
|
+
- mcp-ui component : `<FeedbackInline>` (exported from `@seed-ship/mcp-ui-solid`)
|
|
141
|
+
- mcp-ui types : `FeedbackInlineProps`, `FeedbackInlineContext`
|
|
142
|
+
- Deposium endpoint : `POST /api/feedback` — see deposium_MCPs `src/routes/feedback.ts`
|