@humain/terminal 0.1.1 → 0.2.1

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 (193) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/bundle/chunks/{anthropic-messages-XULXUBIG.js → anthropic-messages-QFCEKGI3.js} +1 -1
  3. package/dist/bundle/chunks/{azure-openai-responses-JEG2XOYC.js → azure-openai-responses-G4ZMHV37.js} +1 -1
  4. package/dist/bundle/chunks/chunk-7T6XCLJF.js +22 -0
  5. package/dist/bundle/chunks/chunk-D6HHRSB5.js +103 -0
  6. package/dist/bundle/chunks/chunk-EY4DWHQU.js +2 -0
  7. package/dist/bundle/chunks/{chunk-LYAMSPR2.js → chunk-I47ZWR55.js} +483 -506
  8. package/dist/bundle/chunks/{chunk-JUPSJTFQ.js → chunk-K4JAZYQH.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-HPPQHK3J.js → chunk-KKIB2TVR.js} +1 -1
  10. package/dist/bundle/chunks/chunk-LI2YVPWP.js +2 -0
  11. package/dist/bundle/chunks/{chunk-BZQ53VGE.js → chunk-XSR434CO.js} +1 -1
  12. package/dist/bundle/chunks/compat-QCV26HNS.js +2 -0
  13. package/dist/bundle/chunks/dist-PRPIUY3U.js +2 -0
  14. package/dist/bundle/chunks/esm-E7UVEYTO.js +4 -0
  15. package/dist/bundle/chunks/{extract-TRW5FBY6.js → extract-CTIFQZDJ.js} +15 -14
  16. package/dist/bundle/chunks/{google-generative-ai-OX6VSCA6.js → google-generative-ai-RSUU4MWO.js} +1 -1
  17. package/dist/bundle/chunks/{google-vertex-GW76GSS2.js → google-vertex-CKQPOITO.js} +1 -1
  18. package/dist/bundle/chunks/host-WP3MVKS2.js +2 -0
  19. package/dist/bundle/chunks/{node-KOIP55YZ.js → node-NF43OPII.js} +1 -1
  20. package/dist/bundle/chunks/{node-XY3CBY3C.js → node-UMFM7CDZ.js} +1 -1
  21. package/dist/bundle/chunks/{openai-codex-responses-NPLWP6ZC.js → openai-codex-responses-S34V5ZSD.js} +1 -1
  22. package/dist/bundle/chunks/{openai-completions-OT2GTJKC.js → openai-completions-T2IGVUEI.js} +1 -1
  23. package/dist/bundle/chunks/{openai-responses-5A6CR7GI.js → openai-responses-GYZODCT3.js} +1 -1
  24. package/dist/bundle/chunks/readability-LAGF34IB.js +3 -0
  25. package/dist/bundle/chunks/turndown.cjs-HRMZVRUL.js +2 -0
  26. package/dist/bundle/cli.js +1 -1
  27. package/dist/bundle/index.js +1 -1
  28. package/dist/bundle/rpc-entry.js +1 -1
  29. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
  30. package/dist/core/tools/file-mutation-queue.js +21 -8
  31. package/dist/core/tools/file-mutation-queue.js.map +1 -1
  32. package/dist/core/tools/subagent-env.d.ts +18 -0
  33. package/dist/core/tools/subagent-env.d.ts.map +1 -0
  34. package/dist/core/tools/subagent-env.js +43 -0
  35. package/dist/core/tools/subagent-env.js.map +1 -0
  36. package/dist/core/tools/subagent-scheduler.d.ts +31 -0
  37. package/dist/core/tools/subagent-scheduler.d.ts.map +1 -0
  38. package/dist/core/tools/subagent-scheduler.js +203 -0
  39. package/dist/core/tools/subagent-scheduler.js.map +1 -0
  40. package/dist/core/tools/subagent.d.ts +83 -3
  41. package/dist/core/tools/subagent.d.ts.map +1 -1
  42. package/dist/core/tools/subagent.js +214 -76
  43. package/dist/core/tools/subagent.js.map +1 -1
  44. package/dist/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
  45. package/dist/humain/mcp-adapter/vendor/README.md +125 -19
  46. package/dist/humain/mcp-adapter/vendor/agent-plugin-loader.ts +69 -30
  47. package/dist/humain/mcp-adapter/vendor/agent-plugin-provenance.ts +36 -0
  48. package/dist/humain/mcp-adapter/vendor/claude-plugin-loader.ts +365 -0
  49. package/dist/humain/mcp-adapter/vendor/commands.ts +13 -15
  50. package/dist/humain/mcp-adapter/vendor/config.ts +198 -30
  51. package/dist/humain/mcp-adapter/vendor/consent-manager.ts +40 -9
  52. package/dist/humain/mcp-adapter/vendor/direct-tool-surface.ts +250 -0
  53. package/dist/humain/mcp-adapter/vendor/direct-tools.ts +9 -228
  54. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js +74 -32
  55. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js.map +1 -1
  56. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.d.ts +1 -0
  57. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js +31 -0
  58. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js.map +1 -0
  59. package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.d.ts +9 -0
  60. package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js +362 -0
  61. package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js.map +1 -0
  62. package/dist/humain/mcp-adapter/vendor/dist/config.d.ts +3 -1
  63. package/dist/humain/mcp-adapter/vendor/dist/config.js +189 -27
  64. package/dist/humain/mcp-adapter/vendor/dist/config.js.map +1 -1
  65. package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js +7 -16
  66. package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js.map +1 -1
  67. package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js +5 -17
  68. package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js.map +1 -1
  69. package/dist/humain/mcp-adapter/vendor/dist/types.d.ts +30 -4
  70. package/dist/humain/mcp-adapter/vendor/dist/types.js +26 -0
  71. package/dist/humain/mcp-adapter/vendor/dist/types.js.map +1 -1
  72. package/dist/humain/mcp-adapter/vendor/dist/utils.d.ts +6 -0
  73. package/dist/humain/mcp-adapter/vendor/dist/utils.js +65 -6
  74. package/dist/humain/mcp-adapter/vendor/dist/utils.js.map +1 -1
  75. package/dist/humain/mcp-adapter/vendor/host-html-template.ts +20 -19
  76. package/dist/humain/mcp-adapter/vendor/http-ca.ts +84 -0
  77. package/dist/humain/mcp-adapter/vendor/index.bundle.mjs +37102 -15103
  78. package/dist/humain/mcp-adapter/vendor/index.ts +828 -164
  79. package/dist/humain/mcp-adapter/vendor/init.ts +48 -10
  80. package/dist/humain/mcp-adapter/vendor/lazy-loader.ts +7 -0
  81. package/dist/humain/mcp-adapter/vendor/mcp-auth-fetch.ts +113 -0
  82. package/dist/humain/mcp-adapter/vendor/mcp-auth-flow.ts +256 -77
  83. package/dist/humain/mcp-adapter/vendor/mcp-auth.ts +316 -54
  84. package/dist/humain/mcp-adapter/vendor/mcp-code.ts +45 -18
  85. package/dist/humain/mcp-adapter/vendor/mcp-install.ts +60 -0
  86. package/dist/humain/mcp-adapter/vendor/mcp-oauth-provider.ts +70 -6
  87. package/dist/humain/mcp-adapter/vendor/mcp-output-guard.ts +53 -42
  88. package/dist/humain/mcp-adapter/vendor/mcp-panel-theme.ts +105 -0
  89. package/dist/humain/mcp-adapter/vendor/mcp-panel.ts +366 -331
  90. package/dist/humain/mcp-adapter/vendor/mcp-references.ts +9 -14
  91. package/dist/humain/mcp-adapter/vendor/mcp-script-worker.mjs +3 -1
  92. package/dist/humain/mcp-adapter/vendor/mcp-setup-panel.ts +383 -341
  93. package/dist/humain/mcp-adapter/vendor/mcp-status.ts +5 -0
  94. package/dist/humain/mcp-adapter/vendor/metadata-cache.ts +7 -16
  95. package/dist/humain/mcp-adapter/vendor/namespace-tools.ts +61 -3
  96. package/dist/humain/mcp-adapter/vendor/oauth.ts +6 -2
  97. package/dist/humain/mcp-adapter/vendor/package-mcp-loader.ts +5 -17
  98. package/dist/humain/mcp-adapter/vendor/package.json +13 -3
  99. package/dist/humain/mcp-adapter/vendor/prompts.ts +35 -3
  100. package/dist/humain/mcp-adapter/vendor/proxy-modes.ts +108 -7
  101. package/dist/humain/mcp-adapter/vendor/runtime-owner.ts +16 -1
  102. package/dist/humain/mcp-adapter/vendor/sampling-handler.ts +9 -32
  103. package/dist/humain/mcp-adapter/vendor/sandbox-proxy-template.ts +20 -93
  104. package/dist/humain/mcp-adapter/vendor/server-manager.ts +227 -40
  105. package/dist/humain/mcp-adapter/vendor/session-approvals.ts +187 -0
  106. package/dist/humain/mcp-adapter/vendor/skills/mcp-scripting/SKILL.md +9 -1
  107. package/dist/humain/mcp-adapter/vendor/state.ts +10 -1
  108. package/dist/humain/mcp-adapter/vendor/tool-approval.ts +10 -22
  109. package/dist/humain/mcp-adapter/vendor/tool-metadata.ts +14 -0
  110. package/dist/humain/mcp-adapter/vendor/types.ts +57 -4
  111. package/dist/humain/mcp-adapter/vendor/ui-server.ts +36 -28
  112. package/dist/humain/mcp-adapter/vendor/utils.ts +64 -6
  113. package/dist/humain/web-access/vendor/abortable.d.ts +3 -0
  114. package/dist/humain/web-access/vendor/abortable.d.ts.map +1 -0
  115. package/dist/humain/web-access/vendor/abortable.js +22 -0
  116. package/dist/humain/web-access/vendor/abortable.js.map +1 -0
  117. package/dist/humain/web-access/vendor/crawl4ai.d.ts +9 -0
  118. package/dist/humain/web-access/vendor/crawl4ai.d.ts.map +1 -0
  119. package/dist/humain/web-access/vendor/crawl4ai.js +187 -0
  120. package/dist/humain/web-access/vendor/crawl4ai.js.map +1 -0
  121. package/dist/humain/web-access/vendor/credential-source.d.ts.map +1 -1
  122. package/dist/humain/web-access/vendor/credential-source.js +1 -0
  123. package/dist/humain/web-access/vendor/credential-source.js.map +1 -1
  124. package/dist/humain/web-access/vendor/curator-page.d.ts.map +1 -1
  125. package/dist/humain/web-access/vendor/curator-page.js +20 -2
  126. package/dist/humain/web-access/vendor/curator-page.js.map +1 -1
  127. package/dist/humain/web-access/vendor/curator-run.d.ts +11 -0
  128. package/dist/humain/web-access/vendor/curator-run.d.ts.map +1 -0
  129. package/dist/humain/web-access/vendor/curator-run.js +38 -0
  130. package/dist/humain/web-access/vendor/curator-run.js.map +1 -0
  131. package/dist/humain/web-access/vendor/curator-server.d.ts +1 -0
  132. package/dist/humain/web-access/vendor/curator-server.d.ts.map +1 -1
  133. package/dist/humain/web-access/vendor/curator-server.js +2 -0
  134. package/dist/humain/web-access/vendor/curator-server.js.map +1 -1
  135. package/dist/humain/web-access/vendor/duckduckgo.d.ts.map +1 -1
  136. package/dist/humain/web-access/vendor/duckduckgo.js +1 -1
  137. package/dist/humain/web-access/vendor/duckduckgo.js.map +1 -1
  138. package/dist/humain/web-access/vendor/extract.d.ts +1 -1
  139. package/dist/humain/web-access/vendor/extract.d.ts.map +1 -1
  140. package/dist/humain/web-access/vendor/extract.js +58 -11
  141. package/dist/humain/web-access/vendor/extract.js.map +1 -1
  142. package/dist/humain/web-access/vendor/fetch-params.js +1 -1
  143. package/dist/humain/web-access/vendor/fetch-params.js.map +1 -1
  144. package/dist/humain/web-access/vendor/gemini-search.d.ts +2 -2
  145. package/dist/humain/web-access/vendor/gemini-search.d.ts.map +1 -1
  146. package/dist/humain/web-access/vendor/gemini-search.js +12 -5
  147. package/dist/humain/web-access/vendor/gemini-search.js.map +1 -1
  148. package/dist/humain/web-access/vendor/index.d.ts.map +1 -1
  149. package/dist/humain/web-access/vendor/index.js +23 -19
  150. package/dist/humain/web-access/vendor/index.js.map +1 -1
  151. package/dist/humain/web-access/vendor/openai-search.d.ts.map +1 -1
  152. package/dist/humain/web-access/vendor/openai-search.js +40 -14
  153. package/dist/humain/web-access/vendor/openai-search.js.map +1 -1
  154. package/dist/humain/web-access/vendor/page-query.d.ts.map +1 -1
  155. package/dist/humain/web-access/vendor/page-query.js +6 -2
  156. package/dist/humain/web-access/vendor/page-query.js.map +1 -1
  157. package/dist/humain/web-access/vendor/query-rewrite.d.ts.map +1 -1
  158. package/dist/humain/web-access/vendor/query-rewrite.js +6 -2
  159. package/dist/humain/web-access/vendor/query-rewrite.js.map +1 -1
  160. package/dist/humain/web-access/vendor/serpapi.d.ts +4 -0
  161. package/dist/humain/web-access/vendor/serpapi.d.ts.map +1 -0
  162. package/dist/humain/web-access/vendor/serpapi.js +215 -0
  163. package/dist/humain/web-access/vendor/serpapi.js.map +1 -0
  164. package/dist/humain/web-access/vendor/ssrf-protection.d.ts.map +1 -1
  165. package/dist/humain/web-access/vendor/ssrf-protection.js +5 -1
  166. package/dist/humain/web-access/vendor/ssrf-protection.js.map +1 -1
  167. package/dist/humain/web-access/vendor/storage.d.ts.map +1 -1
  168. package/dist/humain/web-access/vendor/storage.js +9 -0
  169. package/dist/humain/web-access/vendor/storage.js.map +1 -1
  170. package/dist/humain/web-access/vendor/summary-review.d.ts +1 -1
  171. package/dist/humain/web-access/vendor/summary-review.d.ts.map +1 -1
  172. package/dist/humain/web-access/vendor/summary-review.js +35 -24
  173. package/dist/humain/web-access/vendor/summary-review.js.map +1 -1
  174. package/dist/humain/web-access/vendor/utils.d.ts.map +1 -1
  175. package/dist/humain/web-access/vendor/utils.js +5 -3
  176. package/dist/humain/web-access/vendor/utils.js.map +1 -1
  177. package/dist/modes/json-event.d.ts +8 -0
  178. package/dist/modes/json-event.d.ts.map +1 -1
  179. package/dist/modes/json-event.js +17 -0
  180. package/dist/modes/json-event.js.map +1 -1
  181. package/dist/modes/print-mode.d.ts.map +1 -1
  182. package/dist/modes/print-mode.js +3 -2
  183. package/dist/modes/print-mode.js.map +1 -1
  184. package/docs/environment-variables.md +8 -0
  185. package/docs/json.md +16 -0
  186. package/npm-shrinkwrap.json +6 -6
  187. package/package.json +2 -2
  188. package/src/humain/mcp-adapter/UPSTREAM.md +56 -7
  189. package/src/humain/mcp-adapter/upstream.json +6 -4
  190. package/src/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
  191. package/src/humain/web-access/UPSTREAM.md +18 -8
  192. package/src/humain/web-access/upstream.json +6 -5
  193. package/dist/bundle/chunks/chunk-2LC3S3VV.js +0 -105
