@fitchmultz/pi-mcp-adapter 8.0.5 → 8.0.7

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 (88) hide show
  1. package/CHANGELOG.md +140 -7
  2. package/OAUTH.md +13 -16
  3. package/README.md +8 -8
  4. package/cli.js +67 -28
  5. package/dist/abort.js +18 -9
  6. package/dist/client-operations.js +4 -2
  7. package/dist/commands.js +77 -43
  8. package/dist/config.js +194 -105
  9. package/dist/connection-runtime.d.ts +2 -2
  10. package/dist/connection-runtime.js +29 -18
  11. package/dist/consent-manager.d.ts +2 -2
  12. package/dist/consent-manager.js +5 -2
  13. package/dist/direct-tools.d.ts +1 -1
  14. package/dist/direct-tools.js +13 -7
  15. package/dist/elicitation-handler.d.ts +2 -3
  16. package/dist/elicitation-handler.js +113 -58
  17. package/dist/gateway-arguments.d.ts +1 -1
  18. package/dist/gateway-arguments.js +90 -23
  19. package/dist/glimpse-ui.js +10 -5
  20. package/dist/host-html-template.d.ts +3 -1
  21. package/dist/host-html-template.js +9 -12
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +264 -119
  24. package/dist/init.d.ts +1 -3
  25. package/dist/init.js +91 -51
  26. package/dist/legacy-migration.d.ts +1 -1
  27. package/dist/legacy-migration.js +17 -12
  28. package/dist/lifecycle.d.ts +3 -3
  29. package/dist/lifecycle.js +33 -17
  30. package/dist/logger.d.ts +2 -2
  31. package/dist/logger.js +4 -2
  32. package/dist/mcp-auth-flow.js +150 -58
  33. package/dist/mcp-auth.d.ts +7 -7
  34. package/dist/mcp-auth.js +189 -138
  35. package/dist/mcp-callback-server.js +19 -8
  36. package/dist/mcp-code.js +168 -54
  37. package/dist/mcp-keyring-helper.cjs +57 -23
  38. package/dist/mcp-oauth-provider.d.ts +6 -7
  39. package/dist/mcp-oauth-provider.js +146 -72
  40. package/dist/mcp-oauth-runtime.js +29 -16
  41. package/dist/mcp-output-guard.js +193 -65
  42. package/dist/mcp-output-preview.js +41 -20
  43. package/dist/mcp-panel.d.ts +9 -9
  44. package/dist/mcp-panel.js +141 -70
  45. package/dist/mcp-script-worker.mjs +135 -43
  46. package/dist/mcp-setup-panel.d.ts +8 -8
  47. package/dist/mcp-setup-panel.js +130 -44
  48. package/dist/mcp-status.js +14 -7
  49. package/dist/mcp-trace.d.ts +10 -5
  50. package/dist/mcp-trace.js +127 -61
  51. package/dist/metadata-cache.d.ts +3 -4
  52. package/dist/metadata-cache.js +83 -43
  53. package/dist/npx-resolver.js +101 -60
  54. package/dist/onboarding-state.js +4 -2
  55. package/dist/plain-result-reader.js +78 -31
  56. package/dist/prompts.js +36 -20
  57. package/dist/proxy-modes.js +670 -193
  58. package/dist/resource-tools.js +30 -9
  59. package/dist/runtime-owner.js +36 -18
  60. package/dist/sampling-handler.d.ts +1 -2
  61. package/dist/sampling-handler.js +41 -21
  62. package/dist/search-ranking.d.ts +1 -1
  63. package/dist/search-ranking.js +67 -34
  64. package/dist/server-manager.d.ts +11 -11
  65. package/dist/server-manager.js +283 -166
  66. package/dist/session-recovery.js +113 -61
  67. package/dist/tool-approval.js +9 -3
  68. package/dist/tool-loader.js +154 -55
  69. package/dist/tool-metadata.d.ts +1 -1
  70. package/dist/tool-metadata.js +51 -23
  71. package/dist/tool-registrar.js +32 -10
  72. package/dist/tool-result-renderer.d.ts +3 -3
  73. package/dist/tool-result-renderer.js +65 -44
  74. package/dist/types.d.ts +2 -2
  75. package/dist/types.js +39 -18
  76. package/dist/ui-message-history.js +33 -13
  77. package/dist/ui-resource-handler.d.ts +3 -3
  78. package/dist/ui-resource-handler.js +37 -21
  79. package/dist/ui-server.js +101 -43
  80. package/dist/ui-session.js +66 -29
  81. package/dist/ui-stream-types.js +14 -4
  82. package/dist/ui-tool-visibility.js +12 -6
  83. package/dist/unix-socket-transport.d.ts +1 -2
  84. package/dist/unix-socket-transport.js +19 -12
  85. package/dist/utils.d.ts +2 -0
  86. package/dist/utils.js +60 -35
  87. package/package.json +29 -5
  88. package/skills/mcp-scripting/SKILL.md +6 -6
