@fitchmultz/pi-mcp-adapter 8.0.6 → 8.0.7

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 (87) hide show
  1. package/CHANGELOG.md +128 -8
  2. package/OAUTH.md +13 -16
  3. package/README.md +8 -8
  4. package/cli.js +67 -28
  5. package/dist/abort.js +18 -9
  6. package/dist/client-operations.js +4 -2
  7. package/dist/commands.js +77 -43
  8. package/dist/config.js +194 -105
  9. package/dist/connection-runtime.d.ts +2 -2
  10. package/dist/connection-runtime.js +29 -18
  11. package/dist/consent-manager.d.ts +2 -2
  12. package/dist/consent-manager.js +5 -2
  13. package/dist/direct-tools.d.ts +1 -1
  14. package/dist/direct-tools.js +13 -7
  15. package/dist/elicitation-handler.d.ts +2 -3
  16. package/dist/elicitation-handler.js +113 -58
  17. package/dist/gateway-arguments.d.ts +1 -1
  18. package/dist/gateway-arguments.js +90 -23
  19. package/dist/glimpse-ui.js +10 -5
  20. package/dist/host-html-template.d.ts +3 -1
  21. package/dist/host-html-template.js +9 -12
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +264 -119
  24. package/dist/init.d.ts +1 -3
  25. package/dist/init.js +91 -51
  26. package/dist/legacy-migration.d.ts +1 -1
  27. package/dist/legacy-migration.js +17 -12
  28. package/dist/lifecycle.d.ts +3 -3
  29. package/dist/lifecycle.js +33 -17
  30. package/dist/logger.d.ts +2 -2
  31. package/dist/logger.js +4 -2
  32. package/dist/mcp-auth-flow.js +150 -58
  33. package/dist/mcp-auth.d.ts +7 -7
  34. package/dist/mcp-auth.js +189 -138
  35. package/dist/mcp-callback-server.js +19 -8
  36. package/dist/mcp-code.js +168 -54
  37. package/dist/mcp-keyring-helper.cjs +57 -23
  38. package/dist/mcp-oauth-provider.d.ts +6 -7
  39. package/dist/mcp-oauth-provider.js +146 -72
  40. package/dist/mcp-oauth-runtime.js +29 -16
  41. package/dist/mcp-output-guard.js +193 -65
  42. package/dist/mcp-output-preview.js +41 -20
  43. package/dist/mcp-panel.d.ts +9 -9
  44. package/dist/mcp-panel.js +141 -70
  45. package/dist/mcp-script-worker.mjs +135 -43
  46. package/dist/mcp-setup-panel.d.ts +8 -8
  47. package/dist/mcp-setup-panel.js +130 -44
  48. package/dist/mcp-status.js +14 -7
  49. package/dist/mcp-trace.d.ts +10 -5
  50. package/dist/mcp-trace.js +127 -61
  51. package/dist/metadata-cache.d.ts +3 -4
  52. package/dist/metadata-cache.js +83 -43
  53. package/dist/npx-resolver.js +101 -60
  54. package/dist/onboarding-state.js +4 -2
  55. package/dist/plain-result-reader.js +78 -31
  56. package/dist/prompts.js +36 -20
  57. package/dist/proxy-modes.js +670 -193
  58. package/dist/resource-tools.js +30 -9
  59. package/dist/runtime-owner.js +36 -18
  60. package/dist/sampling-handler.d.ts +1 -2
  61. package/dist/sampling-handler.js +41 -21
  62. package/dist/search-ranking.d.ts +1 -1
  63. package/dist/search-ranking.js +67 -34
  64. package/dist/server-manager.d.ts +11 -11
  65. package/dist/server-manager.js +283 -166
  66. package/dist/session-recovery.js +113 -61
  67. package/dist/tool-approval.js +9 -3
  68. package/dist/tool-loader.js +154 -55
  69. package/dist/tool-metadata.d.ts +1 -1
  70. package/dist/tool-metadata.js +51 -23
  71. package/dist/tool-registrar.js +32 -10
  72. package/dist/tool-result-renderer.js +58 -33
  73. package/dist/types.d.ts +2 -2
  74. package/dist/types.js +39 -18
  75. package/dist/ui-message-history.js +33 -13
  76. package/dist/ui-resource-handler.d.ts +3 -3
  77. package/dist/ui-resource-handler.js +37 -21
  78. package/dist/ui-server.js +101 -43
  79. package/dist/ui-session.js +66 -29
  80. package/dist/ui-stream-types.js +14 -4
  81. package/dist/ui-tool-visibility.js +12 -6
  82. package/dist/unix-socket-transport.d.ts +1 -2
  83. package/dist/unix-socket-transport.js +19 -12
  84. package/dist/utils.d.ts +2 -0
  85. package/dist/utils.js +60 -35
  86. package/package.json +26 -2
  87. package/skills/mcp-scripting/SKILL.md +6 -6
