@fitchmultz/pi-mcp-adapter 8.0.6 → 8.0.8
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 +142 -8
- package/OAUTH.md +13 -16
- package/README.md +8 -8
- package/cli.js +67 -28
- package/dist/abort.js +18 -9
- package/dist/client-operations.js +4 -2
- package/dist/commands.js +77 -43
- package/dist/config.js +194 -105
- package/dist/connection-runtime.d.ts +2 -2
- package/dist/connection-runtime.js +29 -18
- package/dist/consent-manager.d.ts +2 -2
- package/dist/consent-manager.js +5 -2
- package/dist/direct-tools.d.ts +1 -1
- package/dist/direct-tools.js +13 -7
- package/dist/elicitation-handler.d.ts +2 -3
- package/dist/elicitation-handler.js +113 -58
- package/dist/gateway-arguments.d.ts +1 -1
- package/dist/gateway-arguments.js +90 -23
- package/dist/glimpse-ui.js +10 -5
- package/dist/host-html-template.d.ts +3 -1
- package/dist/host-html-template.js +9 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +264 -119
- package/dist/init.d.ts +1 -3
- package/dist/init.js +91 -51
- package/dist/legacy-migration.d.ts +1 -1
- package/dist/legacy-migration.js +17 -12
- package/dist/lifecycle.d.ts +3 -3
- package/dist/lifecycle.js +33 -17
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +4 -2
- package/dist/mcp-auth-flow.js +150 -58
- package/dist/mcp-auth.d.ts +7 -7
- package/dist/mcp-auth.js +189 -138
- package/dist/mcp-callback-server.js +19 -8
- package/dist/mcp-code.js +168 -54
- package/dist/mcp-keyring-helper.cjs +57 -23
- package/dist/mcp-oauth-provider.d.ts +6 -7
- package/dist/mcp-oauth-provider.js +146 -72
- package/dist/mcp-oauth-runtime.js +29 -16
- package/dist/mcp-output-guard.js +193 -65
- package/dist/mcp-output-preview.js +41 -20
- package/dist/mcp-panel.d.ts +9 -9
- package/dist/mcp-panel.js +141 -70
- package/dist/mcp-script-worker.mjs +135 -43
- package/dist/mcp-setup-panel.d.ts +8 -8
- package/dist/mcp-setup-panel.js +130 -44
- package/dist/mcp-status.js +14 -7
- package/dist/mcp-trace.d.ts +10 -5
- package/dist/mcp-trace.js +127 -61
- package/dist/metadata-cache.d.ts +3 -4
- package/dist/metadata-cache.js +83 -43
- package/dist/npx-resolver.js +101 -60
- package/dist/onboarding-state.js +4 -2
- package/dist/plain-result-reader.js +78 -31
- package/dist/prompts.js +36 -20
- package/dist/proxy-modes.js +670 -193
- package/dist/resource-tools.js +30 -9
- package/dist/runtime-owner.js +36 -18
- package/dist/sampling-handler.d.ts +1 -2
- package/dist/sampling-handler.js +41 -21
- package/dist/search-ranking.d.ts +1 -1
- package/dist/search-ranking.js +67 -34
- package/dist/server-manager.d.ts +11 -11
- package/dist/server-manager.js +283 -166
- package/dist/session-recovery.js +113 -61
- package/dist/tool-approval.js +9 -3
- package/dist/tool-loader.js +154 -55
- package/dist/tool-metadata.d.ts +1 -1
- package/dist/tool-metadata.js +51 -23
- package/dist/tool-registrar.js +32 -10
- package/dist/tool-result-renderer.js +58 -33
- package/dist/types.d.ts +2 -2
- package/dist/types.js +39 -18
- package/dist/ui-message-history.js +33 -13
- package/dist/ui-resource-handler.d.ts +3 -3
- package/dist/ui-resource-handler.js +37 -21
- package/dist/ui-server.js +101 -43
- package/dist/ui-session.js +66 -29
- package/dist/ui-stream-types.js +14 -4
- package/dist/ui-tool-visibility.js +12 -6
- package/dist/unix-socket-transport.d.ts +1 -2
- package/dist/unix-socket-transport.js +19 -12
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +60 -35
- package/package.json +28 -2
- 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
|
-
|
|
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, {
|
|
193
|
-
|
|
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
|
-
|
|
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 &&
|
|
284
|
-
|
|
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, {
|
|
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]) ||
|
|
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 ||
|
|
309
|
-
|
|
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, {
|
|
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 &&
|
|
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
|
|
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 &&
|
|
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
|
|
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 =
|
|
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 &&
|
|
416
|
-
(
|
|
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
|
|
422
|
-
|
|
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", {
|
|
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 ??
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
|
|
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, {
|
|
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 +
|
|
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) {
|
package/dist/ui-session.js
CHANGED
|
@@ -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
|
|
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 &&
|
|
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
|
-
|
|
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: [
|
|
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: [
|
|
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: {
|
|
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:
|
|
283
|
-
hasStructured:
|
|
284
|
-
hasUpdate:
|
|
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: [
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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");
|
package/dist/ui-stream-types.js
CHANGED
|
@@ -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([
|
|
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
|
|
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
|
-
})
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
20
|
+
}
|
|
21
|
+
if (!values.includes(entry)) {
|
|
17
22
|
values.push(entry);
|
|
23
|
+
}
|
|
18
24
|
}
|
|
19
25
|
return values;
|
|
20
26
|
}
|