@fitchmultz/pi-mcp-adapter 8.0.1 → 8.0.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +3 -3
  3. package/dist/app-bridge.bundle.js +13 -13
  4. package/dist/client-operations.d.ts +5 -0
  5. package/dist/client-operations.js +19 -0
  6. package/dist/commands.d.ts +1 -1
  7. package/dist/commands.js +3 -1
  8. package/dist/connection-runtime.d.ts +503 -0
  9. package/dist/connection-runtime.js +98 -0
  10. package/dist/direct-tools.js +1 -1
  11. package/dist/host-html-template.js +24 -12
  12. package/dist/index.js +36 -29
  13. package/dist/init.d.ts +1 -1
  14. package/dist/init.js +66 -38
  15. package/dist/lifecycle.d.ts +1 -1
  16. package/dist/lifecycle.js +1 -1
  17. package/dist/mcp-auth-flow.d.ts +2 -43
  18. package/dist/mcp-auth-flow.js +3 -138
  19. package/dist/mcp-callback-server.js +1 -1
  20. package/dist/mcp-code.d.ts +0 -1
  21. package/dist/mcp-code.js +20 -7
  22. package/dist/mcp-oauth-callback-config.d.ts +8 -0
  23. package/dist/mcp-oauth-callback-config.js +28 -0
  24. package/dist/mcp-oauth-provider.d.ts +1 -8
  25. package/dist/mcp-oauth-provider.js +2 -28
  26. package/dist/mcp-oauth-runtime.d.ts +83 -0
  27. package/dist/mcp-oauth-runtime.js +145 -0
  28. package/dist/mcp-output-guard.d.ts +1 -1
  29. package/dist/mcp-output-guard.js +5 -108
  30. package/dist/mcp-output-preview.d.ts +29 -0
  31. package/dist/mcp-output-preview.js +220 -0
  32. package/dist/mcp-panel.js +1 -2
  33. package/dist/mcp-script-worker.mjs +14 -2
  34. package/dist/metadata-cache.d.ts +1 -1
  35. package/dist/metadata-cache.js +56 -14
  36. package/dist/plain-result-reader.d.ts +7 -0
  37. package/dist/plain-result-reader.js +201 -0
  38. package/dist/prompts.js +1 -1
  39. package/dist/proxy-modes.d.ts +1 -1
  40. package/dist/proxy-modes.js +14 -5
  41. package/dist/public-types.d.ts +2 -0
  42. package/dist/public-types.js +2 -0
  43. package/dist/resource-tools.d.ts +0 -1
  44. package/dist/resource-tools.js +0 -13
  45. package/dist/server-manager.d.ts +4 -4
  46. package/dist/server-manager.js +25 -125
  47. package/dist/tool-loader.js +10 -4
  48. package/dist/tool-metadata.js +20 -3
  49. package/dist/tool-result-renderer.d.ts +1 -1
  50. package/dist/tool-result-renderer.js +10 -3
  51. package/dist/types.d.ts +1 -1
  52. package/dist/types.js +0 -3
  53. package/dist/ui-message-history.js +3 -2
  54. package/dist/ui-resource-handler.d.ts +2 -1
  55. package/dist/ui-resource-handler.js +4 -3
  56. package/dist/ui-server.js +2 -1
  57. package/dist/ui-session.js +2 -1
  58. package/dist/utils.d.ts +1 -0
  59. package/dist/utils.js +1 -0
  60. package/package.json +4 -4
@@ -332,11 +332,6 @@ export function buildHostHtmlTemplate(input) {
332
332
  bridge.setHostContext(JSON.parse(event.data));
333
333
  } catch {}
334
334
  });
335
- eventSource.addEventListener("session-complete", async () => {
336
- await bridge.teardownResource({}).catch(() => {});
337
- eventSource.close();
338
- closeOrShowDone();
339
- });
340
335
  eventSource.onerror = () => {
341
336
  setStatus("Connection lost", true);
342
337
  };
@@ -345,18 +340,35 @@ export function buildHostHtmlTemplate(input) {
345
340
  post("/proxy/ui/heartbeat", {}).catch(() => {});
346
341
  }, 10000);