@@ -21,7 +21,7 @@
21
21
  // - treat generic -32000/ConnectionClosed errors as session expiry
22
22
  // - treat AbortError/cancellation as a session failure
23
23
  import { AsyncLocalStorage } from "node:async_hooks";
24
- import { SdkError, SdkHttpError, SdkErrorCode, ProtocolError, InsufficientScopeError, UnauthorizedError } from "@modelcontextprotocol/client";
24
+ import { SdkError, SdkHttpError, SdkErrorCode, ProtocolError, InsufficientScopeError, UnauthorizedError, } from "@modelcontextprotocol/client";
25
25
  import { logger } from "./logger.js";
26
26
  import { abortable, throwIfAborted } from "./abort.js";
27
27
  import { isServerDisabled, isNonInteractiveOAuth } from "./types.js";
@@ -43,9 +43,12 @@ export async function trackToolCallOutcome(fn, signal) {
43
43
  return await fn();
44
44
  }
45
45
  catch (error) {
46
- if (outcome.pending && error instanceof Error
47
- && (signal?.aborted || (error instanceof SdkError
48
- && (error.code === SdkErrorCode.RequestTimeout || error.code === SdkErrorCode.ConnectionClosed)))) {
46
+ if (outcome.pending &&
47
+ error instanceof Error &&
48
+ (signal?.aborted ||
49
+ (error instanceof SdkError &&
50
+ (error.code === SdkErrorCode.RequestTimeout ||
51
+ error.code === SdkErrorCode.ConnectionClosed)))) {
49
52
  // Abort reasons can be shared by calls that never reached dispatch.
50
53
  const interrupted = new Error(error.message, { cause: error });
51
54
  interruptedToolCalls.add(interrupted);
@@ -54,8 +57,9 @@ export async function trackToolCallOutcome(fn, signal) {
54
57
  throw error;
55
58
  }
56
59
  finally {
57
- for (const cleanup of outcome.cleanup)
60
+ for (const cleanup of outcome.cleanup) {
58
61
  cleanup();
62
+ }
59
63
  }
60
64
  });
61
65
  }
@@ -63,8 +67,9 @@ const toolSend = new AsyncLocalStorage();
63
67
  /** Retain fetch/body-error origin without parsing or buffering the response. */
