@fitchmultz/pi-mcp-adapter 6.0.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/OAUTH.md +1 -1
  3. package/README.md +27 -18
  4. package/dist/app-bridge.bundle.js +14 -14
  5. package/dist/config.js +41 -16
  6. package/dist/direct-tools.js +1 -1
  7. package/dist/gateway-arguments.d.ts +1 -1
  8. package/dist/index.js +38 -59
  9. package/dist/lifecycle.d.ts +2 -2
  10. package/dist/lifecycle.js +2 -2
  11. package/dist/mcp-auth-flow.js +2 -2
  12. package/dist/mcp-auth.d.ts +1 -1
  13. package/dist/mcp-auth.js +3 -3
  14. package/dist/mcp-code.d.ts +6 -6
  15. package/dist/mcp-code.js +11 -10
  16. package/dist/mcp-oauth-provider.d.ts +5 -5
  17. package/dist/mcp-oauth-provider.js +5 -5
  18. package/dist/mcp-output-guard.d.ts +1 -1
  19. package/dist/mcp-output-guard.js +1 -1
  20. package/dist/mcp-panel.d.ts +2 -2
  21. package/dist/mcp-panel.js +2 -2
  22. package/dist/mcp-setup-panel.d.ts +5 -4
  23. package/dist/mcp-setup-panel.js +15 -7
  24. package/dist/mcp-trace.d.ts +1 -1
  25. package/dist/mcp-trace.js +1 -1
  26. package/dist/npx-resolver.js +8 -4
  27. package/dist/proxy-modes.d.ts +2 -2
  28. package/dist/proxy-modes.js +20 -19
  29. package/dist/sampling-handler.js +1 -0
  30. package/dist/search-ranking.js +6 -2
  31. package/dist/server-manager.d.ts +2 -2
  32. package/dist/server-manager.js +44 -27
  33. package/dist/session-recovery.d.ts +7 -4
  34. package/dist/session-recovery.js +76 -10
  35. package/dist/tool-loader.d.ts +7 -2
  36. package/dist/tool-loader.js +56 -5
  37. package/dist/tool-metadata.d.ts +43 -43
  38. package/dist/tool-result-renderer.d.ts +3 -3
  39. package/dist/tool-result-renderer.js +3 -3
  40. package/dist/ui-resource-handler.d.ts +2 -2
  41. package/dist/ui-resource-handler.js +2 -2
  42. package/dist/ui-server.js +5 -4
  43. package/dist/ui-stream-types.d.ts +8 -8
  44. package/dist/unix-socket-transport.d.ts +1 -1
  45. package/dist/unix-socket-transport.js +1 -1
  46. package/dist/utils.d.ts +1 -0
  47. package/dist/utils.js +14 -0
  48. package/package.json +34 -98
  49. package/skills/mcp-scripting/SKILL.md +2 -0
  50. package/abort.ts +0 -34
  51. package/agent-dir.ts +0 -24
  52. package/app-bridge.bundle.js +0 -109
  53. package/banner.png +0 -0
  54. package/checkpoint.ts +0 -55
  55. package/commands.ts +0 -622
  56. package/config.ts +0 -1204
  57. package/consent-manager.ts +0 -25
  58. package/direct-tools.ts +0 -75
  59. package/elicitation-handler.ts +0 -349
  60. package/error-signal.ts +0 -21
  61. package/gateway-arguments.ts +0 -73
  62. package/glimpse-ui.ts +0 -80
  63. package/host-html-template.ts +0 -456
  64. package/index.ts +0 -767
  65. package/init.ts +0 -613
  66. package/legacy-migration.ts +0 -47
  67. package/lifecycle.ts +0 -165
  68. package/logger.ts +0 -127
  69. package/mcp-auth-flow.ts +0 -975
  70. package/mcp-auth.ts +0 -793
  71. package/mcp-callback-server.ts +0 -450
  72. package/mcp-code.ts +0 -412
  73. package/mcp-keyring-helper.cjs +0 -89
  74. package/mcp-oauth-provider.ts +0 -774
  75. package/mcp-output-guard.ts +0 -530
  76. package/mcp-panel.ts +0 -907
  77. package/mcp-script-worker.mjs +0 -131
  78. package/mcp-setup-panel.ts +0 -666
  79. package/mcp-status.ts +0 -110
  80. package/mcp-trace.ts +0 -295
  81. package/metadata-cache.ts +0 -271
  82. package/npx-resolver.ts +0 -488
  83. package/onboarding-state.ts +0 -58
  84. package/panel-keys.ts +0 -37
  85. package/prompts.ts +0 -337
  86. package/proxy-modes.ts +0 -1109
  87. package/resource-tools.ts +0 -52
  88. package/runtime-owner.ts +0 -123
  89. package/sampling-handler.ts +0 -276
  90. package/scripts/build-app-bridge.mjs +0 -15
  91. package/scripts/build.mjs +0 -132
  92. package/scripts/prepare.mjs +0 -103
  93. package/search-ranking.ts +0 -132
  94. package/server-manager.ts +0 -1320
  95. package/session-recovery.ts +0 -316
  96. package/state.ts +0 -64
  97. package/tool-approval.ts +0 -78
  98. package/tool-loader.ts +0 -153
  99. package/tool-metadata.ts +0 -240
  100. package/tool-registrar.ts +0 -74
  101. package/tool-result-renderer.ts +0 -294
  102. package/tsconfig.build.json +0 -13
  103. package/tsconfig.json +0 -16
  104. package/types.ts +0 -785
  105. package/ui-resource-handler.ts +0 -249
  106. package/ui-server.ts +0 -808
  107. package/ui-session.ts +0 -568
  108. package/ui-stream-types.ts +0 -89
  109. package/ui-tool-visibility.ts +0 -25
  110. package/unix-socket-transport.ts +0 -85
  111. package/utils.ts +0 -307