package/dist/ui-server.js CHANGED
@@ -7,7 +7,7 @@ import { ContentBlockSchema } from "@modelcontextprotocol/core";
7
7
  import { formatAuthRequiredMessage } from "./utils.js";
8
8
  import { buildHostHtmlTemplate, buildCspMetaContent } from "./host-html-template.js";
9
9
  import { logger } from "./logger.js";
10
- import { SessionRecoveryAuthRequiredError, withSessionRecovery } from "./session-recovery.js";
10
+ import { SessionRecoveryAuthRequiredError, withSessionRecovery, } from "./session-recovery.js";
11
11
  import { ensureToolCallApproved, isToolCallApprovalRequired } from "./tool-approval.js";
12
12
  import { extractUiToolVisibility, isUiToolCallableByApp } from "./ui-tool-visibility.js";
13
13
  import { createUiMessageHistory } from "./ui-message-history.js";
@@ -68,8 +68,9 @@ export async function startUiServer(options) {
68
68
  };
69
69
  const updateStreamSummary = (payload) => {
70
70
  const envelope = getVisualizationStreamEnvelope(payload?.structuredContent);
71
- if (!envelope)
71
+ if (!envelope) {
72
72
  return;
73
+ }
73
74
  if (!streamSummary) {
74
75
  streamSummary = {
75
76
  streamId: envelope.streamId,
@@ -83,10 +84,12 @@ export async function startUiServer(options) {
83
84
  streamSummary.phases.push(envelope.phase);
84
85
  }
85
86
  streamSummary.finalStatus = envelope.status;
86
- if (envelope.message !== undefined)
87
+ if (envelope.message !== undefined) {
87
88
  streamSummary.lastMessage = envelope.message;
88
- else
89
+ }
90
+ else {
89
91
  delete streamSummary.lastMessage;
92
+ }
90
93
  };
91
94
  const serializeEvent = (eventId, name, payload) => {
92
95
  return `id: ${eventId}\nevent: ${name}\ndata: ${JSON.stringify(payload)}\n\n`;
@@ -94,8 +97,9 @@ export async function startUiServer(options) {
94
97
  const getLatestCheckpointIndex = () => {
95
98
  for (let index = eventLog.length - 1; index >= 0; index -= 1) {
96
99
  const entry = eventLog[index];
97
- if (!entry)
100
+ if (!entry) {
98
101
  continue;
102
+ }
99
103
  const envelope = getVisualizationStreamEnvelope(entry.payload?.structuredContent);
100
104
  if (envelope?.frameType === "checkpoint" || envelope?.frameType === "final") {
101
105
  return index;
@@ -104,8 +108,9 @@ export async function startUiServer(options) {
104
108
  return -1;
105
109
  };
106
110
  const pruneEventLog = () => {
107
- if (eventLog.length <= MAX_EVENT_LOG)
111
+ if (eventLog.length <= MAX_EVENT_LOG) {
108
112
  return;
113
+ }
109
114
  const latestCheckpointIndex = getLatestCheckpointIndex();
110
115
  if (latestCheckpointIndex > 0) {
111
116
  eventLog.splice(0, latestCheckpointIndex);
@@ -115,8 +120,9 @@ export async function startUiServer(options) {
115
120
  }
116
121
  };
117
122
  const pushEvent = (name, payload) => {
118
- if (completed)
123
+ if (completed) {
119
124
  return;
125
+ }
120
126
  const eventId = nextEventId++;
121
127
  eventLog.push({ id: eventId, name, payload });
122
128
  updateStreamSummary(payload);
@@ -159,14 +165,16 @@ export async function startUiServer(options) {
159
165
  sseClients.clear();
160
166
  };
161
167
  const stopWatchdog = () => {
162
- if (!watchdog)
168
+ if (!watchdog) {
163
169
  return;
170
+ }
164
171
  clearInterval(watchdog);
165
172
  watchdog = null;
166
173
  };
167
174
  const markCompleted = (reason) => {
168
- if (completed)
175
+ if (completed) {
169
176
  return;
177
+ }
170
178
  log.debug("Session completed", { reason });
171
179
  pushEvent("session-complete", { reason });
172
180
  completed = true;
@@ -189,13 +197,17 @@ export async function startUiServer(options) {
189
197
  }
190
198
  if (method === "GET" && url.pathname === "/") {
191
199
  if (!url.searchParams.has("session")) {
192
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
193
- res.end("<!doctype html><html><head><meta charset=\"utf-8\"><title>MCP UI</title></head>" +
200
+ res.writeHead(200, {
201
+ "Content-Type": "text/html; charset=utf-8",
202
+ "Cache-Control": "no-store",
203
+ });
204
+ res.end('<!doctype html><html><head><meta charset="utf-8"><title>MCP UI</title></head>' +
194
205
  "<body><p>Open the authenticated MCP UI URL shown by Pi.</p></body></html>");
195
206
  return;
196
207
  }
197
- if (!validateTokenQuery(url, sessionToken, res))
208
+ if (!validateTokenQuery(url, sessionToken, res)) {
198
209
  return;
210
+ }
199
211
  touchHeartbeat();
200
212
  const html = buildHostHtmlTemplate({
201
213
  sessionToken,
@@ -216,8 +228,9 @@ export async function startUiServer(options) {
216
228
  return;
217
229
  }
218
230
  if (method === "GET" && url.pathname === "/events") {
219
- if (!validateTokenQuery(url, sessionToken, res))
231
+ if (!validateTokenQuery(url, sessionToken, res)) {
220
232
  return;
233
+ }
221
234
  touchHeartbeat();
222
235
  log.debug("SSE client connected", { clientCount: sseClients.size + 1 });
223
236
  res.writeHead(200, {
@@ -235,14 +248,16 @@ export async function startUiServer(options) {
235
248
  return;
236
249
  }
237
250
  if (method === "GET" && url.pathname === "/health") {
238
- if (!validateTokenQuery(url, sessionToken, res))
251
+ if (!validateTokenQuery(url, sessionToken, res)) {
239
252
  return;
253
+ }
240
254
  sendJson(res, 200, { ok: true, result: { healthy: true } });
241
255
  return;
242
256
  }
243
257
  if (method === "GET" && url.pathname === "/ui-app") {
244
- if (!validateTokenQuery(url, uiResourceToken, res, "resource"))
258
+ if (!validateTokenQuery(url, uiResourceToken, res, "resource")) {
245
259
  return;
260
+ }
246
261
  touchHeartbeat();
247
262
  // Enforce host metadata independently of where app HTML places its document head.
248
263
  const cspContent = buildCspMetaContent(options.resource.meta.csp);
@@ -275,13 +290,19 @@ export async function startUiServer(options) {
275
290
  return;
276
291
  }
277
292
  const body = await parseBody(req, res);
278
- if (!body)
293
+ if (!body) {
279
294
  return;
280
- if (!validateTokenBody(body, sessionToken, res))
295
+ }
296
+ if (!validateTokenBody(body, sessionToken, res)) {
281
297
  return;
298
+ }
282
299
  // A partial upload is not accepted until its authenticated body is complete.
283
- if (completed && (url.pathname === "/proxy/ui/message" || url.pathname === "/proxy/ui/context")) {
284
- sendJson(res, 409, { ok: false, error: "UI session is complete; message was not accepted" });
300
+ if (completed &&
301
+ (url.pathname === "/proxy/ui/message" || url.pathname === "/proxy/ui/context")) {
302
+ sendJson(res, 409, {
303
+ ok: false,
304
+ error: "UI session is complete; message was not accepted",
305
+ });
285
306
  return;
286
307
  }
287
308
  const params = body.params ?? {};
@@ -295,28 +316,41 @@ export async function startUiServer(options) {
295
316
  }
296
317
  const connection = options.manager.getConnection(options.serverName);
297
318
  if (!connection || connection.status !== "connected") {
298
- sendJson(res, 503, { ok: false, error: `Server "${options.serverName}" is not connected` });
319
+ sendJson(res, 503, {
320
+ ok: false,
321
+ error: `Server "${options.serverName}" is not connected`,
322
+ });
299
323
  return;
300
324
  }
301
- if (isServerDisabled(options.config?.mcpServers[options.serverName]) || isServerDisabled(connection.definition)) {
325
+ if (isServerDisabled(options.config?.mcpServers[options.serverName]) ||
326
+ isServerDisabled(connection.definition)) {
302
327
  sendJson(res, 503, { ok: false, error: `Server "${options.serverName}" is disabled` });
303
328
  return;
304
329
  }
305
330
  const definition = options.config?.mcpServers[options.serverName] ?? connection.definition;
306
331
  const toolDefinitions = Array.isArray(connection.tools) ? connection.tools : [];
307
332
  const toolDefinition = toolDefinitions.find((tool) => tool.name === callParams.name);
308
- if (!toolDefinition || !isToolAllowed(callParams.name, options.serverName, resolveToolPrefix(definition, options.config?.settings?.toolPrefix), definition?.includeTools, definition?.excludeTools)) {
309
- sendJson(res, 403, { ok: false, error: `MCP tool "${callParams.name}" is not callable by apps` });
333
+ if (!toolDefinition ||
334
+ !isToolAllowed(callParams.name, options.serverName, resolveToolPrefix(definition, options.config?.settings?.toolPrefix), definition?.includeTools, definition?.excludeTools)) {
335
+ sendJson(res, 403, {
336
+ ok: false,
337
+ error: `MCP tool "${callParams.name}" is not callable by apps`,
338
+ });
310
339
  return;
311
340
  }
312
341
  const uiVisibility = extractUiToolVisibility(toolDefinition._meta);
313
342
  if (!isUiToolCallableByApp(uiVisibility)) {
314
- sendJson(res, 403, { ok: false, error: `MCP tool "${callParams.name}" is not callable by apps` });
343
+ sendJson(res, 403, {
344
+ ok: false,
345
+ error: `MCP tool "${callParams.name}" is not callable by apps`,
346
+ });
315
347
  return;
316
348
  }
317
349
  const callArgs = {
318
350
  name: callParams.name,
319
- arguments: callParams.arguments && typeof callParams.arguments === "object" && !Array.isArray(callParams.arguments)
351
+ arguments: callParams.arguments &&
352
+ typeof callParams.arguments === "object" &&
353
+ !Array.isArray(callParams.arguments)
320
354
  ? callParams.arguments
321
355
  : {},
322
356
  };
@@ -324,15 +358,18 @@ export async function startUiServer(options) {
324
358
  name: callParams.name,
325
359
  originalName: callParams.name,
326
360
  description: toolDefinition?.description ?? "",
327
- ...(toolDefinition?.inputSchema !== undefined ? { inputSchema: toolDefinition.inputSchema } : {}),
361
+ ...(toolDefinition?.inputSchema !== undefined
362
+ ? { inputSchema: toolDefinition.inputSchema }
363
+ : {}),
328
364
  ...(uiVisibility !== undefined ? { uiVisibility } : {}),
329
365
  };
330
366
  const approval = options.state
331
367
  ? await ensureToolCallApproved(options.state, options.serverName, toolMeta, callArgs.arguments, options.state.owner?.signal)
332
- : options.config && isToolCallApprovalRequired(options.config, options.serverName, toolMeta)
368
+ : options.config &&
369
+ isToolCallApprovalRequired(options.config, options.serverName, toolMeta)
333
370
  ? { ok: false, reason: "approval_required_headless" }
334
371
  : { ok: true };
335
- if (approval.ok === false) {
372
+ if (!approval.ok) {
336
373
  const denied = approval.reason === "denied";
337
374
  const message = denied
338
375
  ? `The user declined approval to run MCP tool "${callParams.name}" on server "${options.serverName}".`
@@ -369,7 +406,7 @@ export async function startUiServer(options) {
369
406
  return;
370
407
  }
371
408
  if (url.pathname === "/proxy/ui/consent") {
372
- const approved = !!params.approved;
409
+ const approved = Boolean(params.approved);
373
410
  options.consentManager.registerDecision(options.serverName, approved);
374
411
  sendJson(res, 200, { ok: true, result: { approved } });
375
412
  return;
@@ -412,14 +449,23 @@ export async function startUiServer(options) {
412
449
  if (url.pathname === "/proxy/ui/context") {
413
450
  const content = params.content;
414
451
  const structuredContent = params.structuredContent;
415
- if ((content !== undefined && (!Array.isArray(content) || content.some((block) => !ContentBlockSchema.safeParse(block).success))) ||
416
- (structuredContent !== undefined && (!structuredContent || typeof structuredContent !== "object" || Array.isArray(structuredContent)))) {
452
+ if ((content !== undefined &&
453
+ (!Array.isArray(content) ||
454
+ content.some((block) => !ContentBlockSchema.safeParse(block).success))) ||
455
+ (structuredContent !== undefined &&
456
+ (!structuredContent ||
457
+ typeof structuredContent !== "object" ||
458
+ Array.isArray(structuredContent)))) {
417
459
  sendJson(res, 400, { ok: false, error: "Invalid update-model-context params" });
418
460
  return;
419
461
  }
420
462
  const ctxParams = {
421
- ...(content !== undefined ? { content: content } : {}),
422
- ...(structuredContent !== undefined ? { structuredContent: structuredContent } : {}),
463
+ ...(content !== undefined
464
+ ? { content: content }
465
+ : {}),
466
+ ...(structuredContent !== undefined
467
+ ? { structuredContent: structuredContent }
468
+ : {}),
423
469
  };
424
470
  const update = createUiModelContextUpdate(ctxParams);
425
471
  if (update) {
@@ -428,7 +474,10 @@ export async function startUiServer(options) {
428
474
  sessionMessages.contexts.shift();
429
475
  }
430
476
  }
431
- log.debug("UI context update", { hasContent: !!ctxParams.content, hasUpdate: !!update });
477
+ log.debug("UI context update", {
478
+ hasContent: Boolean(ctxParams.content),
479
+ hasUpdate: Boolean(update),
480
+ });
432
481
  await options.onContextUpdate?.(ctxParams);
433
482
  sendJson(res, 200, { ok: true, result: {} });
434
483
  return;
@@ -490,9 +539,10 @@ export async function startUiServer(options) {
490
539
  catch (error) {
491
540
  if (error instanceof SessionRecoveryAuthRequiredError) {
492
541
  const fallback = `Server "${options.serverName}" requires OAuth authentication. If mcp is inactive, first mcp_search({ enable: ["gateway"] }). Run mcp({ action: "auth-start", server: "${options.serverName}" }) to get a browser URL, or /mcp-auth ${options.serverName} in an interactive local session.`;
493
- const message = error.authMessage ?? (options.config
494
- ? formatAuthRequiredMessage(options.config, options.serverName, fallback)
495
- : fallback);
542
+ const message = error.authMessage ??
543
+ (options.config
544
+ ? formatAuthRequiredMessage(options.config, options.serverName, fallback)
545
+ : fallback);
496
546
  sendJson(res, 401, { ok: false, error: message });
497
547
  return;
498
548
  }
@@ -511,10 +561,12 @@ export async function startUiServer(options) {
511
561
  });
512
562
  }
513
563
  watchdog = setInterval(() => {
514
- if (completed)
564
+ if (completed) {
515
565
  return;
516
- if (Date.now() - lastHeartbeatAt <= ABANDONED_GRACE_MS)
566
+ }
567
+ if (Date.now() - lastHeartbeatAt <= ABANDONED_GRACE_MS) {
517
568
  return;
569
+ }
518
570
  markCompleted("stale");
519
571
  try {
520
572
  server.close();
@@ -599,7 +651,10 @@ async function parseBody(req, res) {
599
651
  return body;
600
652
  }
601
653
  catch (error) {
602
- sendJson(res, 400, { ok: false, error: error instanceof Error ? error.message : "Invalid body" });
654
+ sendJson(res, 400, {
655
+ ok: false,
656
+ error: error instanceof Error ? error.message : "Invalid body",
657
+ });
603
658
  return null;
604
659
  }
605
660
  }
@@ -628,20 +683,23 @@ function readBody(req) {
628
683
  });
629
684
  }
630
685
  function resolvePortCandidates(port) {
631
- if (port !== undefined)
686
+ if (port !== undefined) {
632
687
  return [port];
688
+ }
633
689
  const candidates = [];
634
690
  const count = MOSHI_DISCOVERY_PORT_END - MOSHI_DISCOVERY_PORT_START + 1;
635
691
  for (let offset = 0; offset < count; offset += 1) {
636
- const candidate = MOSHI_DISCOVERY_PORT_START + ((nextMoshiDiscoveryPort - MOSHI_DISCOVERY_PORT_START + offset) % count);
692
+ const candidate = MOSHI_DISCOVERY_PORT_START +
693
+ ((nextMoshiDiscoveryPort - MOSHI_DISCOVERY_PORT_START + offset) % count);
637
694
  candidates.push(candidate);
638
695
  }
639
696
  candidates.push(0);
640
697
  return candidates;
641
698
  }
642
699
  function rememberMoshiDiscoveryPort(port) {
643
- if (port < MOSHI_DISCOVERY_PORT_START || port > MOSHI_DISCOVERY_PORT_END)
700
+ if (port < MOSHI_DISCOVERY_PORT_START || port > MOSHI_DISCOVERY_PORT_END) {
644
701
  return;
702
+ }
645
703
  nextMoshiDiscoveryPort = port >= MOSHI_DISCOVERY_PORT_END ? MOSHI_DISCOVERY_PORT_START : port + 1;
646
704
  }
647
705
  function isAllowedHost(hostname) {
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import net from "node:net";
4
4
  import { UrlElicitationRequiredError } from "@modelcontextprotocol/client";
5
5
  import { createUiModelContextUpdate, extractUiPromptText, } from "./types.js";
6
- import { UI_STREAM_HOST_CONTEXT_KEY, UI_STREAM_REQUEST_META_KEY, UI_STREAM_STRUCTURED_CONTENT_KEY } from "./ui-stream-types.js";
6
+ import { UI_STREAM_HOST_CONTEXT_KEY, UI_STREAM_REQUEST_META_KEY, UI_STREAM_STRUCTURED_CONTENT_KEY, } from "./ui-stream-types.js";
7
7
  import { logger } from "./logger.js";
8
8
  import { startUiServer } from "./ui-server.js";
9
9
  import { isGlimpseAvailable, openGlimpseWindow } from "./glimpse-ui.js";
@@ -18,7 +18,9 @@ export function summarizeUiSessionResult(uiSession) {
18
18
  };
19
19
  }
20
20
  if (!uiSession.windowOpen) {
21
- const action = uiSession.reused ? "Updated the suppressed MCP UI session." : "MCP UI window was suppressed.";
21
+ const action = uiSession.reused
22
+ ? "Updated the suppressed MCP UI session."
23
+ : "MCP UI window was suppressed.";
22
24
  return {
23
25
  message: `${action} Open manually: ${uiSession.url}`,
24
26
  uiOpen: false,
@@ -40,7 +42,9 @@ function withStreamEnvelope(result, streamId, sequence) {
40
42
  if (!streamId) {
41
43
  return result;
42
44
  }
43
- const structuredContent = result.structuredContent && typeof result.structuredContent === "object" && !Array.isArray(result.structuredContent)
45
+ const structuredContent = result.structuredContent &&
46
+ typeof result.structuredContent === "object" &&
47
+ !Array.isArray(result.structuredContent)
44
48
  ? { ...result.structuredContent }
45
49
  : {};
46
50
  const rawEnvelope = structuredContent[UI_STREAM_STRUCTURED_CONTENT_KEY];
@@ -69,11 +73,13 @@ async function openInBrowser(state, url, signal) {
69
73
  return null;
70
74
  }
71
75
  catch (error) {
72
- if (isAbortError(error, signal))
76
+ if (isAbortError(error, signal)) {
73
77
  throw error;
78
+ }
74
79
  const message = error instanceof Error ? error.message : String(error);
75
- if (state.owner?.isActive() === false)
80
+ if (state.owner?.isActive() === false) {
76
81
  return null;
82
+ }
77
83
  state.ui?.notify(`MCP UI browser open failed: ${message}`, "warning");
78
84
  return message;
79
85
  }
@@ -149,10 +155,12 @@ export async function maybeStartUiSession(state, request) {
149
155
  existingHandle.sendToolInput(request.toolArgs);
150
156
  if (streamToken) {
151
157
  state.manager.registerUiStreamListener(streamToken, (serverName, notification) => {
152
- if (!active || state.uiServer !== existingHandle)
158
+ if (!active || state.uiServer !== existingHandle) {
153
159
  return;
154
- if (serverName !== request.serverName)
160
+ }
161
+ if (serverName !== request.serverName) {
155
162
  return;
163
+ }
156
164
  nextStreamSequence += 1;
157
165
  existingHandle.sendResultPatch(withStreamEnvelope(notification.result, streamId, nextStreamSequence));
158
166
  });
@@ -170,20 +178,23 @@ export async function maybeStartUiSession(state, request) {
170
178
  windowOpen: existingHandle.windowOpen ?? true,
171
179
  isActive: () => active && state.uiServer === existingHandle,
172
180
  sendToolResult: (result) => {
173
- if (!active || state.uiServer !== existingHandle)
181
+ if (!active || state.uiServer !== existingHandle) {
174
182
  return;
183
+ }
175
184
  nextStreamSequence += 1;
176
185
  existingHandle.sendToolResult(withStreamEnvelope(result, streamId, nextStreamSequence));
177
186
  },
178
187
  sendResultPatch: (result) => {
179
- if (!active || state.uiServer !== existingHandle)
188
+ if (!active || state.uiServer !== existingHandle) {
180
189
  return;
190
+ }
181
191
  nextStreamSequence += 1;
182
192
  existingHandle.sendResultPatch(withStreamEnvelope(result, streamId, nextStreamSequence));
183
193
  },
184
194
  sendToolCancelled: (reason) => {
185
- if (!active || state.uiServer !== existingHandle)
195
+ if (!active || state.uiServer !== existingHandle) {
186
196
  return;
197
+ }
187
198
  nextStreamSequence += 1;
188
199
  existingHandle.sendToolResult(withStreamEnvelope({
189
200
  isError: true,
@@ -248,7 +259,12 @@ export async function maybeStartUiSession(state, request) {
248
259
  if (state.sendMessage) {
249
260
  state.sendMessage({
250
261
  customType: "mcp-ui-prompt",
251
- content: [{ type: "text", text: `User sent prompt from ${request.serverName} UI: "${prompt}"` }],
262
+ content: [
263
+ {
264
+ type: "text",
265
+ text: `User sent prompt from ${request.serverName} UI: "${prompt}"`,
266
+ },
267
+ ],
252
268
  display: `💬 UI Prompt: ${prompt}`,
253
269
  details: { server: request.serverName, tool: request.toolName, prompt },
254
270
  }, { triggerTurn: true });
@@ -262,9 +278,19 @@ export async function maybeStartUiSession(state, request) {
262
278
  const paramsStr = intentParams ? ` ${JSON.stringify(intentParams)}` : "";
263
279
  state.sendMessage({
264
280
  customType: "mcp-ui-intent",
265
- content: [{ type: "text", text: `User triggered intent from ${request.serverName} UI: ${intent}${paramsStr}` }],
281
+ content: [
282
+ {
283
+ type: "text",
284
+ text: `User triggered intent from ${request.serverName} UI: ${intent}${paramsStr}`,
285
+ },
286
+ ],
266
287
  display: `🎯 UI Intent: ${intent}`,
267
- details: { server: request.serverName, tool: request.toolName, intent, params: intentParams },
288
+ details: {
289
+ server: request.serverName,
290
+ tool: request.toolName,
291
+ intent,
292
+ params: intentParams,
293
+ },
268
294
  }, { triggerTurn: true });
269
295
  log.debug("Triggered agent turn for UI intent", { intent });
270
296
  }
@@ -279,14 +305,19 @@ export async function maybeStartUiSession(state, request) {
279
305
  onContextUpdate: (params) => {
280
306
  const update = createUiModelContextUpdate(params);
281
307
  log.debug("Model context update from UI", {
282
- hasContent: !!params.content,
283
- hasStructured: !!params.structuredContent,
284
- hasUpdate: !!update,
308
+ hasContent: Boolean(params.content),
309
+ hasStructured: Boolean(params.structuredContent),
310
+ hasUpdate: Boolean(update),
285
311
  });
286
312
  if (update && state.sendMessage) {
287
313
  state.sendMessage({
288
314
  customType: "mcp-ui-context",
289
- content: [{ type: "text", text: `User submitted model context from ${request.serverName} UI:\n${update.summary}` }],
315
+ content: [
316
+ {
317
+ type: "text",
318
+ text: `User submitted model context from ${request.serverName} UI:\n${update.summary}`,
319
+ },
320
+ ],
290
321
  display: "UI Context submitted",
291
322
  details: { server: request.serverName, tool: request.toolName, context: update },
292
323
  }, { triggerTurn: true });
@@ -298,12 +329,12 @@ export async function maybeStartUiSession(state, request) {
298
329
  if (state.uiServer === handle) {
299
330
  const messages = handle.getSessionMessages();
300
331
  const stream = handle.getStreamSummary();
301
- const hasContent = !!messages.historyCount ||
332
+ const hasContent = Boolean(messages.historyCount) ||
302
333
  messages.prompts.length > 0 ||
303
334
  messages.intents.length > 0 ||
304
335
  messages.notifications.length > 0 ||
305
336
  messages.contexts.length > 0 ||
306
- !!stream;
337
+ Boolean(stream);
307
338
  if (hasContent) {
308
339
  state.completedUiSessions.push({
309
340
  serverName: handle.serverName,
@@ -340,10 +371,12 @@ export async function maybeStartUiSession(state, request) {
340
371
  }
341
372
  if (streamToken) {
342
373
  state.manager.registerUiStreamListener(streamToken, (serverName, notification) => {
343
- if (!active || state.uiServer !== handle)
374
+ if (!active || state.uiServer !== handle) {
344
375
  return;
345
- if (serverName !== request.serverName)
376
+ }
377
+ if (serverName !== request.serverName) {
346
378
  return;
379
+ }
347
380
  nextStreamSequence += 1;
348
381
  handle.sendResultPatch(withStreamEnvelope(notification.result, streamId, nextStreamSequence));
349
382
  });
@@ -361,7 +394,7 @@ export async function maybeStartUiSession(state, request) {
361
394
  log.info("Suppressing MCP UI window (MCP_UI_VIEWER=" + viewerPref + ")", { url: handle.url });
362
395
  }
363
396
  else {
364
- const remoteLikely = remoteByEnv || await hasActiveRemoteLogin();
397
+ const remoteLikely = remoteByEnv || (await hasActiveRemoteLogin());
365
398
  const emitRemoteHint = async (openError, openedOnHost = false) => {
366
399
  state.ui?.notify(remoteAccessHint({
367
400
  url: handle.url,
@@ -372,8 +405,7 @@ export async function maybeStartUiSession(state, request) {
372
405
  }), openError === null ? "info" : "warning");
373
406
  };
374
407
  const glimpseDetected = !remoteByEnv && isGlimpseAvailable();
375
- const useGlimpse = !remoteByEnv && (viewerPref === "glimpse" ||
376
- (viewerPref !== "browser" && glimpseDetected));
408
+ const useGlimpse = !remoteByEnv && (viewerPref === "glimpse" || (viewerPref !== "browser" && glimpseDetected));
377
409
  if (useGlimpse) {
378
410
  try {
379
411
  const glimpseHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{margin:0;padding:0;width:100vw;height:100vh;overflow:hidden}iframe{width:100%;height:100%;border:none}</style></head><body><iframe src="${handle.url}"></iframe></body></html>`;
@@ -382,8 +414,9 @@ export async function maybeStartUiSession(state, request) {
382
414
  width: 1000,
383
415
  height: 800,
384
416
  onClosed: () => {
385
- if (active)
417
+ if (active) {
386
418
  handle.close("glimpse-closed");
419
+ }
387
420
  },
388
421
  });
389
422
  if (state.owner?.isActive() === false || runtimeSignal.aborted) {
@@ -431,20 +464,23 @@ export async function maybeStartUiSession(state, request) {
431
464
  windowOpen,
432
465
  isActive: () => active && state.uiServer === handle,
433
466
  sendToolResult: (result) => {
434
- if (!active || state.uiServer !== handle)
467
+ if (!active || state.uiServer !== handle) {
435
468
  return;
469
+ }
436
470
  nextStreamSequence += 1;
437
471
  handle.sendToolResult(withStreamEnvelope(result, streamId, nextStreamSequence));
438
472
  },
439
473
  sendResultPatch: (result) => {
440
- if (!active || state.uiServer !== handle)
474
+ if (!active || state.uiServer !== handle) {
441
475
  return;
476
+ }
442
477
  nextStreamSequence += 1;
443
478
  handle.sendResultPatch(withStreamEnvelope(result, streamId, nextStreamSequence));
444
479
  },
445
480
  sendToolCancelled: (reason) => {
446
- if (!active || state.uiServer !== handle)
481
+ if (!active || state.uiServer !== handle) {
447
482
  return;
483
+ }
448
484
  handle.sendToolCancelled(reason);
449
485
  },
450
486
  close: (reason) => {
@@ -455,8 +491,9 @@ export async function maybeStartUiSession(state, request) {
455
491
  };
456
492
  }
457
493
  catch (error) {
458
- if (error instanceof UrlElicitationRequiredError || isAbortError(error, runtimeSignal))
494
+ if (error instanceof UrlElicitationRequiredError || isAbortError(error, runtimeSignal)) {
459
495
  throw error;
496
+ }
460
497
  const message = error instanceof Error ? error.message : String(error);
461
498
  log.error("Failed to start UI session", error instanceof Error ? error : undefined);
462
499
  state.ui?.notify(`MCP UI unavailable for ${request.toolName} (${request.serverName}): ${message}`, "warning");
@@ -5,7 +5,13 @@ export const UI_STREAM_RESULT_PATCH_METHOD = "notifications/pi-mcp-adapter/ui-re
5
5
  export const SERVER_STREAM_RESULT_PATCH_METHOD = "notifications/pi-mcp-adapter/result-patch";
6
6
  export const UI_STREAM_STRUCTURED_CONTENT_KEY = "pi-mcp-adapter/stream";
7
7
  export const uiStreamModeSchema = z.enum(["eager", "stream-first"]);
8
- export const visualizationStreamPhaseSchema = z.enum(["shell", "narrative", "structure", "detail", "settled"]);
8
+ export const visualizationStreamPhaseSchema = z.enum([
9
+ "shell",
10
+ "narrative",
11
+ "structure",
12
+ "detail",
13
+ "settled",
14
+ ]);
9
15
  export const visualizationStreamFrameTypeSchema = z.enum(["patch", "checkpoint", "final"]);
10
16
  export const visualizationStreamStatusSchema = z.enum(["ok", "error"]);
11
17
  const looseRecordSchema = z.record(z.string(), z.unknown());
@@ -26,12 +32,14 @@ export const visualizationStreamEnvelopeSchema = z.object({
26
32
  spec: looseRecordSchema.optional(),
27
33
  checkpoint: looseRecordSchema.optional(),
28
34
  });
29
- export const uiStreamCallToolResultSchema = z.object({
35
+ export const uiStreamCallToolResultSchema = z
36
+ .object({
30
37
  content: looseArraySchema.optional(),
31
38
  structuredContent: looseRecordSchema.optional(),
32
39
  isError: z.boolean().optional(),
33
40
  _meta: looseRecordSchema.optional(),
34
- }).passthrough();
41
+ })
42
+ .passthrough();
35
43
  export const uiStreamResultPatchNotificationSchema = z.object({
36
44
  method: z.literal(UI_STREAM_RESULT_PATCH_METHOD),
37
45
  params: uiStreamCallToolResultSchema,
@@ -49,7 +57,9 @@ export function getUiStreamHostContext(hostContext) {
49
57
  return parsed.success ? parsed.data : undefined;
50
58
  }
51
59
  export function getVisualizationStreamEnvelope(structuredContent) {
52
- if (!structuredContent || typeof structuredContent !== "object" || Array.isArray(structuredContent)) {
60
+ if (!structuredContent ||
61
+ typeof structuredContent !== "object" ||
62
+ Array.isArray(structuredContent)) {
53
63
  return undefined;
54
64
  }
55
65
  const candidate = structuredContent[UI_STREAM_STRUCTURED_CONTENT_KEY];
@@ -1,20 +1,26 @@
1
1
  export function extractUiToolVisibility(meta) {
2
- if (!meta || typeof meta !== "object")
2
+ if (!meta || typeof meta !== "object") {
3
3
  return undefined;
4
+ }
4
5
  const ui = meta.ui;
5
- if (!ui || typeof ui !== "object" || Array.isArray(ui))
6
+ if (!ui || typeof ui !== "object" || Array.isArray(ui)) {
6
7
  return undefined;
8
+ }
7
9
  const visibility = ui.visibility;
8
- if (visibility === undefined)
10
+ if (visibility === undefined) {
9
11
  return undefined;
10
- if (!Array.isArray(visibility))
12
+ }
13
+ if (!Array.isArray(visibility)) {
11
14
  return [];
15
+ }
12
16
  const values = [];
13
17
  for (const entry of visibility) {
14
- if (entry !== "model" && entry !== "app")
18
+ if (entry !== "model" && entry !== "app") {
15
19
  return [];
16
- if (!values.includes(entry))
20
+ }
21
+ if (!values.includes(entry)) {
17
22
  values.push(entry);
23
+ }
18
24
  }
19
25
  return values;
20
26
  }