64
68
  export const trackToolTransportFailure = async (input, init) => {
65
69
  const pending = toolSend.getStore();
66
- if (pending)
70
+ if (pending) {
67
71
  pending.bodyFailure = undefined;
72
+ }
68
73
  let toolPost = false;
69
74
  if (pending && init?.method === "POST" && typeof init.body === "string") {
70
75
  try {
@@ -73,18 +78,21 @@ export const trackToolTransportFailure = async (input, init) => {
73
78
  }
74
79
  catch { }
75
80
  }
76
- if (toolPost && pending?.outcome)
81
+ if (toolPost && pending?.outcome) {
77
82
  pending.outcome.pending = true;
83
+ }
78
84
  try {
79
85
  const response = await fetch(input, init);
80
- if (toolPost && !response.ok && response.status < 500 && pending?.outcome)
86
+ if (toolPost && !response.ok && response.status < 500 && pending?.outcome) {
81
87
  pending.outcome.pending = false;
88
+ }
82
89
  const mediaType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
83
90
  const observeBody = response.ok
84
91
  ? mediaType === "text/event-stream" || (toolPost && mediaType === "application/json")
85
92
  : toolPost && response.status >= 500;
86
- if (!pending || !response.body || !observeBody)
93
+ if (!pending || !response.body || !observeBody) {
87
94
  return response;
95
+ }
88
96
  pending.streaming = response.ok && mediaType === "text/event-stream";
89
97
  // The native end hook has no error argument. Only a failed body read proves loss.
90
98
  const reader = response.body.getReader();
@@ -96,28 +104,33 @@ export const trackToolTransportFailure = async (input, init) => {
96
104
  controller.close();
97
105
  reader.releaseLock();
98
106
  }
99
- else
107
+ else {
100
108
  controller.enqueue(value);
109
+ }
101
110
  }
102
111
  catch (error) {
103
112
  // Native intentional closure suppresses onRequestStreamEnd.
104
- if (init?.signal?.aborted)
113
+ if (init?.signal?.aborted) {
105
114
  pending.finishSend();
115
+ }
106
116
  else if (error instanceof Error) {
107
117
  pending.bodyFailure = error;
108
- if (!pending.streaming)
118
+ if (!pending.streaming) {
109
119
  interruptedToolCalls.add(error);
120
+ }
110
121
  }
111
122
  controller.error(error);
112
123
  reader.releaseLock();
113
124
  }
114
125
  },
115
- cancel: reason => reader.cancel(reason).finally(() => reader.releaseLock()),
126
+ cancel: (reason) => reader.cancel(reason).finally(() => reader.releaseLock()),
116
127
  }), response);
117
128
  }
