@humain/terminal 0.0.3-beta.0 → 0.0.3-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +4 -3
  4. package/dist/config.js.map +1 -1
  5. package/dist/humain/mcp-adapter/vendor/CHANGELOG.md +13 -0
  6. package/dist/humain/mcp-adapter/vendor/README.md +4 -1
  7. package/dist/humain/mcp-adapter/vendor/agent-dir.ts +12 -4
  8. package/dist/humain/mcp-adapter/vendor/cli.js +25 -4
  9. package/dist/humain/mcp-adapter/vendor/config.ts +4 -4
  10. package/dist/humain/mcp-adapter/vendor/direct-tools.ts +18 -15
  11. package/dist/humain/mcp-adapter/vendor/index.bundle.mjs +416 -60
  12. package/dist/humain/mcp-adapter/vendor/mcp-setup-panel.ts +2 -1
  13. package/dist/humain/mcp-adapter/vendor/metadata-cache.ts +18 -8
  14. package/dist/humain/mcp-adapter/vendor/package.json +177 -176
  15. package/dist/humain/mcp-adapter/vendor/request-headers-command.ts +336 -0
  16. package/dist/humain/mcp-adapter/vendor/server-manager.ts +5 -0
  17. package/dist/humain/mcp-adapter/vendor/tool-metadata.ts +38 -18
  18. package/dist/humain/mcp-adapter/vendor/types.ts +93 -10
  19. package/dist/humain/web-access/vendor/auth-fetch.d.ts +13 -0
  20. package/dist/humain/web-access/vendor/auth-fetch.d.ts.map +1 -0
  21. package/dist/humain/web-access/vendor/auth-fetch.js +134 -0
  22. package/dist/humain/web-access/vendor/auth-fetch.js.map +1 -0
  23. package/dist/humain/web-access/vendor/chrome-cookies.d.ts +13 -0
  24. package/dist/humain/web-access/vendor/chrome-cookies.d.ts.map +1 -1
  25. package/dist/humain/web-access/vendor/chrome-cookies.js +106 -22
  26. package/dist/humain/web-access/vendor/chrome-cookies.js.map +1 -1
  27. package/dist/humain/web-access/vendor/curator-page.d.ts +1 -0
  28. package/dist/humain/web-access/vendor/curator-page.d.ts.map +1 -1
  29. package/dist/humain/web-access/vendor/curator-page.js +3 -1
  30. package/dist/humain/web-access/vendor/curator-page.js.map +1 -1
  31. package/dist/humain/web-access/vendor/curator-server.d.ts +1 -0
  32. package/dist/humain/web-access/vendor/curator-server.d.ts.map +1 -1
  33. package/dist/humain/web-access/vendor/curator-server.js +2 -0
  34. package/dist/humain/web-access/vendor/curator-server.js.map +1 -1
  35. package/dist/humain/web-access/vendor/extract.d.ts +2 -0
  36. package/dist/humain/web-access/vendor/extract.d.ts.map +1 -1
  37. package/dist/humain/web-access/vendor/extract.js +79 -17
  38. package/dist/humain/web-access/vendor/extract.js.map +1 -1
  39. package/dist/humain/web-access/vendor/fetch-params.d.ts +2 -0
  40. package/dist/humain/web-access/vendor/fetch-params.d.ts.map +1 -1
  41. package/dist/humain/web-access/vendor/fetch-params.js +17 -3
  42. package/dist/humain/web-access/vendor/fetch-params.js.map +1 -1
  43. package/dist/humain/web-access/vendor/firecrawl.d.ts +7 -0
  44. package/dist/humain/web-access/vendor/firecrawl.d.ts.map +1 -1
  45. package/dist/humain/web-access/vendor/firecrawl.js +141 -10
  46. package/dist/humain/web-access/vendor/firecrawl.js.map +1 -1
  47. package/dist/humain/web-access/vendor/gemini-search.d.ts +2 -2
  48. package/dist/humain/web-access/vendor/gemini-search.d.ts.map +1 -1
  49. package/dist/humain/web-access/vendor/gemini-search.js +22 -4
  50. package/dist/humain/web-access/vendor/gemini-search.js.map +1 -1
  51. package/dist/humain/web-access/vendor/index.d.ts.map +1 -1
  52. package/dist/humain/web-access/vendor/index.js +122 -48
  53. package/dist/humain/web-access/vendor/index.js.map +1 -1
  54. package/dist/humain/web-access/vendor/summary-review.d.ts +3 -1
  55. package/dist/humain/web-access/vendor/summary-review.d.ts.map +1 -1
  56. package/dist/humain/web-access/vendor/summary-review.js +6 -3
  57. package/dist/humain/web-access/vendor/summary-review.js.map +1 -1
  58. package/dist/humain/web-access/vendor/youtube-extract.d.ts.map +1 -1
  59. package/dist/humain/web-access/vendor/youtube-extract.js +2 -2
  60. package/dist/humain/web-access/vendor/youtube-extract.js.map +1 -1
  61. package/dist/utils/clipboard-image.d.ts.map +1 -1
  62. package/dist/utils/clipboard-image.js +2 -0
  63. package/dist/utils/clipboard-image.js.map +1 -1
  64. package/npm-shrinkwrap.json +7 -7
  65. package/package.json +6 -6
  66. package/src/humain/mcp-adapter/UPSTREAM.md +1 -1
  67. package/src/humain/mcp-adapter/upstream.json +3 -3
  68. package/src/humain/mcp-adapter/vendor/CHANGELOG.md +13 -0
  69. package/src/humain/web-access/UPSTREAM.md +1 -1
  70. package/src/humain/web-access/upstream.json +4 -4