347
342
 
348
- const complete = async (reason) => {
349
- try {
350
- await post("/proxy/ui/complete", { reason });
351
- } catch {}
352
- try {
353
- await bridge.teardownResource({});
354
- } catch {}
343
+ let completionRequested = false;
344
+ let finished = false;
345
+ const finish = async () => {
346
+ if (finished) return;
347
+ finished = true;
355
348
  clearInterval(heartbeat);
356
349
  eventSource.close();
350
+
351
+ try {
352
+ await bridge.teardownResource({});
353
+ } catch (error) {
354
+ // Apps may omit the optional teardown handler.
355
+ if (error?.code !== -32601) console.error("[host] Resource teardown failed:", error);
356
+ }
357
357
  closeOrShowDone();
358
358
  };
359
359
 
360
+ const complete = async (reason) => {
361
+ if (completionRequested || finished) return;
362
+ completionRequested = true;
363
+ try {
364
+ await post("/proxy/ui/complete", { reason });
365
+ } catch (error) {
366
+ console.error("[host] Session completion failed:", error);
367
+ }
368
+ await finish();
369
+ };
370
+
371
+ eventSource.addEventListener("session-complete", finish);
360
372
  doneBtn.addEventListener("click", () => complete("done"));
361
373
  cancelBtn.addEventListener("click", () => complete("cancel"));
362
374
  window.addEventListener("keydown", (event) => {
package/dist/index.js CHANGED
@@ -1,22 +1,17 @@
1
1
  import { Type } from "typebox";
2
- import { showStatus, showTools, showPrompts, reconnectServer, reconnectServers, authenticateServer, logoutServer, openMcpAuthPanel, openMcpPanel, openMcpSetup } from "./commands.js";
3
2
  import { cloneMcpConfig, isPathInsideProject, loadMcpConfig, writeProjectServerDisabledOverride } from "./config.js";
4
3
  import { buildProxyDescription, directToolSelection, getMissingConfiguredDirectToolServers } from "./direct-tools.js";
5
- import { flushMetadataCache, initializeMcp, lazyConnect, updateStatusBar } from "./init.js";
6
4
  import { getMetadataCachePath, isServerCacheValid, loadMetadataCache, reconstructToolMetadata } from "./metadata-cache.js";
7
5
  import { createPromptCommand, resolveCachedPrompts } from "./prompts.js";
8
6
  import { logger } from "./logger.js";
9
- import { executeAuthComplete, executeAuthStart, executeCall, executeConnect, executeDescribe, executeInstructions, executeList, executeSearch, executeStatus, executeUiMessages } from "./proxy-modes.js";
10
- import { formatTerminalError } from "./utils.js";
11
- import { createOAuthRuntime, shutdownOAuth } from "./mcp-auth-flow.js";
7
+ import { DEFAULT_MCP_SCRIPT_TIMEOUT_MS, formatTerminalError } from "./utils.js";
8
+ import { createOAuthRuntime, shutdownOAuth } from "./mcp-oauth-runtime.js";
12
9
  import { renderMcpProxyToolCall, renderMcpToolResult } from "./tool-result-renderer.js";
13
10
  import { createMcpRuntimeOwner, createOwnedUi, isAbortError } from "./runtime-owner.js";
14
11
  import { publishMcpStatusShutdown } from "./mcp-status.js";
15
- import { DEFAULT_MCP_SCRIPT_TIMEOUT_MS, runMcpScript } from "./mcp-code.js";
16
12
  import { MAX_PAGE_SIZE, paginate, rankToolMatches } from "./search-ranking.js";
17
13
  import { createToolLoader } from "./tool-loader.js";
18
14
  import { gatewayParameters, prepareGatewayArguments } from "./gateway-arguments.js";
19
- import { executeResourceList, executeResourceRead } from "./resource-tools.js";
20
15
  import { guardMcpOutput, guardedMcpDetails, readMcpResult, resolveMcpOutputGuardOptions } from "./mcp-output-guard.js";
21
16
  import { abortable, throwIfAborted } from "./abort.js";
22
17
  export { MCP_STATUS_EVENT, MCP_STATUS_SNAPSHOT_VERSION, } from "./types.js";
@@ -47,6 +42,9 @@ function installMcpAdapter(pi, options) {
47
42
  let currentOAuthRuntime = null;
48
43
  let lifecycleGeneration = 0;
49
44
  let currentConfigPath;
45
+ // init loads during session_start, after the cached surface registers, keeping it off the extension-load path.
46
+ // Command, panel, gateway, script and resource modules load on first use.
47
+ let initModule;
50
48
  async function shutdownState(currentState, reason) {
51
49
  if (!currentState) {
52
50
  publishMcpStatusShutdown(pi.events);
@@ -67,7 +65,7 @@ function installMcpAdapter(pi, options) {
67
65
  }
68
66
  currentState.uiServer = null;
69
67
  try {
70
- flushMetadataCache(currentState);
68
+ initModule?.flushMetadataCache(currentState);
71
69
  }
72
70
  catch (error) {
73
71
  failures.push(error);
@@ -132,8 +130,8 @@ function installMcpAdapter(pi, options) {
132
130
  description: "Path to MCP config file",
133
131
  type: "string",
134
132
  });
135
- function startInitialization(ctx, owner, oauthRuntime, runtimeConfig, generation) {
136
- const promise = initializeMcp(pi, ctx, owner, {
133
+ function startInitialization(init, ctx, owner, oauthRuntime, runtimeConfig, generation) {
134
+ const promise = init.initializeMcp(pi, ctx, owner, {
137
135
  ...(programmaticConfig ? { config: runtimeConfig } : { resolvedConfig: runtimeConfig }),
138
136
  ...(options.outputDirectory !== undefined ? { outputDirectory: options.outputDirectory } : {}),
139
137
  oauthRuntime,
@@ -164,12 +162,12 @@ function installMcpAdapter(pi, options) {
164
162
  };
165
163
  for (const server of toolLoader.selectedServers()) {
166
164
  if (!nextState.toolMetadata.has(server) && nextState.config.mcpServers[server])
167
- await lazyConnect(nextState, server, owner.signal);
165
+ await init.lazyConnect(nextState, server, owner.signal);
168
166
  }
169
167
  owner.throwIfInactive();
170
168
  syncPromptCommands();
171
169
  syncToolSurface(ctx);
172
- updateStatusBar(nextState);
170
+ init.updateStatusBar(nextState);
173
171
  initPromise = null;
174
172
  if (nextState.config.settings?.freezeDirectTools === true) {
175
173
  directToolsFrozen = true;
@@ -246,7 +244,11 @@ function installMcpAdapter(pi, options) {
246
244
  registerPromptCommands(resolveCachedPrompts(runtimeConfig, metadataCacheEnabled));
247
245
  const runtimeCache = loadMetadataCache(metadataCacheEnabled);
248
246
  toolLoader.sync(runtimeConfig, runtimeCache, envRaw === "__none__" ? null : envDirectToolOverride, specs => syncGateways(runtimeConfig, runtimeCache, specs));
249
- const initialization = startInitialization(ctx, owner, oauthRuntime, runtimeConfig, generation);
247
+ const init = await import("./init.js");
248
+ initModule = init;
249
+ if (generation !== lifecycleGeneration || !owner.isActive())
250
+ return;
251
+ const initialization = startInitialization(init, ctx, owner, oauthRuntime, runtimeConfig, generation);
250
252
  if (envRaw !== undefined && envRaw !== "__none__") {
251
253
  const missingEnvDirectTools = getMissingConfiguredDirectToolServers(runtimeConfig, runtimeCache, envDirectToolOverride);
252
254
  if (missingEnvDirectTools.length > 0) {
@@ -358,6 +360,7 @@ function installMcpAdapter(pi, options) {
358
360
  const commandState = await ensureCommandState(commandOwner, commandCtx);
359
361
  if (!commandState)
360
362
  return;
363
+ const { showStatus, showTools, showPrompts, reconnectServers, logoutServer, openMcpPanel, openMcpSetup } = await import("./commands.js");
361
364
  const parts = args?.trim()?.split(/\s+/) ?? [];
362
365
  const subcommand = parts[0] ?? "";
363
366
  const targetServer = parts[1];
@@ -467,6 +470,7 @@ function installMcpAdapter(pi, options) {
467
470
  const commandState = await ensureCommandState(commandOwner, commandCtx);
468
471
  if (!commandState)
469
472
  return;
473
+ const { authenticateServer, openMcpAuthPanel, reconnectServer } = await import("./commands.js");
470
474
  if (!serverName) {
471
475
  if (programmaticConfig) {
472
476
  commandCtx.ui?.notify("Use /mcp-auth <server> to authenticate a server from the in-memory SDK config.", "info");
@@ -519,8 +523,9 @@ function installMcpAdapter(pi, options) {
519
523
  const ready = await getToolState(signal);
520
524
  if ("content" in ready)
521
525
  return { ...ready, tools: [] };
526
+ const proxy = await import("./proxy-modes.js");
522
527
  if (params.server && !ready.toolMetadata.has(params.server)) {
523
- const discovery = await executeConnect(ready, params.server, signal);
528
+ const discovery = await proxy.executeConnect(ready, params.server, signal);
524
529
  if (discovery.details.error)
525
530
  return { ...discovery, tools: [] };
526
531
  }
@@ -531,7 +536,7 @@ function installMcpAdapter(pi, options) {
531
536
  const tools = toolLoader.activate(page.items, params.enable);
532
537
  const unavailable = unavailableFeatures(params.enable, tools);
533
538
  const typedCount = tools.filter(tool => tool.name !== "mcp" && tool.name !== "mcp_script").length;
534
- const result = executeSearch(ready, params.query, params.server, typedCount < page.items.length, params.limit ?? 5, params.offset, "mcp_search");
539
+ const result = proxy.executeSearch(ready, params.query, params.server, typedCount < page.items.length, params.limit ?? 5, params.offset, "mcp_search");
535
540
  const guarded = await guardMcpOutput([
536
541
  ...result.content,
537
542
  { 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.' },
@@ -548,7 +553,7 @@ function installMcpAdapter(pi, options) {
548
553
  server: Type.Optional(Type.String()),
549
554
  limit: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_PAGE_SIZE, description: "Tools to load (default 5)." })),
550
555
  offset: Type.Optional(Type.Integer({ minimum: 0 })),
551
- }, { anyOf: [{ required: ["query"] }, { required: ["enable"] }] }),
556
+ }, { anyOf: [{ type: "object", required: ["query"] }, { type: "object", required: ["enable"] }] }),
552
557
  renderResult: renderMcpToolResult,
553
558
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
554
559
  if (beforeExecute) {
@@ -582,6 +587,7 @@ function installMcpAdapter(pi, options) {
582
587
  const ready = await getToolState(signal);
583
588
  if ("content" in ready)
584
589
  return ready;
590
+ const { runMcpScript } = await import("./mcp-code.js");
585
591
  return runMcpScript(ready, params.code, params.timeoutMs ?? options.defaultScriptTimeoutMs, getPiTools, signal, toolCallId, beforeExecute ? (callSignal, operation) => beforeExecute(toolCallId, { ...ctx, signal: callSignal }, operation) : undefined);
586
592
  },
587
593
  });
@@ -607,6 +613,7 @@ function installMcpAdapter(pi, options) {
607
613
  const ready = await getToolState(signal);
608
614
  if ("content" in ready)
609
615
  return ready;
616
+ const proxy = await import("./proxy-modes.js");
610
617
  const present = async (result) => {
611
618
  const guarded = await guardMcpOutput(result.content, resolveMcpOutputGuardOptions(ready.config.settings, ready.outputDirectory));
612
619
  return { content: guarded.content, details: { ...result.details, ...guardedMcpDetails(guarded) } };
@@ -615,34 +622,34 @@ function installMcpAdapter(pi, options) {
615
622
  ? (callSignal, operation) => beforeExecute(toolCallId, { ...ctx, signal: callSignal }, operation)
616
623
  : undefined;
617
624
  if (params.server && ["list", "search", "describe", "instructions"].includes(params.action) && !ready.toolMetadata.has(params.server)) {
618
- const discovery = await executeConnect(ready, params.server, signal);
625
+ const discovery = await proxy.executeConnect(ready, params.server, signal);
619
626
  if (discovery.details.error)
620
627
  return discovery;
621
628
  syncToolSurface(ctx);
622
629
  }
623
630
  switch (params.action) {
624
- case "ui-messages": return executeUiMessages(ready);
625
- case "auth-start": return executeAuthStart(ready, params.server, signal);
631
+ case "ui-messages": return proxy.executeUiMessages(ready);
632
+ case "auth-start": return proxy.executeAuthStart(ready, params.server, signal);
626
633
  case "auth-complete": {
627
634
  const input = params.args?.redirectUrl ?? params.args?.code ?? params.args?.input;
628
635
  if (input !== undefined && (typeof input !== "string" || input.trim().length === 0))
629
636
  throw new Error("auth-complete requires a non-empty redirectUrl, code, or input");
630
- return executeAuthComplete(ready, params.server, input, signal);
637
+ return proxy.executeAuthComplete(ready, params.server, input, signal);
631
638
  }
632
- case "call": return executeCall(ready, params.tool, params.args, params.server, getPiTools, signal, { toolCallId }, beforeDispatch);
639
+ case "call": return proxy.executeCall(ready, params.tool, params.args, params.server, getPiTools, signal, { toolCallId }, beforeDispatch);
633
640
  case "connect": {
634
- const result = await executeConnect(ready, params.server, signal, params.limit, params.offset);
641
+ const result = await proxy.executeConnect(ready, params.server, signal, params.limit, params.offset);
635
642
  syncToolSurface(ctx);
636
643
  return present(result);
637
644
  }
638
- case "describe": return present(executeDescribe(ready, params.tool, params.server));
639
- case "instructions": return present(executeInstructions(ready, params.server));
640
- case "search": return present(executeSearch(ready, params.query, params.server, params.includeSchemas, params.limit, params.offset));
641
- case "list": return present(executeList(ready, params.server, params.limit, params.offset));
642
- case "resources": return present(await executeResourceList(ready, params.server, params.limit, params.offset, signal));
643
- case "read-resource": return executeResourceRead(ready, params.server, params.uri, signal, { toolCallId }, beforeDispatch);
645
+ case "describe": return present(proxy.executeDescribe(ready, params.tool, params.server));
646
+ case "instructions": return present(proxy.executeInstructions(ready, params.server));
647
+ case "search": return present(proxy.executeSearch(ready, params.query, params.server, params.includeSchemas, params.limit, params.offset));
648
+ case "list": return present(proxy.executeList(ready, params.server, params.limit, params.offset));
649
+ case "resources": return present(await (await import("./resource-tools.js")).executeResourceList(ready, params.server, params.limit, params.offset, signal));
650
+ case "read-resource": return (await import("./resource-tools.js")).executeResourceRead(ready, params.server, params.uri, signal, { toolCallId }, beforeDispatch);
644
651
  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));
645
- case "status": return present(executeStatus(ready));
652
+ case "status": return present(proxy.executeStatus(ready));
646
653
  }
647
654
  },
648
655
  });
package/dist/init.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { McpExtensionState } from "./state.ts";
3
3
  import { type McpAdapterOptions, type McpConfig } from "./types.ts";
4
- import { type McpOAuthRuntime } from "./mcp-auth-flow.ts";
4
+ import { type McpOAuthRuntime } from "./mcp-oauth-runtime.ts";
5
5
  import { type McpRuntimeOwner } from "./runtime-owner.ts";
6
6
  export declare function clearFailure(state: McpExtensionState, serverName: string): void;
7
7
  export declare function recordFailure(state: McpExtensionState, serverName: string, message: string): void;
package/dist/init.js CHANGED
@@ -10,7 +10,7 @@ import { formatMcpStatus, openUrl, parallelLimit, sanitizeTerminalText } from ".
10
10
  import { logger } from "./logger.js";
11
11
  import { throwIfAborted } from "./abort.js";
12
12
  import { getAuthStorageOptions } from "./mcp-auth.js";
13
- import { createOAuthRuntime, hasPendingAuth, shutdownOAuth } from "./mcp-auth-flow.js";
13
+ import { createOAuthRuntime, hasPendingAuth, shutdownOAuth } from "./mcp-oauth-runtime.js";
14
14
  import { combineAbortSignals, createMcpRuntimeOwner, createOwnedUi, isAbortError, } from "./runtime-owner.js";
15
15
  import { publishMcpStatusSnapshot } from "./mcp-status.js";
16
16
  const FAILURE_BACKOFF_MS = 60 * 1000;
@@ -229,40 +229,48 @@ export async function initializeMcp(pi, ctx, owner = createMcpRuntimeOwner(), op
229
229
  if (initialSignal?.aborted)
230
230
  return state;
231
231
  owner.throwIfInactive();
232
- for (const { name, definition, connection, error } of results) {
233
- owner.throwIfInactive();
234
- if (error || !connection) {
235
- if (initialSignal?.aborted)
232
+ const startupCache = { entries: new Map() };
233
+ try {
234
+ for (const { name, definition, connection, error } of results) {
235
+ owner.throwIfInactive();
236
+ if (error || !connection) {
237
+ if (initialSignal?.aborted)
238
+ continue;
239
+ saveMetadataCacheUpdates(state, startupCache);
240
+ if (error)
241
+ recordFailure(state, name, error);
242
+ const displayError = sanitizeTerminalText(error ?? "Unknown connection failure");
243
+ if (ui) {
244
+ ui.notify(`MCP: Failed to connect to ${name}: ${displayError}`, "error");
245
+ }
246
+ console.error(`MCP: Failed to connect to ${name}: ${displayError}`);
236
247
  continue;
237
- if (error)
238
- recordFailure(state, name, error);
239
- const displayError = sanitizeTerminalText(error ?? "Unknown connection failure");
240
- if (ui) {
241
- ui.notify(`MCP: Failed to connect to ${name}: ${displayError}`, "error");
242
248
  }
243
- console.error(`MCP: Failed to connect to ${name}: ${displayError}`);
244
- continue;
245
- }
246
- const { metadata, failedTools } = buildToolMetadata(connection.tools, connection.resources, definition, name, prefix);
247
- toolMetadata.set(name, metadata);
248
- resourceCounts.set(name, connection.resources.length);
249
- if (!connection.promptDiscoveryFailed) {
250
- promptMetadata.set(name, reconstructPromptMetadata(name, connection.prompts ?? [], prefix, definition));
251
- promptMetadataLive.add(name);
252
- }
253
- if (connection.instructions) {
254
- serverInstructions.set(name, connection.instructions);
255
- }
256
- else {
257
- serverInstructions.delete(name);
258
- }
259
- updateMetadataCache(state, name);
260
- notifyToolMetadataUpdated(state, name, "startup");
261
- markKeepAliveAfterConnect(state, name);
262
- if (failedTools.length > 0 && ui) {
263
- ui.notify(`MCP: ${name} - ${failedTools.length} tools skipped`, "warning");
249
+ const { metadata, failedTools } = buildToolMetadata(connection.tools, connection.resources, definition, name, prefix);
250
+ toolMetadata.set(name, metadata);
251
+ resourceCounts.set(name, connection.resources.length);
252
+ if (!connection.promptDiscoveryFailed) {
253
+ promptMetadata.set(name, reconstructPromptMetadata(name, connection.prompts ?? [], prefix, definition));
254
+ promptMetadataLive.add(name);
255
+ }
256
+ if (connection.instructions) {
257
+ serverInstructions.set(name, connection.instructions);
258
+ }
259
+ else {
260
+ serverInstructions.delete(name);
261
+ }
262
+ collectMetadataCacheUpdate(state, name, startupCache);
263
+ notifyToolMetadataUpdated(state, name, "startup");
264
+ markKeepAliveAfterConnect(state, name);
265
+ if (failedTools.length > 0 && ui) {
266
+ saveMetadataCacheUpdates(state, startupCache);
267
+ ui.notify(`MCP: ${name} - ${failedTools.length} tools skipped`, "warning");
268
+ }
264
269
  }
265
270
  }
271
+ finally {
272
+ saveMetadataCacheUpdates(state, startupCache);
273
+ }
266
274
  const connectedCount = results.filter(r => r.connection).length;
267
275
  const failedCount = results.filter(r => r.error).length;
268
276
  if (ui && connectedCount > 0) {
@@ -382,7 +390,7 @@ export function updateServerMetadata(state, serverName) {
382
390
  state.serverInstructions?.delete(serverName);
383
391
  }
384
392
  }
385
- export function updateMetadataCache(state, serverName, options = {}) {
393
+ function collectMetadataCacheUpdate(state, serverName, updates, options = {}) {
386
394
  const connection = state.manager.getConnection(serverName);
387
395
  if (!connection || connection.status !== "connected")
388
396
  return;
@@ -390,8 +398,9 @@ export function updateMetadataCache(state, serverName, options = {}) {
390
398
  if (!definition || isServerDisabled(definition))
391
399
  return;
392
400
  const configHash = computeServerHash(definition);
393
- const existing = loadMetadataCache(state.metadataCacheEnabled);
394
- const existingEntry = existing?.servers?.[serverName];
401
+ if (updates.existing === undefined)
402
+ updates.existing = loadMetadataCache(state.metadataCacheEnabled);
403
+ const existingEntry = updates.existing?.servers?.[serverName];
395
404
  const tools = serializeTools(connection.tools);
396
405
  let resources = definition.exposeResources === false ? [] : serializeResources(connection.resources);
397
406
  const prompts = connection.promptDiscoveryFailed
@@ -412,7 +421,20 @@ export function updateMetadataCache(state, serverName, options = {}) {
412
421
  ...(connection.instructions !== undefined ? { instructions: connection.instructions } : {}),
413
422
  cachedAt: Date.now(),
414
423
  };
415
- saveMetadataCache({ version: 1, servers: { [serverName]: entry } }, state.metadataCacheEnabled);
424
+ updates.entries.set(serverName, entry);
425
+ }
426
+ function saveMetadataCacheUpdates(state, updates) {
427
+ if (updates.entries.size === 0)
428
+ return;
429
+ const servers = Object.fromEntries(updates.entries);
430
+ updates.entries.clear();
431
+ delete updates.existing;
432
+ saveMetadataCache({ version: 1, servers }, state.metadataCacheEnabled, Object.keys(state.config.mcpServers));
433
+ }
434
+ export function updateMetadataCache(state, serverName, options = {}) {
435
+ const updates = { entries: new Map() };
436
+ collectMetadataCacheUpdate(state, serverName, updates, options);
437
+ saveMetadataCacheUpdates(state, updates);
416
438
  }
417
439
  export function notifyToolMetadataUpdated(state, serverName, reason) {
418
440
  try {
@@ -430,11 +452,17 @@ export function notifyToolMetadataUpdated(state, serverName, reason) {
430
452
  }
431
453
  }
432
454
  export function flushMetadataCache(state) {
433
- for (const [name, connection] of state.manager.getAllConnections()) {
434
- if (connection.status === "connected") {
435
- updateMetadataCache(state, name);
455
+ const updates = { entries: new Map() };
456
+ try {
457
+ for (const [name, connection] of state.manager.getAllConnections()) {
458
+ if (connection.status === "connected") {
459
+ collectMetadataCacheUpdate(state, name, updates);
460
+ }
436
461
  }
437
462
  }
463
+ finally {
464
+ saveMetadataCacheUpdates(state, updates);
465
+ }
438
466
  }
439
467
  export function updateStatusBar(state) {
440
468
  publishMcpStatusSnapshot(state);
@@ -1,6 +1,6 @@
1
1
  import { type ServerDefinition } from "./types.ts";
2
2
  import type { McpServerManager } from "./server-manager.ts";
3
- import { hasPendingAuth } from "./mcp-auth-flow.ts";
3
+ import { hasPendingAuth } from "./mcp-oauth-runtime.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 {
package/dist/lifecycle.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isServerDisabled } from "./types.js";
2
- import { hasPendingAuth } from "./mcp-auth-flow.js";
2
+ import { hasPendingAuth } from "./mcp-oauth-runtime.js";
3
3
  import { logger } from "./logger.js";
4
4
  import { formatTerminalError, sanitizeTerminalText } from "./utils.js";
5
5
  export class McpLifecycleManager {
@@ -5,36 +5,14 @@
5
5
  */
6
6
  import { UnauthorizedError, InsufficientScopeError, SdkHttpError } from "@modelcontextprotocol/client";
7
7
  import { type McpOAuthConfig } from "./mcp-oauth-provider.ts";
8
- import { type AuthStorageOptions } from "./mcp-auth.ts";
9
8
  import { type ServerEntry } from "./types.ts";
10
- /** Auth status for a server */
11
- export type AuthStatus = "authenticated" | "expired" | "not_authenticated";
12
- export interface McpOAuthRuntime {
13
- readonly signal: AbortSignal;
14
- }
15
- export interface AuthenticateOptions {
16
- onAuthorizationUrl?: (authorizationUrl: string) => void | Promise<void>;
17
- authStorageOptions?: AuthStorageOptions;
18
- signal?: AbortSignal;
19
- runtime?: McpOAuthRuntime;
20
- }
21
- export declare function createOAuthRuntime(signal?: AbortSignal): McpOAuthRuntime;
22
- type OAuthRequest = {
23
- serverName: string;
24
- serverUrl: string;
25
- requestedScope?: string;
26
- challenge?: {
27
- requiredScope?: string;
28
- resourceMetadataUrl?: URL;
29
- };
30
- issuer?: string;
31
- };
9
+ import { type AuthStatus, type AuthenticateOptions, type McpOAuthRuntime, type OAuthRequest } from "./mcp-oauth-runtime.ts";
10
+ export { createOAuthRuntime, hasPendingAuth, initializeOAuth, shutdownOAuth, supportsOAuth, type AuthStatus, type AuthenticateOptions, type McpOAuthRuntime, } from "./mcp-oauth-runtime.ts";
32
11
  export declare function isOAuthChallenge(error: unknown): error is InsufficientScopeError | UnauthorizedError | SdkHttpError;
33
12
  export declare function getOAuthRequest(serverName: string, serverUrl: string, storageBase: string, runtime?: McpOAuthRuntime): OAuthRequest | undefined;
34
13
  /** Observe native challenges only; no credential reads or auth work here. */
35
14
  export declare function recordOAuthChallenge(serverName: string, serverUrl: string, storageBase: string, error: unknown, runtime?: McpOAuthRuntime): void;
36
15
  export declare function bindOAuthRequestIssuer(serverName: string, serverUrl: string, storageBase: string, issuer: string, runtime?: McpOAuthRuntime): void;
37
- export declare function hasPendingAuth(serverName: string, options?: AuthStorageOptions, runtime?: McpOAuthRuntime): boolean;
38
16
  /**
39
17
  * Extract OAuth configuration from a ServerEntry.
40
18
  */
@@ -86,22 +64,3 @@ export declare function authenticate(serverName: string, serverUrl: string, defi
86
64
  * @param serverName - The name of the MCP server
87
65
  */
88
66
  export declare function removeAuth(serverName: string, options?: AuthenticateOptions): Promise<void>;
89
- /**
90
- * Check if OAuth is supported for a server configuration.
91
- * OAuth is supported for HTTP servers unless explicitly disabled.
92
- *
93
- * @param definition - The server definition
94
- * @returns True if OAuth is supported
95
- */
96
- export declare function supportsOAuth(definition: ServerEntry): boolean;
97
- /**
98
- * Initialize the OAuth system on startup.
99
- * OAuth callback binding is lazy and starts from startAuth() only.
100
- */
101
- export declare function initializeOAuth(runtimeOrSignal?: McpOAuthRuntime | AbortSignal): Promise<McpOAuthRuntime>;
102
- /**
103
- * Shutdown one OAuth runtime. The callback server remains process-shared while
104
- * another runtime has pending/reserved callback state or is still active.
105
- */
106
- export declare function shutdownOAuth(runtime?: McpOAuthRuntime): Promise<void>;
107
- export {};