mcp-server-value-picker 1.0.2 → 1.0.4

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 (3) hide show
  1. package/README.md +450 -0
  2. package/dist/index.js +40 -10
  3. package/package.json +2 -2
package/README.md ADDED
@@ -0,0 +1,450 @@
1
+ # Value Picker Server
2
+
3
+ A **debug/test MCP server** for validating that values selected in an MCP App UI are correctly passed to the AI model via `ui/update-model-context`. This package tests the complete View ↔ Host ↔ Model communication flow defined in [SEP-1865](../../specification/2026-01-26/apps.mdx).
4
+
5
+ The tool explicitly tells the AI model that this is a test — the model should simply confirm which value it received, not analyze or elaborate on the selection.
6
+
7
+ ## Purpose
8
+
9
+ This example validates MCP Apps protocol features across all three layers:
10
+
11
+ - **Server**: Tool registration, UI resource serving, `structuredContent` data passing
12
+ - **Host**: Context injection, message proxying, lifecycle notifications
13
+ - **View**: Theme handling, safe areas, tool result rendering, model context updates
14
+
15
+ ---
16
+
17
+ ## Server-Side Features Tested
18
+
19
+ ### Tool Registration with UI Metadata
20
+
21
+ Uses `registerAppTool()` to associate a tool with a UI resource via `_meta.ui.resourceUri`:
22
+
23
+ ```typescript
24
+ // From server.ts
25
+ registerAppTool(server, "pick_value", {
26
+ title: "Pick a Value",
27
+ description: "DEBUG/TEST TOOL: Tests MCP Apps communication between UI and model. The user picks a value in the UI, and you must confirm whether you received it. This validates that ui/update-model-context is working correctly. Do not treat this as a real decision — just report what value you received.",
28
+ inputSchema: {},
29
+ outputSchema: z.object({
30
+ values: z.array(z.object({
31
+ id: z.string(),
32
+ label: z.string(),
33
+ description: z.string(),
34
+ })),
35
+ }),
36
+ _meta: { ui: { resourceUri: "ui://pick-value/mcp-app.html" } },
37
+ }, async () => { /* handler */ });
38
+ ```
39
+
40
+ **Spec Reference** ([Resource Discovery](../../specification/2026-01-26/apps.mdx#resource-discovery)):
41
+ ```typescript
42
+ interface Tool {
43
+ _meta?: {
44
+ ui?: {
45
+ resourceUri?: string; // URI of UI resource for rendering
46
+ visibility?: Array<"model" | "app">;
47
+ };
48
+ };
49
+ }
50
+ ```
51
+
52
+ ### UI Resource Registration
53
+
54
+ Uses `registerAppResource()` to serve HTML content with the required MIME type:
55
+
56
+ ```typescript
57
+ // From server.ts
58
+ registerAppResource(server, resourceUri, resourceUri,
59
+ { mimeType: RESOURCE_MIME_TYPE }, // "text/html;profile=mcp-app"
60
+ async () => ({
61
+ contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }],
62
+ })
63
+ );
64
+ ```
65
+
66
+ **Spec Reference** ([UI Resource Format](../../specification/2026-01-26/apps.mdx#ui-resource-format)):
67
+ > `mimeType` MUST be `text/html;profile=mcp-app`
68
+
69
+ ### Dual Content Model (`content` + `structuredContent`)
70
+
71
+ Tool returns both model-facing text and UI-facing structured data:
72
+
73
+ ```typescript
74
+ // From server.ts
75
+ return {
76
+ content: [{
77
+ type: "text",
78
+ text: `[MCP Apps Test] This is a debug tool for testing value communication...
79
+ Your job: Simply report back the value you received. This tests whether the MCP Apps context injection is working. Do not provide detailed analysis of the values — just confirm what was selected.`,
80
+ }],
81
+ structuredContent: {
82
+ values: VALUES, // Array of {id, label, description}
83
+ instruction: "Wait for the user to select a value via the UI.",
84
+ },
85
+ };
86
+ ```
87
+
88
+ **Spec Reference** ([Data Passing](../../specification/2026-01-26/apps.mdx#data-passing)):
89
+ > - `content`: Text representation for model context and text-only hosts
90
+ > - `structuredContent`: Structured data optimized for UI rendering (not added to model context)
91
+
92
+ ### Transport Modes
93
+
94
+ Supports both STDIO and Streamable HTTP transports with auto-detection:
95
+
96
+ ```typescript
97
+ // From main.ts
98
+ function resolveTransport(): "stdio" | "http" {
99
+ if (process.argv.includes("--stdio")) return "stdio";
100
+ if (process.argv.includes("--http")) return "http";
101
+ return process.stdin.isTTY ? "http" : "stdio";
102
+ }
103
+ ```
104
+
105
+ ---
106
+
107
+ ## View-Side Features Tested
108
+
109
+ ### App Initialization & Connection
110
+
111
+ Uses the `App` class with `PostMessageTransport` to connect to the Host:
112
+
113
+ ```typescript
114
+ // From src/mcp-app.ts
115
+ const app = new App({ name: "Value Picker", version: "1.0.0" });
116
+ await app.connect();
117
+ ```
118
+
119
+ **Spec Reference** ([Transport Layer](../../specification/2026-01-26/apps.mdx#transport-layer)):
120
+ ```typescript
121
+ const transport = new MessageTransport(window.parent);
122
+ const client = new Client({ name: "ui-view", version: "1.0.0" });
123
+ await client.connect(transport);
124
+ ```
125
+
126
+ ### Host Context Handling
127
+
128
+ Retrieves and responds to host context after connection:
129
+
130
+ ```typescript
131
+ // From src/mcp-app.ts
132
+ app.connect().then(() => {
133
+ const ctx = app.getHostContext();
134
+ if (ctx) handleHostContextChanged(ctx);
135
+ });
136
+
137
+ app.onhostcontextchanged = handleHostContextChanged;
138
+ ```
139
+
140
+ **Spec Reference** ([Host Context in McpUiInitializeResult](../../specification/2026-01-26/apps.mdx#host-context-in-mcpuiinitializeresult)):
141
+ ```typescript
142
+ interface HostContext {
143
+ theme?: "light" | "dark";
144
+ styles?: { variables?: Record<string, string>; css?: { fonts?: string } };
145
+ safeAreaInsets?: { top: number; right: number; bottom: number; left: number };
146
+ // ... other fields
147
+ }
148
+ ```
149
+
150
+ ### Theme Application
151
+
152
+ Applies the host's color scheme preference:
153
+
154
+ ```typescript
155
+ // From src/mcp-app.ts
156
+ if (ctx.theme) {
157
+ applyDocumentTheme(ctx.theme);
158
+ }
159
+ ```
160
+
161
+ **Spec Reference** ([Theming](../../specification/2026-01-26/apps.mdx#theming)):
162
+ > Views can use the `applyDocumentTheme` utility to easily respond to Host Context `theme` changes
163
+
164
+ ### CSS Variable Injection
165
+
166
+ Applies host-provided CSS custom properties for visual cohesion:
167
+
168
+ ```typescript
169
+ // From src/mcp-app.ts
170
+ if (ctx.styles?.variables) {
171
+ applyHostStyleVariables(ctx.styles.variables);
172
+ }
173
+ ```
174
+
175
+ **Spec Reference** ([Theming - Current Standardized Variables](../../specification/2026-01-26/apps.mdx#current-standardized-variables)):
176
+ ```typescript
177
+ type McpUiStyleVariableKey =
178
+ | "--color-background-primary"
179
+ | "--color-text-primary"
180
+ | "--font-sans"
181
+ // ... 80+ standardized variables
182
+ ```
183
+
184
+ ### Custom Font Loading
185
+
186
+ Injects host-provided font CSS (e.g., `@font-face` or `@import`):
187
+
188
+ ```typescript
189
+ // From src/mcp-app.ts
190
+ if (ctx.styles?.css?.fonts) {
191
+ applyHostFonts(ctx.styles.css.fonts);
192
+ }
193
+ ```
194
+
195
+ **Spec Reference** ([Custom Fonts](../../specification/2026-01-26/apps.mdx#custom-fonts)):
196
+ > Hosts can provide custom fonts via `styles.css.fonts`
197
+
198
+ ### Safe Area Insets
199
+
200
+ Respects host-provided padding for notches, toolbars, etc.:
201
+
202
+ ```typescript
203
+ // From src/mcp-app.ts
204
+ if (ctx.safeAreaInsets) {
205
+ mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`;
206
+ mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`;
207
+ mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`;
208
+ mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`;
209
+ }
210
+ ```
211
+
212
+ ### Tool Result Handler
213
+
214
+ Receives structured data via `ui/notifications/tool-result`:
215
+
216
+ ```typescript
217
+ // From src/mcp-app.ts
218
+ app.ontoolresult = (result) => {
219
+ const structured = result.structuredContent as { values?: Value[] } | undefined;
220
+ if (structured?.values) {
221
+ renderValues(structured.values);
222
+ }
223
+ };
224
+ ```
225
+
226
+ **Spec Reference** ([ui/notifications/tool-result](../../specification/2026-01-26/apps.mdx#notifications-host--view)):
227
+ ```typescript
228
+ {
229
+ jsonrpc: "2.0",
230
+ method: "ui/notifications/tool-result",
231
+ params: CallToolResult // { content, structuredContent, _meta }
232
+ }
233
+ ```
234
+
235
+ ### Tool Input Handler
236
+
237
+ Receives tool call arguments (empty in this case):
238
+
239
+ ```typescript
240
+ // From src/mcp-app.ts
241
+ app.ontoolinput = (params) => {
242
+ console.info("Received tool input:", params);
243
+ };
244
+ ```
245
+
246
+ **Spec Reference** ([ui/notifications/tool-input](../../specification/2026-01-26/apps.mdx#notifications-host--view)):
247
+ ```typescript
248
+ {
249
+ jsonrpc: "2.0",
250
+ method: "ui/notifications/tool-input",
251
+ params: { arguments: Record<string, unknown> }
252
+ }
253
+ ```
254
+
255
+ ### Tool Cancellation Handler
256
+
257
+ Handles cancelled tool execution:
258
+
259
+ ```typescript
260
+ // From src/mcp-app.ts
261
+ app.ontoolcancelled = (params) => {
262
+ statusEl.textContent = "Tool call was cancelled";
263
+ };
264
+ ```
265
+
266
+ **Spec Reference** ([ui/notifications/tool-cancelled](../../specification/2026-01-26/apps.mdx#notifications-host--view)):
267
+ > Host MUST send this notification if the tool execution was cancelled, for any reason
268
+
269
+ ### Teardown Handler
270
+
271
+ Gracefully handles resource teardown:
272
+
273
+ ```typescript
274
+ // From src/mcp-app.ts
275
+ app.onteardown = async () => {
276
+ console.info("Value Picker app is being torn down");
277
+ return {};
278
+ };
279
+ ```
280
+
281
+ **Spec Reference** ([ui/resource-teardown](../../specification/2026-01-26/apps.mdx#notifications-host--view)):
282
+ > Host MUST send this notification before tearing down the UI resource
283
+
284
+ ### Update Model Context
285
+
286
+ Injects the user's selection into the model's context:
287
+
288
+ ```typescript
289
+ // From src/mcp-app.ts
290
+ await app.updateModelContext({
291
+ content: [{
292
+ type: "text",
293
+ text: `The user selected "${value.label}" (id: ${value.id}). Description: ${value.description}.`,
294
+ }],
295
+ });
296
+ ```
297
+
298
+ **Spec Reference** ([ui/update-model-context](../../specification/2026-01-26/apps.mdx#requests-view--host)):
299
+ ```typescript
300
+ {
301
+ jsonrpc: "2.0",
302
+ method: "ui/update-model-context",
303
+ params: {
304
+ content?: ContentBlock[],
305
+ structuredContent?: Record<string, unknown>
306
+ }
307
+ }
308
+ ```
309
+
310
+ > The View MAY send this request to update the Host's model context. This context will be used in future turns.
311
+
312
+ ### Send User Message
313
+
314
+ Triggers an AI response after selection:
315
+
316
+ ```typescript
317
+ // From src/mcp-app.ts
318
+ await app.sendMessage({
319
+ role: "user",
320
+ content: [{
321
+ type: "text",
322
+ text: `I have picked a value, can you tell me what it is?`,
323
+ }],
324
+ });
325
+ ```
326
+
327
+ **Spec Reference** ([ui/message](../../specification/2026-01-26/apps.mdx#requests-view--host)):
328
+ ```typescript
329
+ {
330
+ jsonrpc: "2.0",
331
+ method: "ui/message",
332
+ params: {
333
+ role: "user",
334
+ content: { type: "text", text: string }
335
+ }
336
+ }
337
+ ```
338
+
339
+ > Host SHOULD add the message to the conversation context, preserving the specified role.
340
+
341
+ ---
342
+
343
+ ## Test Pattern: "Blind Selection"
344
+
345
+ This example implements a "blind selection" test pattern to validate context injection:
346
+
347
+ 1. User clicks a value card in the UI
348
+ 2. View sends `ui/update-model-context` with the selection details
349
+ 3. View sends `ui/message` with "I have picked a value, can you tell me what it is?"
350
+ 4. The AI must read the context to respond with the correct value
351
+
352
+ This proves that `updateModelContext` successfully injects data that the model can access, without the user message containing the answer.
353
+
354
+ ### Expected Model Behavior
355
+
356
+ The tool description explicitly tells the model this is a debug/test tool. The model should:
357
+
358
+ - **Do**: Simply confirm which value it received (e.g., "You selected Alpha Protocol")
359
+ - **Don't**: Provide detailed analysis, recommendations, or elaborate on the value meanings
360
+
361
+ If the model goes into detail about the values, the test still passes (context injection worked), but the model didn't follow the debug instructions.
362
+
363
+ ---
364
+
365
+ ## Installation
366
+
367
+ ### Via npm (Recommended)
368
+
369
+ Install the published package from [npmjs.com/package/mcp-server-value-picker](https://www.npmjs.com/package/mcp-server-value-picker):
370
+
371
+ ```bash
372
+ npm install -g mcp-server-value-picker
373
+ ```
374
+
375
+ Run the server:
376
+
377
+ ```bash
378
+ # Run (auto-detects transport mode)
379
+ mcp-server-value-picker
380
+
381
+ # Force STDIO mode (for Claude Desktop)
382
+ mcp-server-value-picker --stdio
383
+
384
+ # Force HTTP mode
385
+ mcp-server-value-picker --http
386
+ ```
387
+
388
+ Or run directly with npx (no install required):
389
+
390
+ ```bash
391
+ npx mcp-server-value-picker
392
+ ```
393
+
394
+ ### Local Development
395
+
396
+ For modifying the example or contributing:
397
+
398
+ ```bash
399
+ # Install dependencies
400
+ npm install
401
+
402
+ # Build UI and server
403
+ npm run build
404
+
405
+ # Run (auto-detects transport mode)
406
+ npm start
407
+
408
+ # Force STDIO mode
409
+ npm start -- --stdio
410
+
411
+ # Force HTTP mode
412
+ npm start -- --http
413
+ ```
414
+
415
+ Default HTTP endpoint: `http://localhost:3456/mcp`
416
+
417
+ ---
418
+
419
+ ## Files
420
+
421
+ | File | Purpose |
422
+ |------|---------|
423
+ | `server.ts` | MCP server with `pick_value` tool and UI resource |
424
+ | `src/mcp-app.ts` | View implementation (`App` class, handlers, context) |
425
+ | `src/mcp-app.css` | View styling with CSS variable fallbacks |
426
+ | `mcp-app.html` | HTML template with color-scheme meta tag |
427
+ | `main.ts` | Entry point with STDIO/HTTP transport selection |
428
+
429
+ ---
430
+
431
+ ## Specification Coverage
432
+
433
+ | Feature | Spec Section | Tested |
434
+ |---------|--------------|--------|
435
+ | `text/html;profile=mcp-app` MIME type | UI Resource Format | ✓ |
436
+ | `ui://` URI scheme | UI Resource Format | ✓ |
437
+ | `_meta.ui.resourceUri` linkage | Resource Discovery | ✓ |
438
+ | `ui/initialize` / `ui/notifications/initialized` | Lifecycle | ✓ |
439
+ | `ui/notifications/tool-input` | Data Passing | ✓ |
440
+ | `ui/notifications/tool-result` | Data Passing | ✓ |
441
+ | `ui/notifications/tool-cancelled` | Notifications | ✓ |
442
+ | `ui/resource-teardown` | Cleanup | ✓ |
443
+ | `ui/update-model-context` | Requests | ✓ |
444
+ | `ui/message` | Requests | ✓ |
445
+ | `ui/notifications/host-context-changed` | Notifications | ✓ |
446
+ | `HostContext.theme` | Theming | ✓ |
447
+ | `HostContext.styles.variables` | Theming | ✓ |
448
+ | `HostContext.styles.css.fonts` | Custom Fonts | ✓ |
449
+ | `HostContext.safeAreaInsets` | Container Dimensions | ✓ |
450
+ | `content` + `structuredContent` dual model | Data Passing | ✓ |
package/dist/index.js CHANGED
@@ -34120,9 +34120,17 @@ class StreamableHTTPServerTransport {
34120
34120
 
34121
34121
  // main.ts
34122
34122
  var import_cors = __toESM(require_lib4(), 1);
34123
+ import { createServer as createNetServer } from "node:net";
34123
34124
  import { createServer } from "./server.js";
34124
- var isStdio = process.argv.includes("--stdio");
34125
- if (isStdio) {
34125
+ function resolveTransport() {
34126
+ if (process.argv.includes("--stdio"))
34127
+ return "stdio";
34128
+ if (process.argv.includes("--http"))
34129
+ return "http";
34130
+ return process.stdin.isTTY ? "http" : "stdio";
34131
+ }
34132
+ var transport = resolveTransport();
34133
+ if (transport === "stdio") {
34126
34134
  const noop = () => {};
34127
34135
  console.log = noop;
34128
34136
  console.info = noop;
@@ -34130,22 +34138,44 @@ if (isStdio) {
34130
34138
  console.error = noop;
34131
34139
  console.debug = noop;
34132
34140
  }
34141
+ function findAvailablePort(preferred) {
34142
+ return new Promise((resolve, reject) => {
34143
+ const srv = createNetServer();
34144
+ srv.once("error", (err) => {
34145
+ if (err.code === "EADDRINUSE") {
34146
+ srv.listen(0, "0.0.0.0", () => {
34147
+ const addr = srv.address();
34148
+ const port = typeof addr === "object" && addr ? addr.port : 0;
34149
+ srv.close(() => resolve(port));
34150
+ });
34151
+ } else {
34152
+ reject(err);
34153
+ }
34154
+ });
34155
+ srv.listen(preferred, "0.0.0.0", () => {
34156
+ const addr = srv.address();
34157
+ const port = typeof addr === "object" && addr ? addr.port : preferred;
34158
+ srv.close(() => resolve(port));
34159
+ });
34160
+ });
34161
+ }
34133
34162
  async function startStreamableHTTPServer(createServer2) {
34134
- const port = parseInt(process.env.PORT ?? "3456", 10);
34163
+ const preferredPort = parseInt(process.env.PORT ?? "3456", 10);
34164
+ const port = await findAvailablePort(preferredPort);
34135
34165
  const app = createMcpExpressApp({ host: "0.0.0.0" });
34136
34166
  app.use(import_cors.default());
34137
34167
  app.all("/mcp", async (req, res) => {
34138
34168
  const server = createServer2();
34139
- const transport = new StreamableHTTPServerTransport({
34169
+ const transport2 = new StreamableHTTPServerTransport({
34140
34170
  sessionIdGenerator: undefined
34141
34171
  });
34142
34172
  res.on("close", () => {
34143
- transport.close().catch(() => {});
34173
+ transport2.close().catch(() => {});
34144
34174
  server.close().catch(() => {});
34145
34175
  });
34146
34176
  try {
34147
- await server.connect(transport);
34148
- await transport.handleRequest(req, res, req.body);
34177
+ await server.connect(transport2);
34178
+ await transport2.handleRequest(req, res, req.body);
34149
34179
  } catch (error2) {
34150
34180
  console.error("MCP error:", error2);
34151
34181
  if (!res.headersSent) {
@@ -34172,11 +34202,11 @@ async function startStreamableHTTPServer(createServer2) {
34172
34202
  }
34173
34203
  async function startStdioServer(createServer2) {
34174
34204
  const server = createServer2();
34175
- const transport = new StdioServerTransport;
34176
- await server.connect(transport);
34205
+ const stdioTransport = new StdioServerTransport;
34206
+ await server.connect(stdioTransport);
34177
34207
  }
34178
34208
  async function main() {
34179
- if (isStdio) {
34209
+ if (transport === "stdio") {
34180
34210
  await startStdioServer(createServer);
34181
34211
  } else {
34182
34212
  await startStreamableHTTPServer(createServer);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "mcp-server-value-picker",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "type": "module",
5
- "description": "Test MCP App: 10 selectable values + ui/update-model-context for Phase 5 testing",
5
+ "description": "Debug/test MCP App: Validates that values selected in UI are correctly passed to the AI model via ui/update-model-context",
6
6
  "main": "dist/server.js",
7
7
  "files": [
8
8
  "dist"