@fitchmultz/pi-mcp-adapter 5.0.0 → 6.0.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 (73) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/OAUTH.md +3 -3
  3. package/README.md +153 -85
  4. package/app-bridge.bundle.js +75 -33
  5. package/commands.ts +9 -6
  6. package/direct-tools.ts +25 -358
  7. package/dist/app-bridge.bundle.js +75 -33
  8. package/dist/commands.js +9 -6
  9. package/dist/direct-tools.d.ts +2 -4
  10. package/dist/direct-tools.js +21 -319
  11. package/dist/gateway-arguments.d.ts +33 -0
  12. package/dist/gateway-arguments.js +63 -0
  13. package/dist/index.js +181 -276
  14. package/dist/init.js +6 -20
  15. package/dist/mcp-code.d.ts +2 -1
  16. package/dist/mcp-code.js +87 -41
  17. package/dist/mcp-output-guard.d.ts +57 -2
  18. package/dist/mcp-output-guard.js +129 -24
  19. package/dist/mcp-panel.d.ts +1 -1
  20. package/dist/mcp-panel.js +34 -97
  21. package/dist/mcp-script-worker.mjs +5 -2
  22. package/dist/mcp-status.js +2 -1
  23. package/dist/metadata-cache.d.ts +1 -1
  24. package/dist/metadata-cache.js +25 -88
  25. package/dist/prompts.d.ts +1 -1
  26. package/dist/prompts.js +1 -1
  27. package/dist/proxy-modes.d.ts +40 -6
  28. package/dist/proxy-modes.js +270 -485
  29. package/dist/resource-tools.d.ts +15 -0
  30. package/dist/resource-tools.js +26 -1
  31. package/dist/sampling-handler.js +4 -1
  32. package/dist/search-ranking.d.ts +0 -2
  33. package/dist/search-ranking.js +66 -137
  34. package/dist/state.d.ts +1 -1
  35. package/dist/tool-loader.d.ts +32 -0
  36. package/dist/tool-loader.js +152 -0
  37. package/dist/tool-metadata.d.ts +67 -0
  38. package/dist/tool-metadata.js +23 -17
  39. package/dist/tool-registrar.d.ts +4 -4
  40. package/dist/tool-registrar.js +18 -13
  41. package/dist/tool-result-renderer.d.ts +3 -0
  42. package/dist/tool-result-renderer.js +8 -3
  43. package/dist/types.d.ts +11 -36
  44. package/dist/utils.js +2 -4
  45. package/gateway-arguments.ts +73 -0
  46. package/index.ts +166 -299
  47. package/init.ts +6 -20
  48. package/mcp-code.ts +71 -45
  49. package/mcp-output-guard.ts +145 -27
  50. package/mcp-panel.ts +38 -109
  51. package/mcp-script-worker.mjs +5 -2
  52. package/mcp-status.ts +2 -1
  53. package/metadata-cache.ts +31 -93
  54. package/package.json +14 -9
  55. package/prompts.ts +1 -1
  56. package/proxy-modes.ts +265 -517
  57. package/resource-tools.ts +36 -1
  58. package/sampling-handler.ts +4 -1
  59. package/scripts/build-app-bridge.mjs +15 -0
  60. package/scripts/build.mjs +3 -2
  61. package/scripts/prepare.mjs +1 -0
  62. package/search-ranking.ts +71 -135
  63. package/skills/mcp-scripting/SKILL.md +67 -20
  64. package/state.ts +1 -1
  65. package/tool-loader.ts +153 -0
  66. package/tool-metadata.ts +22 -17
  67. package/tool-registrar.ts +18 -13
  68. package/tool-result-renderer.ts +10 -2
  69. package/types.ts +15 -40
  70. package/utils.ts +2 -4
  71. package/dist/ts-shape.d.ts +0 -2
  72. package/dist/ts-shape.js +0 -147
  73. package/ts-shape.ts +0 -145
package/dist/index.js CHANGED
@@ -1,30 +1,29 @@
1
1
  import { Type } from "typebox";
2
2
  import { showStatus, showTools, showPrompts, reconnectServer, reconnectServers, authenticateServer, logoutServer, openMcpAuthPanel, openMcpPanel, openMcpSetup } from "./commands.js";
3
3
  import { cloneMcpConfig, isPathInsideProject, loadMcpConfig, writeProjectServerDisabledOverride } from "./config.js";
4
- import { buildProxyDescription, createDirectToolExecutor, getMissingConfiguredDirectToolServers, resolveDirectTools } from "./direct-tools.js";
5
- import { flushMetadataCache, initializeMcp, updateStatusBar } from "./init.js";
6
- import { getMetadataCachePath, loadMetadataCache } from "./metadata-cache.js";
4
+ import { buildProxyDescription, directToolSelection, getMissingConfiguredDirectToolServers } from "./direct-tools.js";
5
+ import { flushMetadataCache, initializeMcp, lazyConnect, updateStatusBar } from "./init.js";
6
+ import { getMetadataCachePath, isServerCacheValid, loadMetadataCache, reconstructToolMetadata } from "./metadata-cache.js";
7
7
  import { createPromptCommand, resolveCachedPrompts } from "./prompts.js";
8
8
  import { logger } from "./logger.js";