@@ -1,17 +1,19 @@
1
1
  import type { ToolInfo } from "@earendil-works/pi-coding-agent";
2
2
  import { formatWithOptions } from "node:util";
3
3
  import { Worker } from "node:worker_threads";
4
+ import { throwIfAborted } from "./abort.ts";
4
5
  import { guardMcpOutput, guardedMcpDetails, resolveMcpOutputGuardOptions } from "./mcp-output-guard.ts";
5
6
  import { executeCall } from "./proxy-modes.ts";
6
7
  import { combineAbortSignals } from "./runtime-owner.ts";
7
8
  import { paginate, rankSuggestions, rankToolMatches } from "./search-ranking.ts";
8
9
  import { isServerInActiveFailureBackoff } from "./failure-backoff.ts";
9
10
  import type { McpExtensionState } from "./state.ts";
10
- import { findToolByName, formatSchema } from "./tool-metadata.ts";
11
+ import { findToolByName, formatSchema, hasSchemaDescriptions } from "./tool-metadata.ts";
11
12
  import { renderTsShape } from "./ts-shape.ts";
12
13
  import type { ContentBlock } from "./types.ts";
13
14
 
14
15
  export const DEFAULT_MCP_SCRIPT_TIMEOUT_MS = 30_000;
16
+ const MCP_SCRIPT_INTERMEDIATE_MAX_BYTES = 16 * 1024 * 1024;
15
17
 