package/dist/config.js CHANGED
@@ -1,7 +1,9 @@
1
1
  // config.ts - Config loading with import support
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync, realpathSync, renameSync } from "node:fs";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, realpathSync, renameSync, statSync, chmodSync } from "node:fs";
3
+ import { randomUUID } from "node:crypto";
3
4
  import { homedir } from "node:os";
4
5
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import { isDeepStrictEqual } from "node:util";
5
7
  import { parse as parseToml } from "smol-toml";
6
8
  import stripJsonComments from "strip-json-comments";
7
9
  import { getAdapterPath } from "./agent-dir.js";
@@ -367,6 +369,14 @@ function mergeServerMaps(base, next) {
367
369
  delete baseEntry.oauth;
368
370
  }
369
371
  }
372
+ // Stdio env belongs to the program that received it, not just the server name.
373
+ if (existing?.command && ((definition.command !== undefined && definition.command !== existing.command)
374
+ || (definition.args !== undefined && !isDeepStrictEqual(definition.args, existing.args ?? []))
375
+ || (definition.cwd !== undefined && definition.cwd !== existing.cwd))) {
376
+ if (baseEntry === existing)
377
+ baseEntry = { ...existing };
378
+ delete baseEntry.env;
379
+ }
370
380
  merged[name] = { ...baseEntry, ...definition };
371
381
  }
372
382
  return merged;
@@ -533,8 +543,10 @@ function mergeOpenCodeConfigs(base, next) {
533
543
  delete safeBase.cwd;
534
544
  }
535
545
  }
546
+ if (Object.hasOwn(override, "cwd") && override.cwd !== safeBase.cwd)
547
+ delete safeBase.environment;
536
548
  const mergedEntry = { ...safeBase, ...override };