9
9
  import { executeAuthComplete, executeAuthStart, executeCall, executeConnect, executeDescribe, executeInstructions, executeList, executeSearch, executeStatus, executeUiMessages } from "./proxy-modes.js";
10
- import { formatTerminalError, normalizeDirectToolInputSchema, truncateAtWord } from "./utils.js";
10
+ import { formatTerminalError } from "./utils.js";
11
11
  import { createOAuthRuntime, shutdownOAuth } from "./mcp-auth-flow.js";
12
- import { createMcpDirectToolCallRenderer, renderMcpProxyToolCall, renderMcpToolResult } from "./tool-result-renderer.js";
12
+ import { renderMcpProxyToolCall, renderMcpToolResult } from "./tool-result-renderer.js";
13
13
  import { toolErrorOverride } from "./error-signal.js";
14
14
  import { createMcpRuntimeOwner, createOwnedUi, isAbortError } from "./runtime-owner.js";
15
15
  import { publishMcpStatusShutdown } from "./mcp-status.js";
16
16
  import { DEFAULT_MCP_SCRIPT_TIMEOUT_MS, runMcpScript } from "./mcp-code.js";
17
- import { MAX_PAGE_SIZE, MAX_TOOL_NAME_LENGTH } from "./search-ranking.js";
17
+ import { MAX_PAGE_SIZE, paginate, rankToolMatches } from "./search-ranking.js";
18
+ import { createToolLoader, supportsNativeAsync } from "./tool-loader.js";
19
+ import { gatewayParameters, prepareGatewayArguments } from "./gateway-arguments.js";
20
+ import { executeResourceList, executeResourceRead } from "./resource-tools.js";
21
+ import { guardMcpOutput, guardedMcpDetails, readMcpResult, resolveMcpOutputGuardOptions } from "./mcp-output-guard.js";
18
22
  import { abortable } from "./abort.js";
19
23
  import { onSessionCheckpoint, prepareMcpCheckpoint } from "./checkpoint.js";
20
24
  export { MCP_STATUS_EVENT, MCP_STATUS_SNAPSHOT_VERSION, } from "./types.js";
21
25
  const INIT_WAIT_TIMEOUT_MS = 30_000;
22
26
  const INIT_WAIT_TIMED_OUT = Symbol("init-wait-timed-out");
