@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
@@ -0,0 +1,201 @@
1
+ import { appendFileSync, fstatSync, realpathSync, statSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { StringDecoder } from "node:string_decoder";
4
+ import { open } from "node:fs/promises";
5
+ import { byteLength } from "./mcp-output-preview.js";
6
+ const indexes = new Map();
7
+ const readers = new Map();
8
+ const MAX_INDEXES = 8;
9
+ const MAX_POINTS = 1024;
10
+ function sameRevision(a, b) {
11
+ return a.dev === b.dev && a.ino === b.ino && a.size === b.size
12
+ && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs;
13
+ }
14
+ /** Raw full-content SHA-256. Returns "" on a short read so a truncated verify never matches. */
15
+ async function digestOf(file, size) {
16
+ const hash = createHash("sha256");
17
+ const buffer = Buffer.allocUnsafe(64 * 1024);
18
+ let position = 0;
19
+ while (position < size) {
20
+ const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, size - position), position);
21
+ if (!bytesRead)
22
+ return "";
23
+ hash.update(buffer.subarray(0, bytesRead));
24
+ position += bytesRead;
25
+ }
26
+ return hash.digest("hex");
27
+ }
28
+ function checkpoint(index, point) {
29
+ if (point.bytes - index.points.at(-1).bytes < index.stride)
30
+ return;
31
+ index.points.push(point);
32
+ if (index.points.length > MAX_POINTS) {
33
+ index.points = index.points.filter((_, i) => i % 2 === 0);
34
+ index.stride *= 2;
35
+ }
36
+ }
37
+ function remember(ref, index) {
38
+ indexes.delete(ref);
39
+ indexes.set(ref, index);
40
+ if (indexes.size > MAX_INDEXES)
41
+ indexes.delete(indexes.keys().next().value);
42
+ }
43
+ function extend(index, after, text) {
44
+ // Clone before updating so a shared cached hasher never advances.
45
+ index.hasher = index.hasher.copy();
46
+ index.hasher.update(text, "utf8");
47
+ let position = Number(index.revision.size);
48
+ checkpoint(index, { bytes: position, characters: index.total });
49
+ // Index only the accepted delta, keeping checkpoints on complete UTF-8 code points.
50
+ for (let start = 0; start < text.length;) {
51
+ let end = Math.min(text.length, start + 16 * 1024);
52
+ if (end < text.length && text.codePointAt(end - 1) > 0xffff)
53
+ end++;
54
+ position += byteLength(text.slice(start, end));
55
+ checkpoint(index, { bytes: position, characters: index.total + end });
56
+ start = end;
57
+ }
58
+ index.total += text.length;
59
+ index.revision = after;
60
+ index.digest = index.hasher.copy().digest("hex");
61
+ }
62
+ /** Only the native, synchronous history writer can certify an append. External changes invalidate. */
63
+ export function appendPlainOutput(ref, text) {
64
+ let key = ref, cached, before;
65
+ try {
66
+ key = indexes.size || readers.size ? realpathSync(ref) : ref;
67
+ cached = indexes.get(key);
68
+ indexes.delete(key);
69
+ if (cached || readers.has(key))
70
+ before = statSync(ref, { bigint: true });
71
+ }
72
+ catch { /* Cache bookkeeping must not change write acceptance. */ }
73
+ appendFileSync(ref, text, { flush: true });
74
+ if (!before)
75
+ return;
76
+ try {
77
+ const after = statSync(ref, { bigint: true });
78
+ if (after.dev !== before.dev || after.ino !== before.ino
79
+ || after.size !== before.size + BigInt(byteLength(text)))
80
+ return;
81
+ if (cached && sameRevision(cached.revision, before) && !text.includes("\ufffd")) {
82
+ const next = { ...cached, points: [...cached.points] };
83
+ extend(next, after, text);
84
+ remember(key, next);
85
+ }
86
+ for (const progress of readers.get(key) ?? []) {
87
+ if (!sameRevision(progress.revision, before))
88
+ continue;
89
+ extend(progress, after, text);
90
+ if (text.includes("\ufffd"))
91
+ progress.cacheable = false;
92
+ }
93
+ }
94
+ catch { /* The durable append already succeeded; leave its index invalidated. */ }
95
+ }
96
+ /** Sparse byte/UTF16 checkpoints, never retained file contents. Changed/uncacheable files scan afresh. */
97
+ export async function readPlainResult(ref, offset, limit, maxBytes, maxLines) {
98
+ for (let attempt = 0; attempt < 2; attempt++) {
99
+ const file = await open(ref, "r");
100
+ let progress;
101
+ try {
102
+ // Pair snapshot registration/validation with the synchronous writer without yielding between them.
103
+ const revision = fstatSync(file.fd, { bigint: true });
104
+ const cached = indexes.get(ref);
105
+ const matched = cached !== undefined && sameRevision(cached.revision, revision);
106
+ const fresh = { revision, total: 0, points: [{ bytes: 0, characters: 0 }],
107
+ stride: 64 * 1024, digest: "", hasher: createHash("sha256") };
108
+ let index = matched ? cached : fresh;
109
+ // Append characters stay relative until the cold scan knows the opened prefix's total.
110
+ progress = { revision, total: 0, points: [{ bytes: Number(revision.size), characters: 0 }],
111
+ stride: index.stride, cacheable: true, digest: "", hasher: index.hasher.copy() };
112
+ const active = readers.get(ref) ?? new Set();
113
+ active.add(progress);
114
+ readers.set(ref, active);
115
+ // Matching stat fields are not an immutability proof: verify the cached digest against current
116
+ // bytes. Registering the snapshot first keeps live appends paired during the verification read.
117
+ let valid = matched ? cached : undefined;
118
+ if (valid && valid.digest !== (await digestOf(file, Number(revision.size)))) {
119
+ indexes.delete(ref);
120
+ valid = undefined;
121
+ index = fresh;
122
+ progress.hasher = createHash("sha256");
123
+ progress.stride = fresh.stride;
124
+ }
125
+ let point = index.points[0];
126
+ if (valid) {
127
+ for (const candidate of index.points) {
128
+ if (candidate.characters > offset)
129
+ break;
130
+ point = candidate;
131
+ }
132
+ }
133
+ let total = point.characters, position = point.bytes;
134
+ let bytes = 0, lines = 1, page = "", finished = false, cacheable = true;
135
+ // Bound each attempt to its opened file snapshot, including while native history appends.
136
+ if (revision.size > 0n) {
137
+ const decoder = new StringDecoder("utf8");
138
+ const buffer = Buffer.allocUnsafe(64 * 1024);
139
+ let readPosition = point.bytes;
140
+ while (readPosition < Number(revision.size)) {
141
+ const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, Number(revision.size) - readPosition), readPosition);
142
+ if (!bytesRead)
143
+ break;
144
+ readPosition += bytesRead;
145
+ const chunk = decoder.write(buffer.subarray(0, bytesRead)) + (readPosition === Number(revision.size) ? decoder.end() : "");
146
+ if (!valid) {
147
+ progress.hasher.update(buffer.subarray(0, bytesRead));
148
+ checkpoint(index, { bytes: position, characters: total });
149
+ // Invalid/incomplete UTF8 can re-decode differently after append. Keep the full-scan fallback.
150
+ if (chunk.includes("\ufffd"))
151
+ cacheable = false;
152
+ position += byteLength(chunk);
153
+ }
154
+ const start = Math.max(0, offset - total);
155
+ if (!finished && start < chunk.length) {
156
+ if (start > 0 && chunk.codePointAt(start - 1) > 0xffff)
157
+ throw new Error("offset must not split a Unicode surrogate pair");
158
+ for (const char of chunk.slice(start)) {
159
+ const nextBytes = bytes + byteLength(char);
160
+ const nextLines = lines + (char === "\n" ? 1 : 0);
161
+ if (page && (nextBytes > maxBytes || nextLines > maxLines || page.length + char.length > limit)) {
162
+ finished = true;
163
+ break;
164
+ }
165
+ page += char;
166
+ bytes = nextBytes;
167
+ lines = nextLines;
168
+ }
169
+ }
170
+ total += chunk.length;
171
+ if (valid && finished)
172
+ break;
173
+ }
174
+ }
175
+ // Only verified native appends may advance this snapshot. External edits still invalidate.
176
+ if (!sameRevision(progress.revision, statSync(ref, { bigint: true })))
177
+ continue;
178
+ if (!valid)
179
+ index.total = total;
180
+ // Serve the opened prefix; retain verified append checkpoints for the next read.
181
+ if (cacheable && progress.cacheable) {
182
+ const next = { ...index, revision: progress.revision, total: index.total + progress.total,
183
+ points: [...index.points], digest: progress.hasher.copy().digest("hex"), hasher: progress.hasher };
184
+ for (const point of progress.points)
185
+ checkpoint(next, { bytes: point.bytes, characters: index.total + point.characters });
186
+ remember(ref, next);
187
+ }
188
+ return { page, total: index.total };
189
+ }
190
+ finally {
191
+ if (progress) {
192
+ const active = readers.get(ref);
193
+ active.delete(progress);
194
+ if (!active.size)
195
+ readers.delete(ref);
196
+ }
197
+ await file.close();
198
+ }
199
+ }
200
+ throw new Error("MCP output artifact changed while reading; retry the page");
201
+ }
package/dist/prompts.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { isServerDisabled } from "./types.js";
2
- import { lazyConnect } from "./init.js";
3
2
  import { isServerCacheValid, loadMetadataCache, reconstructPromptMetadata } from "./metadata-cache.js";