537
- for (const field of ["environment", "headers", "oauth"]) {
549
+ for (const field of ["headers", "oauth"]) {
538
550
  const baseField = safeBase[field];
539
551
  const nextField = override[field];
540
552
  if (baseField && typeof baseField === "object" && !Array.isArray(baseField)
@@ -714,24 +726,37 @@ function buildConfigWritePreview(filePath, nextRaw) {
714
726
  function readRawConfigObject(filePath) {
715
727
  if (!existsSync(filePath))
716
728
  return {};
729
+ let raw;
717
730
  try {
718
- const raw = parseJsonConfig(readFileSync(filePath, "utf-8"));
719
- return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
731
+ raw = parseJsonConfig(readFileSync(filePath, "utf-8"));
720
732
  }
721
- catch {
722
- return {};
733
+ catch (error) {
734
+ throw new Error(`Cannot read MCP config at ${filePath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
735
+ }
736
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
737
+ throw new Error(`Invalid MCP config at ${filePath}: expected an object`);
723
738
  }
739
+ return raw;
724
740
  }
725
741
  function writeRawConfigObject(filePath, raw) {
726
742
  mkdirSync(dirname(filePath), { recursive: true });
727
- const tmpPath = `${filePath}.${process.pid}.tmp`;
728
- writeFileSync(tmpPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
743
+ const existingMode = existsSync(filePath) ? statSync(filePath).mode & 0o7777 : undefined;
744
+ const tmpPath = `${filePath}.${randomUUID()}.tmp`;
745
+ writeFileSync(tmpPath, `${JSON.stringify(raw, null, 2)}\n`, {
746
+ encoding: "utf-8",
747
+ mode: existingMode ?? 0o666,
748
+ flag: "wx",
749
+ });
750
+ if (existingMode !== undefined)
751
+ chmodSync(tmpPath, existingMode);
729
752
  renameSync(tmpPath, filePath);
730
753
  }
731
- function getServersObject(raw) {
732
- const existing = raw.mcpServers ?? raw["mcp-servers"] ?? {};
754
+ function getServersObject(raw, filePath) {
755
+ const existing = raw.mcpServers !== undefined ? raw.mcpServers
756
+ : raw["mcp-servers"] !== undefined ? raw["mcp-servers"]
757
+ : {};
733
758
  if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
734
- return {};
759
+ throw new Error(`Invalid MCP config at ${filePath}: mcpServers must be an object`);
735
760
  }
736
761
  return existing;
737
762
  }
@@ -886,7 +911,7 @@ export function previewCompatibilityImports(importKinds, overridePath) {
886
911
  const currentImports = Array.isArray(raw.imports) ? raw.imports.filter((value) => typeof value === "string") : [];
887
912
  const merged = [...new Set([...currentImports, ...importKinds])];
888
913
  const nextRaw = { ...raw, imports: merged };
889
- setServersObject(nextRaw, getServersObject(nextRaw));
914
+ setServersObject(nextRaw, getServersObject(nextRaw, targetPath));
890
915
  return buildConfigWritePreview(targetPath, nextRaw);
891
916
  }
892
917
  export function ensureCompatibilityImports(importKinds, overridePath) {
@@ -899,7 +924,7 @@ export function ensureCompatibilityImports(importKinds, overridePath) {
899
924
  return { path: targetPath, added: [] };
900
925
  }
901
926
  raw.imports = merged;
902
- const servers = getServersObject(raw);
927
+ const servers = getServersObject(raw, targetPath);
903
928
  setServersObject(raw, servers);
904
929
  writeRawConfigObject(targetPath, raw);
905
930
  return { path: targetPath, added };
@@ -923,14 +948,14 @@ export function writeStarterProjectConfig(cwd = process.cwd()) {
923
948
  export function previewSharedServerEntry(filePath, serverName, entry) {
924
949
  const raw = readRawConfigObject(filePath);
925
950
  const nextRaw = { ...raw };
926
- const servers = getServersObject(nextRaw);
951
+ const servers = getServersObject(nextRaw, filePath);
927
952
  servers[serverName] = entry;
928
953
  setServersObject(nextRaw, servers);
929
954
  return buildConfigWritePreview(filePath, nextRaw);
930
955
  }
931
956
  export function writeSharedServerEntry(filePath, serverName, entry) {
932
957
  const raw = readRawConfigObject(filePath);
933
- const servers = getServersObject(raw);
958
+ const servers = getServersObject(raw, filePath);
934
959
  servers[serverName] = entry;
935
960
  setServersObject(raw, servers);
936
961
  writeRawConfigObject(filePath, raw);
@@ -991,7 +1016,7 @@ export function writeDirectToolsConfig(changes, provenance) {
991
1016
  }
992
1017
  for (const [filePath, entries] of byPath) {
993
1018
  const raw = readRawConfigObject(filePath);
994
- const servers = getServersObject(raw);
1019
+ const servers = getServersObject(raw, filePath);
995
1020
  for (const { name, value } of entries) {
996
1021
  // Keep inherited connection details in their source file.
997
1022
  servers[name] = { ...servers[name], directTools: value };
@@ -15,7 +15,7 @@ export function directToolSelection(config, serverName, envOverride) {
15
15
  export function buildProxyDescription(config) {
16
16
  const servers = Object.entries(config.mcpServers).filter(([, definition]) => !isServerDisabled(definition)).map(([name]) => name).sort();
17
17
  return [
18
- "MCP gateway. Use mcp_search to discover and load typed tools; mcp_script for multi-call composition. Native Pi tools are called directly.",
18
+ 'MCP gateway. Use mcp_search to discover typed tools; enable mcp_script with mcp_search({ enable: ["script"] }) for multi-call composition. Native Pi tools are called directly.',
19
19
  `Configured servers: ${servers.join(", ") || "none"}. Catalog discovery is lazy.`,
20
20
  "Actions: status; list/search/describe; call (tool,args,server?); connect (server); instructions (server); resources (server); read-resource (server,uri); read-result (ref,path?,fields?,offset?,limit?); auth-start/auth-complete (server,args?); ui-messages.",
21
21
  "Use call when a discovered direct tool is unavailable in this host. read-result reads a retained response without repeating the remote operation.",
@@ -15,7 +15,7 @@ export type GatewayArguments = {
15
15
  fields?: string[];
16
16
  };
17
17
  export declare const gatewayParameters: Type.TObject<{
18
- action: Type.TUnsafe<"status" | "resources" | "connect" | "search" | "list" | "instructions" | "call" | "auth-start" | "auth-complete" | "describe" | "read-resource" | "read-result" | "ui-messages">;
18
+ action: Type.TUnsafe<"auth-complete" | "auth-start" | "call" | "connect" | "describe" | "instructions" | "list" | "read-resource" | "read-result" | "resources" | "search" | "status" | "ui-messages">;
19
19
  server: Type.TOptional<Type.TString>;
20
20
  tool: Type.TOptional<Type.TString>;
21
21
  args: Type.TOptional<Type.TObject<{}>>;
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import { createToolLoader, supportsNativeAsync } from "./tool-loader.js";
19
19
  import { gatewayParameters, prepareGatewayArguments } from "./gateway-arguments.js";
20
20
  import { executeResourceList, executeResourceRead } from "./resource-tools.js";
21
21
  import { guardMcpOutput, guardedMcpDetails, readMcpResult, resolveMcpOutputGuardOptions } from "./mcp-output-guard.js";
22
- import { abortable } from "./abort.js";
22
+ import { abortable, throwIfAborted } from "./abort.js";
23
23
  import { onSessionCheckpoint, prepareMcpCheckpoint } from "./checkpoint.js";
24
24
  export { MCP_STATUS_EVENT, MCP_STATUS_SNAPSHOT_VERSION, } from "./types.js";
25
25
  const INIT_WAIT_TIMEOUT_MS = 30_000;
@@ -90,29 +90,15 @@ function installMcpAdapter(pi, options) {
90
90
  ? cloneMcpConfig(sessionConfig)
91
91
  : { mcpServers: {} };
92
92
  const earlyCache = null;
93
+ let currentConfig = earlyConfig;
93
94
  const envRaw = process.env.MCP_DIRECT_TOOLS;
94
95
  const envDirectToolOverride = envRaw?.split(",").map(s => s.trim()).filter(Boolean);
95
96
  const toolLoader = createToolLoader(pi, () => state, () => initPromise, beforeExecute);
96
- const deactivatedTools = new Set();
97
97
  let runtimeCwd;
98
98
  let nativeAsyncSupported = false;
99
99
  let proxyToolRegistered = false;
100
100
  let proxyToolDescription = null;
101
101
  let directToolsFrozen = false;
102
- function deactivateTools(toolNames) {
103
- if (toolNames.length === 0)
104
- return;
105
- const remove = new Set(toolNames);
106
- const activeTools = pi.getActiveTools();
107
- for (const toolName of toolNames) {
108
- if (activeTools.includes(toolName))
109
- deactivatedTools.add(toolName);
110
- }
111
- const nextActiveTools = activeTools.filter((name) => !remove.has(name));
112
- if (nextActiveTools.length !== activeTools.length) {
113
- pi.setActiveTools(nextActiveTools);
114
- }
115
- }
116
102
  function applyDirectToolConfigChanges(changes) {
117
103
  if (!state)
118
104
  return;
@@ -124,12 +110,9 @@ function installMcpAdapter(pi, options) {
124
110
  }
125
111
  }
126
112
  function syncToolSurface(ctx) {
127
- const config = state?.config ?? earlyConfig;
113
+ const config = state?.config ?? currentConfig;
128
114
  const cache = loadMetadataCache(state?.metadataCacheEnabled ?? false);
129
- toolLoader.sync(config, cache, envRaw === "__none__" ? null : envDirectToolOverride, nativeAsyncSupported, specs => {
130
- syncProxyTool(config, cache, specs);
131
- syncScriptTool(config);
132
- });
115
+ toolLoader.sync(config, cache, envRaw === "__none__" ? null : envDirectToolOverride, nativeAsyncSupported, specs => syncGateways(config, cache, specs));
133
116
  }
134
117
  const registeredPromptCommands = new Set();
135
118
  function registerPromptCommands(specs) {
@@ -260,15 +243,13 @@ function installMcpAdapter(pi, options) {
260
243
  if (options.transformConfig) {
261
244
  runtimeConfig = cloneMcpConfig(options.transformConfig(cloneMcpConfig(runtimeConfig), ctx));
262
245
  }
246
+ currentConfig = runtimeConfig;
263
247
  const metadataCacheEnabled = ctx.isProjectTrusted()
264
248
  || !isPathInsideProject(getMetadataCachePath(), ctx.cwd);
265
249
  toolLoader.restore(ctx);
266
250
  registerPromptCommands(resolveCachedPrompts(runtimeConfig, metadataCacheEnabled));
267
251
  const runtimeCache = loadMetadataCache(metadataCacheEnabled);
268
- toolLoader.sync(runtimeConfig, runtimeCache, envRaw === "__none__" ? null : envDirectToolOverride, nativeAsyncSupported, specs => {
269
- syncProxyTool(runtimeConfig, runtimeCache, specs);
270
- syncScriptTool(runtimeConfig);
271
- });
252
+ toolLoader.sync(runtimeConfig, runtimeCache, envRaw === "__none__" ? null : envDirectToolOverride, nativeAsyncSupported, specs => syncGateways(runtimeConfig, runtimeCache, specs));
272
253
  const initialization = startInitialization(ctx, owner, oauthRuntime, runtimeConfig, generation);
273
254
  if (envRaw !== undefined && envRaw !== "__none__") {
274
255
  const missingEnvDirectTools = getMissingConfiguredDirectToolServers(runtimeConfig, runtimeCache, envDirectToolOverride);
@@ -538,7 +519,17 @@ function installMcpAdapter(pi, options) {
538
519
  owner?.throwIfInactive();
539
520
  return state ?? { content: [{ type: "text", text: "MCP not initialized" }], details: { error: "not_initialized" } };
540
521
  }
522
+ const unavailableFeatures = (enable = [], tools) => enable.filter(feature => !tools.some(tool => tool.name === (feature === "gateway" ? "mcp" : "mcp_script") && !tool.namespace));
541
523
  async function searchAndLoad(params, signal, ctx) {
524
+ throwIfAborted(signal);
525
+ if (params.query === undefined && !params.enable?.length)
526
+ throw new Error("Provide query or enable.");
527
+ // Loading front doors does not require initialization or connect to a server.
528
+ if (params.query === undefined) {
529
+ const tools = toolLoader.activate([], params.enable);
530
+ const unavailable = unavailableFeatures(params.enable, tools);
531
+ return { content: [{ type: "text", text: `Loaded for the next request: ${JSON.stringify(tools)}${unavailable.length ? `. Unavailable under configuration or host policy: ${unavailable.join(", ")}` : ""}` }], details: { loaded: tools, unavailable }, tools };
532
+ }
542
533
  const ready = await getToolState(signal);
543
534
  if ("content" in ready)
544
535
  return { ...ready, tools: [] };
@@ -551,23 +542,27 @@ function installMcpAdapter(pi, options) {
551
542
  const matches = params.query.trim() ? rankToolMatches(ready, params.query, params.server)
552
543
  : params.server ? (ready.toolMetadata.get(params.server) ?? []).filter(tool => !tool.resourceUri).map(tool => ({ server: params.server, tool })).sort((a, b) => a.tool.name.localeCompare(b.tool.name)) : [];
553
544
  const page = paginate(matches, params.offset ?? 0, params.limit ?? 5);
554
- const tools = toolLoader.activate(page.items);
555
- const result = executeSearch(ready, params.query, params.server, tools.length < page.items.length, params.limit ?? 5, params.offset);
545
+ const tools = toolLoader.activate(page.items, params.enable);
546
+ const unavailable = unavailableFeatures(params.enable, tools);
547
+ const typedCount = tools.filter(tool => tool.namespace || (tool.name !== "mcp" && tool.name !== "mcp_script")).length;
548
+ const result = executeSearch(ready, params.query, params.server, typedCount < page.items.length, params.limit ?? 5, params.offset, "mcp_search");
556
549
  const guarded = await guardMcpOutput([
557
550
  ...result.content,
558
- { type: "text", text: tools.length ? `Loaded for the next request: ${JSON.stringify(tools)}` : "No direct tools loaded. Use mcp action call or tools.call in mcp_script with the returned exact names." },
551
+ { type: "text", text: tools.length ? `Loaded for the next request: ${JSON.stringify(tools)}` : 'No direct tools loaded. Enable gateway or script with mcp_search({ enable: ["gateway"] }) or mcp_search({ enable: ["script"] }), then use mcp action call or tools.call with the returned exact names.' },
552
+ ...(unavailable.length ? [{ type: "text", text: `Unavailable under configuration or host policy: ${unavailable.join(", ")}` }] : []),
559
553
  ], resolveMcpOutputGuardOptions(ready.config.settings, ready.outputDirectory));
560
- return { content: guarded.content, details: { ...result.details, ...guardedMcpDetails(guarded), loaded: tools }, tools };
554
+ return { content: guarded.content, details: { ...result.details, ...guardedMcpDetails(guarded), loaded: tools, ...(unavailable.length ? { unavailable } : {}) }, tools };
561
555
  }
562
556
  const searchDefinition = {
563
557
  name: "mcp_search", label: "MCP Search",
564
- description: "Discover MCP tools by name, description, and parameter guidance, and load matching tools with their complete schemas for the next request. Start with a specific task query. Specify server to discover an uncached server; an empty query with server browses it. Results report incomplete catalog coverage. Resources use mcp action resources/read-resource.",
558
+ description: "Discover and load typed MCP tools for the next request. Use query; server discovers an uncached server (empty query browses it). Results report incomplete coverage. Or enable gateway for mcp status/list/search/describe/call/connect/instructions, resources/read-resource, read-result, auth-start/auth-complete and ui-messages; enable script for JavaScript composition, resources and saved-result readback. Enable alone performs no integration action. Loaded schemas stay available in this session/branch.",
565
559
  parameters: Type.Object({
566
- query: Type.String(),
560
+ query: Type.Optional(Type.String()),
561
+ enable: Type.Optional(Type.Array(Type.Union([Type.Literal("gateway"), Type.Literal("script")]), { minItems: 1, uniqueItems: true })),
567
562
  server: Type.Optional(Type.String()),
568
563
  limit: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_PAGE_SIZE, description: "Tools to load (default 5)." })),
569
564
  offset: Type.Optional(Type.Integer({ minimum: 0 })),
570
- }),
565
+ }, { anyOf: [{ required: ["query"] }, { required: ["enable"] }] }),
571
566
  renderResult: renderMcpToolResult,
572
567
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
573
568
  if (beforeExecute) {
@@ -587,7 +582,7 @@ function installMcpAdapter(pi, options) {
587
582
  function registerScriptTool() {
588
583
  if (scriptToolRegistered)
589
584
  return;
590
- pi.registerTool({
585
+ toolLoader.registerFeature("script", {
591
586
  ...(beforeExecute ? { executionMode: "sequential" } : {}),
592
587
  name: "mcp_script",
593
588
  label: "MCP Script",
@@ -609,21 +604,8 @@ function installMcpAdapter(pi, options) {
609
604
  });
610
605
  scriptToolRegistered = true;
611
606
  }
612
- function syncScriptTool(config) {
613
- if (config.settings?.scriptMode === false) {
614
- if (scriptToolRegistered)
615
- deactivateTools(["mcp_script"]);
616
- return;
617
- }
618
- registerScriptTool();
619
- if (deactivatedTools.delete("mcp_script")) {
620
- const activeTools = pi.getActiveTools();
621
- if (!activeTools.includes("mcp_script"))
622
- pi.setActiveTools([...activeTools, "mcp_script"]);
623
- }
624
- }
625
607
  function registerProxyTool(description) {
626
- pi.registerTool({
608
+ toolLoader.registerFeature("gateway", {
627
609
  ...(beforeExecute ? { executionMode: "sequential" } : {}),
628
610
  name: "mcp", label: "MCP", description,
629
611
  promptSnippet: "MCP server operations, resources, saved result readback, and fallback tool calls",
@@ -684,7 +666,10 @@ function installMcpAdapter(pi, options) {
684
666
  proxyToolRegistered = true;
685
667
  proxyToolDescription = description;
686
668
  }
687
- function syncProxyTool(config, cache, directSpecs) {
669
+ function syncGateways(config, cache, directSpecs) {
670
+ const policy = {};
671
+ if (config.settings?.scriptMode !== false)
672
+ policy.script = {};
688
673
  const missingConfiguredDirectToolServers = getMissingConfiguredDirectToolServers(config, cache, envRaw === undefined || envRaw === "__none__" ? undefined : envDirectToolOverride);
689
674
  const hasPinnedResources = envRaw !== "__none__" && Object.entries(config.mcpServers).some(([server, definition]) => {
690
675
  if (definition.disabled === true)
@@ -706,21 +691,15 @@ function installMcpAdapter(pi, options) {
706
691
  if (!proxyToolRegistered || proxyToolDescription !== description) {
707
692
  registerProxyTool(description);
708
693
  }
709
- if (deactivatedTools.delete("mcp")) {
710
- const activeTools = pi.getActiveTools();
711
- if (!activeTools.includes("mcp")) {
712
- pi.setActiveTools([...activeTools, "mcp"]);
713
- }
714
- }
715
- return;
694
+ policy.gateway = { eager: hasPinnedResources };
716
695
  }
717
- if (proxyToolRegistered)
718
- deactivateTools(["mcp"]);
696
+ if (policy.script)
697
+ registerScriptTool();
698
+ return policy;
719
699
  }
720
700
  // A transform needs the session context before any configured tool surface is registered.
721
701
  if (!options.transformConfig) {
722
- syncProxyTool(earlyConfig, earlyCache, []);
723
- syncScriptTool(earlyConfig);
702
+ syncGateways(earlyConfig, earlyCache, []);
724
703
  }
725
704
  }
726
705
  export function createMcpAdapter(options = {}) {
@@ -4,8 +4,6 @@ import { hasPendingAuth } from "./mcp-auth-flow.ts";
4
4
  export type ReconnectCallback = (serverName: string) => void;
5
5
  export type ReconnectFailureCallback = (serverName: string, error: unknown) => void;
6
6
  export declare class McpLifecycleManager {
7
- private readonly manager;
8
- private readonly hasPendingAuthForServer;
9
7
  private keepAliveServers;
10
8
  private allServers;
11
9
  private serverSettings;
@@ -23,6 +21,8 @@ export declare class McpLifecycleManager {
23
21
  pauseForCheckpoint(): () => void;
24
22
  private healthSignal;
25
23
  private removeHealthAbortListener;
24
+ private readonly manager;
25
+ private readonly hasPendingAuthForServer;
26
26
  constructor(manager: McpServerManager, hasPendingAuthForServer?: typeof hasPendingAuth);
27
27
  setReconnectCallback(callback: ReconnectCallback): void;
28
28
  setReconnectFailureCallback(callback: ReconnectFailureCallback): void;
package/dist/lifecycle.js CHANGED
@@ -3,8 +3,6 @@ import { hasPendingAuth } from "./mcp-auth-flow.js";
3
3
  import { logger } from "./logger.js";
4
4
  import { formatTerminalError, sanitizeTerminalText } from "./utils.js";
5
5
  export class McpLifecycleManager {
6
- manager;
7
- hasPendingAuthForServer;
8
6
  keepAliveServers = new Map();
9
7
  allServers = new Map();
10
8
  serverSettings = new Map();
@@ -31,6 +29,8 @@ export class McpLifecycleManager {
31
29
  }
32
30
  healthSignal;
33
31
  removeHealthAbortListener;
32
+ manager;
33
+ hasPendingAuthForServer;
34
34
  constructor(manager, hasPendingAuthForServer = hasPendingAuth) {
35
35
  this.manager = manager;
36
36
  this.hasPendingAuthForServer = hasPendingAuthForServer;
@@ -9,7 +9,7 @@ import { McpOAuthProvider, issuersMatch, loopbackRedirectsMatch, validateOAuthCl
9
9
  import { ensureCallbackServer, waitForCallback, cancelPendingCallback, stopCallbackServer, releaseCallbackServer, takeCallbackResult, } from "./mcp-callback-server.js";
10
10
  import { getAuthForUrl, clearAllCredentials, clearClientInfo, clearTokens, clearCodeVerifier, getOAuthState, clearOAuthState, getAuthBaseDir, } from "./mcp-auth.js";
11
11
  import { isServerDisabled, isNonInteractiveOAuth, validateServerProtocolConfig } from "./types.js";
12
- import { formatTerminalError, interpolateEnvRecord, interpolateEnvVars, normalizeRequestTimeoutMs } from "./utils.js";
12
+ import { ADAPTER_VERSION, formatTerminalError, interpolateEnvRecord, interpolateEnvVars, normalizeRequestTimeoutMs } from "./utils.js";
13
13
  import { abortable, throwIfAborted } from "./abort.js";
14
14
  import { combineAbortSignals, isAbortError } from "./runtime-owner.js";
15
15
  function applyConfiguredScope(discovery, config) {
@@ -255,7 +255,7 @@ async function probeAuthDiscovery(serverUrl, definition, signal) {
255
255
  const timeout = normalizeRequestTimeoutMs(definition?.requestTimeoutMs) ?? 5000;
256
256
  const discoverySignal = combineAbortSignals(signal, AbortSignal.timeout(Math.ceil(timeout)));
257
257
  let discovery = {};
258
- const client = new Client({ name: "pi-mcp-auth-discovery", version: "4.3.1" }, {
258
+ const client = new Client({ name: "pi-mcp-auth-discovery", version: ADAPTER_VERSION }, {
259
259
  versionNegotiation: { mode: definition?.protocolVersion ?? "auto" },
260
260
  ...(isNonInteractiveOAuth(definition?.oauth)
261
261
  ? { capabilities: { extensions: { [definition?.oauth && definition.oauth.crossAppAccess
@@ -51,8 +51,8 @@ export interface AuthStorageOptions {
51
51
  baseDir?: string;
52
52
  }
53
53
  export declare class OAuthCredentialStoreError extends Error {
54
- readonly operation: 'read' | 'write' | 'remove';
55
54
  readonly code = "OAUTH_CREDENTIAL_STORE_UNAVAILABLE";
55
+ readonly operation: 'read' | 'write' | 'remove';
56
56
  constructor(message: string, operation: 'read' | 'write' | 'remove', cause: unknown);
57
57
  }
58
58
  export type OAuthCredentialStatus = {
package/dist/mcp-auth.js CHANGED
@@ -15,8 +15,8 @@ import { createRequire } from 'module';
15
15
  import { readFileSync, existsSync, rmSync } from 'fs';
16
16
  import { dirname, join } from 'path';
17
17
  import { fileURLToPath } from 'url';
18
- import { getAdapterPath, getAgentPath } from "./agent-dir.js";
19
- import { resolveConfiguredOAuthDir } from "./config.js";
18
+ import { getAdapterPath, getAgentPath } from './agent-dir.js';
19
+ import { resolveConfiguredOAuthDir } from './config.js';
20
20
  const require = createRequire(import.meta.url);
21
21
  const AUTH_SECRET_SERVICE = 'fitch-mcp-adapter.oauth';
22
22
  const LEGACY_AUTH_SECRET_SERVICE = 'pi-mcp-adapter.oauth';
@@ -30,8 +30,8 @@ const TEST_LINUX_KEYRING_RECOVERY_ENV = 'PI_MCP_ADAPTER_TEST_LINUX_KEYRING_RECOV
30
30
  const KEYRING_RECOVERY_TIMEOUT_MS = 10_000;
31
31
  const AUTH_CHUNK_MANIFEST_KEY = '__piMcpAdapterOAuthChunked';
32
32
  export class OAuthCredentialStoreError extends Error {
33
- operation;
34
33
  code = 'OAUTH_CREDENTIAL_STORE_UNAVAILABLE';
34
+ operation;
35
35
  constructor(message, operation, cause) {
36
36
  super(isAndroidKeyringLoadFailure(cause)
37
37
  ? `${message}. Android/Termux: @napi-rs/keyring could not load a native binding. Persistent OAuth requires a supported platform with an OS credential store; no plaintext fallback is used.`
@@ -6,6 +6,12 @@ export declare const runMcpScript: typeof runMcpScriptOperation;
6
6
  declare function runMcpScriptOperation(state: McpExtensionState, code: string, timeoutMs?: number | null, getPiTools?: () => ToolInfo[], signal?: AbortSignal, toolCallId?: string, beforeDispatch?: (signal: AbortSignal | undefined, operation: McpOperationContext) => Promise<void>): Promise<{
7
7
  content: ContentBlock[];
8
8
  details: {
9
+ mode: string;
10
+ resultRefs?: string[];
11
+ error?: "aborted" | "call_capture_failed" | "script_error" | "timeout";
12
+ message?: string | undefined;
13
+ timeoutMs: number | null;
14
+ recovery?: {} | null;
9
15
  calls?: ({
10
16
  operation: "call";
11
17
  path: string;
@@ -41,12 +47,6 @@ declare function runMcpScriptOperation(state: McpExtensionState, code: string, t
41
47
  error: string;
42
48
  durationMs: number;
43
49
  })[];
44
- recovery?: {} | null;
45
- timeoutMs: number | null;
46
- error?: "aborted" | "timeout" | "call_capture_failed" | "script_error";
47
- message?: string | undefined;
48
- resultRefs?: string[];
49
- mode: string;
50
50
  };
51
51
  }>;
52
52
  export {};
package/dist/mcp-code.js CHANGED
@@ -122,7 +122,7 @@ async function runMcpScriptOperation(state, code, timeoutMs = DEFAULT_MCP_SCRIPT
122
122
  ? Math.max(0, Date.now() - startedAt)
123
123
  : operation.durationMs,
124
124
  }));
125
- let callsSnapshot;
125
+ const pendingOperations = new Set();
126
126
  const resultRefs = new Set();
127
127
  const payloadNotices = new Set();
128
128
  const recordCall = async (path, dispatch) => {
@@ -149,11 +149,11 @@ async function runMcpScriptOperation(state, code, timeoutMs = DEFAULT_MCP_SCRIPT
149
149
  : textFromContent(result.content);
150
150
  calls[index] = {
151
151
  operation: "call", path, ok: false, error: errorCode, durationMs: Date.now() - startedAt, startedAt,
152
- ...(errorCode === "ambiguous_outcome" ? { recovery: details.recovery } : {}),
152
+ ...(details.recovery !== undefined ? { recovery: details.recovery } : {}),
153
153
  };
154
154
  if (errorCode === "call_capture_failed")
155
155
  throw new McpScriptCaptureError(details.recovery, message);
156
- if (errorCode === "ambiguous_outcome")
156
+ if (details.recovery !== undefined)
157
157
  output.push({ type: "text", text: message });
158
158
  return {
159
159
  ok: false,
@@ -301,7 +301,7 @@ async function runMcpScriptOperation(state, code, timeoutMs = DEFAULT_MCP_SCRIPT
301
301
  reject(new Error(message.message));
302
302
  return;
303
303
  }
304
- void (async () => {
304
+ const operation = (async () => {
305
305
  let envelope;
306
306
  if (message.type === "call") {
307
307
  envelope = await callTool(message.id, message.path, message.args, message.server);
@@ -332,7 +332,9 @@ async function runMcpScriptOperation(state, code, timeoutMs = DEFAULT_MCP_SCRIPT
332
332
  }
333
333
  const response = { type: "result", id: message.id, envelope };
334
334
  activeWorker.postMessage(response);
335
- })().catch(reject);
335
+ })();
336
+ pendingOperations.add(operation);
337
+ void operation.catch(reject).finally(() => pendingOperations.delete(operation));
336
338
  });
337
339
  activeWorker.once("error", reject);
338
340
  activeWorker.once("exit", (code) => {
@@ -345,7 +347,6 @@ async function runMcpScriptOperation(state, code, timeoutMs = DEFAULT_MCP_SCRIPT
345
347
  return;
346
348
  const timeoutError = new McpScriptTimeoutError(resolvedTimeoutMs);
347
349
  timer = setTimeout(() => {
348
- callsSnapshot = snapshotCalls();
349
350
  timeoutController.abort(timeoutError);
350
351
  void activeWorker.terminate();
351
352
  reject(timeoutError);
@@ -354,7 +355,6 @@ async function runMcpScriptOperation(state, code, timeoutMs = DEFAULT_MCP_SCRIPT
354
355
  const aborted = externalSignal
355
356
  ? new Promise((_resolve, reject) => {
356
357
  const onAbort = () => {
357
- callsSnapshot = snapshotCalls();
358
358
  void activeWorker.terminate();
359
359
  reject(abortReasonError(externalSignal.reason));
360
360
  };
@@ -387,14 +387,15 @@ async function runMcpScriptOperation(state, code, timeoutMs = DEFAULT_MCP_SCRIPT
387
387
  finally {
388
388
  clearTimeout(timer);
389
389
  removeAbortListener();
390
- // "incomplete" means the call had not settled when the script finished
391
- // (deadline, abort, or early return). Snapshot before aborting stragglers.
392
- callsSnapshot ??= snapshotCalls();
390
+ worker?.removeAllListeners("message");
393
391
  // A script may finish without awaiting every call; abort leftovers so
394
392
  // parent-side dispatches do not outlive the script.
395
393
  timeoutController.abort(new Error("mcp_script finished"));
394
+ // Let cancelled dispatches report whether an outcome is known before taking the trace.
395
+ await Promise.allSettled(pendingOperations);
396
396
  await worker?.terminate();
397
397
  }
398
+ const callsSnapshot = snapshotCalls();
398
399
  // Snapshot before the asynchronous output guard; the terminated worker can no longer emit.
399
400
  const guarded = await guardMcpOutput([...(output.length > 0 ? output : [{ type: "text", text: "(no output)" }]), ...[...payloadNotices].map(text => ({ type: "text", text })), ...[...resultRefs].map(ref => ({ type: "text", text: formatMcpResultReference(ref) }))], resolveMcpOutputGuardOptions(state.config.settings, state.outputDirectory));
400
401
  return {
@@ -49,11 +49,6 @@ export interface McpOAuthCallbacks {
49
49
  * Implements the OAuthClientProvider interface from the MCP SDK.
50
50
  */
51
51
  export declare class McpOAuthProvider implements OAuthClientProvider {
52
- private serverName;
53
- private serverUrl;
54
- private config;
55
- private callbacks;
56
- private storageOptions;
57
52
  private readonly redirectUrlSnapshot;
58
53
  readonly clientMetadataUrl?: string;
59
54
  private readonly controller;
@@ -66,6 +61,11 @@ export declare class McpOAuthProvider implements OAuthClientProvider {
66
61
  private flowDiscoveryState;
67
62
  private flowIssuerMismatch;
68
63
  private flowState;
64
+ private serverName;
65
+ private serverUrl;
66
+ private config;
67
+ private callbacks;
68
+ private storageOptions;
69
69
  constructor(serverName: string, serverUrl: string, config: McpOAuthConfig, callbacks: McpOAuthCallbacks, storageOptions?: AuthStorageOptions, runtimeSignal?: AbortSignal, initialState?: string);
70
70
  private get isNonInteractive();
71
71
  private get discoveredIssuer();
@@ -152,11 +152,6 @@ function addAuthorizationParams(authorizationUrl, params) {
152
152
  * Implements the OAuthClientProvider interface from the MCP SDK.
153
153
  */
154
154
  export class McpOAuthProvider {
155
- serverName;
156
- serverUrl;
157
- config;
158
- callbacks;
159
- storageOptions;
160
155
  redirectUrlSnapshot;
161
156
  clientMetadataUrl;
162
157
  controller = new AbortController();
@@ -169,6 +164,11 @@ export class McpOAuthProvider {
169
164
  flowDiscoveryState;
170
165
  flowIssuerMismatch = false;
171
166
  flowState;
167
+ serverName;
168
+ serverUrl;
169
+ config;
170
+ callbacks;
171
+ storageOptions;
172
172
  constructor(serverName, serverUrl, config, callbacks, storageOptions = {}, runtimeSignal, initialState) {
173
173
  this.serverName = serverName;
174
174
  this.serverUrl = serverUrl;
@@ -94,11 +94,11 @@ export declare function readMcpResult(input: ReadMcpResultInput, options?: McpOu
94
94
  text: string;
95
95
  }[];
96
96
  details: {
97
- path?: string;
98
97
  ref: string;
99
98
  offset: number;
100
99
  nextOffset: number | null;
101
100
  totalCharacters: number;
101
+ path?: string;
102
102
  error?: never;
103
103
  message?: never;
104
104
  };
@@ -223,7 +223,7 @@ export function formatMcpPayloadFile(file) {
223
223
  return `[MCP ${file.kind === "audio" ? "audio" : "binary resource"}${file.uri ? ` (${file.uri})` : ""}: ${file.mimeType ?? "unknown MIME type"}; not rendered. ${file.path ? `Saved file: ${file.path}` : `Could not save: ${file.error}`}]`;
224
224
  }
225
225
  export function formatMcpResultReference(ref) {
226
- return `[MCP result saved: ${JSON.stringify(ref)}. Inspect without repeating the call using mcp action "read-result" or tools.readResult with this ref. Optionally select a JSON Pointer with path.]`;
226
+ return `[MCP result saved: ${JSON.stringify(ref)}. Inspect without repeating the call using mcp action "read-result" or tools.readResult with this ref. If inactive, first mcp_search({ enable: ["gateway"] }) or mcp_search({ enable: ["script"] }). Optionally select a JSON Pointer with path.]`;
227
227
  }
228
228
  function summarizeMcpResult(result, rawBytes, artifact) {
229
229
  const { path: fullResultPath, error: resultWriteError } = artifact;
@@ -2,8 +2,6 @@ import { type PanelKeybindings } from "./panel-keys.ts";
2
2
  import type { McpConfig, McpPanelCallbacks, McpPanelResult, ServerProvenance } from "./types.ts";
3
3
  import { type MetadataCache } from "./metadata-cache.ts";
4
4
  declare class McpPanel {
5
- private callbacks;
6
- private done;
7
5
  private noticeLines;
8
6
  private prefix;
9
7
  private servers;
@@ -25,6 +23,8 @@ declare class McpPanel {
25
23
  private keys;
26
24
  private static readonly MAX_VISIBLE;
27
25
  private static readonly INACTIVITY_MS;
26
+ private callbacks;
27
+ private done;
28
28
  constructor(config: McpConfig, cache: MetadataCache | null, provenance: Map<string, ServerProvenance>, callbacks: McpPanelCallbacks, tui: {
29
29
  requestRender(): void;
30
30
  }, done: (result: McpPanelResult) => void, options?: {