@@ -0,0 +1,336 @@
1
+ import { spawn, spawnSync, type ChildProcess } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import type { FetchLike } from "@modelcontextprotocol/client";
4
+ import type { HttpRequestHeadersCommand } from "./types.ts";
5
+ import { interpolateEnvVars } from "./utils.ts";
6
+
7
+ const DEFAULT_TIMEOUT_MS = 10_000;
8
+ const MAX_OUTPUT_BYTES = 64 * 1024;
9
+ const USE_PROCESS_GROUP = process.platform !== "win32";
10
+ const CLEANUP_TOKEN_ENV = "PI_MCP_REQUEST_HEADERS_CLEANUP_TOKEN";
11
+
12
+ function isNoSuchProcessError(error: unknown): boolean {
13
+ return typeof error === "object" && error !== null && "code" in error && (error as NodeJS.ErrnoException).code === "ESRCH";
14
+ }
15
+
16
+ function runPosixPs(args: string[]): { status: number | null; stdout: string } {
17
+ if (process.env.PI_MCP_ADAPTER_TEST_FAIL_PS === "1") return { status: 1, stdout: "" };
18
+ return spawnSync("ps", args, { encoding: "utf8" });
19
+ }
20
+
21
+ function collectPosixDescendantPids(rootPid: number): number[] {
22
+ const result = runPosixPs(["-axo", "pid=,ppid="]);
23
+ if (result.status !== 0) {
24
+ throw new Error(`HTTP request headers command cleanup failed: ps exited with code ${result.status ?? "unknown"}`);
25
+ }
26
+
27
+ const childrenByParent = new Map<number, number[]>();
28
+ for (const line of result.stdout.split("\n")) {
29
+ const [pidText, ppidText] = line.trim().split(/\s+/, 2);
30
+ const pid = Number(pidText);
31
+ const ppid = Number(ppidText);
32
+ if (!Number.isInteger(pid) || !Number.isInteger(ppid)) continue;
33
+ const children = childrenByParent.get(ppid);
34
+ if (children) children.push(pid);
35
+ else childrenByParent.set(ppid, [pid]);
36
+ }
37
+
38
+ const descendants: number[] = [];
39
+ const stack = [...(childrenByParent.get(rootPid) ?? [])];
40
+ while (stack.length > 0) {
41
+ const pid = stack.pop()!;
42
+ descendants.push(pid);
43
+ stack.push(...(childrenByParent.get(pid) ?? []));
44
+ }
45
+ return descendants;
46
+ }
47
+
48
+ function collectPosixCleanupTokenPids(cleanupToken: string): number[] {
49
+ const result = runPosixPs(["axeww", "-o", "pid=,command="]);
50
+ if (result.status !== 0) {
51
+ throw new Error(`HTTP request headers command cleanup failed: ps exited with code ${result.status ?? "unknown"}`);
52
+ }
53
+
54
+ const needle = `${CLEANUP_TOKEN_ENV}=${cleanupToken}`;
55
+ const pids: number[] = [];
56
+ for (const line of result.stdout.split("\n")) {
57
+ const trimmed = line.trim();
58
+ if (!trimmed.includes(needle)) continue;
59
+ const [pidText] = trimmed.split(/\s+/, 1);
60
+ const pid = Number(pidText);
61
+ if (Number.isInteger(pid) && pid !== process.pid) pids.push(pid);
62
+ }
63
+ return pids;
64
+ }
65
+
66
+ function assertPosixProcessDiscoveryAvailable(): void {
67
+ collectPosixDescendantPids(process.pid);
68
+ collectPosixCleanupTokenPids(`${process.pid}-preflight`);
69
+ }
70
+
71
+ function isTaskkillNoSuchProcess(result: ReturnType<typeof spawnSync>): boolean {
72
+ return `${result.stdout ?? ""}\n${result.stderr ?? ""}`.toLowerCase().includes("not found");
73
+ }
74
+
75
+ function signalPid(pid: number, signal: NodeJS.Signals): void {
76
+ try {
77
+ process.kill(pid, signal);
78
+ } catch (error) {
79
+ if (!isNoSuchProcessError(error)) throw error;
80
+ }
81
+ }
82
+
83
+ function signalProcessGroup(pid: number, signal: NodeJS.Signals): void {
84
+ try {
85
+ process.kill(-pid, signal);
86
+ } catch (error) {
87
+ if (!isNoSuchProcessError(error)) throw error;
88
+ }
89
+ }
90
+
91
+ function killRequestHeadersCommand(child: ChildProcess, trackedPosixDescendantPids = new Set<number>(), cleanupToken?: string): void {
92
+ if (process.platform === "win32" && child.pid !== undefined) {
93
+ const result = spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
94
+ encoding: "utf8",
95
+ windowsHide: true,
96
+ });
97
+ if (result.status === 0 || isTaskkillNoSuchProcess(result)) return;
98
+ throw new Error(`HTTP request headers command cleanup failed: taskkill exited with code ${result.status ?? "unknown"}`);
99
+ }
100
+
101
+ if (USE_PROCESS_GROUP && child.pid !== undefined) {
102
+ const frozenPids = new Set<number>();
103
+ let cleanupError: Error | undefined;
104
+ try {
105
+ signalProcessGroup(child.pid, "SIGSTOP");
106
+ for (const pid of trackedPosixDescendantPids) {
107
+ signalPid(pid, "SIGSTOP");
108
+ frozenPids.add(pid);
109
+ }
110
+ let stablePasses = 0;
111
+ for (let pass = 0; pass < 16; pass++) {
112
+ const candidates = [
113
+ ...collectPosixDescendantPids(child.pid),
114
+ ...(cleanupToken ? collectPosixCleanupTokenPids(cleanupToken) : []),
115
+ ];
116
+ const newPids = candidates.filter(pid => !frozenPids.has(pid));
117
+ if (newPids.length === 0) {
118
+ stablePasses++;
119
+ if (stablePasses >= 2) return;
120
+ continue;
121
+ }
122
+ stablePasses = 0;
123
+ for (const pid of newPids) {
124
+ signalPid(pid, "SIGSTOP");
125
+ frozenPids.add(pid);
126
+ }
127
+ }
128
+ cleanupError = new Error("HTTP request headers command cleanup failed: descendant process tree did not stabilize");
129
+ } catch (error) {
130
+ cleanupError = error instanceof Error ? error : new Error(String(error));
131
+ } finally {
132
+ signalProcessGroup(child.pid, "SIGKILL");
133
+ for (const pid of frozenPids) signalPid(pid, "SIGKILL");
134
+ }
135
+ if (cleanupError) throw cleanupError;
136
+ return;
137
+ }
138
+
139
+ child.kill("SIGKILL");
140
+ }
141
+
142
+ export interface HttpRequestCommandEnvelope {
143
+ version: 1;
144
+ method: string;
145
+ url: string;
146
+ bodyBase64: string;
147
+ }
148
+
149
+ type CommandResult =
150
+ | { status: "error"; error: Error }
151
+ | { status: "success"; headers: Headers };
152
+
153
+ function resolvedCommand(config: HttpRequestHeadersCommand): {
154
+ command: string;
155
+ args: string[];
156
+ env: NodeJS.ProcessEnv;
157
+ timeoutMs: number;
158
+ } {
159
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
160
+ throw new Error("HTTP request headers command must be an object");
161
+ }
162
+ if (typeof config.command !== "string" || config.command.trim() === "") {
163
+ throw new Error("HTTP request headers command requires a non-empty command");
164
+ }
165
+ if (config.args !== undefined && (!Array.isArray(config.args) || config.args.some(arg => typeof arg !== "string"))) {
166
+ throw new Error("HTTP request headers command args must be strings");
167
+ }
168
+ if (config.env !== undefined && (
169
+ typeof config.env !== "object"
170
+ || Array.isArray(config.env)
171
+ || Object.values(config.env).some(value => typeof value !== "string")
172
+ )) {
173
+ throw new Error("HTTP request headers command env values must be strings");
174
+ }
175
+ const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
176
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 60_000) {
177
+ throw new Error("HTTP request headers command timeoutMs must be an integer between 1 and 60000");
178
+ }
179
+ return {
180
+ command: interpolateEnvVars(config.command),
181
+ args: (config.args ?? []).map(interpolateEnvVars),
182
+ env: {
183
+ ...process.env,
184
+ ...Object.fromEntries(
185
+ Object.entries(config.env ?? {}).map(([key, value]) => [key, interpolateEnvVars(value)]),
186
+ ),
187
+ },
188
+ timeoutMs,
189
+ };
190
+ }
191
+
192
+ async function invokeRequestHeadersCommand(
193
+ config: HttpRequestHeadersCommand,
194
+ envelope: HttpRequestCommandEnvelope,
195
+ signal: AbortSignal,
196
+ ): Promise<Headers> {
197
+ const resolved = resolvedCommand(config);
198
+ if (USE_PROCESS_GROUP) assertPosixProcessDiscoveryAvailable();
199
+ return new Promise<Headers>((resolve, reject) => {
200
+ let stdout = Buffer.alloc(0);
201
+ let settled = false;
202
+ const cleanupToken = randomUUID();
203
+ const child = spawn(resolved.command, resolved.args, {
204
+ env: { ...resolved.env, [CLEANUP_TOKEN_ENV]: cleanupToken },
205
+ stdio: ["pipe", "pipe", "ignore"],
206
+ windowsHide: true,
207
+ detached: USE_PROCESS_GROUP,
208
+ });
209
+
210
+ const trackedPosixDescendantPids = new Set<number>();
211
+ let trackingError: Error | undefined;
212
+ const trackPosixDescendants = () => {
213
+ if (!USE_PROCESS_GROUP || child.pid === undefined || settled || trackingError) return;
214
+ try {
215
+ for (const pid of collectPosixDescendantPids(child.pid)) trackedPosixDescendantPids.add(pid);
216
+ for (const pid of collectPosixCleanupTokenPids(cleanupToken)) trackedPosixDescendantPids.add(pid);
217
+ } catch (error) {
218
+ trackingError = error instanceof Error ? error : new Error(String(error));
219
+ }
220
+ };
221
+ const descendantTracker = USE_PROCESS_GROUP ? setInterval(trackPosixDescendants, 50) : undefined;
222
+ descendantTracker?.unref();
223
+
224
+ const finish = (result: CommandResult) => {
225
+ if (settled) return;
226
+ settled = true;
227
+ clearTimeout(timer);
228
+ if (descendantTracker) clearInterval(descendantTracker);
229
+ signal.removeEventListener("abort", abort);
230
+ if (result.status === "error") reject(result.error);
231
+ else resolve(result.headers);
232
+ };
233
+ const finishAfterKill = (result: CommandResult) => {
234
+ try {
235
+ killRequestHeadersCommand(child, trackedPosixDescendantPids, cleanupToken);
236
+ finish(result);
237
+ } catch (cleanupError) {
238
+ finish({ status: "error", error: cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError)) });
239
+ }
240
+ };
241
+ const failAfterKill = (message: string) => {
242
+ finishAfterKill({ status: "error", error: trackingError ?? new Error(message) });
243
+ };
244
+ const abort = () => {
245
+ failAfterKill("HTTP request headers command aborted");
246
+ };
247
+ const timer = setTimeout(() => {
248
+ failAfterKill(`HTTP request headers command timed out after ${resolved.timeoutMs}ms`);
249
+ }, resolved.timeoutMs);
250
+
251
+ signal.addEventListener("abort", abort, { once: true });
252
+ if (signal.aborted) {
253
+ abort();
254
+ return;
255
+ }
256
+
257
+ child.on("error", () => finish({ status: "error", error: new Error("HTTP request headers command failed to start") }));
258
+ child.stdout.on("data", (chunk: Buffer | string) => {
259
+ if (settled) return;
260
+ stdout = Buffer.concat([stdout, Buffer.from(chunk)]);
261
+ if (stdout.byteLength > MAX_OUTPUT_BYTES) {
262
+ failAfterKill("HTTP request headers command output exceeded 64 KiB");
263
+ }
264
+ });
265
+ child.on("close", code => {
266
+ if (settled) return;
267
+ if (trackingError) {
268
+ failAfterKill(trackingError.message);
269
+ return;
270
+ }
271
+ if (code !== 0) {
272
+ failAfterKill(`HTTP request headers command exited with code ${code ?? "unknown"}`);
273
+ return;
274
+ }
275
+ let parsed: unknown;
276
+ try {
277
+ parsed = JSON.parse(stdout.toString("utf8"));
278
+ } catch {
279
+ finishAfterKill({ status: "error", error: new Error("HTTP request headers command returned invalid JSON") });
280
+ return;
281
+ }
282
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
283
+ finishAfterKill({ status: "error", error: new Error("HTTP request headers command must return a JSON object") });
284
+ return;
285
+ }
286
+ const entries = Object.entries(parsed);
287
+ if (entries.some(([, value]) => typeof value !== "string")) {
288
+ finishAfterKill({ status: "error", error: new Error("HTTP request headers command values must be strings") });
289
+ return;
290
+ }
291
+ try {
292
+ finishAfterKill({ status: "success", headers: new Headers(entries as Array<[string, string]>) });
293
+ } catch {
294
+ finishAfterKill({ status: "error", error: new Error("HTTP request headers command returned an invalid header") });
295
+ }
296
+ });
297
+
298
+ child.stdin.on("error", () => {});
299
+ child.stdin.end(JSON.stringify(envelope));
300
+ });
301
+ }
302
+
303
+ /** Wrap fetch so a trusted command can derive headers from the exact request. */
304
+ export function createRequestHeadersCommandFetch(
305
+ config: HttpRequestHeadersCommand,
306
+ delegate: FetchLike = globalThis.fetch,
307
+ ): FetchLike {
308
+ // Validate static configuration before the first request.
309
+ resolvedCommand(config);
310
+ return async (input: URL | RequestInfo, init?: RequestInit): Promise<Response> => {
311
+ const request = new Request(input, init);
312
+ const body = Buffer.from(await request.clone().arrayBuffer());
313
+ const derived = await invokeRequestHeadersCommand(config, {
314
+ version: 1,
315
+ method: request.method.toUpperCase(),
316
+ url: request.url,
317
+ bodyBase64: body.toString("base64"),
318
+ }, request.signal);
319
+ const headers = new Headers(request.headers);
320
+ derived.forEach((value, name) => headers.set(name, value));
321
+ return delegate(new URL(request.url), {
322
+ method: request.method,
323
+ headers,
324
+ ...(request.method === "GET" || request.method === "HEAD" ? {} : { body }),
325
+ signal: request.signal,
326
+ cache: request.cache,
327
+ credentials: request.credentials,
328
+ integrity: request.integrity,
329
+ keepalive: request.keepalive,
330
+ mode: request.mode,
331
+ redirect: request.redirect,
332
+ referrer: request.referrer,
333
+ referrerPolicy: request.referrerPolicy,
334
+ });
335
+ };
336
+ }
@@ -56,6 +56,7 @@ import {
56
56
  traceTransportKind,
57
57
  wrapTransportWithMcpTrace,
58
58
  } from "./mcp-trace.ts";
