@herbertgao/pi-extensions 2026.8.1 → 2026.8.2
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/README.md +5 -2
- package/node_modules/@czottmann/pi-automode/README.md +16 -2
- package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +35 -0
- package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +18 -9
- package/node_modules/@czottmann/pi-automode/docs/defaults.md +6 -2
- package/node_modules/@czottmann/pi-automode/examples/automode.local.json +6 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +11 -7
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +100 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +32 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +78 -3
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +16 -4
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +23 -1
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +22 -1
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +18 -2
- package/node_modules/@czottmann/pi-automode/package.json +4 -4
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +5 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +23 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +99 -0
- package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +30 -0
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +16 -0
- package/node_modules/@dietrichgebert/ponytail/AGENTS.md +32 -0
- package/node_modules/@dietrichgebert/ponytail/LICENSE +21 -0
- package/node_modules/@dietrichgebert/ponytail/README.es.md +298 -0
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +315 -0
- package/node_modules/@dietrichgebert/ponytail/README.md +351 -0
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +21 -0
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +62 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +115 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +27 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +27 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +44 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +21 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +96 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +169 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +98 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +130 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +85 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-statusline.ps1 +24 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-statusline.sh +18 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +77 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +26 -0
- package/node_modules/@dietrichgebert/ponytail/package.json +47 -0
- package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +211 -0
- package/node_modules/@dietrichgebert/ponytail/pi-extension/package.json +8 -0
- package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +60 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +120 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +41 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +44 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +50 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +71 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +57 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -3
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +10 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary.ts +6 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +24 -83
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/markdown-enhance.ts +119 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +73 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-hover.ts +130 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-interaction.ts +65 -542
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-layout.ts +160 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-packets.ts +143 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-scroll.ts +184 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool-diff/diff-renderer.ts +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool-grouping.ts +9 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/transcript-refresh.ts +91 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/agent-summary.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json +1 -0
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-light.json +2 -2
- package/node_modules/@herbertgao/pi-stash/README.md +9 -9
- package/node_modules/@herbertgao/pi-stash/package.json +1 -1
- package/node_modules/@herbertgao/pi-stash/src/index.ts +2 -1
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +14 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +6 -2
- package/node_modules/@herbertgao/pi-subagents/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +85 -11
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +28 -4
- package/node_modules/@herbertgao/pi-subagents/src/settings.ts +12 -0
- package/node_modules/@herbertgao/pi-subagents/src/types.ts +2 -0
- package/node_modules/@narumitw/pi-btw/LICENSE +21 -0
- package/node_modules/@narumitw/pi-btw/README.md +210 -0
- package/node_modules/@narumitw/pi-btw/package.json +53 -0
- package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +632 -0
- package/node_modules/@narumitw/pi-btw/src/btw.ts +996 -0
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +339 -0
- package/node_modules/@narumitw/pi-btw/src/index.ts +1 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +296 -0
- package/node_modules/@narumitw/pi-btw/src/settings.ts +237 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +238 -0
- package/node_modules/@narumitw/pi-btw/src/text.ts +10 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +620 -0
- package/node_modules/@pi-plugins/fast-mode/LICENSE +21 -0
- package/node_modules/@pi-plugins/fast-mode/README.md +92 -0
- package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts +6 -0
- package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -0
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +2026 -0
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -0
- package/node_modules/@pi-plugins/fast-mode/package.json +73 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +59 -0
- package/node_modules/pi-mcp-adapter/README.md +62 -13
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +377 -0
- package/node_modules/pi-mcp-adapter/commands.ts +9 -7
- package/node_modules/pi-mcp-adapter/config.ts +110 -49
- package/node_modules/pi-mcp-adapter/direct-tools.ts +34 -19
- package/node_modules/pi-mcp-adapter/elicitation-handler.ts +37 -36
- package/node_modules/pi-mcp-adapter/errors.ts +21 -17
- package/node_modules/pi-mcp-adapter/host-html-template.ts +13 -7
- package/node_modules/pi-mcp-adapter/index.ts +37 -14
- package/node_modules/pi-mcp-adapter/init.ts +9 -7
- package/node_modules/pi-mcp-adapter/json-schema-validator.ts +2 -2
- package/node_modules/pi-mcp-adapter/lifecycle.ts +8 -8
- package/node_modules/pi-mcp-adapter/logger.ts +1 -1
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +36 -22
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +120 -2
- package/node_modules/pi-mcp-adapter/mcp-code.ts +13 -11
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +19 -13
- package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +15 -11
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +34 -15
- package/node_modules/pi-mcp-adapter/mcp-probe.ts +131 -34
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +2 -2
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +7 -4
- package/node_modules/pi-mcp-adapter/mcp-trace.ts +59 -64
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +34 -26
- package/node_modules/pi-mcp-adapter/npx-resolver.ts +6 -4
- package/node_modules/pi-mcp-adapter/oauth-handler.ts +1 -1
- package/node_modules/pi-mcp-adapter/onboarding-state.ts +19 -11
- package/node_modules/pi-mcp-adapter/package.json +13 -8
- package/node_modules/pi-mcp-adapter/panel-keys.ts +20 -4
- package/node_modules/pi-mcp-adapter/prompts.ts +2 -2
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +48 -11
- package/node_modules/pi-mcp-adapter/sampling-handler.ts +22 -15
- package/node_modules/pi-mcp-adapter/search-ranking.ts +2 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +165 -113
- package/node_modules/pi-mcp-adapter/session-recovery.ts +11 -12
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +3 -3
- package/node_modules/pi-mcp-adapter/state.ts +3 -1
- package/node_modules/pi-mcp-adapter/tool-approval.ts +67 -4
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +6 -5
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +14 -2
- package/node_modules/pi-mcp-adapter/ts-shape.ts +9 -2
- package/node_modules/pi-mcp-adapter/types.ts +109 -22
- package/node_modules/pi-mcp-adapter/ui-app-bridge-helpers.ts +40 -0
- package/node_modules/pi-mcp-adapter/ui-resource-handler.ts +28 -19
- package/node_modules/pi-mcp-adapter/ui-server.ts +90 -58
- package/node_modules/pi-mcp-adapter/ui-session.ts +16 -14
- package/node_modules/pi-mcp-adapter/unix-socket-transport.ts +4 -4
- package/node_modules/pi-mcp-adapter/utils.ts +23 -9
- package/package.json +22 -7
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
-
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
4
2
|
import {
|
|
3
|
+
Client,
|
|
4
|
+
SdkHttpError,
|
|
5
|
+
SSEClientTransport,
|
|
5
6
|
StreamableHTTPClientTransport,
|
|
6
|
-
|
|
7
|
-
} from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
8
|
-
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
9
|
-
import type { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js";
|
|
10
|
-
import {
|
|
11
|
-
ElicitationCompleteNotificationSchema,
|
|
7
|
+
UnauthorizedError,
|
|
12
8
|
type GetPromptResult,
|
|
13
9
|
type ReadResourceResult,
|
|
10
|
+
type RequestOptions,
|
|
14
11
|
type UrlElicitationRequiredError,
|
|
15
|
-
|
|
12
|
+
type VersionNegotiationOptions,
|
|
13
|
+
} from "@modelcontextprotocol/client";
|
|
14
|
+
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
16
15
|
import { UnixSocketClientTransport } from "./unix-socket-transport.ts";
|
|
17
16
|
import { probeMcpEndpoint } from "./mcp-probe.ts";
|
|
18
17
|
import {
|
|
@@ -24,6 +23,7 @@ import {
|
|
|
24
23
|
type ServerStreamResultPatchNotification,
|
|
25
24
|
type Transport,
|
|
26
25
|
type McpTraceSettings,
|
|
26
|
+
SERVER_STREAM_RESULT_PATCH_METHOD,
|
|
27
27
|
serverStreamResultPatchNotificationSchema,
|
|
28
28
|
} from "./types.ts";
|
|
29
29
|
import { resolveNpxBinary } from "./npx-resolver.ts";
|
|
@@ -67,7 +67,26 @@ type HttpAuthProviderState =
|
|
|
67
67
|
| { status: "implicit-challenged"; provider: McpOAuthProvider };
|
|
68
68
|
|
|
69
69
|
function isUnauthorizedHttpError(error: unknown): boolean {
|
|
70
|
-
return error instanceof UnauthorizedError || (error instanceof
|
|
70
|
+
return error instanceof UnauthorizedError || (error instanceof SdkHttpError && error.status === 401);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function shouldFallbackToSse(error: unknown, definition: ServerDefinition): boolean {
|
|
74
|
+
if (definition.protocolVersion === "2026-07-28") return false;
|
|
75
|
+
return error instanceof SdkHttpError && [404, 405, 406, 415].includes(error.status);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveVersionNegotiation(definition: ServerDefinition): VersionNegotiationOptions | undefined {
|
|
79
|
+
switch (definition.protocolVersion) {
|
|
80
|
+
case undefined:
|
|
81
|
+
case "legacy":
|
|
82
|
+
return undefined;
|
|
83
|
+
case "auto":
|
|
84
|
+
return { mode: "auto" };
|
|
85
|
+
case "2026-07-28":
|
|
86
|
+
return { mode: { pin: "2026-07-28" } };
|
|
87
|
+
default:
|
|
88
|
+
throw new Error(`Invalid MCP protocolVersion: ${String(definition.protocolVersion)}`);
|
|
89
|
+
}
|
|
71
90
|
}
|
|
72
91
|
|
|
73
92
|
function boundedStderrChunk(chunk: Buffer | string): Buffer {
|
|
@@ -307,7 +326,6 @@ export class McpServerManager {
|
|
|
307
326
|
requestSignal?: AbortSignal,
|
|
308
327
|
): Promise<ServerConnection> {
|
|
309
328
|
throwIfAborted(signal);
|
|
310
|
-
const client = this.createClient(name);
|
|
311
329
|
|
|
312
330
|
const tracingEnabled = isMcpTraceEnabled(definition, this.traceSettings);
|
|
313
331
|
const traceWriter = tracingEnabled
|
|
@@ -317,7 +335,10 @@ export class McpServerManager {
|
|
|
317
335
|
? { record: event => traceWriter.write(event) }
|
|
318
336
|
: undefined;
|
|
319
337
|
|
|
338
|
+
let client: Client;
|
|
320
339
|
let transport: Transport;
|
|
340
|
+
let clientConnected = false;
|
|
341
|
+
let transportAlreadyTraced = false;
|
|
321
342
|
let stderrTail: Buffer<ArrayBufferLike> = Buffer.alloc(0);
|
|
322
343
|
const configuredTransports = [definition.command, definition.url, definition.socket]
|
|
323
344
|
.filter(value => typeof value === "string" && value.length > 0);
|
|
@@ -325,7 +346,10 @@ export class McpServerManager {
|
|
|
325
346
|
throw new Error(`Server ${name} must configure exactly one of command, url, or socket`);
|
|
326
347
|
}
|
|
327
348
|
|
|
349
|
+
const requestOptions = this.buildRequestOptions(definition, requestSignal);
|
|
350
|
+
|
|
328
351
|
if (definition.command) {
|
|
352
|
+
client = this.createClient(name, definition);
|
|
329
353
|
let command = definition.command;
|
|
330
354
|
let args = definition.args ?? [];
|
|
331
355
|
|
|
@@ -339,11 +363,13 @@ export class McpServerManager {
|
|
|
339
363
|
}
|
|
340
364
|
throwIfAborted(signal);
|
|
341
365
|
|
|
366
|
+
if (definition.pluginDataDir) mkdirSync(definition.pluginDataDir, { recursive: true });
|
|
367
|
+
const cwd = resolveConfigPath(definition.cwd) ?? this.defaultCwd;
|
|
342
368
|
const stdioTransport = new StdioClientTransport({
|
|
343
369
|
command,
|
|
344
370
|
args,
|
|
345
|
-
env: resolveEnv(definition.env, name),
|
|
346
|
-
cwd
|
|
371
|
+
env: resolveEnv(definition.env, name, definition.literalEnv === true),
|
|
372
|
+
...(cwd !== undefined ? { cwd } : {}),
|
|
347
373
|
stderr: definition.debug ? "inherit" : "pipe",
|
|
348
374
|
});
|
|
349
375
|
// Keep non-debug child diagnostics available for connection failures without
|
|
@@ -355,24 +381,48 @@ export class McpServerManager {
|
|
|
355
381
|
}
|
|
356
382
|
transport = stdioTransport;
|
|
357
383
|
} else if (definition.url) {
|
|
358
|
-
|
|
359
|
-
|
|
384
|
+
const httpConnection = await this.connectHttpClient(
|
|
385
|
+
definition,
|
|
386
|
+
name,
|
|
387
|
+
requestOptions,
|
|
388
|
+
signal,
|
|
389
|
+
traceObserver,
|
|
390
|
+
);
|
|
391
|
+
client = httpConnection.client;
|
|
392
|
+
transport = httpConnection.transport;
|
|
393
|
+
if (httpConnection.status === "needs-auth") {
|
|
394
|
+
return {
|
|
395
|
+
client,
|
|
396
|
+
transport,
|
|
397
|
+
definition,
|
|
398
|
+
tools: [],
|
|
399
|
+
resources: [],
|
|
400
|
+
prompts: [],
|
|
401
|
+
lastUsedAt: Date.now(),
|
|
402
|
+
inFlight: 0,
|
|
403
|
+
status: "needs-auth",
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
clientConnected = true;
|
|
407
|
+
transportAlreadyTraced = traceObserver !== undefined;
|
|
360
408
|
} else {
|
|
409
|
+
client = this.createClient(name, definition);
|
|
361
410
|
transport = new UnixSocketClientTransport(resolveConfigPath(definition.socket!)!);
|
|
362
411
|
}
|
|
363
412
|
|
|
364
|
-
if (traceObserver) {
|
|
413
|
+
if (traceObserver && !transportAlreadyTraced) {
|
|
365
414
|
const traceTransportKindValue = traceTransportKind(definition, transport);
|
|
366
415
|
transport = wrapTransportWithMcpTrace(transport, name, traceTransportKindValue, traceObserver);
|
|
367
416
|
}
|
|
368
417
|
|
|
369
|
-
throwIfAborted(signal);
|
|
370
|
-
const requestOptions = this.buildRequestOptions(definition, requestSignal);
|
|
371
|
-
|
|
372
418
|
try {
|
|
373
|
-
|
|
419
|
+
throwIfAborted(signal);
|
|
420
|
+
if (!clientConnected) {
|
|
421
|
+
await this.connectClientWithAbort(client, transport, requestOptions, signal);
|
|
422
|
+
}
|
|
374
423
|
this.attachAdapterNotificationHandlers(name, client);
|
|
375
424
|
|
|
425
|
+
const instructions = client.getInstructions?.();
|
|
376
426
|
const connection: ServerConnection = {
|
|
377
427
|
client,
|
|
378
428
|
transport,
|
|
@@ -380,22 +430,15 @@ export class McpServerManager {
|
|
|
380
430
|
tools: [],
|
|
381
431
|
resources: [],
|
|
382
432
|
prompts: [],
|
|
383
|
-
instructions:
|
|
433
|
+
...(instructions !== undefined ? { instructions } : {}),
|
|
384
434
|
lastUsedAt: Date.now(),
|
|
385
435
|
inFlight: 0,
|
|
386
436
|
status: "connected",
|
|
387
437
|
};
|
|
388
438
|
|
|
389
439
|
// Reflect the SDK's own close signal in connection status, guarded by
|
|
390
|
-
// identity so a stale connection's late close
|
|
391
|
-
// connection
|
|
392
|
-
// clobber a fresh connection that has since taken its place in
|
|
393
|
-
// `this.connections`. This intentionally uses `client.onclose`
|
|
394
|
-
// (Protocol's public hook), not `transport.onclose` — the SDK's
|
|
395
|
-
// Protocol takes ownership of that one internally for pending-request
|
|
396
|
-
// rejection, and overwriting it would break that. `client.onerror` is
|
|
397
|
-
// avoided too: it can fire on benign events (e.g. the optional GET
|
|
398
|
-
// SSE stream failing) that don't mean the connection is closed.
|
|
440
|
+
// identity so a stale connection's late close can never clobber a fresh
|
|
441
|
+
// connection. The SDK client owns the transport callbacks.
|
|
399
442
|
client.onclose = () => {
|
|
400
443
|
if (this.connections.get(name) === connection) {
|
|
401
444
|
connection.status = "closed";
|
|
@@ -417,8 +460,7 @@ export class McpServerManager {
|
|
|
417
460
|
return connection;
|
|
418
461
|
} catch (error) {
|
|
419
462
|
// If connectClientWithAbort closed the transport, await that exact close.
|
|
420
|
-
// Otherwise the SDK client owns its transport
|
|
421
|
-
// single cleanup operation rather than closing the transport twice.
|
|
463
|
+
// Otherwise the SDK client owns its transport and performs cleanup once.
|
|
422
464
|
const abortCleanup = abortCleanupPromises.get(transport);
|
|
423
465
|
const abortCleanupFailed = error instanceof AggregateError && error.message === "MCP connection abort cleanup failed";
|
|
424
466
|
const cleanupResults = abortCleanupFailed
|
|
@@ -432,8 +474,8 @@ export class McpServerManager {
|
|
|
432
474
|
reportedError = new AggregateError([error, ...cleanupFailures], "MCP connection setup failed");
|
|
433
475
|
}
|
|
434
476
|
|
|
435
|
-
//
|
|
436
|
-
//
|
|
477
|
+
// A cleanup failure remains a setup failure rather than being hidden
|
|
478
|
+
// behind needs-auth.
|
|
437
479
|
if (isUnauthorizedHttpError(error) && supportsOAuth(definition) && cleanupFailures.length === 0) {
|
|
438
480
|
return {
|
|
439
481
|
client,
|
|
@@ -515,13 +557,15 @@ export class McpServerManager {
|
|
|
515
557
|
};
|
|
516
558
|
}
|
|
517
559
|
|
|
518
|
-
private createClient(serverName: string): Client {
|
|
560
|
+
private createClient(serverName: string, definition: ServerDefinition): Client {
|
|
519
561
|
const capabilities = this.buildClientCapabilities();
|
|
562
|
+
const versionNegotiation = resolveVersionNegotiation(definition);
|
|
520
563
|
let client: Client;
|
|
521
564
|
client = new Client(
|
|
522
565
|
{ name: `pi-mcp-${serverName}`, version: "1.0.0" },
|
|
523
566
|
{
|
|
524
567
|
jsonSchemaValidator: createJsonSchemaValidator(),
|
|
568
|
+
...(versionNegotiation ? { versionNegotiation } : {}),
|
|
525
569
|
...(Object.keys(capabilities).length > 0 ? { capabilities } : {}),
|
|
526
570
|
listChanged: {
|
|
527
571
|
tools: {
|
|
@@ -552,7 +596,7 @@ export class McpServerManager {
|
|
|
552
596
|
onUrlAccepted: elicitationId => this.rememberUrlElicitation(serverName, elicitationId),
|
|
553
597
|
});
|
|
554
598
|
if (this.elicitationConfig.allowUrl) {
|
|
555
|
-
client.setNotificationHandler(
|
|
599
|
+
client.setNotificationHandler("notifications/elicitation/complete", notification => {
|
|
556
600
|
if (this.runtimeSignal?.aborted) return;
|
|
557
601
|
const accepted = this.acceptedUrlElicitations.get(serverName);
|
|
558
602
|
if (!accepted?.delete(notification.params.elicitationId)) return;
|
|
@@ -644,18 +688,19 @@ export class McpServerManager {
|
|
|
644
688
|
accepted.add(elicitationId);
|
|
645
689
|
}
|
|
646
690
|
|
|
647
|
-
private async
|
|
691
|
+
private async connectHttpClient(
|
|
648
692
|
definition: ServerDefinition,
|
|
649
693
|
serverName: string,
|
|
694
|
+
requestOptions: RequestOptions | undefined,
|
|
650
695
|
signal?: AbortSignal,
|
|
651
|
-
requestSignal?: AbortSignal,
|
|
652
696
|
traceObserver?: McpTraceObserver,
|
|
653
|
-
): Promise<Transport> {
|
|
697
|
+
): Promise<{ client: Client; transport: Transport; status: "connected" | "needs-auth" }> {
|
|
654
698
|
throwIfAborted(signal);
|
|
655
699
|
const serverUrl = resolveServerUrl(definition)!;
|
|
656
700
|
const url = new URL(serverUrl);
|
|
657
701
|
|
|
658
|
-
// Resolve secret commands only for this connection attempt, without
|
|
702
|
+
// Resolve secret commands only for this connection attempt, without
|
|
703
|
+
// mutating the persisted configuration.
|
|
659
704
|
const hasCommandHeader = Object.values(definition.headers ?? {})
|
|
660
705
|
.some(value => value.startsWith("!") && !value.startsWith("!!"));
|
|
661
706
|
const headers = resolveCommandSecretsRecord(
|
|
@@ -663,7 +708,8 @@ export class McpServerManager {
|
|
|
663
708
|
key => `MCP server "${serverName}" HTTP header "${key}"`,
|
|
664
709
|
) ?? {};
|
|
665
710
|
|
|
666
|
-
//
|
|
711
|
+
// Resolve bearer auth before creating requestInit so every attempted
|
|
712
|
+
// transport receives the same headers.
|
|
667
713
|
const commandBearer = definition.bearerToken?.startsWith("!") && !definition.bearerToken.startsWith("!!")
|
|
668
714
|
? definition.bearerToken
|
|
669
715
|
: undefined;
|
|
@@ -682,98 +728,94 @@ export class McpServerManager {
|
|
|
682
728
|
}
|
|
683
729
|
}
|
|
684
730
|
|
|
685
|
-
// Create request init with headers (Authorization now included for bearer auth)
|
|
686
731
|
const requestInit = Object.keys(headers).length > 0 ? { headers } : undefined;
|
|
687
732
|
const createAuthProvider = (): McpOAuthProvider => new McpOAuthProvider(
|
|
688
733
|
serverName,
|
|
689
734
|
serverUrl,
|
|
690
735
|
extractOAuthConfig(definition),
|
|
691
|
-
{
|
|
692
|
-
onRedirect: async (_authUrl) => {
|
|
693
|
-
// URL is captured by startAuth, no need to log
|
|
694
|
-
},
|
|
695
|
-
},
|
|
736
|
+
{ onRedirect: async () => {} },
|
|
696
737
|
this.authStorageOptions,
|
|
697
738
|
this.oauthRuntime?.signal,
|
|
698
739
|
);
|
|
699
740
|
|
|
700
|
-
// Explicit OAuth
|
|
741
|
+
// Explicit OAuth checks secure storage immediately. Implicit OAuth defers
|
|
742
|
+
// provider construction until the server proves authentication is needed.
|
|
701
743
|
let authState: HttpAuthProviderState = supportsOAuth(definition)
|
|
702
744
|
? definition.auth === undefined
|
|
703
745
|
? { status: "implicit-deferred" }
|
|
704
746
|
: { status: "explicit", provider: createAuthProvider() }
|
|
705
747
|
: { status: "disabled" };
|
|
706
748
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
749
|
+
const attempt = async (
|
|
750
|
+
kind: "streamable-http" | "sse",
|
|
751
|
+
): Promise<
|
|
752
|
+
| { status: "connected"; client: Client; transport: Transport }
|
|
753
|
+
| { status: "failed"; client: Client; transport: Transport; error: unknown }
|
|
754
|
+
> => {
|
|
710
755
|
const authProvider = "provider" in authState ? authState.provider : undefined;
|
|
711
|
-
const
|
|
712
|
-
requestInit,
|
|
713
|
-
authProvider,
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
probeCleanupAttempted = true;
|
|
728
|
-
try {
|
|
729
|
-
await testClient.close();
|
|
730
|
-
} catch (cleanupError) {
|
|
731
|
-
throw new AggregateError([cleanupError], "MCP HTTP probe cleanup failed");
|
|
732
|
-
}
|
|
756
|
+
const transportOptions = {
|
|
757
|
+
...(requestInit !== undefined ? { requestInit } : {}),
|
|
758
|
+
...(authProvider !== undefined ? { authProvider } : {}),
|
|
759
|
+
...(authProvider !== undefined
|
|
760
|
+
&& definition.oauth !== false
|
|
761
|
+
&& definition.oauth?.skipIssuerMetadataValidation === true
|
|
762
|
+
? { skipIssuerMetadataValidation: true }
|
|
763
|
+
: {}),
|
|
764
|
+
};
|
|
765
|
+
const baseTransport: Transport = kind === "streamable-http"
|
|
766
|
+
? new StreamableHTTPClientTransport(url, transportOptions)
|
|
767
|
+
: new SSEClientTransport(url, transportOptions);
|
|
768
|
+
const transport = traceObserver
|
|
769
|
+
? wrapTransportWithMcpTrace(baseTransport, serverName, kind, traceObserver)
|
|
770
|
+
: baseTransport;
|
|
771
|
+
const client = this.createClient(serverName, definition);
|
|
733
772
|
|
|
734
|
-
|
|
735
|
-
|
|
773
|
+
try {
|
|
774
|
+
await this.connectClientWithAbort(client, transport, requestOptions, signal);
|
|
775
|
+
return { status: "connected", client, transport };
|
|
736
776
|
} catch (error) {
|
|
737
|
-
|
|
738
|
-
error.message === "MCP connection abort cleanup failed"
|
|
739
|
-
|
|
740
|
-
)) {
|
|
741
|
-
throw error;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// StreamableHTTP failed, close through the SDK client and try SSE fallback.
|
|
745
|
-
// If connectClientWithAbort already owned the close, await that operation
|
|
746
|
-
// instead of closing the same transport twice.
|
|
747
|
-
if (!probeCleanupAttempted) {
|
|
748
|
-
probeCleanupAttempted = true;
|
|
777
|
+
const abortCleanupFailed = error instanceof AggregateError
|
|
778
|
+
&& error.message === "MCP connection abort cleanup failed";
|
|
779
|
+
if (!abortCleanupFailed) {
|
|
749
780
|
try {
|
|
750
|
-
await (abortCleanupPromises.get(
|
|
781
|
+
await (abortCleanupPromises.get(transport) ?? client.close());
|
|
751
782
|
} catch (cleanupError) {
|
|
752
|
-
throw new AggregateError([error, cleanupError], "MCP HTTP
|
|
783
|
+
throw new AggregateError([error, cleanupError], "MCP HTTP connection cleanup failed");
|
|
753
784
|
}
|
|
754
785
|
}
|
|
786
|
+
return { status: "failed", client, transport, error };
|
|
787
|
+
}
|
|
788
|
+
};
|
|
755
789
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
790
|
+
// Connect the real client once. Retry Streamable HTTP only for an implicit
|
|
791
|
+
// OAuth challenge; use SSE only for definitive endpoint incompatibility.
|
|
792
|
+
// Agent Plugins set httpTransport, and their declared transport is used without fallback.
|
|
793
|
+
let kind: "streamable-http" | "sse" = definition.httpTransport ?? "streamable-http";
|
|
794
|
+
for (;;) {
|
|
795
|
+
const result = await attempt(kind);
|
|
796
|
+
if (result.status === "connected") return result;
|
|
797
|
+
if (result.error instanceof AggregateError
|
|
798
|
+
&& result.error.message === "MCP connection abort cleanup failed") {
|
|
799
|
+
throw result.error;
|
|
800
|
+
}
|
|
801
|
+
if (signal?.aborted) throwIfAborted(signal);
|
|
768
802
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
803
|
+
if (authState.status === "implicit-deferred" && isUnauthorizedHttpError(result.error)) {
|
|
804
|
+
authState = { status: "implicit-challenged", provider: createAuthProvider() };
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
if (isUnauthorizedHttpError(result.error)) {
|
|
808
|
+
if (supportsOAuth(definition)) {
|
|
809
|
+
return { client: result.client, transport: result.transport, status: "needs-auth" };
|
|
772
810
|
}
|
|
811
|
+
throw result.error;
|
|
812
|
+
}
|
|
773
813
|
|
|
774
|
-
|
|
775
|
-
|
|
814
|
+
if (definition.httpTransport === undefined && kind === "streamable-http" && shouldFallbackToSse(result.error, definition)) {
|
|
815
|
+
kind = "sse";
|
|
816
|
+
continue;
|
|
776
817
|
}
|
|
818
|
+
throw result.error;
|
|
777
819
|
}
|
|
778
820
|
}
|
|
779
821
|
|
|
@@ -839,11 +881,15 @@ export class McpServerManager {
|
|
|
839
881
|
}
|
|
840
882
|
|
|
841
883
|
private attachAdapterNotificationHandlers(serverName: string, client: Client): void {
|
|
842
|
-
client.setNotificationHandler(
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
884
|
+
client.setNotificationHandler(
|
|
885
|
+
SERVER_STREAM_RESULT_PATCH_METHOD,
|
|
886
|
+
{ params: serverStreamResultPatchNotificationSchema.shape.params },
|
|
887
|
+
params => {
|
|
888
|
+
const listener = this.uiStreamListeners.get(params.streamToken);
|
|
889
|
+
if (!listener) return;
|
|
890
|
+
listener(serverName, params);
|
|
891
|
+
},
|
|
892
|
+
);
|
|
847
893
|
}
|
|
848
894
|
|
|
849
895
|
registerUiStreamListener(streamToken: string, listener: UiStreamListener): void {
|
|
@@ -984,6 +1030,10 @@ export class McpServerManager {
|
|
|
984
1030
|
return false;
|
|
985
1031
|
}
|
|
986
1032
|
|
|
1033
|
+
isConnecting(name: string): boolean {
|
|
1034
|
+
return this.connectPromises.has(name);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
987
1037
|
getConnection(name: string): ServerConnection | undefined {
|
|
988
1038
|
return this.connections.get(name);
|
|
989
1039
|
}
|
|
@@ -1024,11 +1074,13 @@ export class McpServerManager {
|
|
|
1024
1074
|
/**
|
|
1025
1075
|
* Resolve environment variables with interpolation.
|
|
1026
1076
|
*/
|
|
1027
|
-
function resolveEnv(env: Record<string, string> | undefined, serverName: string): Record<string, string> {
|
|
1077
|
+
function resolveEnv(env: Record<string, string> | undefined, serverName: string, literalEnv = false): Record<string, string> {
|
|
1028
1078
|
const resolved: Record<string, string> = {};
|
|
1029
1079
|
for (const [key, value] of Object.entries(process.env)) {
|
|
1030
1080
|
if (value !== undefined) resolved[key] = value;
|
|
1031
1081
|
}
|
|
1082
|
+
if (literalEnv) return env ? { ...resolved, ...env } : resolved;
|
|
1083
|
+
|
|
1032
1084
|
const overrides = resolveCommandSecretsRecord(
|
|
1033
1085
|
env,
|
|
1034
1086
|
key => `MCP server "${serverName}" stdio env "${key}"`,
|
|
@@ -20,8 +20,7 @@
|
|
|
20
20
|
// many things other than "your session is gone"
|
|
21
21
|
// - treat generic -32000/ConnectionClosed errors as session expiry
|
|
22
22
|
// - treat AbortError/cancellation as a session failure
|
|
23
|
-
import {
|
|
24
|
-
import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
23
|
+
import { ProtocolError, SdkHttpError } from "@modelcontextprotocol/client";
|
|
25
24
|
import { logger } from "./logger.ts";
|
|
26
25
|
import { throwIfAborted } from "./abort.ts";
|
|
27
26
|
import { isServerDisabled, type McpConfig } from "./types.ts";
|
|
@@ -34,25 +33,25 @@ import type { McpServerManager, ServerConnection } from "./server-manager.ts";
|
|
|
34
33
|
* gate response some servers emit before dispatching to a handler.
|
|
35
34
|
*
|
|
36
35
|
* `hadSessionId` must reflect the transport's session id from *before* the
|
|
37
|
-
* call that produced `err` was made.
|
|
38
|
-
*
|
|
39
|
-
* before the call rather than rely on catch-time transport state.
|
|
36
|
+
* call that produced `err` was made. Callers must capture it before the call
|
|
37
|
+
* rather than rely on catch-time transport state.
|
|
40
38
|
*/
|
|
39
|
+
const CONNECTION_CLOSED_PROTOCOL_CODE = -32000;
|
|
41
40
|
const SERVER_NOT_INITIALIZED_MCP_MESSAGES = new Set([
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
"Server not initialized",
|
|
42
|
+
"Bad Request: Server not initialized",
|
|
44
43
|
]);
|
|
45
44
|
|
|
46
45
|
export function isTerminatedSession(err: unknown, hadSessionId: boolean): boolean {
|
|
47
46
|
if (!hadSessionId) return false;
|
|
48
|
-
if (err instanceof
|
|
49
|
-
return err.
|
|
50
|
-
|| (err.
|
|
47
|
+
if (err instanceof SdkHttpError) {
|
|
48
|
+
return err.status === 404
|
|
49
|
+
|| (err.status === 400
|
|
51
50
|
&& /"code"\s*:\s*-32000/.test(err.message)
|
|
52
51
|
&& /"message"\s*:\s*"Bad Request: Server not initialized"/.test(err.message));
|
|
53
52
|
}
|
|
54
|
-
return err instanceof
|
|
55
|
-
&& err.code ===
|
|
53
|
+
return err instanceof ProtocolError
|
|
54
|
+
&& err.code === CONNECTION_CLOSED_PROTOCOL_CODE
|
|
56
55
|
&& SERVER_NOT_INITIALIZED_MCP_MESSAGES.has(err.message);
|
|
57
56
|
}
|
|
58
57
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mcp-scripting
|
|
3
|
-
description: Write
|
|
3
|
+
description: Write mcpScript JavaScript for discovering, inspecting, and calling MCP tools.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# MCP scripting
|
|
7
7
|
|
|
8
|
-
For multi-call MCP work, write ordinary JavaScript with loops, filtering, chaining, fan-out, or other logic between calls. Run that source with `
|
|
8
|
+
For multi-call MCP work, write ordinary JavaScript with loops, filtering, chaining, fan-out, or other logic between calls. Run that source with `mcpScript`; it is the primary MCP orchestration surface. For a single MCP search, describe, status check, auth action, or tool call, use `mcp` instead.
|
|
9
9
|
|
|
10
|
-
Write the source naturally, then pass it as `
|
|
10
|
+
Write the source naturally, then pass it as `mcpScript`'s `code` argument:
|
|
11
11
|
|
|
12
12
|
```js
|
|
13
13
|
const { items } = await tools.search({ query: "search issues", server: "github" });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { ConsentManager } from "./consent-manager.ts";
|
|
3
3
|
import type { McpLifecycleManager } from "./lifecycle.ts";
|
|
4
4
|
import type { McpServerManager } from "./server-manager.ts";
|
|
@@ -48,6 +48,8 @@ export interface McpExtensionState {
|
|
|
48
48
|
failureMessages: Map<string, string>;
|
|
49
49
|
/** Session-only approvals keyed by server and original tool name. */
|
|
50
50
|
approvedToolCalls: Map<string, true>;
|
|
51
|
+
/** Shared event bus used by permission extensions to broker MCP approvals. */
|
|
52
|
+
approvalEvents?: ExtensionAPI["events"];
|
|
51
53
|
uiResourceHandler: UiResourceHandler;
|
|
52
54
|
consentManager: ConsentManager;
|
|
53
55
|
uiServer: UiServerHandle | null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { abortable } from "./abort.ts";
|
|
2
3
|
import { combineAbortSignals } from "./runtime-owner.ts";
|
|
3
4
|
import type { McpExtensionState } from "./state.ts";
|
|
@@ -5,7 +6,12 @@ import {
|
|
|
5
6
|
getToolNameCandidates,
|
|
6
7
|
matchesToolPattern,
|
|
7
8
|
resolveToolPrefix,
|
|
9
|
+
MCP_TOOL_APPROVAL_REQUEST_EVENT,
|
|
8
10
|
type McpConfig,
|
|
11
|
+
type McpToolApprovalDecision,
|
|
12
|
+
type McpToolApprovalHandler,
|
|
13
|
+
type McpToolApprovalOrigin,
|
|
14
|
+
type McpToolApprovalRequest,
|
|
9
15
|
type ToolMetadata,
|
|
10
16
|
} from "./types.ts";
|
|
11
17
|
import { sanitizeTerminalText } from "./utils.ts";
|
|
@@ -34,19 +40,76 @@ export function isToolCallApprovalRequired(
|
|
|
34
40
|
);
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
function isMcpToolApprovalDecision(value: unknown): value is McpToolApprovalDecision {
|
|
44
|
+
return value === "allow_once"
|
|
45
|
+
|| value === "allow_for_session"
|
|
46
|
+
|| value === "deny"
|
|
47
|
+
|| value === "abstain";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function requestBrokerApproval(
|
|
51
|
+
state: McpExtensionState,
|
|
52
|
+
serverName: string,
|
|
53
|
+
toolMeta: ToolMetadata,
|
|
54
|
+
args: Record<string, unknown> | undefined,
|
|
55
|
+
origin: McpToolApprovalOrigin,
|
|
56
|
+
signal?: AbortSignal,
|
|
57
|
+
): Promise<McpToolApprovalDecision> {
|
|
58
|
+
if (!state.approvalEvents) return "abstain";
|
|
59
|
+
|
|
60
|
+
let acceptingClaim = true;
|
|
61
|
+
let handler: McpToolApprovalHandler | undefined;
|
|
62
|
+
const request: McpToolApprovalRequest = {
|
|
63
|
+
requestId: randomUUID(),
|
|
64
|
+
serverName,
|
|
65
|
+
originalToolName: toolMeta.originalName,
|
|
66
|
+
prefixedToolName: toolMeta.name,
|
|
67
|
+
args: args ?? {},
|
|
68
|
+
origin,
|
|
69
|
+
...(signal !== undefined ? { signal } : {}),
|
|
70
|
+
claim(candidate: McpToolApprovalHandler) {
|
|
71
|
+
if (!acceptingClaim || handler) return false;
|
|
72
|
+
handler = candidate;
|
|
73
|
+
return true;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
state.approvalEvents.emit(MCP_TOOL_APPROVAL_REQUEST_EVENT, request);
|
|
78
|
+
acceptingClaim = false;
|
|
79
|
+
if (!handler) return "abstain";
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const decision = await abortable(Promise.resolve().then(handler), signal);
|
|
83
|
+
return isMcpToolApprovalDecision(decision) ? decision : "deny";
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (signal?.aborted) throw error;
|
|
86
|
+
return "deny";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
37
90
|
export async function ensureToolCallApproved(
|
|
38
91
|
state: McpExtensionState,
|
|
39
92
|
serverName: string,
|
|
40
93
|
toolMeta: ToolMetadata,
|
|
41
94
|
args: Record<string, unknown> | undefined,
|
|
42
95
|
signal?: AbortSignal,
|
|
96
|
+
origin: McpToolApprovalOrigin = toolMeta.resourceUri ? "resource" : "proxy",
|
|
43
97
|
): Promise<ToolCallApprovalResult> {
|
|
44
|
-
|
|
98
|
+
const cacheKey = `${serverName}\u0000${toolMeta.originalName}`;
|
|
99
|
+
const approvedToolCalls = state.approvedToolCalls ??= new Map<string, true>();
|
|
100
|
+
if (approvedToolCalls.has(cacheKey)) {
|
|
45
101
|
return { ok: true };
|
|
46
102
|
}
|
|
47
103
|
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
104
|
+
const brokerDecision = await requestBrokerApproval(state, serverName, toolMeta, args, origin, signal);
|
|
105
|
+
if (brokerDecision === "allow_once") return { ok: true };
|
|
106
|
+
if (brokerDecision === "allow_for_session") {
|
|
107
|
+
approvedToolCalls.set(cacheKey, true);
|
|
108
|
+
return { ok: true };
|
|
109
|
+
}
|
|
110
|
+
if (brokerDecision === "deny") return { ok: false, reason: "denied" };
|
|
111
|
+
|
|
112
|
+
if (!isToolCallApprovalRequired(state.config, serverName, toolMeta)) {
|
|
50
113
|
return { ok: true };
|
|
51
114
|
}
|
|
52
115
|
|
|
@@ -71,7 +134,7 @@ export async function ensureToolCallApproved(
|
|
|
71
134
|
return { ok: true };
|
|
72
135
|
}
|
|
73
136
|
if (decision === "Allow for session") {
|
|
74
|
-
|
|
137
|
+
approvedToolCalls.set(cacheKey, true);
|
|
75
138
|
return { ok: true };
|
|
76
139
|
}
|
|
77
140
|
return { ok: false, reason: "denied" };
|