23
- const inactiveDirectToolsKey = Symbol.for("fitch-mcp-adapter.inactive-direct-tools");
24
- const inactiveDirectToolsBySession = (() => {
25
- const shared = globalThis;
26
- return shared[inactiveDirectToolsKey] ??= new Map();
27
- })();
28
27
  async function awaitWithTimeout(promise, timeoutMs) {
29
28
  let timer;
30
29
  try {
@@ -93,52 +92,13 @@ function installMcpAdapter(pi, options) {
93
92
  const earlyCache = null;
94
93
  const envRaw = process.env.MCP_DIRECT_TOOLS;
95
94
  const envDirectToolOverride = envRaw?.split(",").map(s => s.trim()).filter(Boolean);
96
- const registeredDirectTools = new Map();
95
+ const toolLoader = createToolLoader(pi, () => state, () => initPromise, beforeExecute);
97
96
  const deactivatedTools = new Set();
98
- const preservedInactiveTools = new Set();
99
- let activationKey = null;
97
+ let runtimeCwd;
98
+ let nativeAsyncSupported = false;
100
99
  let proxyToolRegistered = false;
101
100
  let proxyToolDescription = null;
102
101
  let directToolsFrozen = false;
103
- function directToolFingerprint(spec) {
104
- return JSON.stringify({
105
- serverName: spec.serverName,
106
- originalName: spec.originalName,
107
- prefixedName: spec.prefixedName,
108
- description: spec.description,
109
- inputSchema: spec.inputSchema,
110
- annotations: spec.annotations,
111
- resourceUri: spec.resourceUri,
112
- uiResourceUri: spec.uiResourceUri,
113
- uiStreamMode: spec.uiStreamMode,
114
- });
115
- }
116
- function registerDirectTool(spec) {
117
- const execute = createDirectToolExecutor(() => state, () => initPromise, spec, beforeExecute);
118
- pi.registerTool({
119
- ...(beforeExecute ? { executionMode: "sequential" } : {}),
120
- name: spec.prefixedName,
121
- label: `MCP: ${spec.originalName}`,
122
- description: spec.description || "(no description)",
123
- promptSnippet: truncateAtWord(spec.description, 100) || `MCP tool from ${spec.serverName}`,
124
- parameters: Type.Unsafe(normalizeDirectToolInputSchema(spec.inputSchema)),
125
- execute,
126
- renderCall: createMcpDirectToolCallRenderer(spec.prefixedName),
127
- renderResult: renderMcpToolResult,
128
- });
129
- if (preservedInactiveTools.has(spec.prefixedName)) {
130
- const activeTools = pi.getActiveTools();
131
- if (activeTools.includes(spec.prefixedName)) {
132
- pi.setActiveTools(activeTools.filter((name) => name !== spec.prefixedName));
133
- }
134
- }
135
- }
136
- function resolveCurrentDirectTools(config, cache) {
137
- if (envRaw === "__none__")
138
- return [];
139
- const prefix = config.settings?.toolPrefix ?? "server";
140
- return resolveDirectTools(config, cache, prefix, envDirectToolOverride);
141
- }
142
102
  function deactivateTools(toolNames) {
143
103
  if (toolNames.length === 0)
144
104
  return;
@@ -153,36 +113,6 @@ function installMcpAdapter(pi, options) {
153
113
  pi.setActiveTools(nextActiveTools);
154
114
  }
155
115
  }
156
- function syncDirectTools(config, cache) {
157
- const specs = resolveCurrentDirectTools(config, cache);
158
- const nextNames = new Set(specs.map((spec) => spec.prefixedName));
159
- const added = [];
160
- const updated = [];
161
- const deactivated = [];
162
- for (const spec of specs) {
163
- const fingerprint = directToolFingerprint(spec);
164
- const previous = registeredDirectTools.get(spec.prefixedName);
165
- if (previous !== fingerprint) {
166
- registerDirectTool(spec);
167
- registeredDirectTools.set(spec.prefixedName, fingerprint);
168
- if (deactivatedTools.delete(spec.prefixedName)) {
169
- const activeTools = pi.getActiveTools();
170
- if (!activeTools.includes(spec.prefixedName)) {
171
- pi.setActiveTools([...activeTools, spec.prefixedName]);
172
- }
173
- }
174
- (previous ? updated : added).push(spec.prefixedName);
175
- }
176
- }
177
- for (const toolName of [...registeredDirectTools.keys()]) {
178
- if (nextNames.has(toolName))
179
- continue;
180
- registeredDirectTools.delete(toolName);
181
- deactivated.push(toolName);
182
- }
183
- deactivateTools(deactivated);
184
- return { specs, added, updated, deactivated };
185
- }
186
116
  function applyDirectToolConfigChanges(changes) {
187
117
  if (!state)
188
118
  return;
@@ -196,13 +126,10 @@ function installMcpAdapter(pi, options) {
196
126
  function syncToolSurface(ctx) {
197
127
  const config = state?.config ?? earlyConfig;
198
128
  const cache = loadMetadataCache(state?.metadataCacheEnabled ?? false);
199
- const result = syncDirectTools(config, cache);
200
- syncProxyTool(config, cache, result.specs);
201
- syncScriptTool(config);
202
- const changed = result.added.length + result.updated.length + result.deactivated.length;
203
- if (changed > 0 && ctx?.hasUI) {
204
- ctx.ui.notify(`MCP: direct tools refreshed (+${result.added.length}, ~${result.updated.length}, -${result.deactivated.length})`, "info");
205
- }
129
+ toolLoader.sync(config, cache, envRaw === "__none__" ? null : envDirectToolOverride, nativeAsyncSupported, specs => {
130
+ syncProxyTool(config, cache, specs);
131
+ syncScriptTool(config);
132
+ });
206
133
  }
207
134
  const registeredPromptCommands = new Set();
208
135
  function registerPromptCommands(specs) {
@@ -255,13 +182,18 @@ function installMcpAdapter(pi, options) {
255
182
  }
256
183
  syncToolSurface(ctx);
257
184
  };
185
+ for (const server of toolLoader.selectedServers()) {
186
+ if (!nextState.toolMetadata.has(server) && nextState.config.mcpServers[server])
187
+ await lazyConnect(nextState, server, owner.signal);
188
+ }
189
+ owner.throwIfInactive();
258
190
  syncPromptCommands();
259
191
  syncToolSurface(ctx);
260
192
  updateStatusBar(nextState);
261
193
  initPromise = null;
262
194
  if (nextState.config.settings?.freezeDirectTools === true) {
263
195
  directToolsFrozen = true;
264
- logger.info("MCP: direct tools frozen after initial sync — reconnects won't rebuild the system prompt; use mcp({ connect: \"server\" }) to rediscover");
196
+ logger.info("MCP: direct tools frozen after initial sync — reconnects won't rebuild the system prompt; use mcp({ action: \"connect\", server: \"server\" }) to rediscover");
265
197
  }
266
198
  }).catch(async (err) => {
267
199
  if (!owner.isActive() || generation !== lifecycleGeneration) {
@@ -285,7 +217,10 @@ function installMcpAdapter(pi, options) {
285
217
  }
286
218
  });
287
219
  }
288
- pi.on("session_start", async (_event, ctx) => {
220
+ async function startSession(ctx) {
221
+ runtimeCwd = ctx.cwd;
222
+ nativeAsyncSupported = supportsNativeAsync(ctx);
223
+ directToolsFrozen = false;
289
224
  const generation = ++lifecycleGeneration;
290
225
  const previousState = state;
291
226
  const previousOwner = currentOwner;
@@ -327,16 +262,13 @@ function installMcpAdapter(pi, options) {
327
262
  }
328
263
  const metadataCacheEnabled = ctx.isProjectTrusted()
329
264
  || !isPathInsideProject(getMetadataCachePath(), ctx.cwd);
330
- activationKey = `${ctx.cwd}\0${currentConfigPath ?? ""}`;
331
- preservedInactiveTools.clear();
332
- for (const toolName of inactiveDirectToolsBySession.get(activationKey) ?? []) {
333
- preservedInactiveTools.add(toolName);
334
- }
265
+ toolLoader.restore(ctx);
335
266
  registerPromptCommands(resolveCachedPrompts(runtimeConfig, metadataCacheEnabled));
336
267
  const runtimeCache = loadMetadataCache(metadataCacheEnabled);
337
- const cachedDirectTools = syncDirectTools(runtimeConfig, runtimeCache).specs;
338
- syncProxyTool(runtimeConfig, runtimeCache, cachedDirectTools);
339
- syncScriptTool(runtimeConfig);
268
+ toolLoader.sync(runtimeConfig, runtimeCache, envRaw === "__none__" ? null : envDirectToolOverride, nativeAsyncSupported, specs => {
269
+ syncProxyTool(runtimeConfig, runtimeCache, specs);
270
+ syncScriptTool(runtimeConfig);
271
+ });
340
272
  const initialization = startInitialization(ctx, owner, oauthRuntime, runtimeConfig, generation);
341
273
  if (envRaw !== undefined && envRaw !== "__none__") {
342
274
  const missingEnvDirectTools = getMissingConfiguredDirectToolServers(runtimeConfig, runtimeCache, envDirectToolOverride);
@@ -344,6 +276,20 @@ function installMcpAdapter(pi, options) {
344
276
  await initialization;
345
277
  }
346
278
  }
279
+ }
280
+ pi.on("session_start", (_event, ctx) => startSession(ctx));
281
+ pi.on("session_tree", (_event, ctx) => {
282
+ toolLoader.restore(ctx);
283
+ syncToolSurface(ctx);
284
+ });
285
+ pi.on("model_select", (_event, ctx) => {
286
+ nativeAsyncSupported = supportsNativeAsync(ctx);
287
+ if (state)
288
+ syncToolSurface(ctx);
289
+ });
290
+ pi.on("before_agent_start", async (_event, ctx) => {
291
+ if (ctx.cwd !== runtimeCwd)
292
+ await startSession(ctx);
347
293
  });
348
294
  onSessionCheckpoint(pi, async (event) => {
349
295
  if (initPromise)
@@ -353,14 +299,8 @@ function installMcpAdapter(pi, options) {
353
299
  return prepareMcpCheckpoint(state, event);
354
300
  });
355
301
  pi.on("session_shutdown", async () => {
356
- if (activationKey) {
357
- const activeTools = new Set(pi.getActiveTools());
358
- const inactiveTools = new Set([...registeredDirectTools.keys()].filter((name) => !activeTools.has(name)));
359
- if (inactiveTools.size > 0)
360
- inactiveDirectToolsBySession.set(activationKey, inactiveTools);
361
- else
362
- inactiveDirectToolsBySession.delete(activationKey);
363
- }
302
+ // Cached tool selections exist before background server initialization finishes.
303
+ toolLoader.persist();
364
304
  ++lifecycleGeneration;
365
305
  const currentState = state;
366
306
  const owner = currentOwner;
@@ -575,6 +515,74 @@ function installMcpAdapter(pi, options) {
575
515
  }
576
516
  },
577
517
  });
518
+ async function getToolState(signal) {
519
+ const owner = currentOwner;
520
+ if (!state && initPromise) {
521
+ try {
522
+ const initialized = await awaitWithTimeout(abortable(initPromise, signal), INIT_WAIT_TIMEOUT_MS);
523
+ if (initialized === INIT_WAIT_TIMED_OUT)
524
+ return {
525
+ content: [{ type: "text", text: "MCP initialization is still in progress. Try again shortly." }],
526
+ details: { error: "init_timeout", timeoutMs: INIT_WAIT_TIMEOUT_MS },
527
+ };
528
+ owner?.throwIfInactive();
529
+ state = initialized;
530
+ }
531
+ catch (error) {
532
+ if (signal?.aborted || (owner && isAbortError(error, owner.signal)))
533
+ throw error;
534
+ const message = error instanceof Error ? error.message : String(error);
535
+ return { content: [{ type: "text", text: `MCP initialization failed: ${message}` }], details: { error: "init_failed", message } };
536
+ }
537
+ }
538
+ owner?.throwIfInactive();
539
+ return state ?? { content: [{ type: "text", text: "MCP not initialized" }], details: { error: "not_initialized" } };
540
+ }
541
+ async function searchAndLoad(params, signal, ctx) {
542
+ const ready = await getToolState(signal);
543
+ if ("content" in ready)
544
+ return { ...ready, tools: [] };
545
+ if (params.server && !ready.toolMetadata.has(params.server)) {
546
+ const discovery = await executeConnect(ready, params.server, signal);
547
+ if (discovery.details.error)
548
+ return { ...discovery, tools: [] };
549
+ }
550
+ syncToolSurface(ctx);
551
+ const matches = params.query.trim() ? rankToolMatches(ready, params.query, params.server)
552
+ : 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
+ 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);
556
+ const guarded = await guardMcpOutput([
557
+ ...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." },
559
+ ], resolveMcpOutputGuardOptions(ready.config.settings, ready.outputDirectory));
560
+ return { content: guarded.content, details: { ...result.details, ...guardedMcpDetails(guarded), loaded: tools }, tools };
561
+ }
562
+ const searchDefinition = {
563
+ 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.",
565
+ parameters: Type.Object({
566
+ query: Type.String(),
567
+ server: Type.Optional(Type.String()),
568
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_PAGE_SIZE, description: "Tools to load (default 5)." })),
569
+ offset: Type.Optional(Type.Integer({ minimum: 0 })),
570
+ }),
571
+ renderResult: renderMcpToolResult,
572
+ async execute(toolCallId, params, signal, _onUpdate, ctx) {
573
+ if (beforeExecute) {
574
+ if (currentOwner)
575
+ await currentOwner.runCallback("beforeExecute", () => beforeExecute(toolCallId, ctx));
576
+ else
577
+ await beforeExecute(toolCallId, ctx);
578
+ }
579
+ return searchAndLoad(params, signal, ctx);
580
+ },
581
+ };
582
+ if (toolLoader.native)
583
+ pi.registerToolSearch(searchDefinition);
584
+ else
585
+ pi.registerTool(searchDefinition);
578
586
  let scriptToolRegistered = false;
579
587
  function registerScriptTool() {
580
588
  if (scriptToolRegistered)
@@ -583,7 +591,7 @@ function installMcpAdapter(pi, options) {
583
591
  ...(beforeExecute ? { executionMode: "sequential" } : {}),
584
592
  name: "mcp_script",
585
593
  label: "MCP Script",
586
- description: "Run trusted JavaScript that makes multiple MCP tool calls in one request — loop, filter, chain, or fan out between calls. For a single MCP call, search, describe, status check, or auth action, use the mcp tool instead. Discover with await tools.search({ query }) — resolves to { items: [{ path, name, server, description? }], total, hasMore, nextOffset }, not an { ok, data } envelope. Inspect with await tools.describe({ path }) — resolves to the tool descriptor with inputTypeScript when representable, otherwise the original JSON inputSchema, or { path, error: { code, message, suggestions } }. Then call tools.call(path, args) — resolves to { ok: true, data } or { ok: false, error: { code, message } }; data is the raw MCP result: tool calls usually return { content, structuredContent? }, while resource reads return { contents }. The sandbox has no Node, filesystem, or network globals. Use direct flat calls when the name is already known; use emit(value) for user-visible output. Load the mcp-scripting skill for the full workflow guide.",
594
+ description: "Compose MCP calls with trusted JavaScript: await tools.search({query, server?}), tools.describe({path}), tools.call(path,args). Describe returns complete JSON schemas. Calls return {ok,data} with raw MCP results, or {ok:false,error,data?}. Resources: tools.resources({server}), tools.readResource({server,uri}). Saved results: tools.readResult({ref,path?,fields?,offset?,limit?}); never repeats a remote call. Use emit(value) to return selected data or content blocks. No Node, filesystem, or network globals. Prefer a direct tool for one call; use scripts for loops, filtering and chains.",
587
595
  promptSnippet: "Batch multiple MCP tool calls in one JavaScript request (loop, filter, chain)",
588
596
  parameters: Type.Object({
589
597
  code: Type.String({ description: "Trusted JavaScript MCP script. Use tools.<prefixedToolName>(args) and emit(value)." }),
@@ -593,37 +601,10 @@ function installMcpAdapter(pi, options) {
593
601
  }),
594
602
  renderResult: renderMcpToolResult,
595
603
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
596
- const executeOwner = currentOwner;
597
- if (!state && initPromise) {
598
- try {
599
- const initialized = await awaitWithTimeout(abortable(initPromise, signal), INIT_WAIT_TIMEOUT_MS);
600
- if (initialized === INIT_WAIT_TIMED_OUT) {
601
- return {
602
- content: [{ type: "text", text: "MCP initialization is still in progress. Try again shortly." }],
603
- details: { mode: "script", error: "init_timeout", timeoutMs: INIT_WAIT_TIMEOUT_MS },
604
- };
605
- }
606
- executeOwner?.throwIfInactive();
607
- state = initialized;
608
- }
609
- catch (error) {
610
- if (signal?.aborted || (executeOwner && isAbortError(error, executeOwner.signal)))
611
- throw error;
612
- const message = error instanceof Error ? error.message : String(error);
613
- return {
614
- content: [{ type: "text", text: `MCP initialization failed: ${message}` }],
615
- details: { mode: "script", error: "init_failed", message },
616
- };
617
- }
618
- }
619
- if (!state) {
620
- return {
621
- content: [{ type: "text", text: "MCP not initialized" }],
622
- details: { mode: "script", error: "not_initialized" },
623
- };
624
- }
625
- executeOwner?.throwIfInactive();
626
- return runMcpScript(state, params.code, params.timeoutMs ?? options.defaultScriptTimeoutMs, getPiTools, signal, toolCallId, beforeExecute ? (callSignal, operation) => beforeExecute(toolCallId, { ...ctx, signal: callSignal }, operation) : undefined);
604
+ const ready = await getToolState(signal);
605
+ if ("content" in ready)
606
+ return ready;
607
+ return runMcpScript(ready, params.code, params.timeoutMs ?? options.defaultScriptTimeoutMs, getPiTools, signal, toolCallId, beforeExecute ? (callSignal, operation) => beforeExecute(toolCallId, { ...ctx, signal: callSignal }, operation) : undefined);
627
608
  },
628
609
  });
629
610
  scriptToolRegistered = true;
@@ -644,147 +625,60 @@ function installMcpAdapter(pi, options) {
644
625
  function registerProxyTool(description) {
645
626
  pi.registerTool({
646
627
  ...(beforeExecute ? { executionMode: "sequential" } : {}),
647
- name: "mcp",
648
- label: "MCP",
649
- description,
650
- promptSnippet: "MCP gateway — status, search, describe, auth, and single MCP tool calls",
628
+ name: "mcp", label: "MCP", description,
629
+ promptSnippet: "MCP server operations, resources, saved result readback, and fallback tool calls",
651
630
  renderCall: renderMcpProxyToolCall,
652
- parameters: Type.Object({
653
- tool: Type.Optional(Type.String({ maxLength: MAX_TOOL_NAME_LENGTH, description: "Tool name to call (e.g., 'xcodebuild_list_sims')" })),
654
- args: Type.Optional(Type.Union([
655
- Type.String({ description: "Arguments as a JSON string (e.g., '{\"key\": \"value\"}')" }),
656
- Type.Object({}, {
657
- additionalProperties: true,
658
- description: 'Arguments as a JSON object (e.g., { "key": "value" })',
659
- }),
660
- ], { description: "Tool arguments as a JSON object, or as a JSON string encoding one" })),
661
- connect: Type.Optional(Type.String({ description: "Server name to connect (lazy connect + metadata refresh)" })),
662
- describe: Type.Optional(Type.String({ maxLength: MAX_TOOL_NAME_LENGTH, description: "Tool name to describe (shows parameters)" })),
663
- instructions: Type.Optional(Type.String({ description: "Server name to show that server's usage instructions" })),
664
- search: Type.Optional(Type.String({ description: "Search tools by name/description" })),
665
- includeSchemas: Type.Optional(Type.Boolean({ description: "Include parameter schemas in search results (default: true)" })),
666
- limit: Type.Optional(Type.Number({ minimum: 1, maximum: MAX_PAGE_SIZE, description: "Maximum search or server-list results to return (default: 12, max: 100)" })),
667
- offset: Type.Optional(Type.Number({ minimum: 0, description: "Search or server-list result offset (default: 0)" })),
668
- server: Type.Optional(Type.String({ description: "Filter to specific server (also disambiguates tool calls)" })),
669
- action: Type.Optional(Type.String({ description: "Action: 'ui-messages', 'auth-start', or 'auth-complete'" })),
670
- }),
631
+ parameters: gatewayParameters,
632
+ prepareArguments: prepareGatewayArguments,
671
633
  renderResult: renderMcpToolResult,
672
- async execute(toolCallId, params, signal, _onUpdate, ctx) {
673
- // Non-call modes have no resolved MCP operation; hosts keep their conservative policy.
674
- if (!params.tool || params.action === "ui-messages" || params.action === "auth-start" || params.action === "auth-complete") {
675
- if (beforeExecute) {
676
- if (currentOwner)
677
- await currentOwner.runCallback("beforeExecute", () => beforeExecute(toolCallId, ctx));
678
- else
679
- await beforeExecute(toolCallId, ctx);
680
- }
681
- }
682
- const executeOwner = currentOwner;
683
- let parsedArgs;
684
- if (params.args !== undefined && params.args !== "") {
685
- let args;
686
- if (typeof params.args === "string") {
687
- try {
688
- args = JSON.parse(params.args);
689
- }
690
- catch (error) {
691
- if (error instanceof SyntaxError) {
692
- throw new Error(`Invalid args JSON: ${error.message}`, { cause: error });
693
- }
694
- throw error;
695
- }
696
- }
697
- else {
698
- args = params.args;
699
- }
700
- if (typeof args !== "object" || args === null || Array.isArray(args)) {
701
- const gotType = Array.isArray(args) ? "array" : args === null ? "null" : typeof args;
702
- throw new Error(`Invalid args: expected a JSON object, got ${gotType}`);
703
- }
704
- parsedArgs = args;
705
- }
706
- if (!state && initPromise) {
707
- try {
708
- const initialized = await awaitWithTimeout(abortable(initPromise, signal), INIT_WAIT_TIMEOUT_MS);
709
- if (initialized === INIT_WAIT_TIMED_OUT) {
710
- return {
711
- content: [{ type: "text", text: "MCP initialization is still in progress. Try again shortly." }],
712
- details: { error: "init_timeout", timeoutMs: INIT_WAIT_TIMEOUT_MS },
713
- };
714
- }
715
- executeOwner?.throwIfInactive();
716
- state = initialized;
717
- }
718
- catch (error) {
719
- if (signal?.aborted || (executeOwner && isAbortError(error, executeOwner.signal)))
720
- throw error;
721
- const message = error instanceof Error ? error.message : String(error);
722
- return {
723
- content: [{ type: "text", text: `MCP initialization failed: ${message}` }],
724
- details: { error: "init_failed", message },
725
- };
726
- }
727
- }
728
- if (!state) {
729
- return {
730
- content: [{ type: "text", text: "MCP not initialized" }],
731
- details: { error: "not_initialized" },
732
- };
733
- }
734
- executeOwner?.throwIfInactive();
735
- if (params.action === "ui-messages") {
736
- return executeUiMessages(state);
634
+ async execute(toolCallId, rawParams, signal, _onUpdate, ctx) {
635
+ const params = prepareGatewayArguments(rawParams);
636
+ if (beforeExecute && params.action !== "call" && params.action !== "read-resource") {
637
+ if (currentOwner)
638
+ await currentOwner.runCallback("beforeExecute", () => beforeExecute(toolCallId, ctx));
639
+ else
640
+ await beforeExecute(toolCallId, ctx);
737
641
  }
738
- if (params.action === "auth-start") {
739
- if (!params.server) {
740
- return {
741
- content: [{ type: "text", text: "auth-start requires `server`. Example: mcp({ action: \"auth-start\", server: \"linear-server\" })" }],
742
- details: { mode: "auth-start", error: "missing_server" },
743
- };
744
- }
745
- return signal
746
- ? executeAuthStart(state, params.server, signal)
747
- : executeAuthStart(state, params.server);
642
+ const ready = await getToolState(signal);
643
+ if ("content" in ready)
644
+ return ready;
645
+ const present = async (result) => {
646
+ const guarded = await guardMcpOutput(result.content, resolveMcpOutputGuardOptions(ready.config.settings, ready.outputDirectory));
647
+ return { content: guarded.content, details: { ...result.details, ...guardedMcpDetails(guarded) } };
648
+ };
649
+ const beforeDispatch = beforeExecute
650
+ ? (callSignal, operation) => beforeExecute(toolCallId, { ...ctx, signal: callSignal }, operation)
651
+ : undefined;
652
+ if (params.server && ["list", "search", "describe", "instructions"].includes(params.action) && !ready.toolMetadata.has(params.server)) {
653
+ const discovery = await executeConnect(ready, params.server, signal);
654
+ if (discovery.details.error)
655
+ return discovery;
656
+ syncToolSurface(ctx);
748
657
  }
749
- if (params.action === "auth-complete") {
750
- if (!params.server) {
751
- return {
752
- content: [{ type: "text", text: "auth-complete requires `server`." }],
753
- details: { mode: "auth-complete", error: "missing_server" },
754
- };
658
+ switch (params.action) {
659
+ case "ui-messages": return executeUiMessages(ready);
660
+ case "auth-start": return executeAuthStart(ready, params.server, signal);
661
+ case "auth-complete": {
662
+ const input = params.args?.redirectUrl ?? params.args?.code ?? params.args?.input;
663
+ if (input !== undefined && (typeof input !== "string" || input.trim().length === 0))
664
+ throw new Error("auth-complete requires a non-empty redirectUrl, code, or input");
665
+ return executeAuthComplete(ready, params.server, input, signal);
755
666
  }
756
- const input = parsedArgs?.redirectUrl ?? parsedArgs?.code ?? parsedArgs?.input;
757
- if (input !== undefined && (typeof input !== "string" || input.trim().length === 0)) {
758
- return {
759
- content: [{ type: "text", text: "auth-complete accepts a non-empty `redirectUrl`, `code`, or `input`, or no args to use the browser callback." }],
760
- details: { mode: "auth-complete", error: "missing_input" },
761
- };
667
+ case "call": return executeCall(ready, params.tool, params.args, params.server, getPiTools, signal, { toolCallId }, beforeDispatch);
668
+ case "connect": {
669
+ const result = await executeConnect(ready, params.server, signal, params.limit, params.offset);
670
+ syncToolSurface(ctx);
671
+ return present(result);
762
672
  }
763
- return signal
764
- ? executeAuthComplete(state, params.server, input, signal)
765
- : executeAuthComplete(state, params.server, input);
766
- }
767
- if (params.tool) {
768
- return executeCall(state, params.tool, parsedArgs, params.server, getPiTools, signal, undefined, { toolCallId }, beforeExecute ? (callSignal, operation) => beforeExecute(toolCallId, { ...ctx, signal: callSignal }, operation) : undefined);
769
- }
770
- if (params.connect) {
771
- const result = await executeConnect(state, params.connect, signal, params.limit, params.offset);
772
- syncToolSurface(ctx);
773
- return result;
673
+ case "describe": return present(executeDescribe(ready, params.tool, params.server));
674
+ case "instructions": return present(executeInstructions(ready, params.server));
675
+ case "search": return present(executeSearch(ready, params.query, params.server, params.includeSchemas, params.limit, params.offset));
676
+ case "list": return present(executeList(ready, params.server, params.limit, params.offset));
677
+ case "resources": return present(await executeResourceList(ready, params.server, params.limit, params.offset, signal));
678
+ case "read-resource": return executeResourceRead(ready, params.server, params.uri, signal, { toolCallId }, beforeDispatch);
679
+ case "read-result": return readMcpResult({ ref: params.ref, ...(params.path !== undefined ? { path: params.path } : {}), ...(params.fields ? { fields: params.fields } : {}), ...(params.offset !== undefined ? { offset: params.offset } : {}), ...(params.limit !== undefined ? { limit: params.limit } : {}) }, resolveMcpOutputGuardOptions(ready.config.settings, ready.outputDirectory));
680
+ case "status": return present(executeStatus(ready));
774
681
  }
775
- if (params.describe) {
776
- return executeDescribe(state, params.describe);
777
- }
778
- if (params.instructions) {
779
- return executeInstructions(state, params.instructions);
780
- }
781
- if (params.search !== undefined) {
782
- return executeSearch(state, params.search, params.server, params.includeSchemas, params.limit, params.offset);
783
- }
784
- if (params.server) {
785
- return executeList(state, params.server, params.limit, params.offset);
786
- }
787
- return executeStatus(state);
788
682
  },
789
683
  });
790
684
  proxyToolRegistered = true;
@@ -792,7 +686,19 @@ function installMcpAdapter(pi, options) {
792
686
  }
793
687
  function syncProxyTool(config, cache, directSpecs) {
794
688
  const missingConfiguredDirectToolServers = getMissingConfiguredDirectToolServers(config, cache, envRaw === undefined || envRaw === "__none__" ? undefined : envDirectToolOverride);
689
+ const hasPinnedResources = envRaw !== "__none__" && Object.entries(config.mcpServers).some(([server, definition]) => {
690
+ if (definition.disabled === true)
691
+ return false;
692
+ const selection = directToolSelection(config, server, envDirectToolOverride);
693
+ if (!selection)
694
+ return false;
695
+ const entry = cache?.servers[server];
696
+ const metadata = state?.toolMetadata.get(server) ?? (entry && isServerCacheValid(entry, definition)
697
+ ? reconstructToolMetadata(server, entry, config.settings?.toolPrefix ?? "server", definition) : []);
698
+ return metadata.some(tool => tool.resourceUri && (selection === true || selection.includes(tool.originalName)));
699
+ });
795
700
  const shouldRegisterProxyTool = config.settings?.disableProxyTool !== true
701
+ || hasPinnedResources
796
702
  || directSpecs.length === 0
797
703
  || missingConfiguredDirectToolServers.length > 0;
798
704
  if (shouldRegisterProxyTool) {
@@ -813,8 +719,7 @@ function installMcpAdapter(pi, options) {
813
719
  }
814
720
  // A transform needs the session context before any configured tool surface is registered.
815
721
  if (!options.transformConfig) {
816
- const initialDirectTools = syncDirectTools(earlyConfig, earlyCache).specs;
817
- syncProxyTool(earlyConfig, earlyCache, initialDirectTools);
722
+ syncProxyTool(earlyConfig, earlyCache, []);
818
723
  syncScriptTool(earlyConfig);
819
724
  }
820
725
  }
package/dist/init.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { isServerDisabled } from "./types.js";
2
- import { existsSync } from "node:fs";
3
2
  import { cloneMcpConfig, isPathInsideProject, loadMcpConfig } from "./config.js";
4
3
  import { ConsentManager } from "./consent-manager.js";
5
4
  import { McpLifecycleManager } from "./lifecycle.js";
@@ -177,18 +176,7 @@ export async function initializeMcp(pi, ctx, owner = createMcpRuntimeOwner(), op
177
176
  }
178
177
  const idleSetting = typeof config.settings?.idleTimeout === "number" ? config.settings.idleTimeout : 10;
179
178
  lifecycle.setGlobalIdleTimeout(idleSetting);
180
- const cachePath = getMetadataCachePath();
181
- const cacheFileExists = metadataCacheEnabled && existsSync(cachePath);
182
- let cache = loadMetadataCache(metadataCacheEnabled);
183
- let bootstrapAll = false;
184
- if (!cacheFileExists) {
185
- bootstrapAll = true;
186
- saveMetadataCache({ version: 1, servers: {} }, metadataCacheEnabled);
187
- }
188
- else if (!cache) {
189
- cache = { version: 1, servers: {} };
190
- saveMetadataCache(cache, metadataCacheEnabled);
191
- }
179
+ const cache = loadMetadataCache(metadataCacheEnabled);
192
180
  const prefix = config.settings?.toolPrefix ?? "server";
193
181
  for (const [name, definition] of serverEntries) {
194
182
  const lifecycleMode = definition.lifecycle ?? "lazy";
@@ -213,12 +201,10 @@ export async function initializeMcp(pi, ctx, owner = createMcpRuntimeOwner(), op
213
201
  }
214
202
  }
215
203
  }
216
- const startupServers = bootstrapAll
217
- ? serverEntries
218
- : serverEntries.filter(([, definition]) => {
219
- const mode = definition.lifecycle ?? "lazy";
220
- return mode === "keep-alive" || mode === "eager";
221
- });
204
+ const startupServers = serverEntries.filter(([, definition]) => {
205
+ const mode = definition.lifecycle ?? "lazy";
206
+ return mode === "keep-alive" || mode === "eager";
207
+ });
222
208
  if (ui && startupServers.length > 0) {
223
209
  const status = formatMcpStatus(state.config, `connecting to ${startupServers.length} servers...`);
224
210
  ui.setStatus("mcp", status);
@@ -324,7 +310,7 @@ export async function initializeMcp(pi, ctx, owner = createMcpRuntimeOwner(), op
324
310
  const bootstrapped = bootstrapResults.filter(r => r.ok).map(r => r.name);
325
311
  owner.throwIfInactive();
326
312
  if (bootstrapped.length > 0 && ui) {
327
- ui.notify(`MCP: direct tools for ${bootstrapped.join(", ")} will be available after restart`, "info");
313
+ ui.notify(`MCP: tools discovered for ${bootstrapped.join(", ")}`, "info");
328
314
  }
329
315
  }
330
316
  }