118
129
  catch (error) {
119
- if (error instanceof Error && !init?.signal?.aborted && init?.method === "POST"
120
- && new Headers(init.headers).get("Mcp-Method") === "tools/call") {
130
+ if (error instanceof Error &&
131
+ !init?.signal?.aborted &&
132
+ init?.method === "POST" &&
133
+ new Headers(init.headers).get("Mcp-Method") === "tools/call") {
121
134
  toolTransportFailures.add(error);
122
135
  }
123
136
  throw error;
@@ -128,68 +141,89 @@ export function trackToolTransport(transport, http = false) {
128
141
  const requests = new Map();
129
142
  const onmessage = transport.onmessage;
130
143
  transport.onmessage = (message, extra) => {
131
- const pending = ("id" in message && message.id !== undefined ? requests.get(message.id) : undefined) ?? toolSend.getStore();
132
- if (pending && "id" in message && message.id === pending.id && ("result" in message || "error" in message)) {
144
+ const pending = ("id" in message && message.id !== undefined ? requests.get(message.id) : undefined) ??
145
+ toolSend.getStore();
146
+ if (pending &&
147
+ "id" in message &&
148
+ message.id === pending.id &&
149
+ ("result" in message || "error" in message)) {
133
150
  pending.responseReceived = true;
134
- if (pending.outcome)
151
+ if (pending.outcome) {
135
152
  pending.outcome.pending = false;
153
+ }
136
154
  requests.delete(pending.id);
137
155
  pending.finishSend();
138
156
  }
139
157
  onmessage?.(message, extra);
140
158
  };
141
159
  const onerror = transport.onerror;
142
- transport.onerror = error => {
160
+ transport.onerror = (error) => {
143
161
  const pending = toolSend.getStore();
144
162
  // Reader failures are observed before the SDK reports them; earlier parser errors are not retryable.
145
- if (pending && !pending.bodyFailure)
163
+ if (pending && !pending.bodyFailure) {
146
164
  pending.otherFailure ??= error;
165
+ }
147
166
  onerror?.(error);
148
167
  };
149
168
  const send = transport.send.bind(transport);
150
169
  transport.send = async (message, options) => {
151
170
  try {
152
171
  if (!("method" in message && message.method === "tools/call" && "id" in message)) {
153
- return await toolSend.run(undefined, () => send(message, options));
172
+ await toolSend.run(undefined, () => send(message, options));
173
+ return;
154
174
  }
155
175
  const pending = {
156
- id: message.id, outcome: toolCallOutcome.getStore(), streaming: false, responseReceived: false,
157
- bodyFailure: undefined, otherFailure: undefined,
176
+ id: message.id,
177
+ outcome: toolCallOutcome.getStore(),
178
+ streaming: false,
179
+ responseReceived: false,
180
+ bodyFailure: undefined,
181
+ otherFailure: undefined,
158
182
  finishSend: () => { },
159
183
  };
160
184
  if (pending.outcome) {
161
185
  requests.set(message.id, pending);
162
186
  pending.outcome.cleanup.add(() => requests.delete(message.id));
163
- if (!http)
187
+ if (!http) {
164
188
  pending.outcome.pending = true;
189
+ }
165
190
  }
166
191
  try {
167
192
  if (!options?.requestSignal) {
168
- return await toolSend.run(pending, () => send(message, options));
193
+ await toolSend.run(pending, () => send(message, options));
194
+ return;
169
195
  }
170
196
  await abortable(new Promise((resolve, reject) => {
171
197
  pending.finishSend = resolve;
172
- void toolSend.run(pending, () => send(message, {
198
+ void toolSend
199
+ .run(pending, () => send(message, {
173
200
  ...options,
174
201
  onRequestStreamEnd: () => {
175
202
  options.onRequestStreamEnd?.();
176
203
  const error = pending.otherFailure ?? pending.bodyFailure;
177
204
  if (!pending.responseReceived && error) {
178
- if (!pending.otherFailure)
205
+ if (!pending.otherFailure) {
179
206
  toolTransportFailures.add(error);
207
+ }
180
208
  reject(error);
181
209
  }
182
- else
210
+ else {
183
211
  resolve();
212
+ }
184
213
  },
185
- })).then(() => { if (!pending.streaming)
186
- resolve(); }, reject);
214
+ }))
215
+ .then(() => {
216
+ if (!pending.streaming) {
217
+ resolve();
218
+ }
219
+ }, reject);
187
220
  }), options.requestSignal);
188
221
  }
189
222
  catch (error) {
190
223
  // Local send failures are distinct from a sent request waiting for its reply.
191
- if (!http && pending.outcome)
224
+ if (!http && pending.outcome) {
192
225
  pending.outcome.pending = false;
226
+ }
193
227
  throw error;
194
228
  }
195
229
  finally {
@@ -197,17 +231,21 @@ export function trackToolTransport(transport, http = false) {
197
231
  }
198
232
  }
199
233
  catch (error) {
200
- if ("method" in message && message.method === "tools/call"
201
- && error instanceof SdkHttpError && error.code === SdkErrorCode.ClientHttpNotImplemented
202
- && error.status >= 500 && error.status < 600) {
234
+ if ("method" in message &&
235
+ message.method === "tools/call" &&
236
+ error instanceof SdkHttpError &&
237
+ error.code === SdkErrorCode.ClientHttpNotImplemented &&
238
+ error.status >= 500 &&
239
+ error.status < 600) {
203
240
  let errorBody = false;
204
241
  try {
205
242
  const body = JSON.parse(String(error.data.text));
206
243
  errorBody = body?.jsonrpc === "2.0" && body.error !== undefined;
207
244
  }
208
245
  catch { }
209
- if (!errorBody)
246
+ if (!errorBody) {
210
247
  toolTransportFailures.add(error);
248
+ }
211
249
  }
212
250
  throw error;
213
251
  }
@@ -229,17 +267,18 @@ const SERVER_NOT_INITIALIZED_MCP_MESSAGES = new Set([
229
267
  "Bad Request: Server not initialized",
230
268
  ]);
231
269
  export function isTerminatedSession(err, hadSessionId) {
232
- if (!hadSessionId)
270
+ if (!hadSessionId) {
233
271
  return false;
272
+ }
234
273
  if (err instanceof SdkHttpError) {
235
- return err.status === 404
236
- || (err.status === 400
237
- && /"code"\s*:\s*-32000/.test(err.message)
238
- && /"message"\s*:\s*"Bad Request: Server not initialized"/.test(err.message));
274
+ return (err.status === 404 ||
275
+ (err.status === 400 &&
276
+ /"code"\s*:\s*-32000/.test(err.message) &&
277
+ /"message"\s*:\s*"Bad Request: Server not initialized"/.test(err.message)));
239
278
  }
240
- return err instanceof ProtocolError
241
- && err.code === -32000
242
- && SERVER_NOT_INITIALIZED_MCP_MESSAGES.has(err.message);
279
+ return (err instanceof ProtocolError &&
280
+ err.code === -32000 &&
281
+ SERVER_NOT_INITIALIZED_MCP_MESSAGES.has(err.message));
243
282
  }
244
283
  function hasSessionId(connection) {
245
284
  // Only StreamableHTTPClientTransport exposes `sessionId`; stdio/SSE
@@ -273,8 +312,9 @@ export async function withSessionRecovery(deps, serverName, fn) {
273
312
  }
274
313
  const reconnect = async (stale, error) => {
275
314
  const definition = deps.config.mcpServers[serverName];
276
- if (!definition)
315
+ if (!definition) {
277
316
  throw error;
317
+ }
278
318
  throwIfAborted(deps.signal);
279
319
  let fresh = stale;
280
320
  if (fresh.status !== "needs-auth") {
@@ -284,26 +324,32 @@ export async function withSessionRecovery(deps, serverName, fn) {
284
324
  }
285
325
  throwIfAborted(deps.signal);
286
326
  if (fresh.status === "needs-auth" && deps.onNeedsAuth) {
287
- fresh = await abortable(deps.onNeedsAuth(serverName, deps.signal), deps.signal) ?? fresh;
327
+ fresh = (await abortable(deps.onNeedsAuth(serverName, deps.signal), deps.signal)) ?? fresh;
288
328
  throwIfAborted(deps.signal);
289
329
  }
290
- if (fresh.status === "needs-auth")
330
+ if (fresh.status === "needs-auth") {
291
331
  throw new SessionRecoveryAuthRequiredError(serverName);
292
- if (fresh.status !== "connected")
332
+ }
333
+ if (fresh.status !== "connected") {
293
334
  throw error;
335
+ }
294
336
  return fresh;
295
337
  };
296
338
  const isRecoverableAuth = (error, failed, activating = false) => {
297
339
  const definition = deps.config.mcpServers[serverName];
298
- if (!definition || isServerDisabled(definition) || !supportsOAuth(definition))
340
+ if (!definition || isServerDisabled(definition) || !supportsOAuth(definition)) {
299
341
  return false;
342
+ }
300
343
  if (error instanceof SdkHttpError) {
301
344
  // A provider's exhausted native 401/M2M retry is terminal, not another consent cycle.
302
- return failed.oauthProvider === false && error.status === 401 && error.code !== SdkErrorCode.ClientHttpAuthentication;
345
+ return (failed.oauthProvider === false &&
346
+ error.status === 401 &&
347
+ error.code !== SdkErrorCode.ClientHttpAuthentication);
303
348
  }
304
- if (failed.oauthProvider && isNonInteractiveOAuth(definition.oauth))
349
+ if (failed.oauthProvider && isNonInteractiveOAuth(definition.oauth)) {
305
350
  return false;
306
- return error instanceof InsufficientScopeError || (activating && error instanceof UnauthorizedError);
351
+ }
352
+ return (error instanceof InsufficientScopeError || (activating && error instanceof UnauthorizedError));
307
353
  };
308
354
  const terminalDispatch = async (fresh) => {
309
355
  throwIfAborted(deps.signal);
@@ -312,8 +358,9 @@ export async function withSessionRecovery(deps, serverName, fn) {
312
358
  }
313
359
  catch (error) {
314
360
  throwIfAborted(deps.signal);
315
- if (isRecoverableAuth(error, fresh))
361
+ if (isRecoverableAuth(error, fresh)) {
316
362
  throw new SessionRecoveryAuthRequiredError(serverName);
363
+ }
317
364
  throw error;
318
365
  }
319
366
  };
@@ -333,18 +380,22 @@ export async function withSessionRecovery(deps, serverName, fn) {
333
380
  }
334
381
  catch (providerError) {
335
382
  throwIfAborted(deps.signal);
336
- if (!isRecoverableAuth(providerError, failed, true))
383
+ if (!isRecoverableAuth(providerError, failed, true)) {
337
384
  throw providerError;
385
+ }
338
386
  error = providerError;
339
387
  }
340
388
  }
341
389
  }
342
- if (isNonInteractiveOAuth(definition.oauth))
390
+ if (isNonInteractiveOAuth(definition.oauth)) {
343
391
  throw error;
344
- const fresh = await abortable(deps.onNeedsAuth?.(serverName, deps.signal, { connection: failed, error }) ?? Promise.resolve(undefined), deps.signal);
392
+ }
393
+ const fresh = await abortable(deps.onNeedsAuth?.(serverName, deps.signal, { connection: failed, error }) ??
394
+ Promise.resolve(undefined), deps.signal);
345
395
  throwIfAborted(deps.signal);
346
- if (!fresh || fresh === failed || fresh.status !== "connected")
396
+ if (!fresh || fresh === failed || fresh.status !== "connected") {
347
397
  throw new SessionRecoveryAuthRequiredError(serverName);
398
+ }
348
399
  return terminalDispatch(fresh);
349
400
  };
350
401
  // Nothing has been dispatched yet: wait for replacement/auth begun during UI preparation.
@@ -357,12 +408,13 @@ export async function withSessionRecovery(deps, serverName, fn) {
357
408
  }
358
409
  catch (err) {
359
410
  throwIfAborted(deps.signal);
360
- if (isRecoverableAuth(err, connection))
411
+ if (isRecoverableAuth(err, connection)) {
361
412
  return recoverAuth(connection, err);
362
- if (deps.retryOnTransportFailure === true
363
- && deps.config.mcpServers[serverName]?.retryOnTransportFailure === true
364
- && connection.client.getProtocolEra?.() === "modern"
365
- && isToolTransportFailure(err)) {
413
+ }
414
+ if (deps.retryOnTransportFailure === true &&
415
+ deps.config.mcpServers[serverName]?.retryOnTransportFailure === true &&
416
+ connection.client.getProtocolEra?.() === "modern" &&
417
+ isToolTransportFailure(err)) {
366
418
  return terminalDispatch(connection);
367
419
  }
368
420
  if (!isTerminatedSession(err, hadSessionId)) {
@@ -7,10 +7,12 @@ export function isToolCallApprovalRequired(config, serverName, toolMeta) {
7
7
  const approval = definition?.approveTools !== undefined
8
8
  ? definition.approveTools
9
9
  : config.settings?.approveTools;
10
- if (approval === true)
10
+ if (approval === true) {
11
11
  return true;
12
- if (!Array.isArray(approval) || approval.length === 0)
12
+ }
13
+ if (!Array.isArray(approval) || approval.length === 0) {
13
14
  return false;
15
+ }
14
16
  const prefix = resolveToolPrefix(definition, config.settings?.toolPrefix);
15
17
  return matchesToolPattern(getToolNameCandidates(toolMeta.originalName, serverName, prefix), approval);
16
18
  }
@@ -30,7 +32,11 @@ export async function ensureToolCallApproved(state, serverName, toolMeta, args,
30
32
  const preview = sanitized.length > 500 ? `${sanitized.slice(0, 500)}...` : sanitized;
31
33
  const title = `MCP: ${sanitizeTerminalText(serverName)} wants to run ${sanitizeTerminalText(toolMeta.originalName)}`;
32
34
  const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
33
- const decision = await abortable(state.ui.select(`${title}\n\nArguments:\n${preview}`, ["Allow once", "Allow for session", "Deny"]), ownedSignal);
35
+ const decision = await abortable(state.ui.select(`${title}\n\nArguments:\n${preview}`, [
36
+ "Allow once",
37
+ "Allow for session",
38
+ "Deny",
39
+ ]), ownedSignal);
34
40
  if (decision === "Allow once") {
35
41
  return { ok: true };
36
42
  }