4
3
  import { logger } from "./logger.js";
5
4
  import { truncateAtWord } from "./utils.js";
@@ -233,6 +232,7 @@ export function createPromptCommand(pi, getState, metadata) {
233
232
  }
234
233
  return;
235
234
  }
235
+ const { lazyConnect } = await import("./init.js");
236
236
  const connected = await lazyConnect(state, metadata.serverName, ctx.signal);
237
237
  if (!connected) {
238
238
  if (ctx.hasUI) {
@@ -2,7 +2,7 @@ import type { AgentToolResult, ToolInfo } from "@earendil-works/pi-coding-agent"
2
2
  import type { McpExtensionState } from "./state.ts";
3
3
  import type { ServerConnection } from "./server-manager.ts";
4
4
  import type { ToolMetadata, McpOperationContext, McpToolCallIdentity } from "./types.ts";
5
- import { type SessionRecoveryDeps } from "./session-recovery.ts";
5
+ import type { SessionRecoveryDeps } from "./session-recovery.ts";
6
6
  type ProxyToolResult = AgentToolResult<Record<string, unknown>>;
7
7
  export declare function executeUiMessages(state: McpExtensionState): Promise<ProxyToolResult>;
8
8
  export declare function executeStatus(state: McpExtensionState): ProxyToolResult;
@@ -1,4 +1,4 @@
1
- import { DEFAULT_REQUEST_TIMEOUT_MSEC, UrlElicitationRequiredError } from "@modelcontextprotocol/client";
1
+ import { isDeepStrictEqual } from "node:util";
2
2
  import { getServerPrefix, isServerDisabled, isNonInteractiveOAuth, parseUiPromptHandoff, resolveToolPrefix } from "./types.js";
3
3
  import { lazyConnect, markKeepAliveAfterConnect, notifyToolMetadataUpdated, updateServerMetadata, updateMetadataCache, getFailureAgeSeconds, updateStatusBar, clearFailure, recordFailure } from "./init.js";
4
4
  import { abortable, throwIfAborted } from "./abort.js";
@@ -7,10 +7,8 @@ import { buildToolMetadata, catalogCoverage, findToolByName, formatSchema, toToo
7
7
  import { reconstructPromptMetadata } from "./metadata-cache.js";
8
8
  import { renderMcpResultContent } from "./tool-registrar.js";
9
9
  import { guardMcpOutput, guardedMcpDetails, retainMcpResult, resolveMcpOutputGuardOptions, formatMcpResultReference, saveMcpOutputStream } from "./mcp-output-guard.js";
10
- import { maybeStartUiSession, summarizeUiSessionResult } from "./ui-session.js";
11
10
  import { formatAuthRequiredMessage, formatMcpStatus, resolveServerUrl, truncateAtWord } from "./utils.js";
12
- import { authenticate, completeAuthFromInput, startAuth, supportsOAuth } from "./mcp-auth-flow.js";
13
- import { isInterruptedToolCall, isToolTransportFailure, trackToolCallOutcome, SessionRecoveryAuthRequiredError, withSessionRecovery } from "./session-recovery.js";
11
+ import { supportsOAuth } from "./mcp-oauth-runtime.js";
14
12
  import { paginate, rankSuggestions, rankToolMatches } from "./search-ranking.js";
15
13
  import { ensureToolCallApproved, isToolCallApprovalRequired } from "./tool-approval.js";
16
14
  import { materializeUiMessages, uiMessages } from "./ui-message-history.js";
@@ -96,6 +94,7 @@ async function attemptAutoAuth(state, serverName, signal) {
96
94
  };
97
95
  }
98
96
  try {
97
+ const { authenticate } = await import("./mcp-auth-flow.js");
99
98
  if (state.authStorageOptions) {
100
99
  await authenticate(serverName, serverUrl, definition, signal
101
100
  ? { authStorageOptions: state.authStorageOptions, signal, runtime: state.oauthRuntime }
@@ -340,6 +339,7 @@ export function executeStatus(state) {
340
339
  };
341
340
  }
342
341
  export async function executeAuthStart(state, serverName, signal) {
342
+ const { startAuth } = await import("./mcp-auth-flow.js");
343
343
  const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
344
344
  throwIfAborted(ownedSignal);
345
345
  const definition = state.config.mcpServers[serverName];
@@ -386,6 +386,7 @@ export async function executeAuthStart(state, serverName, signal) {
386
386
  }
387
387
  }
388
388
  export async function executeAuthComplete(state, serverName, input, signal) {
389
+ const { completeAuthFromInput } = await import("./mcp-auth-flow.js");
389
390
  const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
390
391
  throwIfAborted(ownedSignal);
391
392
  const definition = state.config.mcpServers[serverName];
@@ -715,6 +716,9 @@ export async function executeConnect(state, serverName, signal, limit, offset) {
715
716
  * Raw scripts skip content rendering but retain protocol results and artifacts.
716
717
  */
717
718
  export async function runToolCall(state, serverName, target, args, options) {
719
+ const { DEFAULT_REQUEST_TIMEOUT_MSEC, UrlElicitationRequiredError } = await import("@modelcontextprotocol/client");
720
+ const { isInterruptedToolCall, isToolTransportFailure, trackToolCallOutcome, SessionRecoveryAuthRequiredError, withSessionRecovery } = await import("./session-recovery.js");
721
+ const { maybeStartUiSession, summarizeUiSessionResult } = await import("./ui-session.js");
718
722
  const { detailsBase, signal, recoverAuthConnection, authRequiredMessage, autoAuthAttempted } = options;
719
723
  const configuredOptions = state.manager.getRequestOptions?.(serverName, options.ownedSignal);
720
724
  const callerSignal = combineAbortSignals(configuredOptions?.signal, options.ownedSignal);
@@ -746,6 +750,8 @@ export async function runToolCall(state, serverName, target, args, options) {
746
750
  try {
747
751
  state.manager.touch(serverName);
748
752
  state.manager.incrementInFlight(serverName);
753
+ // Loading the connection-only modules can yield to cancellation or shutdown.
754
+ throwIfAborted(callerSignal);
749
755
  uiSession = !target.resourceUri && target.uiResourceUri
750
756
  ? await abortable(maybeStartUiSession(state, {
751
757
  serverName,
@@ -892,6 +898,7 @@ export async function runToolCall(state, serverName, target, args, options) {
892
898
  }
893
899
  /** Connect/auth once for calls and resource discovery. Recovery stays bounded by withSessionRecovery. */
894
900
  export async function prepareMcpConnection(state, serverName, signal) {
901
+ const { SessionRecoveryAuthRequiredError } = await import("./session-recovery.js");
895
902
  const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
896
903
  throwIfAborted(ownedSignal);
897
904
  let autoAuthAttempted = false;
@@ -951,7 +958,9 @@ export async function prepareMcpConnection(state, serverName, signal) {
951
958
  // Use the live descriptor for visibility, filtering, approval, schema and UI hints.
952
959
  if (connection.tools) {
953
960
  const { metadata } = buildToolMetadata(connection.tools, connection.resources ?? [], definition, serverName, state.config.settings?.toolPrefix ?? "server");
954
- state.toolMetadata.set(serverName, metadata);
961
+ // Search indexes by metadata-array identity, so retain it for unchanged projections.
962
+ if (!isDeepStrictEqual(state.toolMetadata.get(serverName), metadata))
963
+ state.toolMetadata.set(serverName, metadata);
955
964
  }
956
965
  return { connection, recoverAuthConnection, autoAuthAttempted: () => autoAuthAttempted };
957
966
  }
@@ -0,0 +1,2 @@
1
+ export * from "./types.ts";
2
+ export { UI_STREAM_HOST_CONTEXT_KEY, UI_STREAM_REQUEST_META_KEY, UI_STREAM_RESULT_PATCH_METHOD, SERVER_STREAM_RESULT_PATCH_METHOD, UI_STREAM_STRUCTURED_CONTENT_KEY, uiStreamModeSchema, visualizationStreamPhaseSchema, visualizationStreamFrameTypeSchema, visualizationStreamStatusSchema, uiStreamHostContextSchema, visualizationStreamEnvelopeSchema, uiStreamCallToolResultSchema, uiStreamResultPatchNotificationSchema, serverStreamResultPatchNotificationSchema, getUiStreamHostContext, getVisualizationStreamEnvelope, } from "./ui-stream-types.ts";
@@ -0,0 +1,2 @@
1
+ export * from "./types.js";
2
+ export { UI_STREAM_HOST_CONTEXT_KEY, UI_STREAM_REQUEST_META_KEY, UI_STREAM_RESULT_PATCH_METHOD, SERVER_STREAM_RESULT_PATCH_METHOD, UI_STREAM_STRUCTURED_CONTENT_KEY, uiStreamModeSchema, visualizationStreamPhaseSchema, visualizationStreamFrameTypeSchema, visualizationStreamStatusSchema, uiStreamHostContextSchema, visualizationStreamEnvelopeSchema, uiStreamCallToolResultSchema, uiStreamResultPatchNotificationSchema, serverStreamResultPatchNotificationSchema, getUiStreamHostContext, getVisualizationStreamEnvelope, } from "./ui-stream-types.js";
@@ -13,4 +13,3 @@ export declare function executeResourceList(state: McpExtensionState, server: st
13
13
  };
14
14
  }>;
15
15
  export declare function executeResourceRead(state: McpExtensionState, server: string, uri: string, signal?: AbortSignal, identity?: McpToolCallIdentity, beforeDispatch?: (signal: AbortSignal | undefined, operation: McpOperationContext) => Promise<void>, raw?: boolean): Promise<import("@earendil-works/pi-agent-core").AgentToolResult<Record<string, unknown>>>;
16
- export declare function resourceNameToToolName(name: string): string;
@@ -24,16 +24,3 @@ export async function executeResourceList(state, server, limit = 12, offset = 0,
24
24
  export async function executeResourceRead(state, server, uri, signal, identity = {}, beforeDispatch, raw = false) {
25
25
  return executeCall(state, uri, {}, server, undefined, signal, identity, beforeDispatch, { resourceUri: uri, raw });
26
26
  }
27
- export function resourceNameToToolName(name) {
28
- let result = name
29
- .replace(/[^a-zA-Z0-9]/g, "_")
30
- .replace(/_+/g, "_")
31
- .replace(/^_+/, "") // Remove leading underscores
32
- .replace(/_+$/, "") // Remove trailing underscores
33
- .toLowerCase();
34
- // Ensure we have a valid name
35
- if (!result || /^\d/.test(result)) {
36
- result = "resource" + (result ? "_" + result : "");
37
- }
38
- return result;
39
- }
@@ -1,9 +1,9 @@
1
- import { Client, type RequestOptions, type GetPromptResult, type ReadResourceResult, type UrlElicitationRequiredError } from "@modelcontextprotocol/client";
1
+ import type { Client, RequestOptions, GetPromptResult, ReadResourceResult, UrlElicitationRequiredError } from "@modelcontextprotocol/client";
2
2
  import { type McpTool, type McpResource, type McpPrompt, type ServerDefinition, type ServerStreamResultPatchNotification, type Transport, type McpTraceSettings } from "./types.ts";
3
- import { type McpOAuthRuntime } from "./mcp-auth-flow.ts";
3
+ import { type McpOAuthRuntime } from "./mcp-oauth-runtime.ts";
4
4
  import { type AuthStorageOptions } from "./mcp-auth.ts";
5
- import { type ServerSamplingConfig } from "./sampling-handler.ts";
6
- import { type ServerElicitationConfig } from "./elicitation-handler.ts";
5
+ import type { ServerSamplingConfig } from "./sampling-handler.ts";
6
+ import type { ServerElicitationConfig } from "./elicitation-handler.ts";
7
7
  export interface ServerConnection {
8
8
  client: Client;
9
9
  transport: Transport;
@@ -1,16 +1,9 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
- import { Client, StreamableHTTPClientTransport, SSEClientTransport, SdkHttpError, auth, extractWWWAuthenticateParams, UnauthorizedError, } from "@modelcontextprotocol/client";
3
- import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
4
- import { UnixSocketClientTransport } from "./unix-socket-transport.js";
5
- import { trackToolTransport, trackToolTransportFailure } from "./session-recovery.js";
6
- import { isServerDisabled, isNonInteractiveOAuth, validateServerProtocolConfig, serverStreamResultPatchNotificationSchema, } from "./types.js";
1
+ import { clientOperations, drainClientCallbacks, drainClientOperations } from "./client-operations.js";
2
+ import { isServerDisabled, isNonInteractiveOAuth, validateServerProtocolConfig, } from "./types.js";
7
3
  import { resolveNpxBinary } from "./npx-resolver.js";
8
4
  import { logger } from "./logger.js";
9
- import { McpOAuthProvider } from "./mcp-oauth-provider.js";
10
- import { extractOAuthConfig, supportsOAuth, isOAuthChallenge, recordOAuthChallenge, getOAuthRequest, bindOAuthRequestIssuer } from "./mcp-auth-flow.js";
5
+ import { supportsOAuth } from "./mcp-oauth-runtime.js";
11
6
  import { getAuthBaseDir } from "./mcp-auth.js";
12
- import { registerSamplingHandler } from "./sampling-handler.js";
13
- import { handleUrlElicitation, registerElicitationHandler, } from "./elicitation-handler.js";
14
7
  import { ADAPTER_VERSION, normalizeRequestTimeoutMs, resolveBearerToken, resolveCommandSecret, resolveCommandSecretsRecord, resolveConfigPath, resolveServerUrl, } from "./utils.js";
15
8
  import { abortable, throwIfAborted } from "./abort.js";
16
9
  import { combineAbortSignals } from "./runtime-owner.js";
@@ -18,108 +11,6 @@ import { createMcpTraceWriter, isMcpTraceEnabled, McpTraceWriter, traceTransport
18
11
  const MAX_CAPTURED_STDERR_BYTES = 8 * 1024;
19
12
  const MAX_CAPTURED_STDERR_LINES = 3;
20
13
  const abortCleanupPromises = new WeakMap();
21
- const clientOperations = new WeakMap();
22
- async function drainClientCallbacks(client) {
23
- for (;;) {
24
- const pending = [...(clientOperations.get(client) ?? [])]
25
- .filter(([, kind]) => kind === "sampling callback" || kind === "elicitation callback")
26
- .map(([operation]) => operation);
27
- if (!pending.length)
28
- return;
29
- await Promise.allSettled(pending);
30
- }
31
- }
32
- async function drainClientOperations(client) {
33
- for (;;) {
34
- const pending = [...(clientOperations.get(client)?.keys() ?? [])];
35
- if (!pending.length)
36
- return;
37
- await Promise.allSettled(pending);
38
- }
39
- }
40
- // Track complete SDK operations, including pagination and pre-request cache awaits.
41
- class ManagedClient extends Client {
42
- serverName = "";
43
- oauthProvider;
44
- own(operation, kind) {
45
- let pending = clientOperations.get(this);
46
- if (!pending)
47
- clientOperations.set(this, pending = new Map());
48
- pending.set(operation, kind);
49
- const settled = () => { pending.delete(operation); };
50
- void operation.then(settled, settled);
51
- return operation;
52
- }
53
- ownNative(run) {
54
- return this.own(run(), "request/refresh");
55
- }
56
- ownSend(run) {
57
- // SDK-generated callback replies also perform HTTP/auth work, without calling callTool().
58
- return this.own(Promise.resolve().then(run), "request/refresh");
59
- }
60
- ownCallback(kind, run) {
61
- // Insert before dispatch. Cancellation/close of the SDK response does not settle this promise.
62
- return this.own(Promise.resolve().then(run), kind);
63
- }
64
- async track(start, signal) {
65
- const controller = this.oauthProvider ? new AbortController() : undefined;
66
- let removeAbortListener = () => { };
67
- const operation = this.oauthProvider
68
- ? this.oauthProvider.runWithSignal(controller.signal, () => {
69
- // Native legacy cancellation notifications must retain their operation's auth context.
70
- const abort = AsyncLocalStorage.bind(() => controller.abort(signal?.reason));
71
- if (signal?.aborted)
72
- abort();
73
- else
74
- signal?.addEventListener("abort", abort, { once: true });
75
- removeAbortListener = () => signal?.removeEventListener("abort", abort);
76
- return start(controller.signal);
77
- })
78
- : start(signal);
79
- void operation.then(removeAbortListener, () => { controller?.abort(); removeAbortListener(); });
80
- return this.own(operation, "request/refresh");
81
- }
82
- callTool(...args) {
83
- return this.track(signal => super.callTool(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
84
- }
85
- readResource(...args) {
86
- return this.track(signal => super.readResource(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
87
- }
88
- getPrompt(...args) {
89
- return this.track(signal => super.getPrompt(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
90
- }
91
- listTools(...args) {
92
- return this.track(signal => super.listTools(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
93
- }
94
- listResources(...args) {
95
- return this.track(signal => super.listResources(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
96
- }
97
- listPrompts(...args) {
98
- return this.track(signal => super.listPrompts(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
99
- }
100
- complete(...args) {
101
- return this.track(signal => super.complete(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
102
- }
103
- setLoggingLevel(...args) {
104
- return this.track(signal => super.setLoggingLevel(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
105
- }
106
- listen(...args) {
107
- return this.track(async (signal) => {
108
- const subscription = await super.listen(args[0], { ...args[1], ...(signal ? { signal } : {}) });
109
- this.own(subscription.closed, "subscription");
110
- return subscription;
111
- }, args[1]?.signal);
112
- }
113
- subscribeResource(...args) {
114
- return this.track(async (signal) => {
115
- throwIfAborted(signal);
116
- return super.subscribeResource(args[0], { ...args[1], ...(signal ? { signal } : {}) });
117
- }, args[1]?.signal);
118
- }
119
- unsubscribeResource(...args) {
120
- return this.track(signal => super.unsubscribeResource(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
121
- }
122
- }
123
14
  function boundedStderrChunk(chunk) {
124
15
  if (Buffer.isBuffer(chunk)) {
125
16
  const start = Math.max(0, chunk.byteLength - MAX_CAPTURED_STDERR_BYTES);
@@ -171,12 +62,12 @@ export class McpServerManager {
171
62
  traceWriter;
172
63
  stopped = false;
173
64
  releaseClientWhenSettled(client) {
174
- if (client instanceof ManagedClient)
65
+ if (this.ownedClients.has(client))
175
66
  void drainClientOperations(client).then(() => this.ownedClients.delete(client));
176
67
  }
177
68
  /** Own native send/auth tails even after a public request has been cancelled. */
178
69
  ownTransportSends(transport, client) {
179
- if (typeof transport.send === "function" && client instanceof ManagedClient) {
70
+ if (typeof transport.send === "function") {
180
71
  const send = transport.send.bind(transport);
181
72
  transport.send = (...args) => client.ownSend(() => send(...args));
182
73
  }
@@ -369,7 +260,10 @@ export class McpServerManager {
369
260
  async createConnection(name, definition, signal, requestSignal) {
370
261
  throwIfAborted(signal);
371
262
  validateServerProtocolConfig(definition);
372
- let client = this.createClient(name, definition);
263
+ const runtime = await import("./connection-runtime.js");
264
+ const { StdioClientTransport, SdkHttpError, UnixSocketClientTransport, trackToolTransport, getOAuthRequest, isOAuthChallenge } = runtime;
265
+ throwIfAborted(signal);
266
+ let client = this.createClient(name, definition, runtime);
373
267
  const tracingEnabled = isMcpTraceEnabled(definition, this.traceSettings);
374
268
  const traceWriter = tracingEnabled
375
269
  ? (this.traceWriter ??= createMcpTraceWriter(this.defaultCwd, this.traceSettings ?? {}))
@@ -420,7 +314,7 @@ export class McpServerManager {
420
314
  }
421
315
  else if (definition.url) {
422
316
  // HTTP transport with fallback
423
- ({ transport, oauthProvider } = this.createHttpTransport(definition, name, client, false, implicitOAuth, onAuthChallenge));
317
+ ({ transport, oauthProvider } = this.createHttpTransport(runtime, definition, name, client, false, implicitOAuth, onAuthChallenge));
424
318
  }
425
319
  else {
426
320
  transport = new UnixSocketClientTransport(resolveConfigPath(definition.socket));
@@ -449,13 +343,13 @@ export class McpServerManager {
449
343
  legacySse = true;
450
344
  await client.close();
451
345
  this.releaseClientWhenSettled(client);
452
- client = this.createClient(name, { ...definition, protocolVersion: "legacy" });
453
- ({ transport, oauthProvider } = this.createHttpTransport(definition, name, client, legacySse, implicitOAuth, onAuthChallenge));
346
+ client = this.createClient(name, { ...definition, protocolVersion: "legacy" }, runtime);
347
+ ({ transport, oauthProvider } = this.createHttpTransport(runtime, definition, name, client, legacySse, implicitOAuth, onAuthChallenge));
454
348
  if (traceObserver)
455
349
  transport = wrapTransportWithMcpTrace(transport, name, traceTransportKind(definition, transport), traceObserver);
456
350
  continue;
457
351
  }
458
- this.attachAdapterNotificationHandlers(name, client);
352
+ this.attachAdapterNotificationHandlers(name, client, runtime.serverStreamResultPatchNotificationSchema);
459
353
  const instructions = client.getInstructions?.();
460
354
  const connection = {
461
355
  client,
@@ -501,8 +395,8 @@ export class McpServerManager {
501
395
  implicitOAuth = true;
502
396
  await client.close();
503
397
  this.releaseClientWhenSettled(client);
504
- client = this.createClient(name, legacySse ? { ...definition, protocolVersion: "legacy" } : definition);
505
- ({ transport, oauthProvider } = this.createHttpTransport(definition, name, client, legacySse, implicitOAuth, onAuthChallenge));
398
+ client = this.createClient(name, legacySse ? { ...definition, protocolVersion: "legacy" } : definition, runtime);
399
+ ({ transport, oauthProvider } = this.createHttpTransport(runtime, definition, name, client, legacySse, implicitOAuth, onAuthChallenge));
506
400
  if (traceObserver)
507
401
  transport = wrapTransportWithMcpTrace(transport, name, traceTransportKind(definition, transport), traceObserver);
508
402
  }
@@ -600,7 +494,8 @@ export class McpServerManager {
600
494
  : {}),
601
495
  };
602
496
  }
603
- createClient(serverName, definition) {
497
+ createClient(serverName, definition, runtime) {
498
+ const { ManagedClient, registerSamplingHandler, registerElicitationHandler } = runtime;
604
499
  const capabilities = this.buildClientCapabilities(definition);
605
500
  let client;
606
501
  client = new ManagedClient({ name: `pi-mcp-${serverName}`, version: ADAPTER_VERSION }, {
@@ -693,6 +588,10 @@ export class McpServerManager {
693
588
  this.metadataListChangedListener?.(serverName, "resources-list-changed");
694
589
  }
695
590
  async handleUrlElicitationRequired(serverName, error) {
591
+ if (this.runtimeSignal?.aborted || !this.elicitationConfig?.allowUrl)
592
+ return "cancel";
593
+ const { handleUrlElicitation } = await import("./elicitation-handler.js");
594
+ // The import yields; recheck cancellation and URL support before prompting.
696
595
  if (this.runtimeSignal?.aborted || !this.elicitationConfig?.allowUrl)
697
596
  return "cancel";
698
597
  for (const params of error.elicitations) {
@@ -716,7 +615,8 @@ export class McpServerManager {
716
615
  }
717
616
  accepted.add(elicitationId);
718
617
  }
719
- createHttpTransport(definition, serverName, client, legacySse = false, implicitOAuth = false, onAuthChallenge) {
618
+ createHttpTransport(connectionRuntime, definition, serverName, client, legacySse = false, implicitOAuth = false, onAuthChallenge) {
619
+ const { StreamableHTTPClientTransport, SSEClientTransport, auth, extractWWWAuthenticateParams, UnauthorizedError, McpOAuthProvider, extractOAuthConfig, isOAuthChallenge, recordOAuthChallenge, getOAuthRequest, bindOAuthRequestIssuer, trackToolTransport, trackToolTransportFailure, } = connectionRuntime;
720
620
  const serverUrl = resolveServerUrl(definition);
721
621
  const url = new URL(serverUrl);
722
622
  const storageBase = getAuthBaseDir(this.authStorageOptions);
@@ -901,8 +801,8 @@ export class McpServerManager {
901
801
  return [];
902
802
  }
903
803
  }
904
- attachAdapterNotificationHandlers(serverName, client) {
905
- client.setNotificationHandler(serverStreamResultPatchNotificationSchema.shape.method.value, { params: serverStreamResultPatchNotificationSchema.shape.params }, params => {
804
+ attachAdapterNotificationHandlers(serverName, client, schema) {
805
+ client.setNotificationHandler(schema.shape.method.value, { params: schema.shape.params }, params => {
906
806
  this.uiStreamListeners.get(params.streamToken)?.(serverName, params);
907
807
  });
908
808
  }
@@ -13,6 +13,7 @@ const reserved = new Set(["read", "bash", "powershell", "edit", "write", "grep",
13
13
  export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
14
14
  pi.registerEntryRenderer(ENTRY, () => undefined);
15
15
  const registered = new Map();
16
+ const fingerprints = new WeakMap();
16
17
  let available = new Set();
17
18
  let selected = new Map();
18
19
  let inactive = new Map();
@@ -118,11 +119,12 @@ export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
118
119
  const metadata = getState()?.toolMetadata.get(server) ?? (entry && isServerCacheValid(entry, definition)
119
120
  ? reconstructToolMetadata(server, entry, config.settings?.toolPrefix ?? "server", definition) : []);
120
121
  const selection = directToolSelection(config, server, env ?? undefined);
121
- for (const { name: prefixedName, ...tool } of metadata) {
122
+ for (const source of metadata) {
123
+ const { name: prefixedName, ...tool } = source;
122
124
  if (tool.resourceUri)
123
125
  continue;
124
126
  const pinned = selection === true || (Array.isArray(selection) && selection.includes(tool.originalName));
125
- specs.push({ spec: { ...tool, prefixedName, serverName: server }, selection: { server, tool: tool.originalName }, name: prefixedName, pinned });
127
+ specs.push({ spec: { ...tool, prefixedName, serverName: server }, source, selection: { server, tool: tool.originalName }, name: prefixedName, pinned });
126
128
  }
127
129
  }
128
130
  const counts = new Map();
@@ -130,13 +132,17 @@ export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
130
132
  counts.set(name, (counts.get(name) ?? 0) + 1);
131
133
  available = new Set();
132
134
  const enabled = [];
133
- for (const { spec, selection, name, pinned } of specs) {
135
+ for (const { spec, source, selection, name, pinned } of specs) {
134
136
  const id = key(selection);
135
137
  if (reserved.has(name) || counts.get(name) > 1 || (existing.has(name) && !previousNames.has(name)))
136
138
  continue;
137
139
  available.add(id);
138
140
  const instructions = getState()?.serverInstructions.get(spec.serverName) ?? cache?.servers[spec.serverName]?.instructions;
139
- const fingerprint = JSON.stringify({ spec, name, instructions });
141
+ const previous = Object.isFrozen(source) ? fingerprints.get(source) : undefined;
142
+ const fingerprint = previous?.server === spec.serverName && previous.instructions === instructions
143
+ ? previous.value : JSON.stringify({ spec, name, instructions });
144
+ if (Object.isFrozen(source))
145
+ fingerprints.set(source, { server: spec.serverName, instructions, value: fingerprint });
140
146
  if (registered.get(id)?.fingerprint !== fingerprint) {
141
147
  pi.registerTool({
142
148
  name,