@seed-ship/mcp-ui-solid 5.1.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +64 -13
  3. package/dist/components/ElicitationForm.cjs +51 -0
  4. package/dist/components/ElicitationForm.cjs.map +1 -0
  5. package/dist/components/ElicitationForm.d.ts +68 -0
  6. package/dist/components/ElicitationForm.d.ts.map +1 -0
  7. package/dist/components/ElicitationForm.js +51 -0
  8. package/dist/components/ElicitationForm.js.map +1 -0
  9. package/dist/components/FeedbackInline.cjs +57 -0
  10. package/dist/components/FeedbackInline.cjs.map +1 -0
  11. package/dist/components/FeedbackInline.d.ts +71 -0
  12. package/dist/components/FeedbackInline.d.ts.map +1 -0
  13. package/dist/components/FeedbackInline.js +57 -0
  14. package/dist/components/FeedbackInline.js.map +1 -0
  15. package/dist/components/index.d.ts +2 -0
  16. package/dist/components/index.d.ts.map +1 -1
  17. package/dist/components.cjs +2 -0
  18. package/dist/components.cjs.map +1 -1
  19. package/dist/components.d.cts +2 -0
  20. package/dist/components.d.ts +2 -0
  21. package/dist/components.js +2 -0
  22. package/dist/components.js.map +1 -1
  23. package/dist/index.cjs +17 -0
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +12 -2
  26. package/dist/index.d.ts +12 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +19 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/services/chat-bus.cjs +71 -0
  31. package/dist/services/chat-bus.cjs.map +1 -1
  32. package/dist/services/chat-bus.d.ts +31 -1
  33. package/dist/services/chat-bus.d.ts.map +1 -1
  34. package/dist/services/chat-bus.js +71 -0
  35. package/dist/services/chat-bus.js.map +1 -1
  36. package/dist/services/chat-prompt-controller.cjs +83 -0
  37. package/dist/services/chat-prompt-controller.cjs.map +1 -0
  38. package/dist/services/chat-prompt-controller.d.ts +93 -0
  39. package/dist/services/chat-prompt-controller.d.ts.map +1 -0
  40. package/dist/services/chat-prompt-controller.js +83 -0
  41. package/dist/services/chat-prompt-controller.js.map +1 -0
  42. package/dist/stores/scratchpad-store.cjs +105 -77
  43. package/dist/stores/scratchpad-store.cjs.map +1 -1
  44. package/dist/stores/scratchpad-store.d.ts +88 -19
  45. package/dist/stores/scratchpad-store.d.ts.map +1 -1
  46. package/dist/stores/scratchpad-store.js +105 -77
  47. package/dist/stores/scratchpad-store.js.map +1 -1
  48. package/dist/stores/server-capabilities-store.cjs +61 -0
  49. package/dist/stores/server-capabilities-store.cjs.map +1 -0
  50. package/dist/stores/server-capabilities-store.d.ts +172 -0
  51. package/dist/stores/server-capabilities-store.d.ts.map +1 -0
  52. package/dist/stores/server-capabilities-store.js +61 -0
  53. package/dist/stores/server-capabilities-store.js.map +1 -0
  54. package/dist/types/chat-bus.d.ts +39 -0
  55. package/dist/types/chat-bus.d.ts.map +1 -1
  56. package/docs/recipes/elicitation-pseudo-spec-adapter.md +171 -0
  57. package/docs/recipes/feedback-inline-wiring.md +142 -0
  58. package/package.json +1 -1
  59. package/src/components/ElicitationForm.test.tsx +197 -0
  60. package/src/components/ElicitationForm.tsx +126 -0
  61. package/src/components/FeedbackInline.test.tsx +117 -0
  62. package/src/components/FeedbackInline.tsx +143 -0
  63. package/src/components/index.ts +4 -0
  64. package/src/index.ts +39 -1
  65. package/src/services/chat-bus.test.ts +154 -2
  66. package/src/services/chat-bus.ts +115 -0
  67. package/src/services/chat-prompt-controller.test.ts +144 -0
  68. package/src/services/chat-prompt-controller.ts +214 -0
  69. package/src/stores/scratchpad-store.test.tsx +140 -0
  70. package/src/stores/scratchpad-store.tsx +244 -0
  71. package/src/stores/server-capabilities-store.test.tsx +206 -0
  72. package/src/stores/server-capabilities-store.tsx +215 -0
  73. package/src/types/chat-bus.ts +40 -0
  74. package/tsconfig.tsbuildinfo +1 -1
  75. package/src/stores/scratchpad-store.ts +0 -126
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Tests for server-capabilities-store — v5.3.0
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest'
6
+ import { render, cleanup } from '@solidjs/testing-library'
7
+ import { createEffect, createRoot } from 'solid-js'
8
+ import {
9
+ createServerCapabilitiesStore,
10
+ setServerCapabilities,
11
+ useServerCapabilities,
12
+ ServerCapabilitiesProvider,
13
+ } from './server-capabilities-store'
14
+ import type { ServerInitializeInfo } from './server-capabilities-store'
15
+
16
+ const sampleInfo: ServerInitializeInfo = {
17
+ protocolVersion: '2025-06-18',
18
+ serverInfo: { name: 'deposium-mcp', version: '1.4.2' },
19
+ capabilities: {
20
+ tools: { listChanged: true },
21
+ prompts: { listChanged: false },
22
+ resources: { listChanged: true, subscribe: false },
23
+ },
24
+ instructions: 'Use deposium_chat for synthesis questions.',
25
+ }
26
+
27
+ describe('createServerCapabilitiesStore — v5.3.0', () => {
28
+ beforeEach(() => {
29
+ cleanup()
30
+ setServerCapabilities(null) // reset module singleton between tests
31
+ })
32
+
33
+ it('two factory stores do not share state', () => {
34
+ const storeA = createServerCapabilitiesStore()
35
+ const storeB = createServerCapabilitiesStore()
36
+
37
+ storeA.set(sampleInfo)
38
+
39
+ expect(storeA.info()?.serverInfo.name).toBe('deposium-mcp')
40
+ expect(storeB.info()).toBeNull()
41
+ })
42
+
43
+ it('accessors return null until set is called', () => {
44
+ const store = createServerCapabilitiesStore()
45
+
46
+ expect(store.info()).toBeNull()
47
+ expect(store.capabilities()).toBeNull()
48
+ expect(store.serverInfo()).toBeNull()
49
+ expect(store.protocolVersion()).toBeNull()
50
+ expect(store.hasCapability('tools')).toBe(false)
51
+ })
52
+
53
+ it('set(info) populates all derived accessors', () => {
54
+ const store = createServerCapabilitiesStore()
55
+ store.set(sampleInfo)
56
+
57
+ expect(store.protocolVersion()).toBe('2025-06-18')
58
+ expect(store.serverInfo()?.version).toBe('1.4.2')
59
+ expect(store.capabilities()?.tools?.listChanged).toBe(true)
60
+ })
61
+
62
+ it('set(null) clears the store', () => {
63
+ const store = createServerCapabilitiesStore()
64
+ store.set(sampleInfo)
65
+ expect(store.info()).not.toBeNull()
66
+
67
+ store.set(null)
68
+ expect(store.info()).toBeNull()
69
+ expect(store.hasCapability('tools')).toBe(false)
70
+ })
71
+
72
+ it('hasCapability returns true for present keys, false for absent', () => {
73
+ const store = createServerCapabilitiesStore()
74
+ store.set(sampleInfo)
75
+
76
+ expect(store.hasCapability('tools')).toBe(true)
77
+ expect(store.hasCapability('prompts')).toBe(true)
78
+ expect(store.hasCapability('resources')).toBe(true)
79
+ expect(store.hasCapability('logging')).toBe(false)
80
+ expect(store.hasCapability('completions')).toBe(false)
81
+ expect(store.hasCapability('experimental')).toBe(false)
82
+ })
83
+ })
84
+
85
+ describe('setServerCapabilities + useServerCapabilities (singleton path)', () => {
86
+ beforeEach(() => {
87
+ cleanup()
88
+ setServerCapabilities(null)
89
+ })
90
+
91
+ it('useServerCapabilities falls back to module singleton outside provider', () => {
92
+ setServerCapabilities(sampleInfo)
93
+
94
+ let captured: ReturnType<typeof useServerCapabilities> | null = null
95
+ const Probe = () => {
96
+ captured = useServerCapabilities()
97
+ return null
98
+ }
99
+ render(() => <Probe />)
100
+
101
+ expect(captured).not.toBeNull()
102
+ expect(captured!.serverInfo()?.name).toBe('deposium-mcp')
103
+ expect(captured!.hasCapability('tools')).toBe(true)
104
+ })
105
+
106
+ it('singleton state survives across renders', () => {
107
+ setServerCapabilities(sampleInfo)
108
+
109
+ const captures: Array<string | undefined> = []
110
+ const Probe = () => {
111
+ const { serverInfo } = useServerCapabilities()
112
+ captures.push(serverInfo()?.name)
113
+ return null
114
+ }
115
+ render(() => <Probe />)
116
+ render(() => <Probe />)
117
+
118
+ expect(captures).toEqual(['deposium-mcp', 'deposium-mcp'])
119
+ })
120
+ })
121
+
122
+ describe('ServerCapabilitiesProvider scoping', () => {
123
+ beforeEach(() => {
124
+ cleanup()
125
+ setServerCapabilities(null)
126
+ })
127
+
128
+ it('provider with explicit store overrides the singleton for descendants', () => {
129
+ setServerCapabilities(sampleInfo) // singleton has deposium-mcp
130
+
131
+ const scopedStore = createServerCapabilitiesStore()
132
+ scopedStore.set({
133
+ ...sampleInfo,
134
+ serverInfo: { name: 'other-mcp', version: '0.1.0' },
135
+ })
136
+
137
+ let inside: string | undefined
138
+ let outside: string | undefined
139
+
140
+ const Inside = () => {
141
+ inside = useServerCapabilities().serverInfo()?.name
142
+ return null
143
+ }
144
+ const Outside = () => {
145
+ outside = useServerCapabilities().serverInfo()?.name
146
+ return null
147
+ }
148
+
149
+ render(() => (
150
+ <>
151
+ <Outside />
152
+ <ServerCapabilitiesProvider store={scopedStore}>
153
+ <Inside />
154
+ </ServerCapabilitiesProvider>
155
+ </>
156
+ ))
157
+
158
+ expect(outside).toBe('deposium-mcp')
159
+ expect(inside).toBe('other-mcp')
160
+ })
161
+
162
+ it('provider without store creates a fresh isolated handle', () => {
163
+ setServerCapabilities(sampleInfo)
164
+
165
+ let inside: ReturnType<typeof useServerCapabilities> | null = null
166
+ const Inside = () => {
167
+ inside = useServerCapabilities()
168
+ return null
169
+ }
170
+ render(() => (
171
+ <ServerCapabilitiesProvider>
172
+ <Inside />
173
+ </ServerCapabilitiesProvider>
174
+ ))
175
+
176
+ expect(inside).not.toBeNull()
177
+ expect(inside!.info()).toBeNull() // fresh, not the singleton's state
178
+ })
179
+
180
+ it('set() updates trigger reactive effects', () => {
181
+ const store = createServerCapabilitiesStore()
182
+ const captures: Array<string | null> = []
183
+
184
+ let dispose: (() => void) | undefined
185
+ createRoot((d) => {
186
+ dispose = d
187
+ createEffect(() => {
188
+ captures.push(store.serverInfo()?.name ?? null)
189
+ })
190
+ })
191
+
192
+ // Initial effect run
193
+ expect(captures).toEqual([null])
194
+
195
+ store.set(sampleInfo)
196
+ expect(captures).toEqual([null, 'deposium-mcp'])
197
+
198
+ store.set({ ...sampleInfo, serverInfo: { name: 'renamed', version: '2.0.0' } })
199
+ expect(captures).toEqual([null, 'deposium-mcp', 'renamed'])
200
+
201
+ store.set(null)
202
+ expect(captures).toEqual([null, 'deposium-mcp', 'renamed', null])
203
+
204
+ dispose?.()
205
+ })
206
+ })
@@ -0,0 +1,215 @@
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
+
42
+ import { createContext, useContext, type ParentComponent, type JSX } from 'solid-js'
43
+ import { createStore } from 'solid-js/store'
44
+
45
+ // ─── Types ────────────────────────────────────────────────────
46
+
47
+ /**
48
+ * Server capabilities object as advertised in the MCP `initialize` response.
49
+ * Mirrors the spec 2025-06-18 `ServerCapabilities` shape with permissive
50
+ * `experimental` for forward compatibility.
51
+ */
52
+ export interface ServerCapabilities {
53
+ experimental?: Record<string, unknown>
54
+ logging?: Record<string, never>
55
+ tools?: { listChanged?: boolean }
56
+ prompts?: { listChanged?: boolean }
57
+ resources?: { listChanged?: boolean; subscribe?: boolean }
58
+ completions?: Record<string, never>
59
+ }
60
+
61
+ /**
62
+ * Subset of the MCP `initialize` response relevant to the UI layer.
63
+ * Consumers may extend this via the `experimental` field.
64
+ */
65
+ export interface ServerInitializeInfo {
66
+ protocolVersion: string
67
+ serverInfo: { name: string; version: string; title?: string; [key: string]: unknown }
68
+ capabilities: ServerCapabilities
69
+ instructions?: string
70
+ }
71
+
72
+ // ─── Handle ───────────────────────────────────────────────────
73
+
74
+ export interface ServerCapabilitiesStoreHandle {
75
+ /** Push a fresh `initialize` snapshot into the store, or clear with `null`. */
76
+ set: (info: ServerInitializeInfo | null) => void
77
+ /** Reactive accessor for the full info (null when no initialize received). */
78
+ info: () => ServerInitializeInfo | null
79
+ /** Reactive accessor for just the `capabilities` field. */
80
+ capabilities: () => ServerCapabilities | null
81
+ /** Reactive accessor for just the `serverInfo` field. */
82
+ serverInfo: () => ServerInitializeInfo['serverInfo'] | null
83
+ /** Reactive accessor for the protocol version string. */
84
+ protocolVersion: () => string | null
85
+ /**
86
+ * Helper : returns true if the server advertised the named capability key
87
+ * with a truthy value (i.e. the key is present, even as an empty object).
88
+ */
89
+ hasCapability: (key: keyof ServerCapabilities) => boolean
90
+ }
91
+
92
+ // ─── Factory ──────────────────────────────────────────────────
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 function createServerCapabilitiesStore(): ServerCapabilitiesStoreHandle {
105
+ const [state, setState] = createStore<{ info: ServerInitializeInfo | null }>({ info: null })
106
+
107
+ return {
108
+ set: (info) => setState('info', info),
109
+ info: () => state.info,
110
+ capabilities: () => state.info?.capabilities ?? null,
111
+ serverInfo: () => state.info?.serverInfo ?? null,
112
+ protocolVersion: () => state.info?.protocolVersion ?? null,
113
+ hasCapability: (key) => Boolean(state.info?.capabilities?.[key]),
114
+ }
115
+ }
116
+
117
+ // ─── Module-level singleton ───────────────────────────────────
118
+
119
+ const defaultStore: ServerCapabilitiesStoreHandle = createServerCapabilitiesStore()
120
+
121
+ /**
122
+ * Push the parsed MCP `initialize` response into the module-level singleton
123
+ * store. Pass `null` to clear (e.g. on disconnect / server change).
124
+ *
125
+ * @experimental
126
+ * @since v5.3.0
127
+ *
128
+ * @example
129
+ * // In your transport adapter, after receiving the initialize response :
130
+ * setServerCapabilities({
131
+ * protocolVersion: response.result.protocolVersion,
132
+ * serverInfo: response.result.serverInfo,
133
+ * capabilities: response.result.capabilities,
134
+ * instructions: response.result.instructions,
135
+ * })
136
+ */
137
+ export function setServerCapabilities(info: ServerInitializeInfo | null): void {
138
+ defaultStore.set(info)
139
+ }
140
+
141
+ // ─── Context ──────────────────────────────────────────────────
142
+
143
+ /**
144
+ * Context for a scoped server-capabilities store. Populated by
145
+ * `<ServerCapabilitiesProvider>`. Read by `useServerCapabilities()` with
146
+ * automatic fallback to the module-level singleton when absent.
147
+ *
148
+ * @experimental
149
+ * @since v5.3.0
150
+ */
151
+ export const ServerCapabilitiesContext = createContext<ServerCapabilitiesStoreHandle | undefined>(
152
+ undefined
153
+ )
154
+
155
+ /**
156
+ * Provide a scoped `ServerCapabilitiesStoreHandle` to a SolidJS subtree.
157
+ * Children calling `useServerCapabilities()` bind to this store instead of
158
+ * the module singleton.
159
+ *
160
+ * If no `store` prop is passed, a fresh store is created for the provider's
161
+ * lifetime. Pass `store` explicitly when you need the handle outside the
162
+ * tree (e.g. in a transport adapter living at the app root).
163
+ *
164
+ * @experimental
165
+ * @since v5.3.0
166
+ */
167
+ export const ServerCapabilitiesProvider: ParentComponent<{
168
+ store?: ServerCapabilitiesStoreHandle
169
+ }> = (props): JSX.Element => {
170
+ const store = props.store ?? createServerCapabilitiesStore()
171
+ return (
172
+ <ServerCapabilitiesContext.Provider value={store}>
173
+ {props.children}
174
+ </ServerCapabilitiesContext.Provider>
175
+ )
176
+ }
177
+
178
+ // ─── Reactive hook ────────────────────────────────────────────
179
+
180
+ /**
181
+ * Hook for components — reads the server capabilities reactively.
182
+ *
183
+ * If called inside a `<ServerCapabilitiesProvider>`, reads the scoped
184
+ * handle; otherwise falls back to the module singleton.
185
+ *
186
+ * @experimental
187
+ * @since v5.3.0
188
+ *
189
+ * @example
190
+ * const { capabilities, serverInfo, hasCapability } = useServerCapabilities()
191
+ *
192
+ * <Show when={capabilities()}>
193
+ * <p>Connected to {serverInfo()?.name} v{serverInfo()?.version}</p>
194
+ * <Show when={hasCapability('tools')}>
195
+ * <ToolPalette />
196
+ * </Show>
197
+ * </Show>
198
+ */
199
+ export function useServerCapabilities(): {
200
+ info: () => ServerInitializeInfo | null
201
+ capabilities: () => ServerCapabilities | null
202
+ serverInfo: () => ServerInitializeInfo['serverInfo'] | null
203
+ protocolVersion: () => string | null
204
+ hasCapability: (key: keyof ServerCapabilities) => boolean
205
+ } {
206
+ const scoped = useContext(ServerCapabilitiesContext)
207
+ const handle = scoped ?? defaultStore
208
+ return {
209
+ info: handle.info,
210
+ capabilities: handle.capabilities,
211
+ serverInfo: handle.serverInfo,
212
+ protocolVersion: handle.protocolVersion,
213
+ hasCapability: handle.hasCapability,
214
+ }
215
+ }
@@ -53,6 +53,7 @@ export interface ChatEvents {
53
53
  // --- Interactions ---
54
54
  onChatPromptResponse: (event: ChatEventBase & { response: ChatPromptResponse }) => void
55
55
  onClarificationNeeded: (event: ChatEventBase & { clarification: ClarificationEvent }) => void
56
+ onElicitation: (event: ChatEventBase & { elicitation: ElicitationEvent }) => void
56
57
 
57
58
  // --- Agentic (handled by app, not MCP-UI) ---
58
59
  onAgentSwitch: (event: ChatEventBase & { agent: AgentContext }) => void
@@ -568,6 +569,45 @@ export interface ToolCallEvent {
568
569
  duration_ms?: number
569
570
  }
570
571
 
572
+ /**
573
+ * MCP `elicitation/create` request payload — server asks the client to
574
+ * collect input from the user according to a JSON Schema.
575
+ *
576
+ * Derived from MCP spec 2025-06-18. See
577
+ * `elicitationToPromptConfig()` in `services/chat-bus.ts` for the
578
+ * helper that converts this to a `ChatPromptConfig`.
579
+ *
580
+ * @experimental
581
+ * @since v5.2.0
582
+ */
583
+ export interface ElicitationEvent {
584
+ /** Question / instruction to present to the user. */
585
+ message: string
586
+ /** JSON Schema describing the expected response shape. Object with primitive properties only. */
587
+ requestedSchema: ElicitationRequestedSchema
588
+ }
589
+
590
+ export interface ElicitationRequestedSchema {
591
+ type: 'object'
592
+ properties: Record<string, ElicitationPropertySchema>
593
+ required?: string[]
594
+ }
595
+
596
+ export interface ElicitationPropertySchema {
597
+ type: 'string' | 'number' | 'integer' | 'boolean'
598
+ title?: string
599
+ description?: string
600
+ /** Enum of allowed values (strings or numbers). */
601
+ enum?: Array<string | number>
602
+ /** Parallel array with display labels for each enum entry. */
603
+ enumNames?: string[]
604
+ default?: unknown
605
+ minimum?: number
606
+ maximum?: number
607
+ /** String format hint — date, date-time, email, uri. */
608
+ format?: 'date' | 'date-time' | 'email' | 'uri'
609
+ }
610
+
571
611
  export interface ClarificationEvent {
572
612
  /** The question to ask the user */
573
613
  question: string