59
+ import { createRequestHeadersCommandFetch } from "./request-headers-command.ts";
59
60
 
60
61
  const MAX_CAPTURED_STDERR_BYTES = 8 * 1024;
61
62
  const MAX_CAPTURED_STDERR_LINES = 3;
@@ -746,6 +747,9 @@ export class McpServerManager {
746
747
  }
747
748
 
748
749
  const requestInit = Object.keys(headers).length > 0 ? { headers } : undefined;
750
+ const requestFetch = definition.requestHeadersCommand
751
+ ? createRequestHeadersCommandFetch(definition.requestHeadersCommand)
752
+ : undefined;
749
753
  const createAuthProvider = (): McpOAuthProvider => new McpOAuthProvider(
750
754
  serverName,
751
755
  serverUrl,
@@ -772,6 +776,7 @@ export class McpServerManager {
772
776
  const authProvider = "provider" in authState ? authState.provider : undefined;
773
777
  const transportOptions = {
774
778
  ...(requestInit !== undefined ? { requestInit } : {}),
779
+ ...(requestFetch !== undefined ? { fetch: requestFetch } : {}),
775
780
  ...(authProvider !== undefined ? { authProvider } : {}),
776
781
  ...(authProvider !== undefined
777
782
  && definition.oauth !== false
@@ -1,7 +1,7 @@
1
1
  import { getToolUiResourceUri } from "./ui-app-bridge-helpers.ts";
2
2
  import type { McpExtensionState } from "./state.ts";
3
3
  import type { ToolMetadata, McpTool, McpResource, ServerEntry, ToolPrefix } from "./types.ts";
4
- import { formatToolName, getToolNameCandidates, isToolAllowed, resolveToolPrefix } from "./types.ts";
4
+ import { createToolSelectorCandidateIndex, formatToolName, getToolNameCandidates, isToolAllowed, resolveToolPrefix } from "./types.ts";
5
5
  import { resourceNameToToolName } from "./resource-tools.ts";
6
6
  import { extractToolUiStreamMode } from "./utils.ts";
7
7
  import { extractUiToolVisibility, isUiToolVisibleToModel } from "./ui-tool-visibility.ts";
@@ -20,48 +20,68 @@ export function buildToolMetadata(
20
20
  const failedTools: string[] = [];
21
21
  const seenNames = new Set<string>();
22
22
  const effectivePrefix = resolveToolPrefix(definition, prefix);
23
- const getOtherCurrentCandidates = (toolName: string): Set<string> | undefined => {
24
- if (!configuredServers) return undefined;
23
+ const hasToolFilters =
24
+ (Array.isArray(definition.includeTools) && definition.includeTools.length > 0) ||
25
+ (Array.isArray(definition.excludeTools) && definition.excludeTools.length > 0);
26
+ const selectorCandidateIndex = hasToolFilters && configuredServers ? (() => {
25
27
  const candidates = new Set<string>();
26
- const addCandidates = (originalName: string, candidateServerName: string, candidatePrefix: ToolPrefix) => {
27
- for (const candidate of getToolNameCandidates(originalName, candidateServerName, candidatePrefix, false)) candidates.add(candidate);
28
+ const additionalCandidatesByToolName = new Map<string, Set<string>>();
29
+ const evaluatedToolNames = new Set<string>();
30
+ const addCandidates = (
31
+ target: Set<string>,
32
+ originalName: string,
33
+ candidateServerName: string,
34
+ candidatePrefix: ToolPrefix,
35
+ ) => {
36
+ for (const candidate of getToolNameCandidates(originalName, candidateServerName, candidatePrefix, false)) target.add(candidate);
28
37
  };
29
38
 
30
39
  for (const tool of tools) {
31
- if (tool?.name) addCandidates(tool.name, serverName, effectivePrefix);
40
+ if (!tool?.name) continue;
41
+ evaluatedToolNames.add(tool.name);
42
+ addCandidates(candidates, tool.name, serverName, effectivePrefix);
32
43
  }
33
44
  if (definition.exposeResources !== false) {
34
45
  for (const resource of resources) {
35
- if (resource?.name && resource?.uri) addCandidates(`read_${resourceNameToToolName(resource.name)}`, serverName, effectivePrefix);
46
+ const baseName = `read_${resourceNameToToolName(resource.name)}`;
47
+ evaluatedToolNames.add(baseName);
48
+ if (resource?.name && resource?.uri) addCandidates(candidates, baseName, serverName, effectivePrefix);
36
49
  }
37
50
  }
38
51
  for (const [otherServerName, otherDefinition] of Object.entries(configuredServers)) {
39
52
  if (otherServerName === serverName) continue;
53
+ const otherPrefix = resolveToolPrefix(otherDefinition, prefix);
40
54
  const knownTools = knownMetadata?.get(otherServerName);
41
55
  if (knownTools) {
42
- const otherPrefix = resolveToolPrefix(otherDefinition, prefix);
43
56
  for (const tool of knownTools) {
44
57
  candidates.add(tool.name);
45
- addCandidates(tool.originalName, otherServerName, otherPrefix);
58
+ addCandidates(candidates, tool.originalName, otherServerName, otherPrefix);
46
59
  }
47
60
  } else if (!knownMetadata || includeMissingConfiguredCandidates) {
48
- const otherPrefix = resolveToolPrefix(otherDefinition, prefix);
49
- addCandidates(toolName, otherServerName, otherPrefix);
50
- if (includeMissingConfiguredCandidates) {
51
- for (const candidate of getToolNameCandidates(toolName, otherServerName, otherPrefix, false)) candidates.add(candidate.replace(/-/g, "_"));
61
+ for (const toolName of evaluatedToolNames) {
62
+ let additionalCandidates = additionalCandidatesByToolName.get(toolName);
63
+ if (!additionalCandidates) {
64
+ additionalCandidates = new Set<string>();
65
+ additionalCandidatesByToolName.set(toolName, additionalCandidates);
66
+ }
67
+ addCandidates(additionalCandidates, toolName, otherServerName, otherPrefix);
68
+ if (includeMissingConfiguredCandidates) {
69
+ for (const candidate of getToolNameCandidates(toolName, otherServerName, otherPrefix, false)) {
70
+ additionalCandidates.add(candidate.replace(/-/g, "_"));
71
+ }
72
+ }
52
73
  }
53
74
  }
54
75
  }
55
- for (const candidate of getToolNameCandidates(toolName, serverName, effectivePrefix, false)) candidates.delete(candidate);
56
- return candidates;
57
- };
76
+ return createToolSelectorCandidateIndex(candidates, additionalCandidatesByToolName);
77
+ })() : undefined;
58
78
 
59
79
  for (const tool of tools) {
60
80
  if (!tool?.name) {
61
81
  failedTools.push("(unnamed)");
62
82
  continue;
63
83
  }
64
- if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, getOtherCurrentCandidates(tool.name))) {
84
+ if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex)) {
65
85
  continue;
66
86
  }
67
87
 
@@ -97,7 +117,7 @@ export function buildToolMetadata(
97
117
  if (definition.exposeResources !== false) {
98
118
  for (const resource of resources) {
99
119
  const baseName = `read_${resourceNameToToolName(resource.name)}`;
100
- if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, getOtherCurrentCandidates(baseName))) {
120
+ if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex)) {
101
121
  continue;
102
122
  }
103
123
 
@@ -356,6 +356,19 @@ export interface OAuthConfig {
356
356
  skipIssuerMetadataValidation?: boolean;
357
357
  }
358
358
 
359
+ /**
360
+ * Trusted executable invoked for every outbound HTTP request. The adapter
361
+ * writes a versioned JSON request envelope to stdin and expects a JSON object
362
+ * containing headers on stdout. This is intended for caller-bound signing
363
+ * schemes whose headers depend on the exact request body.
364
+ */
365
+ export interface HttpRequestHeadersCommand {
366
+ command: string;
367
+ args?: string[];
368
+ env?: Record<string, string>;
369
+ timeoutMs?: number;
370
+ }
371
+
359
372
  // Server configuration
360
373
  export interface ServerEntry {
361
374
  command?: string;
@@ -367,6 +380,8 @@ export interface ServerEntry {
367
380
  // HTTP fields
368
381
  url?: string;
369
382
  headers?: Record<string, string>;
383
+ /** Add or replace HTTP headers by running a trusted command for each request. */
384
+ requestHeadersCommand?: HttpRequestHeadersCommand;
370
385
  /**
371
386
  * Authentication type:
372
387
  * - 'oauth' - Use OAuth 2.1 (auto-discovers endpoints, supports dynamic client registration)
@@ -489,6 +504,8 @@ export interface McpSettings {
489
504
  idleTimeout?: number; // minutes, default 10, 0 to disable
490
505
  requestTimeoutMs?: number; // milliseconds, overrides the SDK request timeout when > 0
491
506
  directTools?: boolean;
507
+ /** Show the advisory when 75 or more direct tools resolve. Defaults to true. */
508
+ warnOnLargeDirectTools?: boolean;
492
509
  /** Register the trusted MCP-only JavaScript scripting tool. Defaults to true; set false to hide it. */
493
510
  scriptMode?: boolean;
494
511
  /** Render MCP tool results as compact self-rendered rows by default, or as the legacy boxed row. */
@@ -789,6 +806,25 @@ function globToRegExp(pattern: string): RegExp {
789
806
  return new RegExp(`^${escaped}$`);
790
807
  }
791
808
 
809
+ export interface ToolSelectorCandidateIndex {
810
+ readonly allCurrentCandidates: ReadonlySet<string>;
811
+ readonly matchingCountByPattern: Map<string, number>;
812
+ readonly matcherByPattern: Map<string, RegExp>;
813
+ readonly additionalCurrentCandidatesByToolName?: ReadonlyMap<string, ReadonlySet<string>>;
814
+ }
815
+
816
+ export function createToolSelectorCandidateIndex(
817
+ allCurrentCandidates: Set<string>,
818
+ additionalCurrentCandidatesByToolName?: ReadonlyMap<string, ReadonlySet<string>>,
819
+ ): ToolSelectorCandidateIndex {
820
+ return {
821
+ allCurrentCandidates,
822
+ matchingCountByPattern: new Map<string, number>(),
823
+ matcherByPattern: new Map<string, RegExp>(),
824
+ ...(additionalCurrentCandidatesByToolName ? { additionalCurrentCandidatesByToolName } : {}),
825
+ };
826
+ }
827
+
792
828
  export function matchesToolPattern(candidates: Set<string>, patterns?: unknown): boolean {
793
829
  if (!Array.isArray(patterns) || patterns.length === 0) return false;
794
830
 
@@ -805,24 +841,71 @@ export function matchesToolPattern(candidates: Set<string>, patterns?: unknown):
805
841
  return false;
806
842
  }
807
843
 
844
+ export type ToolSelectorCandidateContext = Set<string> | ToolSelectorCandidateIndex;
845
+
846
+ function indexHasOtherCurrentMatch(
847
+ index: ToolSelectorCandidateIndex,
848
+ toolName: string,
849
+ currentCandidates: Set<string>,
850
+ pattern: string,
851
+ ): boolean {
852
+ const additionalCandidates = index.additionalCurrentCandidatesByToolName?.get(toolName);
853
+ const hasCandidate = (candidate: string): boolean =>
854
+ index.allCurrentCandidates.has(candidate) || additionalCandidates?.has(candidate) === true;
855
+ const isGlob = pattern.includes("*") || pattern.includes("?");
856
+ if (!isGlob) {
857
+ return hasCandidate(pattern) && !currentCandidates.has(pattern);
858
+ }
859
+
860
+ let matcher = index.matcherByPattern.get(pattern);
861
+ if (!matcher) {
862
+ matcher = globToRegExp(pattern);
863
+ index.matcherByPattern.set(pattern, matcher);
864
+ }
865
+ let matchingCount = index.matchingCountByPattern.get(pattern);
866
+ if (matchingCount === undefined) {
867
+ matchingCount = 0;
868
+ for (const candidate of index.allCurrentCandidates) {
869
+ if (matcher.test(candidate)) matchingCount++;
870
+ }
871
+ index.matchingCountByPattern.set(pattern, matchingCount);
872
+ }
873
+
874
+ let totalMatchingCount = matchingCount;
875
+ if (additionalCandidates) {
876
+ for (const candidate of additionalCandidates) {
877
+ if (!index.allCurrentCandidates.has(candidate) && matcher.test(candidate)) totalMatchingCount++;
878
+ }
879
+ }
880
+ if (totalMatchingCount === 0) return false;
881
+
882
+ let currentMatchingCount = 0;
883
+ for (const candidate of currentCandidates) {
884
+ if (hasCandidate(candidate) && matcher.test(candidate)) currentMatchingCount++;
885
+ }
886
+ return totalMatchingCount > currentMatchingCount;
887
+ }
888
+
808
889
  function matchesToolSelector(
809
890
  toolName: string,
810
891
  serverName: string,
811
892
  prefix: ToolPrefix,
812
893
  patterns: unknown,
813
- otherCurrentCandidates?: Set<string>,
894
+ otherCurrentCandidates?: ToolSelectorCandidateContext,
814
895
  ): boolean {
896
+ if (!Array.isArray(patterns) || patterns.length === 0) return false;
815
897
  const currentCandidates = getToolNameCandidates(toolName, serverName, prefix, false);
816
898
  if (matchesToolPattern(currentCandidates, patterns)) return true;
817
899
  if (!otherCurrentCandidates) return matchesToolPattern(getToolNameCandidates(toolName, serverName, prefix), patterns);
818
900
  const legacyCandidates = getToolNameCandidates(toolName, serverName, prefix);
819
901
  for (const candidate of currentCandidates) legacyCandidates.delete(candidate);
820
- if (!Array.isArray(patterns)) return false;
821
- return patterns.some(pattern =>
822
- typeof pattern === "string"
823
- && !matchesToolPattern(otherCurrentCandidates, [pattern])
824
- && matchesToolPattern(legacyCandidates, [pattern]),
825
- );
902
+ return patterns.some(pattern => {
903
+ if (typeof pattern !== "string" || !matchesToolPattern(legacyCandidates, [pattern])) return false;
904
+ const hasCollision = otherCurrentCandidates instanceof Set
905
+ ? matchesToolPattern(otherCurrentCandidates, [pattern])
906
+ : indexHasOtherCurrentMatch(otherCurrentCandidates, toolName, currentCandidates, pattern);
907
+ return !hasCollision;
908
+ });
826
909
  }
827
910
 
828
911
  export function isToolIncluded(
@@ -830,7 +913,7 @@ export function isToolIncluded(
830
913
  serverName: string,
831
914
  prefix: ToolPrefix,
832
915
  includeTools?: unknown,
833
- otherCurrentCandidates?: Set<string>,
916
+ otherCurrentCandidates?: ToolSelectorCandidateContext,
834
917
  ): boolean {
835
918
  if (!Array.isArray(includeTools) || includeTools.length === 0) return true;
836
919
  return matchesToolSelector(toolName, serverName, prefix, includeTools, otherCurrentCandidates);
@@ -841,7 +924,7 @@ export function isToolExcluded(
841
924
  serverName: string,
842
925
  prefix: ToolPrefix,
843
926
  excludeTools?: unknown,
844
- otherCurrentCandidates?: Set<string>,
927
+ otherCurrentCandidates?: ToolSelectorCandidateContext,
845
928
  ): boolean {
846
929
  return matchesToolSelector(toolName, serverName, prefix, excludeTools, otherCurrentCandidates);
847
930
  }
@@ -852,7 +935,7 @@ export function isToolAllowed(
852
935
  prefix: ToolPrefix,
853
936
  includeTools?: unknown,
854
937
  excludeTools?: unknown,
855
- otherCurrentCandidates?: Set<string>,
938
+ otherCurrentCandidates?: ToolSelectorCandidateContext,
856
939
  ): boolean {
857
940
  return isToolIncluded(toolName, serverName, prefix, includeTools, otherCurrentCandidates)
858
941
  && !isToolExcluded(toolName, serverName, prefix, excludeTools, otherCurrentCandidates);
@@ -0,0 +1,13 @@
1
+ export type AuthFetchRequest = true | string;
2
+ export type AuthFetchCache = "session" | "off";
3
+ export interface AuthFetchProfile {
4
+ name: string;
5
+ hosts: string[];
6
+ chromeProfile?: string;
7
+ redirects: "same-origin";
8
+ cache: AuthFetchCache;
9
+ }
10
+ export declare function resolveAuthFetchProfile(request: AuthFetchRequest): AuthFetchProfile;
11
+ export declare function assertAuthFetchUrl(profile: AuthFetchProfile, rawUrl: string): URL;
12
+ export declare function authFetchRedirectGuard(profile: AuthFetchProfile, from: URL, to: URL): void;
13
+ //# sourceMappingURL=auth-fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-fetch.d.ts","sourceRoot":"","sources":["../../../../src/humain/web-access/vendor/auth-fetch.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG,MAAM,CAAC;AAC7C,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,KAAK,CAAC;AAE/C,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,aAAa,CAAC;IACzB,KAAK,EAAE,cAAc,CAAC;CACtB;AAMD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,gBAAgB,GAAG,gBAAgB,CAenF;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,CAcjF;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,GAAG,IAAI,CAI1F","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { getWebSearchConfigPath } from \"./utils.ts\";\n\nconst WEB_SEARCH_CONFIG_PATH = getWebSearchConfigPath();\nconst AUTH_PROFILE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;\n\nexport type AuthFetchRequest = true | string;\nexport type AuthFetchCache = \"session\" | \"off\";\n\nexport interface AuthFetchProfile {\n\tname: string;\n\thosts: string[];\n\tchromeProfile?: string;\n\tredirects: \"same-origin\";\n\tcache: AuthFetchCache;\n}\n\ninterface AuthFetchConfigRoot {\n\tauthFetch?: unknown;\n}\n\nexport function resolveAuthFetchProfile(request: AuthFetchRequest): AuthFetchProfile {\n\tconst profiles = loadAuthFetchProfiles();\n\tif (profiles.length === 0) {\n\t\tthrow new Error(`auth requires at least one authFetch profile in ${WEB_SEARCH_CONFIG_PATH}`);\n\t}\n\tif (request === true) {\n\t\tif (profiles.length !== 1) {\n\t\t\tthrow new Error(\"auth: true requires exactly one authFetch profile; use a profile name instead\");\n\t\t}\n\t\treturn profiles[0];\n\t}\n\tconst name = request.trim();\n\tconst profile = profiles.find(candidate => candidate.name === name);\n\tif (!profile) throw new Error(`Unknown authFetch profile: ${name}`);\n\treturn profile;\n}\n\nexport function assertAuthFetchUrl(profile: AuthFetchProfile, rawUrl: string): URL {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(rawUrl);\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tthrow new Error(`Authenticated fetch requires an HTTPS URL: ${message}`);\n\t}\n\tif (url.protocol !== \"https:\") throw new Error(\"Authenticated fetch requires an HTTPS URL\");\n\tconst hostname = normalizeHostname(url.hostname);\n\tif (!profile.hosts.some(host => hostMatches(hostname, host))) {\n\t\tthrow new Error(`URL host ${hostname} is not allowed by authFetch profile ${profile.name}`);\n\t}\n\treturn url;\n}\n\nexport function authFetchRedirectGuard(profile: AuthFetchProfile, from: URL, to: URL): void {\n\tif (profile.redirects === \"same-origin\" && to.origin !== from.origin) {\n\t\tthrow new Error(`Authenticated fetch refused cross-origin redirect: ${from.origin} -> ${to.origin}`);\n\t}\n}\n\nfunction loadAuthFetchProfiles(): AuthFetchProfile[] {\n\tif (!existsSync(WEB_SEARCH_CONFIG_PATH)) return [];\n\tconst raw = readFileSync(WEB_SEARCH_CONFIG_PATH, \"utf-8\");\n\tlet parsed: AuthFetchConfigRoot;\n\ttry {\n\t\tconst value: unknown = JSON.parse(raw);\n\t\tif (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"expected a JSON object\");\n\t\tparsed = value as AuthFetchConfigRoot;\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tthrow new Error(`Failed to parse ${WEB_SEARCH_CONFIG_PATH}: ${message}`);\n\t}\n\tif (parsed.authFetch === undefined || parsed.authFetch === null) return [];\n\tif (typeof parsed.authFetch !== \"object\" || Array.isArray(parsed.authFetch)) {\n\t\tthrow new Error(`authFetch in ${WEB_SEARCH_CONFIG_PATH} must be an object`);\n\t}\n\treturn Object.entries(parsed.authFetch as Record<string, unknown>).map(([name, value]) => parseProfile(name, value));\n}\n\nfunction parseProfile(name: string, value: unknown): AuthFetchProfile {\n\tif (!AUTH_PROFILE_NAME_PATTERN.test(name)) {\n\t\tthrow new Error(`authFetch profile name ${JSON.stringify(name)} must start with a letter and contain only letters, numbers, underscores, or hyphens`);\n\t}\n\tif (Array.isArray(value)) {\n\t\treturn { name, hosts: parseHosts(value, `authFetch.${name}`), redirects: \"same-origin\", cache: \"session\" };\n\t}\n\tif (!value || typeof value !== \"object\") {\n\t\tthrow new Error(`authFetch.${name} in ${WEB_SEARCH_CONFIG_PATH} must be an array of hosts or an object`);\n\t}\n\tconst config = value as Record<string, unknown>;\n\tif (!Array.isArray(config.hosts)) {\n\t\tthrow new Error(`authFetch.${name}.hosts in ${WEB_SEARCH_CONFIG_PATH} must be a non-empty array of hostnames`);\n\t}\n\tconst redirects = config.redirects ?? \"same-origin\";\n\tif (redirects !== \"same-origin\") {\n\t\tthrow new Error(`authFetch.${name}.redirects in ${WEB_SEARCH_CONFIG_PATH} must be \"same-origin\"`);\n\t}\n\tconst cache = config.cache ?? \"session\";\n\tif (cache !== \"session\" && cache !== \"off\") {\n\t\tthrow new Error(`authFetch.${name}.cache in ${WEB_SEARCH_CONFIG_PATH} must be \"session\" or \"off\"`);\n\t}\n\tconst chromeProfile = parseChromeProfile(config.chromeProfile, `authFetch.${name}.chromeProfile`);\n\treturn {\n\t\tname,\n\t\thosts: parseHosts(config.hosts, `authFetch.${name}.hosts`),\n\t\t...(chromeProfile ? { chromeProfile } : {}),\n\t\tredirects,\n\t\tcache,\n\t};\n}\n\nfunction parseHosts(value: unknown[], label: string): string[] {\n\tif (value.length === 0) throw new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} must be a non-empty array of hostnames`);\n\tconst hosts = value.map((entry) => {\n\t\tif (typeof entry !== \"string\") throw new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} must contain only hostnames`);\n\t\treturn parseHost(entry, label);\n\t});\n\treturn [...new Set(hosts)];\n}\n\nfunction parseHost(value: string, label: string): string {\n\tconst host = normalizeHostname(value.trim());\n\tif (!host || host.startsWith(\".\") || host.endsWith(\".\") || /\\s|[\\\\/?:#@*]/.test(host)) {\n\t\tthrow new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} contains an invalid hostname: ${JSON.stringify(value)}`);\n\t}\n\tif (host.length > 253 || !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(host)) {\n\t\tthrow new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} contains an invalid hostname: ${JSON.stringify(value)}`);\n\t}\n\treturn host;\n}\n\nfunction parseChromeProfile(value: unknown, label: string): string | undefined {\n\tif (value === undefined || value === null) return undefined;\n\tif (typeof value !== \"string\") throw new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} must be a string`);\n\tconst normalized = value.trim();\n\tif (!normalized || normalized === \".\" || normalized === \"..\" || normalized.includes(\"/\") || normalized.includes(\"\\\\\")) {\n\t\tthrow new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} must be a profile directory name, not a path`);\n\t}\n\treturn normalized;\n}\n\nfunction normalizeHostname(hostname: string): string {\n\treturn hostname.toLowerCase().replace(/^\\[|\\]$/g, \"\").replace(/\\.$/, \"\");\n}\n\nfunction hostMatches(hostname: string, allowedHost: string): boolean {\n\treturn hostname === allowedHost || hostname.endsWith(`.${allowedHost}`);\n}\n"]}