16
18
  class McpScriptTimeoutError extends Error {
17
19
  constructor(timeoutMs: number) {
@@ -30,7 +32,8 @@ type WorkerMessage =
30
32
  | { type: "done"; returnBlock?: unknown }
31
33
  | { type: "error"; message: string };
32
34
 
33
- type WorkerResultMessage = { type: "result"; id: number; envelope: unknown };
35
+ type WorkerResultPayload = { envelope: unknown } | { dataJson: string };
36
+ type WorkerResultMessage = { type: "result"; id: number } & WorkerResultPayload;
34
37
 
35
38
  function needsInspectableFormatting(value: unknown, stack = new WeakSet<object>()): boolean {
36
39
  if (value === undefined || typeof value === "bigint" || typeof value === "function" || typeof value === "symbol") return true;
@@ -136,11 +139,18 @@ export async function runMcpScript(
136
139
  : operation.durationMs,
137
140
  }));
138
141
  let callsSnapshot: ScriptOperation[] | undefined;
139
- const callTool = async (path: string, args?: Record<string, unknown>) => {
142
+ let intermediateBytes = 0;
143
+ const callTool = async (path: string, args?: Record<string, unknown>): Promise<WorkerResultPayload> => {
140
144
  // Record before dispatch so calls still in flight at timeout/abort appear in the trace.
141
145
  const startedAt = Date.now();
142
146
  const index = calls.push({ operation: "call", path, ok: false, error: "incomplete", durationMs: 0, startedAt }) - 1;
143
- const result = await executeCall(state, path, args, undefined, getPiTools, callSignal, "script");
147
+ let dataJson: string | undefined;
148
+ const result = await executeCall(state, path, args, undefined, getPiTools, callSignal, "script", {
149
+ onSuccess(data) {
150
+ // Serialize once for both byte accounting and worker transfer, never for display.
151
+ dataJson = JSON.stringify(data);
152
+ },
153
+ });
144
154
  const details = result.details;
145
155
  if (details.error !== undefined) {
146
156
  const errorCode = String(details.error);
@@ -154,15 +164,26 @@ export async function runMcpScript(
154
164
  : textFromContent(result.content);
155
165
  calls[index] = { operation: "call", path, ok: false, error: errorCode, durationMs: Date.now() - startedAt, startedAt };
156
166
  return {
157
- ok: false as const,
158
- error: { code: errorCode, message },
167
+ envelope: { ok: false, error: { code: errorCode, message } },
168
+ };
169
+ }
170
+ throwIfAborted(callSignal);
171
+ if (dataJson === undefined) throw new Error("MCP intermediate result was not JSON serializable");
172
+ const bytes = Buffer.byteLength(dataJson, "utf8");
173
+ if (bytes > MCP_SCRIPT_INTERMEDIATE_MAX_BYTES - intermediateBytes) {
174
+ const code = "intermediate_result_too_large";
175
+ calls[index] = { operation: "call", path, ok: false, error: code, durationMs: Date.now() - startedAt, startedAt };
176
+ return {
177
+ envelope: {
178
+ ok: false,
179
+ error: { code, message: "MCP result exceeds the remaining mcpScript intermediate transfer budget (16 MiB per script). Request less data or use a new script." },
180
+ },
159
181
  };
160
182
  }
183
+ // Rejected responses do not consume budget. This bounds transfer, not upstream allocation.
184
+ intermediateBytes += bytes;
161
185
  calls[index] = { operation: "call", path, ok: true, durationMs: Date.now() - startedAt, startedAt };
162
- return {
163
- ok: true as const,
164
- data: details.mcpResult !== undefined ? details.mcpResult : textFromContent(result.content),
165
- };
186
+ return { dataJson };
166
187
  };
167
188
 
168
189
  const searchTools = (input?: SearchInput) => {
@@ -206,15 +227,20 @@ export async function runMcpScript(
206
227
  if (isServerInActiveFailureBackoff(state, server)) continue;
207
228
  const tool = findToolByName(metadata, path);
208
229
  if (!tool) continue;
209
- const inputTypeScript = tool.inputSchema
210
- ? renderTsShape(tool.inputSchema) ?? formatSchema(tool.inputSchema)
211
- : null;
230
+ const inputShape = tool.inputSchema ? renderTsShape(tool.inputSchema) : null;
231
+ const inputTypeScript = inputShape ?? (tool.inputSchema ? formatSchema(tool.inputSchema) : null);
212
232
  return {
213
233
  path: tool.name,
214
234
  name: tool.originalName,
215
235
  server,
216
236
  ...(tool.description ? { description: tool.description } : {}),
217
237
  ...(inputTypeScript ? { inputTypeScript } : {}),
238
+ ...(inputShape && hasSchemaDescriptions(tool.inputSchema)
239
+ ? { inputGuidance: formatSchema(tool.inputSchema) } : {}),
240
+ ...(tool.outputSchema !== undefined ? {
241
+ outputSchemaTarget: "data.structuredContent",
242
+ outputSchema: tool.outputSchema,
243
+ } : {}),
218
244
  };
219
245
  }
220
246
  const suggestions = path ? rankSuggestions(state, path, 5) : [];
@@ -279,15 +305,16 @@ export async function runMcpScript(
279
305
  }
280
306
 
281
307
  void (async () => {
282
- let envelope: unknown;
308
+ let payload: WorkerResultPayload;
283
309
  if (message.type === "call") {
284
- envelope = await callTool(message.path, message.args as Record<string, unknown> | undefined);
310
+ payload = await callTool(message.path, message.args as Record<string, unknown> | undefined);
285
311
  } else if (message.type === "search") {
286
- envelope = searchTools(message.input as SearchInput | undefined);
312
+ payload = { envelope: searchTools(message.input as SearchInput | undefined) };
287
313
  } else {
288
- envelope = describeTool(message.input as DescribeInput | undefined);
314
+ payload = { envelope: describeTool(message.input as DescribeInput | undefined) };
289
315
  }
290
- const response: WorkerResultMessage = { type: "result", id: message.id, envelope };
316
+ if (completed || callSignal?.aborted) return;
317
+ const response: WorkerResultMessage = { type: "result", id: message.id, ...payload };
291
318
  activeWorker.postMessage(response);
292
319
  })().catch(reject);
293
320
  });
@@ -0,0 +1,60 @@
1
+ const SERVER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
2
+
3
+ export interface McpInstallRequest {
4
+ url: string;
5
+ serverName?: string;
6
+ }
7
+
8
+ export interface NormalizedMcpInstallRequest {
9
+ url: string;
10
+ serverName: string;
11
+ }
12
+
13
+ function isLoopbackHostname(hostname: string): boolean {
14
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
15
+ }
16
+
17
+ function deriveServerName(url: URL): string {
18
+ const hostname = url.hostname.replace(/^\[|\]$/g, "");
19
+ const firstLabel = isLoopbackHostname(url.hostname) ? "local-mcp" : hostname.split(".")[0];
20
+ const normalized = (firstLabel || "mcp")
21
+ .toLowerCase()
22
+ .replace(/[^a-z0-9_-]+/g, "-")
23
+ .replace(/^-+|-+$/g, "")
24
+ .slice(0, 64);
25
+ return normalized || "mcp";
26
+ }
27
+
28
+ export function normalizeMcpInstallRequest(request: McpInstallRequest): NormalizedMcpInstallRequest {
29
+ if (typeof request.url !== "string" || request.url.trim() === "") {
30
+ throw new Error("MCP install requires a non-empty URL");
31
+ }
32
+
33
+ let parsed: URL;
34
+ try {
35
+ parsed = new URL(request.url.trim());
36
+ } catch (error) {
37
+ throw new Error("MCP install URL must be an absolute URL", { cause: error });
38
+ }
39
+
40
+ if (parsed.username || parsed.password) throw new Error("MCP install URL must not contain credentials");
41
+ if (parsed.hash) throw new Error("MCP install URL must not contain a fragment");
42
+ if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && isLoopbackHostname(parsed.hostname))) {
43
+ throw new Error("MCP install URL must use HTTPS, except for loopback HTTP servers");
44
+ }
45
+
46
+ const serverName = request.serverName?.trim() || deriveServerName(parsed);
47
+ if (!SERVER_NAME_PATTERN.test(serverName)) {
48
+ throw new Error("MCP server name must be 1-64 letters, numbers, underscores, or hyphens and start with a letter or number");
49
+ }
50
+
51
+ return { url: parsed.toString(), serverName };
52
+ }
53
+
54
+ export function canonicalMcpServerUrl(value: string): string | undefined {
55
+ try {
56
+ return new URL(value).toString();
57
+ } catch {
58
+ return undefined;
59
+ }
60
+ }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import {
9
9
  UnauthorizedError,
10
+ type FetchLike,
10
11
  type AddClientAuthentication,
11
12
  type OAuthClientInformationContext,
12
13
  type OAuthClientProvider,
@@ -24,12 +25,15 @@ import {
24
25
  clearAllCredentials,
25
26
  clearCodeVerifier,
26
27
  invalidateAuthEntryCache,
28
+ captureOAuthAuthority,
27
29
  type AuthEntry,
28
30
  type AuthStorageOptions,
31
+ type OAuthAuthority,
29
32
  type StoredTokens,
30
33
  type StoredClientInfo,
31
34
  } from "./mcp-auth.ts"
32
35
  import { OAuthMetadataSchema, OpenIdProviderDiscoveryMetadataSchema } from "@modelcontextprotocol/core"
36
+ import { createOAuthFetch, type OAuthFetch } from "./mcp-auth-fetch.ts"
33
37
  import { resolveCommandSecret } from "./utils.ts"
34
38
  import { getAppClientUri, getAppName } from "./agent-dir.ts"
35
39
 
@@ -125,6 +129,7 @@ export interface McpOAuthConfig {
125
129
  grantType?: "authorization_code" | "client_credentials"
126
130
  clientId?: string
127
131
  clientSecret?: string
132
+ clientMetadataUrl?: string
128
133
  scope?: string
129
134
  authorizationParams?: Record<string, string>
130
135
  redirectUri?: string
@@ -213,11 +218,10 @@ async function loadConfiguredDiscoveryState(
213
218
  metadataUrl: string,
214
219
  serverUrl: string,
215
220
  skipIssuerValidation: boolean,
216
- signal?: AbortSignal,
221
+ fetchFn: FetchLike,
217
222
  ): Promise<OAuthDiscoveryState> {
218
- const response = await fetch(metadataUrl, {
223
+ const response = await fetchFn(metadataUrl, {
219
224
  headers: { accept: "application/json" },
220
- ...(signal !== undefined ? { signal } : {}),
221
225
  })
222
226
  if (!response.ok) {
223
227
  await response.text().catch(() => {})
@@ -247,7 +251,9 @@ async function loadConfiguredDiscoveryState(
247
251
  * Implements the OAuthClientProvider interface from the MCP SDK.
248
252
  */
249
253
  export class McpOAuthProvider implements OAuthClientProvider {
254
+ readonly clientMetadataUrl?: string
250
255
  private readonly redirectUrlSnapshot: string | undefined
256
+ private authFetch: OAuthFetch
251
257
  private active = true
252
258
  private flowClientInfo: StoredClientInfo | undefined
253
259
  private flowCodeVerifier: string | undefined
@@ -256,9 +262,11 @@ export class McpOAuthProvider implements OAuthClientProvider {
256
262
  private flowState: string | undefined
257
263
  private invalidatedAccessToken: string | undefined
258
264
  private invalidatedClientId: string | undefined
265
+ private staleRedirectClientId: string | undefined
259
266
  private lastObservedClientId: string | undefined
260
267
  private lastSavedAccessToken: string | undefined
261
268
  private pendingAuthAccessToken: string | undefined
269
+ private readonly assertAuthority: OAuthAuthority
262
270
 
263
271
  constructor(
264
272
  private serverName: string,
@@ -268,13 +276,24 @@ export class McpOAuthProvider implements OAuthClientProvider {
268
276
  private storageOptions: AuthStorageOptions = {},
269
277
  private runtimeSignal?: AbortSignal,
270
278
  initialState?: string,
279
+ authority?: OAuthAuthority,
271
280
  ) {
281
+ this.assertAuthority = authority ?? captureOAuthAuthority(serverName)
282
+ this.assertAuthority()
283
+ if (config.clientId === undefined && config.clientMetadataUrl !== undefined) {
284
+ this.clientMetadataUrl = config.clientMetadataUrl
285
+ }
286
+ this.authFetch = createOAuthFetch(serverUrl, undefined, runtimeSignal)
272
287
  this.flowState = initialState
273
288
  this.redirectUrlSnapshot = config.grantType === "client_credentials"
274
289
  ? undefined
275
290
  : config.redirectUri ?? `http://localhost:${getOAuthCallbackPort()}${getOAuthCallbackPath()}`
276
291
  }
277
292
 
293
+ setAuthFetch(fetchFn: OAuthFetch): void {
294
+ this.authFetch = fetchFn
295
+ }
296
+
278
297
  private get usesClientCredentials(): boolean {
279
298
  return this.config.grantType === "client_credentials"
280
299
  }
@@ -288,6 +307,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
288
307
  this.active = false
289
308
  this.invalidatedAccessToken = undefined
290
309
  this.invalidatedClientId = undefined
310
+ this.staleRedirectClientId = undefined
291
311
  this.lastObservedClientId = undefined
292
312
  this.lastSavedAccessToken = undefined
293
313
  this.pendingAuthAccessToken = undefined
@@ -313,7 +333,11 @@ export class McpOAuthProvider implements OAuthClientProvider {
313
333
 
314
334
  private throwIfInactive(): void {
315
335
  if (!this.active) throw new Error("OAuth flow is no longer active")
336
+ this.assertAuthority()
316
337
  this.runtimeSignal?.throwIfAborted()
338
+ // The SDK can swallow refresh fetch errors and attempt browser authorization.
339
+ // A failed service credential must stop that fallback and token persistence.
340
+ this.authFetch.throwIfHeaderResolutionFailed()
317
341
  }
318
342
 
319
343
  /**
@@ -367,11 +391,13 @@ export class McpOAuthProvider implements OAuthClientProvider {
367
391
  * Returns undefined if no client info exists or if the server URL has changed.
368
392
  */
369
393
  async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {
394
+ this.throwIfInactive()
370
395
  if (this.invalidatedClientId !== undefined) {
371
396
  invalidateAuthEntryCache(this.serverName)
372
397
  }
373
398
  const issuer = this.discoveredIssuer
374
399
  const stored = await getAuthForUrl(this.serverName, this.serverUrl, this.storageOptions)
400
+ this.throwIfInactive()
375
401
  this.assertStoredIssuerBindings(stored, issuer)
376
402
 
377
403
  // Check config first (pre-registered client). Store only its issuer binding.
@@ -381,6 +407,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
381
407
  ? stored.clientInfo
382
408
  : undefined
383
409
  if (issuer && (storedClient?.issuer !== issuer || storedClient.configPreRegistered !== true)) {
410
+ this.throwIfInactive()
384
411
  updateClientInfo(
385
412
  this.serverName,
386
413
  { clientId: this.config.clientId, issuer, configPreRegistered: true },
@@ -405,6 +432,22 @@ export class McpOAuthProvider implements OAuthClientProvider {
405
432
  // another runtime writes the shared persistent entry for the same name.
406
433
  const clientInfo = this.flowClientInfo ?? stored?.clientInfo
407
434
  if (clientInfo?.clientId === this.invalidatedClientId) return undefined
435
+
436
+ const clientMetadataUrl = this.clientMetadataUrl
437
+ const supportsClientMetadataDocument = this.flowDiscoveryState
438
+ ?.authorizationServerMetadata?.client_id_metadata_document_supported
439
+ if (clientMetadataUrl !== undefined && supportsClientMetadataDocument !== undefined) {
440
+ if (supportsClientMetadataDocument === true
441
+ && clientInfo?.clientId !== clientMetadataUrl
442
+ && (stored?.tokens?.refreshToken === undefined || this.invalidatedAccessToken !== undefined)) {
443
+ // With no DCR refresh pair to preserve, let the SDK select CIMD now.
444
+ return undefined
445
+ }
446
+ if (supportsClientMetadataDocument !== true && clientInfo?.clientId === clientMetadataUrl) {
447
+ return undefined
448
+ }
449
+ }
450
+
408
451
  if (clientInfo) {
409
452
  this.invalidatedClientId = undefined
410
453
  // A stored SEP-2352 issuer stub for a config-pre-registered client
@@ -415,12 +458,14 @@ export class McpOAuthProvider implements OAuthClientProvider {
415
458
  // would let a token refresh go out with a client_id but no secret,
416
459
  // causing invalid_client and credential invalidation. Return undefined
417
460
  // so callers treat this as "no client info".
461
+ const isConfiguredCimd = clientMetadataUrl !== undefined
462
+ && clientInfo.clientId === clientMetadataUrl
418
463
  const isConfigStub = clientInfo.configPreRegistered === true
419
464
  || (clientInfo.clientSecret === undefined
420
465
  && clientInfo.clientIdIssuedAt === undefined
421
466
  && clientInfo.clientSecretExpiresAt === undefined
422
467
  && clientInfo.redirectUris === undefined)
423
- if (isConfigStub) {
468
+ if (isConfigStub && !isConfiguredCimd) {
424
469
  return undefined
425
470
  }
426
471
  // Check if client secret has expired
@@ -433,8 +478,16 @@ export class McpOAuthProvider implements OAuthClientProvider {
433
478
  if (issuer && clientInfo.issuer === undefined) {
434
479
  clientInfo.issuer = issuer
435
480
  this.flowClientInfo = clientInfo
481
+ this.throwIfInactive()
436
482
  updateClientInfo(this.serverName, clientInfo, this.serverUrl, this.storageOptions)
437
483
  }
484
+ // Keep a stale dynamic registration available for its refresh attempt,
485
+ // but suppress it if that attempt invalidates the token and auth falls
486
+ // back to an interactive flow. The next clientInformation() call then
487
+ // makes the SDK register the current callback URI.
488
+ const redirectUriIsStale = this.redirectUrl !== undefined
489
+ && (!Array.isArray(clientInfo.redirectUris) || !clientInfo.redirectUris.includes(this.redirectUrl))
490
+ this.staleRedirectClientId = redirectUriIsStale ? clientInfo.clientId : undefined
438
491
  // Return all registration metadata and the local issuer extension.
439
492
  // This keeps the SDK OAuth view and the stored issuer binding consistent.
440
493
  this.lastObservedClientId = clientInfo.clientId
@@ -499,6 +552,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
499
552
  * Returns undefined if no tokens exist or if the server URL has changed.
500
553
  */
501
554
  async tokens(ctx?: OAuthClientInformationContext): Promise<OAuthTokens | undefined> {
555
+ this.throwIfInactive()
502
556
  // Once this provider rejects a token, bypass its process-local cache until
503
557
  // another process replaces that token in shared secure storage.
504
558
  if (this.invalidatedAccessToken !== undefined) {
@@ -507,12 +561,14 @@ export class McpOAuthProvider implements OAuthClientProvider {
507
561
 
508
562
  // Use getAuthForUrl to validate tokens are for the current server URL.
509
563
  const entry = await getAuthForUrl(this.serverName, this.serverUrl, this.storageOptions)
564
+ this.throwIfInactive()
510
565
  if (!entry?.tokens || entry.tokens.accessToken === this.invalidatedAccessToken) return undefined
511
566
  this.invalidatedAccessToken = undefined
512
567
  const issuer = this.discoveredIssuer
513
568
  this.assertStoredIssuerBindings(entry, issuer)
514
569
  if (issuer && entry.tokens.issuer === undefined) {
515
570
  entry.tokens.issuer = issuer
571
+ this.throwIfInactive()
516
572
  updateTokens(this.serverName, entry.tokens, this.serverUrl, this.storageOptions)
517
573
  }
518
574
  if (ctx !== undefined) {
@@ -607,16 +663,20 @@ export class McpOAuthProvider implements OAuthClientProvider {
607
663
  async discoveryState(): Promise<OAuthDiscoveryState | undefined> {
608
664
  this.throwIfInactive()
609
665
  if (!this.flowDiscoveryState && this.config.authServerMetadataUrl !== undefined) {
610
- this.flowDiscoveryState = await loadConfiguredDiscoveryState(
666
+ const discoveryState = await loadConfiguredDiscoveryState(
611
667
  this.config.authServerMetadataUrl,
612
668
  this.serverUrl,
613
669
  this.config.skipIssuerMetadataValidation === true,
614
- this.runtimeSignal,
670
+ this.authFetch,
615
671
  )
672
+ this.throwIfInactive()
673
+ this.flowDiscoveryState = discoveryState
616
674
  }
675
+ this.throwIfInactive()
617
676
  return this.flowDiscoveryState ? structuredClone(this.flowDiscoveryState) : undefined
618
677
  }
619
678
 
679
+ /** Internal connection-attempt identity check for manager cleanup. */
620
680
  /**
621
681
  * Save the OAuth state parameter for CSRF protection.
622
682
  */
@@ -657,6 +717,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
657
717
  this.flowState = undefined
658
718
  this.invalidatedAccessToken = undefined
659
719
  this.invalidatedClientId = undefined
720
+ this.staleRedirectClientId = undefined
660
721
  this.lastObservedClientId = undefined
661
722
  this.lastSavedAccessToken = undefined
662
723
  this.pendingAuthAccessToken = undefined
@@ -679,6 +740,9 @@ export class McpOAuthProvider implements OAuthClientProvider {
679
740
  this.invalidatedAccessToken = this.pendingAuthAccessToken ?? this.lastSavedAccessToken
680
741
  this.lastSavedAccessToken = undefined
681
742
  this.pendingAuthAccessToken = undefined
743
+ if (this.staleRedirectClientId !== undefined) {
744
+ this.invalidatedClientId = this.staleRedirectClientId
745
+ }
682
746
  invalidateAuthEntryCache(this.serverName)
683
747
  break
684
748
  case "verifier":
@@ -2,10 +2,17 @@ import { randomBytes } from "node:crypto";
2
2
  import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
+ import {
6
+ DEFAULT_MAX_BYTES,
7
+ DEFAULT_MAX_LINES,
8
+ formatSize,
9
+ truncateHead,
10
+ type TruncationResult,
11
+ } from "../../../core/tools/truncate.ts";
5
12
  import type { ContentBlock, McpSettings } from "./types.ts";
6
13
 
7
- export const DEFAULT_MCP_OUTPUT_MAX_BYTES = 50 * 1024;
8
- export const DEFAULT_MCP_OUTPUT_MAX_LINES = 2000;
14
+ export const DEFAULT_MCP_OUTPUT_MAX_BYTES = DEFAULT_MAX_BYTES;
15
+ export const DEFAULT_MCP_OUTPUT_MAX_LINES = DEFAULT_MAX_LINES;
9
16
  export const DEFAULT_MCP_DETAILS_MAX_BYTES = 16 * 1024;
10
17
 
11
18
  const CONTENT_SUMMARY_LIMIT = 20;
@@ -22,6 +29,16 @@ export interface McpOutputGuardDetails {
22
29
  returnedBytes: number;
23
30
  originalLines: number;
24
31
  returnedLines: number;
32
+ /** Host truncation classification and preview counts for the composed text before the MCP notice is added. */
33
+ truncatedBy: TruncationResult["truncatedBy"];
34
+ totalLines: number;
35
+ totalBytes: number;
36
+ outputLines: number;
37
+ outputBytes: number;
38
+ lastLinePartial: boolean;
39
+ firstLineExceedsLimit: boolean;
40
+ maxLines: number;
41
+ maxBytes: number;
25
42
  /** Number of image content blocks returned untouched alongside the truncated text. */
26
43
  imageBlocksPassedThrough?: number;
27
44
  fullOutputPath?: string;
@@ -119,26 +136,43 @@ export async function guardMcpOutput(
119
136
  .map((block) => (block as { text: string }).text)
120
137
  .join("\n");
121
138
  const composedOutput = `${prefix}${textOutput}${suffix}`;
122
- const stats = textStats(composedOutput);
139
+ const truncation = truncateHead(composedOutput, { maxBytes, maxLines });
123
140
 
124
141
  let guardedContent: ContentBlock[] = addAffixes(normalizedContent, prefix, suffix);
125
142
  let outputGuard: McpOutputGuardDetails | undefined;
126
143
 
127
- if (stats.bytes > maxBytes || stats.lines > maxLines) {
144
+ if (truncation.truncated) {
128
145
  const { path: fullOutputPath, error: writeError } = await saveArtifact("output", composedOutput);
129
- const notice = formatTruncationNotice(stats, fullOutputPath, writeError);
130
- const previewBudget = reserveBudget(maxBytes, maxLines, notice);
131
- const preview = truncateHead(composedOutput, previewBudget.maxBytes, previewBudget.maxLines);
146
+ const initialNotice = formatTruncationNotice(truncation, fullOutputPath, writeError);
147
+ const previewBudget = reserveBudget(maxBytes, maxLines, initialNotice);
148
+ const preview = truncateHead(composedOutput, {
149
+ maxBytes: previewBudget.maxBytes,
150
+ maxLines: previewBudget.maxLines,
151
+ });
152
+ const notice = formatTruncationNotice(
153
+ { ...truncation, outputLines: preview.outputLines, outputBytes: preview.outputBytes },
154
+ fullOutputPath,
155
+ writeError,
156
+ );
132
157
  const finalText = `${preview.content}\n\n${notice}`;
133
158
  const finalStats = textStats(finalText);
134
159
 
135
160
  guardedContent = [{ type: "text" as const, text: finalText }, ...imageBlocks];
136
161
  outputGuard = {
137
162
  truncated: true,
138
- originalBytes: stats.bytes,
163
+ originalBytes: truncation.totalBytes,
139
164
  returnedBytes: finalStats.bytes,
140
- originalLines: stats.lines,
165
+ originalLines: truncation.totalLines,
141
166
  returnedLines: finalStats.lines,
167
+ truncatedBy: truncation.truncatedBy,
168
+ totalLines: truncation.totalLines,
169
+ totalBytes: truncation.totalBytes,
170
+ outputLines: preview.outputLines,
171
+ outputBytes: preview.outputBytes,
172
+ lastLinePartial: truncation.lastLinePartial,
173
+ firstLineExceedsLimit: truncation.firstLineExceedsLimit,
174
+ maxLines: truncation.maxLines,
175
+ maxBytes: truncation.maxBytes,
142
176
  ...(imageBlocks.length > 0 ? { imageBlocksPassedThrough: imageBlocks.length } : {}),
143
177
  ...(fullOutputPath !== undefined ? { fullOutputPath } : {}),
144
178
  ...(writeError !== undefined ? { writeError } : {}),
@@ -217,31 +251,6 @@ function reserveBudget(maxBytes: number, maxLines: number, notice: string): { ma
217
251
  };
218
252
  }
219
253
 
220
- function truncateHead(text: string, maxBytes: number, maxLines: number): { content: string; bytes: number; lines: number } {
221
- const lines = text.split("\n");
222
- const output: string[] = [];
223
- let bytes = 0;
224
-
225
- for (const line of lines) {
226
- if (output.length >= maxLines) break;
227
- const separatorBytes = output.length > 0 ? 1 : 0;
228
- const lineBytes = byteLength(line);
229
- if (bytes + separatorBytes + lineBytes > maxBytes) {
230
- const remaining = maxBytes - bytes - separatorBytes;
231
- if (remaining > 0) {
232
- output.push(truncateStringToBytes(line, remaining));
233
- }
234
- break;
235
- }
236
- output.push(line);
237
- bytes += separatorBytes + lineBytes;
238
- }
239
-
240
- const content = output.join("\n");
241
- const stats = textStats(content);
242
- return { content, bytes: stats.bytes, lines: stats.lines };
243
- }
244
-
245
254
  function truncateStringToBytes(value: string, maxBytes: number): string {
246
255
  if (byteLength(value) <= maxBytes) return value;
247
256
  const buffer = Buffer.from(value, "utf8");
@@ -251,11 +260,19 @@ function truncateStringToBytes(value: string, maxBytes: number): string {
251
260
  }
252
261
 
253
262
  function formatTruncationNotice(
254
- stats: { bytes: number; lines: number },
263
+ truncation: TruncationResult,
255
264
  fullOutputPath: string | undefined,
256
265
  writeError: string | undefined,
257
266
  ): string {
258
- const base = `[MCP text output truncated: original ${stats.lines.toLocaleString()} lines / ${formatSize(stats.bytes)}.`;
267
+ let reason: string;
268
+ if (truncation.firstLineExceedsLimit) {
269
+ reason = `First line exceeds ${formatSize(truncation.maxBytes)} limit`;
270
+ } else if (truncation.truncatedBy === "lines") {
271
+ reason = `Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines} line limit)`;
272
+ } else {
273
+ reason = `Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes)} limit)`;
274
+ }
275
+ const base = `[MCP text output truncated: original ${truncation.totalLines.toLocaleString()} lines / ${formatSize(truncation.totalBytes)}. ${reason}.`;
259
276
  if (fullOutputPath) {
260
277
  return `${base} Full text saved to: ${fullOutputPath} — use read with offset/limit or grep to inspect.]`;
261
278
  }
@@ -488,9 +505,3 @@ function envKillSwitch(name: string): boolean | undefined {
488
505
  if (["1", "true", "yes", "on"].includes(value)) return true;
489
506
  return undefined;
490
507
  }
491
-
492
- function formatSize(bytes: number): string {
493
- if (bytes < 1024) return `${bytes} B`;
494
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
495
- return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
496
- }