@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.
- package/CHANGELOG.md +16 -0
- package/dist/bundle/chunks/{anthropic-messages-XULXUBIG.js → anthropic-messages-QFCEKGI3.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-JEG2XOYC.js → azure-openai-responses-G4ZMHV37.js} +1 -1
- package/dist/bundle/chunks/chunk-7T6XCLJF.js +22 -0
- package/dist/bundle/chunks/chunk-D6HHRSB5.js +103 -0
- package/dist/bundle/chunks/chunk-EY4DWHQU.js +2 -0
- package/dist/bundle/chunks/{chunk-LYAMSPR2.js → chunk-I47ZWR55.js} +483 -506
- package/dist/bundle/chunks/{chunk-JUPSJTFQ.js → chunk-K4JAZYQH.js} +1 -1
- package/dist/bundle/chunks/{chunk-HPPQHK3J.js → chunk-KKIB2TVR.js} +1 -1
- package/dist/bundle/chunks/chunk-LI2YVPWP.js +2 -0
- package/dist/bundle/chunks/{chunk-BZQ53VGE.js → chunk-XSR434CO.js} +1 -1
- package/dist/bundle/chunks/compat-QCV26HNS.js +2 -0
- package/dist/bundle/chunks/dist-PRPIUY3U.js +2 -0
- package/dist/bundle/chunks/esm-E7UVEYTO.js +4 -0
- package/dist/bundle/chunks/{extract-TRW5FBY6.js → extract-CTIFQZDJ.js} +15 -14
- package/dist/bundle/chunks/{google-generative-ai-OX6VSCA6.js → google-generative-ai-RSUU4MWO.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-GW76GSS2.js → google-vertex-CKQPOITO.js} +1 -1
- package/dist/bundle/chunks/host-WP3MVKS2.js +2 -0
- package/dist/bundle/chunks/{node-KOIP55YZ.js → node-NF43OPII.js} +1 -1
- package/dist/bundle/chunks/{node-XY3CBY3C.js → node-UMFM7CDZ.js} +1 -1
- package/dist/bundle/chunks/{openai-codex-responses-NPLWP6ZC.js → openai-codex-responses-S34V5ZSD.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-OT2GTJKC.js → openai-completions-T2IGVUEI.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-5A6CR7GI.js → openai-responses-GYZODCT3.js} +1 -1
- package/dist/bundle/chunks/readability-LAGF34IB.js +3 -0
- package/dist/bundle/chunks/turndown.cjs-HRMZVRUL.js +2 -0
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/core/tools/file-mutation-queue.js +21 -8
- package/dist/core/tools/file-mutation-queue.js.map +1 -1
- package/dist/core/tools/subagent-env.d.ts +18 -0
- package/dist/core/tools/subagent-env.d.ts.map +1 -0
- package/dist/core/tools/subagent-env.js +43 -0
- package/dist/core/tools/subagent-env.js.map +1 -0
- package/dist/core/tools/subagent-scheduler.d.ts +31 -0
- package/dist/core/tools/subagent-scheduler.d.ts.map +1 -0
- package/dist/core/tools/subagent-scheduler.js +203 -0
- package/dist/core/tools/subagent-scheduler.js.map +1 -0
- package/dist/core/tools/subagent.d.ts +83 -3
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +214 -76
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
- package/dist/humain/mcp-adapter/vendor/README.md +125 -19
- package/dist/humain/mcp-adapter/vendor/agent-plugin-loader.ts +69 -30
- package/dist/humain/mcp-adapter/vendor/agent-plugin-provenance.ts +36 -0
- package/dist/humain/mcp-adapter/vendor/claude-plugin-loader.ts +365 -0
- package/dist/humain/mcp-adapter/vendor/commands.ts +13 -15
- package/dist/humain/mcp-adapter/vendor/config.ts +198 -30
- package/dist/humain/mcp-adapter/vendor/consent-manager.ts +40 -9
- package/dist/humain/mcp-adapter/vendor/direct-tool-surface.ts +250 -0
- package/dist/humain/mcp-adapter/vendor/direct-tools.ts +9 -228
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js +74 -32
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.d.ts +1 -0
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js +31 -0
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js.map +1 -0
- package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.d.ts +9 -0
- package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js +362 -0
- package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js.map +1 -0
- package/dist/humain/mcp-adapter/vendor/dist/config.d.ts +3 -1
- package/dist/humain/mcp-adapter/vendor/dist/config.js +189 -27
- package/dist/humain/mcp-adapter/vendor/dist/config.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js +7 -16
- package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js +5 -17
- package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/types.d.ts +30 -4
- package/dist/humain/mcp-adapter/vendor/dist/types.js +26 -0
- package/dist/humain/mcp-adapter/vendor/dist/types.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/utils.d.ts +6 -0
- package/dist/humain/mcp-adapter/vendor/dist/utils.js +65 -6
- package/dist/humain/mcp-adapter/vendor/dist/utils.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/host-html-template.ts +20 -19
- package/dist/humain/mcp-adapter/vendor/http-ca.ts +84 -0
- package/dist/humain/mcp-adapter/vendor/index.bundle.mjs +37102 -15103
- package/dist/humain/mcp-adapter/vendor/index.ts +828 -164
- package/dist/humain/mcp-adapter/vendor/init.ts +48 -10
- package/dist/humain/mcp-adapter/vendor/lazy-loader.ts +7 -0
- package/dist/humain/mcp-adapter/vendor/mcp-auth-fetch.ts +113 -0
- package/dist/humain/mcp-adapter/vendor/mcp-auth-flow.ts +256 -77
- package/dist/humain/mcp-adapter/vendor/mcp-auth.ts +316 -54
- package/dist/humain/mcp-adapter/vendor/mcp-code.ts +45 -18
- package/dist/humain/mcp-adapter/vendor/mcp-install.ts +60 -0
- package/dist/humain/mcp-adapter/vendor/mcp-oauth-provider.ts +70 -6
- package/dist/humain/mcp-adapter/vendor/mcp-output-guard.ts +53 -42
- package/dist/humain/mcp-adapter/vendor/mcp-panel-theme.ts +105 -0
- package/dist/humain/mcp-adapter/vendor/mcp-panel.ts +366 -331
- package/dist/humain/mcp-adapter/vendor/mcp-references.ts +9 -14
- package/dist/humain/mcp-adapter/vendor/mcp-script-worker.mjs +3 -1
- package/dist/humain/mcp-adapter/vendor/mcp-setup-panel.ts +383 -341
- package/dist/humain/mcp-adapter/vendor/mcp-status.ts +5 -0
- package/dist/humain/mcp-adapter/vendor/metadata-cache.ts +7 -16
- package/dist/humain/mcp-adapter/vendor/namespace-tools.ts +61 -3
- package/dist/humain/mcp-adapter/vendor/oauth.ts +6 -2
- package/dist/humain/mcp-adapter/vendor/package-mcp-loader.ts +5 -17
- package/dist/humain/mcp-adapter/vendor/package.json +13 -3
- package/dist/humain/mcp-adapter/vendor/prompts.ts +35 -3
- package/dist/humain/mcp-adapter/vendor/proxy-modes.ts +108 -7
- package/dist/humain/mcp-adapter/vendor/runtime-owner.ts +16 -1
- package/dist/humain/mcp-adapter/vendor/sampling-handler.ts +9 -32
- package/dist/humain/mcp-adapter/vendor/sandbox-proxy-template.ts +20 -93
- package/dist/humain/mcp-adapter/vendor/server-manager.ts +227 -40
- package/dist/humain/mcp-adapter/vendor/session-approvals.ts +187 -0
- package/dist/humain/mcp-adapter/vendor/skills/mcp-scripting/SKILL.md +9 -1
- package/dist/humain/mcp-adapter/vendor/state.ts +10 -1
- package/dist/humain/mcp-adapter/vendor/tool-approval.ts +10 -22
- package/dist/humain/mcp-adapter/vendor/tool-metadata.ts +14 -0
- package/dist/humain/mcp-adapter/vendor/types.ts +57 -4
- package/dist/humain/mcp-adapter/vendor/ui-server.ts +36 -28
- package/dist/humain/mcp-adapter/vendor/utils.ts +64 -6
- package/dist/humain/web-access/vendor/abortable.d.ts +3 -0
- package/dist/humain/web-access/vendor/abortable.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/abortable.js +22 -0
- package/dist/humain/web-access/vendor/abortable.js.map +1 -0
- package/dist/humain/web-access/vendor/crawl4ai.d.ts +9 -0
- package/dist/humain/web-access/vendor/crawl4ai.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/crawl4ai.js +187 -0
- package/dist/humain/web-access/vendor/crawl4ai.js.map +1 -0
- package/dist/humain/web-access/vendor/credential-source.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/credential-source.js +1 -0
- package/dist/humain/web-access/vendor/credential-source.js.map +1 -1
- package/dist/humain/web-access/vendor/curator-page.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/curator-page.js +20 -2
- package/dist/humain/web-access/vendor/curator-page.js.map +1 -1
- package/dist/humain/web-access/vendor/curator-run.d.ts +11 -0
- package/dist/humain/web-access/vendor/curator-run.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/curator-run.js +38 -0
- package/dist/humain/web-access/vendor/curator-run.js.map +1 -0
- package/dist/humain/web-access/vendor/curator-server.d.ts +1 -0
- package/dist/humain/web-access/vendor/curator-server.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/curator-server.js +2 -0
- package/dist/humain/web-access/vendor/curator-server.js.map +1 -1
- package/dist/humain/web-access/vendor/duckduckgo.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/duckduckgo.js +1 -1
- package/dist/humain/web-access/vendor/duckduckgo.js.map +1 -1
- package/dist/humain/web-access/vendor/extract.d.ts +1 -1
- package/dist/humain/web-access/vendor/extract.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/extract.js +58 -11
- package/dist/humain/web-access/vendor/extract.js.map +1 -1
- package/dist/humain/web-access/vendor/fetch-params.js +1 -1
- package/dist/humain/web-access/vendor/fetch-params.js.map +1 -1
- package/dist/humain/web-access/vendor/gemini-search.d.ts +2 -2
- package/dist/humain/web-access/vendor/gemini-search.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/gemini-search.js +12 -5
- package/dist/humain/web-access/vendor/gemini-search.js.map +1 -1
- package/dist/humain/web-access/vendor/index.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/index.js +23 -19
- package/dist/humain/web-access/vendor/index.js.map +1 -1
- package/dist/humain/web-access/vendor/openai-search.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/openai-search.js +40 -14
- package/dist/humain/web-access/vendor/openai-search.js.map +1 -1
- package/dist/humain/web-access/vendor/page-query.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/page-query.js +6 -2
- package/dist/humain/web-access/vendor/page-query.js.map +1 -1
- package/dist/humain/web-access/vendor/query-rewrite.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/query-rewrite.js +6 -2
- package/dist/humain/web-access/vendor/query-rewrite.js.map +1 -1
- package/dist/humain/web-access/vendor/serpapi.d.ts +4 -0
- package/dist/humain/web-access/vendor/serpapi.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/serpapi.js +215 -0
- package/dist/humain/web-access/vendor/serpapi.js.map +1 -0
- package/dist/humain/web-access/vendor/ssrf-protection.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/ssrf-protection.js +5 -1
- package/dist/humain/web-access/vendor/ssrf-protection.js.map +1 -1
- package/dist/humain/web-access/vendor/storage.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/storage.js +9 -0
- package/dist/humain/web-access/vendor/storage.js.map +1 -1
- package/dist/humain/web-access/vendor/summary-review.d.ts +1 -1
- package/dist/humain/web-access/vendor/summary-review.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/summary-review.js +35 -24
- package/dist/humain/web-access/vendor/summary-review.js.map +1 -1
- package/dist/humain/web-access/vendor/utils.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/utils.js +5 -3
- package/dist/humain/web-access/vendor/utils.js.map +1 -1
- package/dist/modes/json-event.d.ts +8 -0
- package/dist/modes/json-event.d.ts.map +1 -1
- package/dist/modes/json-event.js +17 -0
- package/dist/modes/json-event.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +3 -2
- package/dist/modes/print-mode.js.map +1 -1
- package/docs/environment-variables.md +8 -0
- package/docs/json.md +16 -0
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
- package/src/humain/mcp-adapter/UPSTREAM.md +56 -7
- package/src/humain/mcp-adapter/upstream.json +6 -4
- package/src/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
- package/src/humain/web-access/UPSTREAM.md +18 -8
- package/src/humain/web-access/upstream.json +6 -5
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
158
|
-
|
|
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
|
|
210
|
-
|
|
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
|
|
308
|
+
let payload: WorkerResultPayload;
|
|
283
309
|
if (message.type === "call") {
|
|
284
|
-
|
|
310
|
+
payload = await callTool(message.path, message.args as Record<string, unknown> | undefined);
|
|
285
311
|
} else if (message.type === "search") {
|
|
286
|
-
|
|
312
|
+
payload = { envelope: searchTools(message.input as SearchInput | undefined) };
|
|
287
313
|
} else {
|
|
288
|
-
|
|
314
|
+
payload = { envelope: describeTool(message.input as DescribeInput | undefined) };
|
|
289
315
|
}
|
|
290
|
-
|
|
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
|
-
|
|
221
|
+
fetchFn: FetchLike,
|
|
217
222
|
): Promise<OAuthDiscoveryState> {
|
|
218
|
-
const response = await
|
|
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
|
-
|
|
666
|
+
const discoveryState = await loadConfiguredDiscoveryState(
|
|
611
667
|
this.config.authServerMetadataUrl,
|
|
612
668
|
this.serverUrl,
|
|
613
669
|
this.config.skipIssuerMetadataValidation === true,
|
|
614
|
-
this.
|
|
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 =
|
|
8
|
-
export const DEFAULT_MCP_OUTPUT_MAX_LINES =
|
|
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
|
|
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 (
|
|
144
|
+
if (truncation.truncated) {
|
|
128
145
|
const { path: fullOutputPath, error: writeError } = await saveArtifact("output", composedOutput);
|
|
129
|
-
const
|
|
130
|
-
const previewBudget = reserveBudget(maxBytes, maxLines,
|
|
131
|
-
const preview = truncateHead(composedOutput,
|
|
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:
|
|
163
|
+
originalBytes: truncation.totalBytes,
|
|
139
164
|
returnedBytes: finalStats.bytes,
|
|
140
|
-
originalLines:
|
|
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
|
-
|
|
263
|
+
truncation: TruncationResult,
|
|
255
264
|
fullOutputPath: string | undefined,
|
|
256
265
|
writeError: string | undefined,
|
|
257
266
|
): string {
|
|
258
|
-
|
|
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
|
-
}
|