tare-mcp 0.2.0
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/LICENSE +21 -0
- package/README.md +668 -0
- package/dist/cli.js +2401 -0
- package/dist/index.js +2199 -0
- package/examples/live-stdio/.mcp.json +8 -0
- package/examples/live-stdio/README.md +28 -0
- package/examples/live-stdio/server.mjs +58 -0
- package/examples/live-streamable-http/.mcp.json +7 -0
- package/examples/live-streamable-http/README.md +40 -0
- package/examples/live-streamable-http/server.mjs +106 -0
- package/examples/scenarios/README.md +44 -0
- package/examples/scenarios/hosted-streamable-http.mcp.json +11 -0
- package/examples/scenarios/local-stdio.mcp.json +8 -0
- package/examples/stdio.mcp.json +11 -0
- package/examples/streamable-http.mcp.json +10 -0
- package/package.json +70 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/version.ts
|
|
4
|
+
var VERSION = "0.2.0";
|
|
5
|
+
|
|
6
|
+
// src/discovery/discoverConfigs.ts
|
|
7
|
+
import os2 from "os";
|
|
8
|
+
import path2 from "path";
|
|
9
|
+
import fg from "fast-glob";
|
|
10
|
+
|
|
11
|
+
// src/utils/fs.ts
|
|
12
|
+
import { access, readFile } from "fs/promises";
|
|
13
|
+
import os from "os";
|
|
14
|
+
import path from "path";
|
|
15
|
+
function expandHome(input, home = os.homedir()) {
|
|
16
|
+
if (input === "~") {
|
|
17
|
+
return home;
|
|
18
|
+
}
|
|
19
|
+
if (input.startsWith("~/")) {
|
|
20
|
+
return path.join(home, input.slice(2));
|
|
21
|
+
}
|
|
22
|
+
return input;
|
|
23
|
+
}
|
|
24
|
+
async function pathExists(filePath) {
|
|
25
|
+
try {
|
|
26
|
+
await access(filePath);
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function readUtf8(filePath) {
|
|
33
|
+
return readFile(filePath, "utf8");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/discovery/discoverConfigs.ts
|
|
37
|
+
var LOCAL_CONFIG_PATTERNS = [".mcp.json", "mcp.json", ".cursor/mcp.json", ".vscode/mcp.json"];
|
|
38
|
+
var HOME_CONFIG_PATTERNS = [
|
|
39
|
+
"~/.claude/mcp.json",
|
|
40
|
+
"~/Library/Application Support/Claude/claude_desktop_config.json",
|
|
41
|
+
"~/.config/Claude/claude_desktop_config.json",
|
|
42
|
+
"~/.config/claude/claude_desktop_config.json",
|
|
43
|
+
"~/.config/tare/mcp.json"
|
|
44
|
+
];
|
|
45
|
+
function getDefaultConfigCandidates(cwd = process.cwd(), home = os2.homedir()) {
|
|
46
|
+
return [
|
|
47
|
+
...LOCAL_CONFIG_PATTERNS.map((candidate) => path2.resolve(cwd, candidate)),
|
|
48
|
+
...HOME_CONFIG_PATTERNS.map((candidate) => expandHome(candidate, home))
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
async function discoverConfigs(cwd = process.cwd(), home = os2.homedir()) {
|
|
52
|
+
const warnings = [];
|
|
53
|
+
const localMatches = await fg(LOCAL_CONFIG_PATTERNS, {
|
|
54
|
+
cwd,
|
|
55
|
+
absolute: true,
|
|
56
|
+
onlyFiles: true,
|
|
57
|
+
dot: true,
|
|
58
|
+
unique: true
|
|
59
|
+
});
|
|
60
|
+
const homeCandidates = HOME_CONFIG_PATTERNS.map((candidate) => expandHome(candidate, home));
|
|
61
|
+
const homeMatches = [];
|
|
62
|
+
for (const candidate of homeCandidates) {
|
|
63
|
+
if (await pathExists(candidate)) {
|
|
64
|
+
homeMatches.push(candidate);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const paths = [.../* @__PURE__ */ new Set([...localMatches, ...homeMatches])].sort();
|
|
68
|
+
return { paths, warnings };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/discovery/parseConfig.ts
|
|
72
|
+
import { z as z2 } from "zod";
|
|
73
|
+
|
|
74
|
+
// src/discovery/normalizeServer.ts
|
|
75
|
+
import { z } from "zod";
|
|
76
|
+
var ServerConfigSchema = z.record(z.string(), z.unknown());
|
|
77
|
+
function readStringArray(value) {
|
|
78
|
+
if (!Array.isArray(value)) {
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
const strings = value.filter((entry) => typeof entry === "string");
|
|
82
|
+
return strings.length === value.length ? strings : void 0;
|
|
83
|
+
}
|
|
84
|
+
function readStringRecord(value) {
|
|
85
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
86
|
+
return void 0;
|
|
87
|
+
}
|
|
88
|
+
const entries = Object.entries(value);
|
|
89
|
+
const strings = entries.filter(
|
|
90
|
+
(entry) => typeof entry[1] === "string"
|
|
91
|
+
);
|
|
92
|
+
return strings.length === entries.length ? Object.fromEntries(strings) : void 0;
|
|
93
|
+
}
|
|
94
|
+
function inferTransport(config) {
|
|
95
|
+
const explicit = String(config.transport ?? config.type ?? "").toLowerCase();
|
|
96
|
+
if (explicit.includes("sse")) {
|
|
97
|
+
return "sse";
|
|
98
|
+
}
|
|
99
|
+
if (typeof config.command === "string") {
|
|
100
|
+
return "stdio";
|
|
101
|
+
}
|
|
102
|
+
if (typeof config.url === "string") {
|
|
103
|
+
return "streamable-http";
|
|
104
|
+
}
|
|
105
|
+
return "unknown";
|
|
106
|
+
}
|
|
107
|
+
function normalizeServer(name, rawConfig, sourceConfigPath) {
|
|
108
|
+
const parsed = ServerConfigSchema.safeParse(rawConfig);
|
|
109
|
+
if (!parsed.success) {
|
|
110
|
+
return {
|
|
111
|
+
warnings: [`${sourceConfigPath}: server "${name}" is malformed and was skipped.`]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const config = parsed.data;
|
|
115
|
+
const warnings = [];
|
|
116
|
+
const command = typeof config.command === "string" ? config.command : void 0;
|
|
117
|
+
const url = typeof config.url === "string" ? config.url : void 0;
|
|
118
|
+
const args = readStringArray(config.args);
|
|
119
|
+
const env = readStringRecord(config.env);
|
|
120
|
+
const headers = readStringRecord(config.headers);
|
|
121
|
+
if (config.args !== void 0 && !args) {
|
|
122
|
+
warnings.push(
|
|
123
|
+
`${sourceConfigPath}: server "${name}" has non-string args and they were ignored.`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (config.env !== void 0 && !env) {
|
|
127
|
+
warnings.push(
|
|
128
|
+
`${sourceConfigPath}: server "${name}" has non-string env values and env was ignored.`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (config.headers !== void 0 && !headers) {
|
|
132
|
+
warnings.push(
|
|
133
|
+
`${sourceConfigPath}: server "${name}" has non-string headers and headers were ignored.`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (!command && !url) {
|
|
137
|
+
warnings.push(
|
|
138
|
+
`${sourceConfigPath}: server "${name}" has no command or url; transport is unknown.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
server: {
|
|
143
|
+
name,
|
|
144
|
+
command,
|
|
145
|
+
args,
|
|
146
|
+
env,
|
|
147
|
+
url,
|
|
148
|
+
headers,
|
|
149
|
+
disabled: config.disabled === true,
|
|
150
|
+
sourceConfigPath,
|
|
151
|
+
transport: inferTransport(config)
|
|
152
|
+
},
|
|
153
|
+
warnings
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/discovery/parseConfig.ts
|
|
158
|
+
var ObjectSchema = z2.record(z2.string(), z2.unknown());
|
|
159
|
+
function readServerMap(raw, keyPath) {
|
|
160
|
+
const segments = keyPath.split(".");
|
|
161
|
+
let current = raw;
|
|
162
|
+
for (const segment of segments) {
|
|
163
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
164
|
+
return void 0;
|
|
165
|
+
}
|
|
166
|
+
current = current[segment];
|
|
167
|
+
}
|
|
168
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
169
|
+
return void 0;
|
|
170
|
+
}
|
|
171
|
+
return current;
|
|
172
|
+
}
|
|
173
|
+
function parseConfigText(text, sourceConfigPath) {
|
|
174
|
+
let raw;
|
|
175
|
+
try {
|
|
176
|
+
raw = JSON.parse(text);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
return {
|
|
179
|
+
path: sourceConfigPath,
|
|
180
|
+
servers: [],
|
|
181
|
+
warnings: [`${sourceConfigPath}: malformed JSON (${error.message}).`]
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const parsed = ObjectSchema.safeParse(raw);
|
|
185
|
+
if (!parsed.success) {
|
|
186
|
+
return {
|
|
187
|
+
path: sourceConfigPath,
|
|
188
|
+
servers: [],
|
|
189
|
+
warnings: [`${sourceConfigPath}: config root must be a JSON object.`]
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const config = parsed.data;
|
|
193
|
+
const maps = [
|
|
194
|
+
readServerMap(config, "mcpServers"),
|
|
195
|
+
readServerMap(config, "servers"),
|
|
196
|
+
readServerMap(config, "mcp.servers")
|
|
197
|
+
].filter((entry) => Boolean(entry));
|
|
198
|
+
const servers = [];
|
|
199
|
+
const warnings = [];
|
|
200
|
+
const seen = /* @__PURE__ */ new Set();
|
|
201
|
+
for (const map of maps) {
|
|
202
|
+
for (const [name, serverConfig] of Object.entries(map)) {
|
|
203
|
+
if (seen.has(name)) {
|
|
204
|
+
warnings.push(`${sourceConfigPath}: duplicate server "${name}" was ignored.`);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const normalized = normalizeServer(name, serverConfig, sourceConfigPath);
|
|
208
|
+
warnings.push(...normalized.warnings);
|
|
209
|
+
if (normalized.server) {
|
|
210
|
+
seen.add(name);
|
|
211
|
+
servers.push(normalized.server);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (maps.length === 0) {
|
|
216
|
+
warnings.push(`${sourceConfigPath}: no MCP server map found.`);
|
|
217
|
+
}
|
|
218
|
+
return { path: sourceConfigPath, servers, warnings };
|
|
219
|
+
}
|
|
220
|
+
async function parseConfigFile(filePath) {
|
|
221
|
+
return parseConfigText(await readUtf8(filePath), filePath);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/inspectors/types.ts
|
|
225
|
+
function toReportTransport(transport) {
|
|
226
|
+
if (transport === "stdio" || transport === "streamable-http" || transport === "sse") {
|
|
227
|
+
return transport;
|
|
228
|
+
}
|
|
229
|
+
if (transport === "http") {
|
|
230
|
+
return "streamable-http";
|
|
231
|
+
}
|
|
232
|
+
return "unknown";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/inspectors/staticInspector.ts
|
|
236
|
+
function urlHost(url) {
|
|
237
|
+
if (!url) {
|
|
238
|
+
return void 0;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
return new URL(url).host;
|
|
242
|
+
} catch {
|
|
243
|
+
return void 0;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function createStaticInspection(server, mode = "static-insufficient", warnings) {
|
|
247
|
+
const defaultWarnings = mode === "static-insufficient" ? [
|
|
248
|
+
"Static inspection only sees MCP config, not exposed tool definitions.",
|
|
249
|
+
"Run without --no-exec to inspect actual tool schemas."
|
|
250
|
+
] : [
|
|
251
|
+
"Live inspection failed. Server may require credentials, authorization, or failed during startup.",
|
|
252
|
+
"Static fallback cannot see actual tool definitions.",
|
|
253
|
+
"Run again after configuring credentials or headers."
|
|
254
|
+
];
|
|
255
|
+
return {
|
|
256
|
+
name: server.name,
|
|
257
|
+
sourceConfigPath: server.sourceConfigPath,
|
|
258
|
+
transport: toReportTransport(server.transport),
|
|
259
|
+
command: server.command,
|
|
260
|
+
args: server.args,
|
|
261
|
+
urlHost: urlHost(server.url),
|
|
262
|
+
toolDefinitions: [],
|
|
263
|
+
inspectionMode: mode,
|
|
264
|
+
confidence: "low",
|
|
265
|
+
warnings: warnings ?? defaultWarnings
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/inspectors/stdioMcpInspector.ts
|
|
270
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
271
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
272
|
+
|
|
273
|
+
// src/utils/redact.ts
|
|
274
|
+
var SECRET_KEY_PATTERN = /(authorization|api[_-]?key|token|secret|password|credential|bearer)/i;
|
|
275
|
+
function collectSecretValues(...records) {
|
|
276
|
+
const values = /* @__PURE__ */ new Set();
|
|
277
|
+
for (const record of records) {
|
|
278
|
+
for (const value of Object.values(record ?? {})) {
|
|
279
|
+
if (value && value.length > 2) {
|
|
280
|
+
values.add(value);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
285
|
+
if (value && value.length > 2 && SECRET_KEY_PATTERN.test(key)) {
|
|
286
|
+
values.add(value);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return [...values].sort((a, b) => b.length - a.length);
|
|
290
|
+
}
|
|
291
|
+
function redactText(input, secrets = []) {
|
|
292
|
+
let text = input instanceof Error ? input.message : String(input ?? "");
|
|
293
|
+
for (const secret of secrets) {
|
|
294
|
+
text = text.split(secret).join("[REDACTED]");
|
|
295
|
+
}
|
|
296
|
+
text = text.replace(/(Authorization\s*:\s*)([^\r\n]+)/gi, "$1[REDACTED]");
|
|
297
|
+
text = text.replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, "$1[REDACTED]");
|
|
298
|
+
text = text.replace(
|
|
299
|
+
/((?:api[_-]?key|token|secret|password)\s*[=:]\s*)[^\s,;]+/gi,
|
|
300
|
+
"$1[REDACTED]"
|
|
301
|
+
);
|
|
302
|
+
return text;
|
|
303
|
+
}
|
|
304
|
+
function isLikelyCredentialError(text) {
|
|
305
|
+
return /(auth|credential|unauthori[sz]ed|forbidden|api key|token|permission|access denied)/i.test(
|
|
306
|
+
text
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/utils/timeout.ts
|
|
311
|
+
var TimeoutError = class extends Error {
|
|
312
|
+
constructor(message, timeoutMs) {
|
|
313
|
+
super(message);
|
|
314
|
+
this.timeoutMs = timeoutMs;
|
|
315
|
+
this.name = "TimeoutError";
|
|
316
|
+
}
|
|
317
|
+
timeoutMs;
|
|
318
|
+
};
|
|
319
|
+
async function withTimeout(work, timeoutMs, onTimeout) {
|
|
320
|
+
let timeout;
|
|
321
|
+
let didTimeout = false;
|
|
322
|
+
let cleanup;
|
|
323
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
324
|
+
timeout = setTimeout(() => {
|
|
325
|
+
didTimeout = true;
|
|
326
|
+
cleanup = Promise.resolve(onTimeout?.()).then(() => void 0);
|
|
327
|
+
reject(new TimeoutError(`Timed out after ${timeoutMs}ms`, timeoutMs));
|
|
328
|
+
}, timeoutMs);
|
|
329
|
+
});
|
|
330
|
+
try {
|
|
331
|
+
return await Promise.race([work, timeoutPromise]);
|
|
332
|
+
} finally {
|
|
333
|
+
if (timeout) {
|
|
334
|
+
clearTimeout(timeout);
|
|
335
|
+
}
|
|
336
|
+
if (didTimeout) {
|
|
337
|
+
await cleanup?.catch(() => void 0);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function formatTimeout(timeoutMs) {
|
|
342
|
+
if (timeoutMs % 1e3 === 0) {
|
|
343
|
+
return `${timeoutMs / 1e3}s`;
|
|
344
|
+
}
|
|
345
|
+
return `${timeoutMs}ms`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/inspectors/stdioMcpInspector.ts
|
|
349
|
+
var EXCLUDED_TARE_ENV = /* @__PURE__ */ new Set([
|
|
350
|
+
"ANTHROPIC_API_KEY",
|
|
351
|
+
"TARE_CLAUDE_TOKENIZER",
|
|
352
|
+
"TARE_ANTHROPIC_MODEL",
|
|
353
|
+
"TARE_DISABLE_ANTHROPIC_TOKEN_API"
|
|
354
|
+
]);
|
|
355
|
+
function buildServerEnv(server) {
|
|
356
|
+
const base = Object.fromEntries(
|
|
357
|
+
Object.entries(process.env).filter(([key]) => !EXCLUDED_TARE_ENV.has(key)).filter((entry) => entry[1] !== void 0)
|
|
358
|
+
);
|
|
359
|
+
return { ...base, ...server.env ?? {} };
|
|
360
|
+
}
|
|
361
|
+
function normalizeTool(tool) {
|
|
362
|
+
return {
|
|
363
|
+
name: tool.name,
|
|
364
|
+
description: tool.description,
|
|
365
|
+
inputSchema: tool.inputSchema,
|
|
366
|
+
annotations: tool.annotations,
|
|
367
|
+
outputSchema: tool.outputSchema,
|
|
368
|
+
metadata: tool.metadata ?? tool._meta
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
async function listAllTools(client) {
|
|
372
|
+
const tools = [];
|
|
373
|
+
let cursor;
|
|
374
|
+
do {
|
|
375
|
+
const result = await client.listTools(cursor ? { cursor } : void 0);
|
|
376
|
+
tools.push(...result.tools.map(normalizeTool));
|
|
377
|
+
cursor = result.nextCursor;
|
|
378
|
+
} while (cursor);
|
|
379
|
+
return tools;
|
|
380
|
+
}
|
|
381
|
+
function failureWarnings(server, error, timeoutMs, stderr, secrets) {
|
|
382
|
+
const safeError = redactText(error, secrets);
|
|
383
|
+
const safeStderr = redactText(stderr, secrets).trim();
|
|
384
|
+
const combined = `${safeError}
|
|
385
|
+
${safeStderr}`;
|
|
386
|
+
if (error instanceof TimeoutError) {
|
|
387
|
+
return [
|
|
388
|
+
`live inspection failed after ${formatTimeout(timeoutMs)}.`,
|
|
389
|
+
"Static config only sees command/args, not actual tool schemas.",
|
|
390
|
+
"Token estimate for this server is insufficient."
|
|
391
|
+
];
|
|
392
|
+
}
|
|
393
|
+
if (isLikelyCredentialError(combined)) {
|
|
394
|
+
const warnings = [
|
|
395
|
+
"live inspection failed.",
|
|
396
|
+
"The server may require credentials or failed during startup.",
|
|
397
|
+
"Static config only sees command/args, not actual tool schemas.",
|
|
398
|
+
"Token estimate for this server is insufficient."
|
|
399
|
+
];
|
|
400
|
+
const detail2 = safeStderr || safeError;
|
|
401
|
+
if (detail2) {
|
|
402
|
+
warnings.push(`Sanitized error: ${detail2}`);
|
|
403
|
+
}
|
|
404
|
+
return warnings;
|
|
405
|
+
}
|
|
406
|
+
const detail = safeStderr || safeError;
|
|
407
|
+
return [
|
|
408
|
+
"live inspection failed.",
|
|
409
|
+
"The server may require credentials or failed during startup.",
|
|
410
|
+
"Static config only sees command/args, not actual tool schemas.",
|
|
411
|
+
"Token estimate for this server is insufficient.",
|
|
412
|
+
detail ? `Sanitized error: ${detail}` : `${server.name}: no error detail was available.`
|
|
413
|
+
];
|
|
414
|
+
}
|
|
415
|
+
async function inspectStdioServer(server, options) {
|
|
416
|
+
if (!server.command) {
|
|
417
|
+
return createStaticInspection(server, "fallback-static-insufficient", [
|
|
418
|
+
"live inspection failed.",
|
|
419
|
+
"Stdio server config has no command.",
|
|
420
|
+
"Static config only sees command/args, not actual tool schemas.",
|
|
421
|
+
"Token estimate for this server is insufficient."
|
|
422
|
+
]);
|
|
423
|
+
}
|
|
424
|
+
const secrets = collectSecretValues(server.env);
|
|
425
|
+
const transport = new StdioClientTransport({
|
|
426
|
+
command: server.command,
|
|
427
|
+
args: server.args ?? [],
|
|
428
|
+
env: buildServerEnv(server),
|
|
429
|
+
stderr: "pipe"
|
|
430
|
+
});
|
|
431
|
+
const client = new Client({ name: "tare-mcp", version: VERSION }, { capabilities: {} });
|
|
432
|
+
let stderr = "";
|
|
433
|
+
transport.stderr?.on("data", (chunk) => {
|
|
434
|
+
stderr += chunk.toString();
|
|
435
|
+
});
|
|
436
|
+
const close = async () => {
|
|
437
|
+
await client.close().catch(() => void 0);
|
|
438
|
+
await transport.close().catch(() => void 0);
|
|
439
|
+
};
|
|
440
|
+
try {
|
|
441
|
+
const tools = await withTimeout(
|
|
442
|
+
(async () => {
|
|
443
|
+
await client.connect(transport);
|
|
444
|
+
return listAllTools(client);
|
|
445
|
+
})(),
|
|
446
|
+
options.timeoutMs,
|
|
447
|
+
close
|
|
448
|
+
);
|
|
449
|
+
await close();
|
|
450
|
+
return {
|
|
451
|
+
name: server.name,
|
|
452
|
+
sourceConfigPath: server.sourceConfigPath,
|
|
453
|
+
transport: "stdio",
|
|
454
|
+
command: server.command,
|
|
455
|
+
args: server.args,
|
|
456
|
+
toolDefinitions: tools,
|
|
457
|
+
inspectionMode: "live",
|
|
458
|
+
confidence: "high",
|
|
459
|
+
warnings: []
|
|
460
|
+
};
|
|
461
|
+
} catch (error) {
|
|
462
|
+
await close();
|
|
463
|
+
return createStaticInspection(
|
|
464
|
+
server,
|
|
465
|
+
"fallback-static-insufficient",
|
|
466
|
+
failureWarnings(server, error, options.timeoutMs, stderr, secrets)
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/inspectors/streamableHttpMcpInspector.ts
|
|
472
|
+
import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
|
|
473
|
+
import {
|
|
474
|
+
StreamableHTTPClientTransport,
|
|
475
|
+
StreamableHTTPError
|
|
476
|
+
} from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
477
|
+
|
|
478
|
+
// src/utils/envInterpolation.ts
|
|
479
|
+
var ENV_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
480
|
+
function resolveHeaderEnv(headers, env = process.env) {
|
|
481
|
+
const resolved = {};
|
|
482
|
+
const missing = /* @__PURE__ */ new Set();
|
|
483
|
+
for (const [key, value] of Object.entries(headers ?? {})) {
|
|
484
|
+
resolved[key] = value.replace(ENV_PATTERN, (_match, name) => {
|
|
485
|
+
const replacement = env[name];
|
|
486
|
+
if (!replacement) {
|
|
487
|
+
missing.add(name);
|
|
488
|
+
return "";
|
|
489
|
+
}
|
|
490
|
+
return replacement;
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
if (missing.size > 0) {
|
|
494
|
+
return { ok: false, headers: resolved, missing: [...missing] };
|
|
495
|
+
}
|
|
496
|
+
return { ok: true, headers: resolved, missing: [] };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/inspectors/streamableHttpMcpInspector.ts
|
|
500
|
+
function normalizeTool2(tool) {
|
|
501
|
+
return {
|
|
502
|
+
name: tool.name,
|
|
503
|
+
description: tool.description,
|
|
504
|
+
inputSchema: tool.inputSchema,
|
|
505
|
+
annotations: tool.annotations,
|
|
506
|
+
outputSchema: tool.outputSchema,
|
|
507
|
+
metadata: tool.metadata ?? tool._meta
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
async function listAllTools2(client) {
|
|
511
|
+
const tools = [];
|
|
512
|
+
let cursor;
|
|
513
|
+
do {
|
|
514
|
+
const result = await client.listTools(cursor ? { cursor } : void 0);
|
|
515
|
+
tools.push(...result.tools.map(normalizeTool2));
|
|
516
|
+
cursor = result.nextCursor;
|
|
517
|
+
} while (cursor);
|
|
518
|
+
return tools;
|
|
519
|
+
}
|
|
520
|
+
function errorStatus(error) {
|
|
521
|
+
if (error instanceof StreamableHTTPError) {
|
|
522
|
+
return error.code;
|
|
523
|
+
}
|
|
524
|
+
if (error && typeof error === "object" && "status" in error) {
|
|
525
|
+
const status = Number(error.status);
|
|
526
|
+
return Number.isFinite(status) ? status : void 0;
|
|
527
|
+
}
|
|
528
|
+
const text = error instanceof Error ? error.message : String(error ?? "");
|
|
529
|
+
const match = text.match(/\b(401|403|404|405|415|500|502|503|504)\b/);
|
|
530
|
+
return match ? Number(match[1]) : void 0;
|
|
531
|
+
}
|
|
532
|
+
function failureWarnings2(error, timeoutMs, secrets) {
|
|
533
|
+
const status = errorStatus(error);
|
|
534
|
+
const safeError = redactText(error, secrets);
|
|
535
|
+
if (error instanceof TimeoutError) {
|
|
536
|
+
return [
|
|
537
|
+
`Streamable HTTP inspection failed after ${formatTimeout(timeoutMs)}.`,
|
|
538
|
+
"The server may require credentials, an Authorization header, or a valid MCP session.",
|
|
539
|
+
"Static config only sees URL/headers metadata, not actual tool schemas.",
|
|
540
|
+
"Token estimate for this server is insufficient."
|
|
541
|
+
];
|
|
542
|
+
}
|
|
543
|
+
if (status === 401 || status === 403) {
|
|
544
|
+
const label = status === 401 ? "401 Unauthorized" : "403 Forbidden";
|
|
545
|
+
return [
|
|
546
|
+
`Streamable HTTP inspection failed with ${label}.`,
|
|
547
|
+
"The server requires valid credentials or Authorization headers.",
|
|
548
|
+
"Static fallback cannot see actual tool schemas."
|
|
549
|
+
];
|
|
550
|
+
}
|
|
551
|
+
if (status === 404 || status === 405 || status === 415) {
|
|
552
|
+
return [
|
|
553
|
+
`Streamable HTTP inspection failed with ${status}.`,
|
|
554
|
+
"The URL may not be the MCP endpoint or may use a different transport.",
|
|
555
|
+
"Static fallback cannot see actual tool schemas."
|
|
556
|
+
];
|
|
557
|
+
}
|
|
558
|
+
return [
|
|
559
|
+
"Streamable HTTP inspection failed.",
|
|
560
|
+
"The server may require credentials, an Authorization header, or a valid MCP session.",
|
|
561
|
+
"Static config only sees URL/headers metadata, not actual tool schemas.",
|
|
562
|
+
"Token estimate for this server is insufficient.",
|
|
563
|
+
safeError ? `Sanitized error: ${safeError}` : "No error detail was available."
|
|
564
|
+
];
|
|
565
|
+
}
|
|
566
|
+
async function inspectStreamableHttpServer(server, options) {
|
|
567
|
+
if (!server.url) {
|
|
568
|
+
return createStaticInspection(server, "fallback-static-insufficient", [
|
|
569
|
+
"Streamable HTTP inspection failed.",
|
|
570
|
+
"HTTP server config has no url.",
|
|
571
|
+
"Static config only sees URL/headers metadata, not actual tool schemas.",
|
|
572
|
+
"Token estimate for this server is insufficient."
|
|
573
|
+
]);
|
|
574
|
+
}
|
|
575
|
+
let url;
|
|
576
|
+
try {
|
|
577
|
+
url = new URL(server.url);
|
|
578
|
+
} catch {
|
|
579
|
+
return createStaticInspection(server, "fallback-static-insufficient", [
|
|
580
|
+
"Streamable HTTP inspection failed.",
|
|
581
|
+
"HTTP server config has an invalid url.",
|
|
582
|
+
"Static config only sees URL/headers metadata, not actual tool schemas.",
|
|
583
|
+
"Token estimate for this server is insufficient."
|
|
584
|
+
]);
|
|
585
|
+
}
|
|
586
|
+
const headerResolution = resolveHeaderEnv(server.headers);
|
|
587
|
+
if (!headerResolution.ok) {
|
|
588
|
+
const firstMissing = headerResolution.missing[0] ?? "unknown";
|
|
589
|
+
const headerName = Object.keys(server.headers ?? {}).find(
|
|
590
|
+
(key) => (server.headers?.[key] ?? "").includes(firstMissing)
|
|
591
|
+
) ?? "configured";
|
|
592
|
+
return createStaticInspection(server, "fallback-static-insufficient", [
|
|
593
|
+
`missing environment variable ${firstMissing} for ${headerName} header.`,
|
|
594
|
+
"Static config only sees URL/headers metadata, not actual tool schemas.",
|
|
595
|
+
"Token estimate for this server is insufficient."
|
|
596
|
+
]);
|
|
597
|
+
}
|
|
598
|
+
const secrets = collectSecretValues(server.headers, headerResolution.headers);
|
|
599
|
+
const transport = new StreamableHTTPClientTransport(url, {
|
|
600
|
+
requestInit: {
|
|
601
|
+
headers: headerResolution.headers
|
|
602
|
+
},
|
|
603
|
+
fetch: options.fetch
|
|
604
|
+
});
|
|
605
|
+
const client = new Client2({ name: "tare-mcp", version: VERSION }, { capabilities: {} });
|
|
606
|
+
const close = async () => {
|
|
607
|
+
await client.close().catch(() => void 0);
|
|
608
|
+
await transport.close().catch(() => void 0);
|
|
609
|
+
};
|
|
610
|
+
try {
|
|
611
|
+
const tools = await withTimeout(
|
|
612
|
+
(async () => {
|
|
613
|
+
await client.connect(transport);
|
|
614
|
+
return listAllTools2(client);
|
|
615
|
+
})(),
|
|
616
|
+
options.timeoutMs,
|
|
617
|
+
close
|
|
618
|
+
);
|
|
619
|
+
await close();
|
|
620
|
+
return {
|
|
621
|
+
name: server.name,
|
|
622
|
+
sourceConfigPath: server.sourceConfigPath,
|
|
623
|
+
transport: "streamable-http",
|
|
624
|
+
urlHost: url.host,
|
|
625
|
+
toolDefinitions: tools,
|
|
626
|
+
inspectionMode: "live",
|
|
627
|
+
confidence: "high",
|
|
628
|
+
warnings: []
|
|
629
|
+
};
|
|
630
|
+
} catch (error) {
|
|
631
|
+
await close();
|
|
632
|
+
return createStaticInspection(
|
|
633
|
+
server,
|
|
634
|
+
"fallback-static-insufficient",
|
|
635
|
+
failureWarnings2(error, options.timeoutMs, secrets)
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// src/utils/stableJson.ts
|
|
641
|
+
function stableValue(value) {
|
|
642
|
+
if (Array.isArray(value)) {
|
|
643
|
+
return value.map((entry) => stableValue(entry));
|
|
644
|
+
}
|
|
645
|
+
if (value && typeof value === "object") {
|
|
646
|
+
const record = value;
|
|
647
|
+
return Object.fromEntries(
|
|
648
|
+
Object.keys(record).sort().map((key) => [key, stableValue(record[key])])
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
return value;
|
|
652
|
+
}
|
|
653
|
+
function stableStringify(value) {
|
|
654
|
+
return JSON.stringify(stableValue(value), null, 2);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/analysis/recommendations.ts
|
|
658
|
+
function buildRecommendations(report) {
|
|
659
|
+
const recommendations = [];
|
|
660
|
+
if (report.servers.some((server) => server.toolCount > 50)) {
|
|
661
|
+
recommendations.push({
|
|
662
|
+
type: "profile",
|
|
663
|
+
message: "Split large MCP servers into task-specific profiles."
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
if (report.overlapClusters.length > 0) {
|
|
667
|
+
recommendations.push({
|
|
668
|
+
type: "overlap",
|
|
669
|
+
message: "Avoid exposing multiple tools for the same intent unless needed."
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
if (report.servers.some(
|
|
673
|
+
(server) => server.tools.some((tool) => /create|update|delete|write|patch/i.test(tool.name))
|
|
674
|
+
)) {
|
|
675
|
+
recommendations.push({
|
|
676
|
+
type: "safety",
|
|
677
|
+
message: "Prefer read-only profiles for common workflows."
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
recommendations.push(
|
|
681
|
+
{
|
|
682
|
+
type: "hygiene",
|
|
683
|
+
message: "Disable rarely used write/admin tools."
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
type: "budget",
|
|
687
|
+
message: "Use `tare-mcp --budget 40000` to enforce a context budget."
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
type: "ci",
|
|
691
|
+
message: "Use `tare-mcp --json` to track this in CI."
|
|
692
|
+
}
|
|
693
|
+
);
|
|
694
|
+
return recommendations;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/analysis/overlapDetector.ts
|
|
698
|
+
import TfIdf from "natural/lib/natural/tfidf/tfidf.js";
|
|
699
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
700
|
+
"a",
|
|
701
|
+
"an",
|
|
702
|
+
"and",
|
|
703
|
+
"are",
|
|
704
|
+
"as",
|
|
705
|
+
"at",
|
|
706
|
+
"be",
|
|
707
|
+
"by",
|
|
708
|
+
"for",
|
|
709
|
+
"from",
|
|
710
|
+
"in",
|
|
711
|
+
"into",
|
|
712
|
+
"is",
|
|
713
|
+
"it",
|
|
714
|
+
"of",
|
|
715
|
+
"on",
|
|
716
|
+
"or",
|
|
717
|
+
"the",
|
|
718
|
+
"to",
|
|
719
|
+
"with"
|
|
720
|
+
]);
|
|
721
|
+
var INTENT_BUCKETS = {
|
|
722
|
+
search: ["search", "find", "query", "grep", "lookup", "list"],
|
|
723
|
+
read: ["get", "read", "fetch", "retrieve", "show"],
|
|
724
|
+
write: ["write", "create", "update", "edit", "patch", "delete", "remove"],
|
|
725
|
+
file: ["file", "path", "directory", "filesystem", "fs"],
|
|
726
|
+
issue: ["issue", "ticket", "task", "bug"],
|
|
727
|
+
repo: ["repo", "repository", "code", "commit", "pull", "pr", "branch"],
|
|
728
|
+
incident: ["incident", "alert", "page", "pager", "oncall"],
|
|
729
|
+
database: ["sql", "query", "table", "database", "db"]
|
|
730
|
+
};
|
|
731
|
+
var VERB_BUCKETS = ["search", "read", "write"];
|
|
732
|
+
var NOUN_BUCKETS = ["file", "issue", "repo", "incident", "database"];
|
|
733
|
+
function splitWords(text) {
|
|
734
|
+
return text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_./:-]+/g, " ").replace(/[^A-Za-z0-9]+/g, " ").toLowerCase().split(/\s+/).map((word) => word.trim()).filter((word) => word.length > 1 && !STOPWORDS.has(word));
|
|
735
|
+
}
|
|
736
|
+
function toolDocument(tool) {
|
|
737
|
+
return [
|
|
738
|
+
`${tool.server}.${tool.name}`,
|
|
739
|
+
tool.description ?? "",
|
|
740
|
+
JSON.stringify(tool.inputSchema ?? {})
|
|
741
|
+
].join("\n");
|
|
742
|
+
}
|
|
743
|
+
function tokensForTool(tool) {
|
|
744
|
+
return splitWords(toolDocument(tool));
|
|
745
|
+
}
|
|
746
|
+
function buckets(tokens) {
|
|
747
|
+
const tokenSet = new Set(tokens);
|
|
748
|
+
const verbs = /* @__PURE__ */ new Set();
|
|
749
|
+
const nouns = /* @__PURE__ */ new Set();
|
|
750
|
+
for (const verb of VERB_BUCKETS) {
|
|
751
|
+
if (INTENT_BUCKETS[verb].some((word) => tokenSet.has(word))) {
|
|
752
|
+
verbs.add(verb);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
for (const noun of NOUN_BUCKETS) {
|
|
756
|
+
if (INTENT_BUCKETS[noun].some((word) => tokenSet.has(word))) {
|
|
757
|
+
nouns.add(noun);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return { verbs, nouns };
|
|
761
|
+
}
|
|
762
|
+
function vectorFromTerms(terms) {
|
|
763
|
+
return new Map(terms.map((term) => [term.term, term.tfidf]));
|
|
764
|
+
}
|
|
765
|
+
function cosineSimilarity(a, b) {
|
|
766
|
+
let dot = 0;
|
|
767
|
+
let aNorm = 0;
|
|
768
|
+
let bNorm = 0;
|
|
769
|
+
for (const value of a.values()) {
|
|
770
|
+
aNorm += value * value;
|
|
771
|
+
}
|
|
772
|
+
for (const value of b.values()) {
|
|
773
|
+
bNorm += value * value;
|
|
774
|
+
}
|
|
775
|
+
for (const [term, value] of a) {
|
|
776
|
+
dot += value * (b.get(term) ?? 0);
|
|
777
|
+
}
|
|
778
|
+
if (aNorm === 0 || bNorm === 0) {
|
|
779
|
+
return 0;
|
|
780
|
+
}
|
|
781
|
+
return dot / (Math.sqrt(aNorm) * Math.sqrt(bNorm));
|
|
782
|
+
}
|
|
783
|
+
function intersection(a, b) {
|
|
784
|
+
return [...a].filter((value) => b.has(value));
|
|
785
|
+
}
|
|
786
|
+
function labelFor(verb, noun) {
|
|
787
|
+
if (verb === "search") {
|
|
788
|
+
return "search intent";
|
|
789
|
+
}
|
|
790
|
+
if (verb === "write" && noun === "issue") {
|
|
791
|
+
return "issue creation";
|
|
792
|
+
}
|
|
793
|
+
if (verb === "write" && noun === "file") {
|
|
794
|
+
return "file write";
|
|
795
|
+
}
|
|
796
|
+
if (verb && noun) {
|
|
797
|
+
return `${noun} ${verb}`;
|
|
798
|
+
}
|
|
799
|
+
return "similar tools";
|
|
800
|
+
}
|
|
801
|
+
function recommendationFor(label, verb) {
|
|
802
|
+
if (label === "search intent") {
|
|
803
|
+
return "Prefer one search surface per workflow.";
|
|
804
|
+
}
|
|
805
|
+
if (verb === "write") {
|
|
806
|
+
return "Disable duplicate write paths unless explicitly needed.";
|
|
807
|
+
}
|
|
808
|
+
return "Create task-specific profiles.";
|
|
809
|
+
}
|
|
810
|
+
function intentEdge(left, right, leftBuckets, rightBuckets) {
|
|
811
|
+
const sharedVerbs = intersection(leftBuckets.verbs, rightBuckets.verbs);
|
|
812
|
+
const sharedNouns = intersection(leftBuckets.nouns, rightBuckets.nouns);
|
|
813
|
+
if (sharedVerbs.includes("search")) {
|
|
814
|
+
return {
|
|
815
|
+
score: 0.75,
|
|
816
|
+
signals: /* @__PURE__ */ new Set(["intent-heuristic"]),
|
|
817
|
+
reason: "tools share a search intent",
|
|
818
|
+
label: "search intent"
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
const strongVerb = sharedVerbs.find((verb) => verb === "write") ?? sharedVerbs[0];
|
|
822
|
+
const noun = sharedNouns[0];
|
|
823
|
+
if (strongVerb && noun) {
|
|
824
|
+
return {
|
|
825
|
+
score: 0.7,
|
|
826
|
+
signals: /* @__PURE__ */ new Set(["intent-heuristic"]),
|
|
827
|
+
reason: `tools share ${strongVerb} and ${noun} intent buckets`,
|
|
828
|
+
label: labelFor(strongVerb, noun)
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
return void 0;
|
|
832
|
+
}
|
|
833
|
+
function mergeLabels(edges) {
|
|
834
|
+
const counts = /* @__PURE__ */ new Map();
|
|
835
|
+
for (const edge of edges) {
|
|
836
|
+
counts.set(edge.label, (counts.get(edge.label) ?? 0) + 1);
|
|
837
|
+
}
|
|
838
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? "similar tools";
|
|
839
|
+
}
|
|
840
|
+
function components(size, edges) {
|
|
841
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
842
|
+
for (const edge of edges) {
|
|
843
|
+
if (!adjacency.has(edge.from)) {
|
|
844
|
+
adjacency.set(edge.from, /* @__PURE__ */ new Set());
|
|
845
|
+
}
|
|
846
|
+
if (!adjacency.has(edge.to)) {
|
|
847
|
+
adjacency.set(edge.to, /* @__PURE__ */ new Set());
|
|
848
|
+
}
|
|
849
|
+
adjacency.get(edge.from)?.add(edge.to);
|
|
850
|
+
adjacency.get(edge.to)?.add(edge.from);
|
|
851
|
+
}
|
|
852
|
+
const visited = /* @__PURE__ */ new Set();
|
|
853
|
+
const found = [];
|
|
854
|
+
for (let index = 0; index < size; index += 1) {
|
|
855
|
+
if (visited.has(index) || !adjacency.has(index)) {
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
const stack = [index];
|
|
859
|
+
const component = [];
|
|
860
|
+
visited.add(index);
|
|
861
|
+
while (stack.length > 0) {
|
|
862
|
+
const current = stack.pop();
|
|
863
|
+
if (current === void 0) {
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
component.push(current);
|
|
867
|
+
for (const next of adjacency.get(current) ?? []) {
|
|
868
|
+
if (!visited.has(next)) {
|
|
869
|
+
visited.add(next);
|
|
870
|
+
stack.push(next);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (component.length > 1) {
|
|
875
|
+
found.push(component);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return found;
|
|
879
|
+
}
|
|
880
|
+
var OverlapDetector = class {
|
|
881
|
+
constructor(threshold = 0.42) {
|
|
882
|
+
this.threshold = threshold;
|
|
883
|
+
}
|
|
884
|
+
threshold;
|
|
885
|
+
detect(tools) {
|
|
886
|
+
if (tools.length < 2) {
|
|
887
|
+
return [];
|
|
888
|
+
}
|
|
889
|
+
const tokenized = tools.map(tokensForTool);
|
|
890
|
+
const bucketed = tokenized.map(buckets);
|
|
891
|
+
const tfidf = new TfIdf();
|
|
892
|
+
for (const [index, tokens] of tokenized.entries()) {
|
|
893
|
+
tfidf.addDocument(tokens, index);
|
|
894
|
+
}
|
|
895
|
+
const vectors = tools.map((_tool, index) => vectorFromTerms(tfidf.listTerms(index)));
|
|
896
|
+
const edges = [];
|
|
897
|
+
for (let left = 0; left < tools.length; left += 1) {
|
|
898
|
+
for (let right = left + 1; right < tools.length; right += 1) {
|
|
899
|
+
if (tools[left]?.server === tools[right]?.server) {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
const similarity = cosineSimilarity(
|
|
903
|
+
vectors[left] ?? /* @__PURE__ */ new Map(),
|
|
904
|
+
vectors[right] ?? /* @__PURE__ */ new Map()
|
|
905
|
+
);
|
|
906
|
+
const heuristic = intentEdge(
|
|
907
|
+
tools[left],
|
|
908
|
+
tools[right],
|
|
909
|
+
bucketed[left],
|
|
910
|
+
bucketed[right]
|
|
911
|
+
);
|
|
912
|
+
if (similarity >= this.threshold || heuristic) {
|
|
913
|
+
const signals = new Set(heuristic?.signals);
|
|
914
|
+
if (similarity >= this.threshold) {
|
|
915
|
+
signals.add("tfidf");
|
|
916
|
+
}
|
|
917
|
+
edges.push({
|
|
918
|
+
from: left,
|
|
919
|
+
to: right,
|
|
920
|
+
score: Math.max(similarity, heuristic?.score ?? 0),
|
|
921
|
+
signals,
|
|
922
|
+
reason: heuristic?.reason ?? `tool definitions have TF-IDF cosine similarity ${similarity.toFixed(2)}`,
|
|
923
|
+
label: heuristic?.label ?? "similar tools"
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return components(tools.length, edges).map((component) => {
|
|
929
|
+
const componentEdges = edges.filter(
|
|
930
|
+
(edge) => component.includes(edge.from) && component.includes(edge.to)
|
|
931
|
+
);
|
|
932
|
+
const signals = /* @__PURE__ */ new Set();
|
|
933
|
+
for (const edge of componentEdges) {
|
|
934
|
+
for (const signal of edge.signals) {
|
|
935
|
+
signals.add(signal);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
const label = mergeLabels(componentEdges);
|
|
939
|
+
const maxScore = Math.max(...componentEdges.map((edge) => edge.score));
|
|
940
|
+
const reason = componentEdges[0]?.reason ?? "tools appear similar";
|
|
941
|
+
const verb = componentEdges.some((edge) => edge.label.includes("write")) ? "write" : void 0;
|
|
942
|
+
return {
|
|
943
|
+
label,
|
|
944
|
+
score: Number(maxScore.toFixed(2)),
|
|
945
|
+
reason,
|
|
946
|
+
signals: [...signals],
|
|
947
|
+
tools: component.map((index) => tools[index]).sort((a, b) => `${a.server}.${a.name}`.localeCompare(`${b.server}.${b.name}`)).map((tool) => ({
|
|
948
|
+
server: tool.server,
|
|
949
|
+
name: tool.name,
|
|
950
|
+
description: tool.description,
|
|
951
|
+
estimatedTokens: {
|
|
952
|
+
claude: tool.estimatedTokens.claude,
|
|
953
|
+
openaiCl100k: tool.estimatedTokens.openaiCl100k
|
|
954
|
+
}
|
|
955
|
+
})),
|
|
956
|
+
recommendation: recommendationFor(label, verb)
|
|
957
|
+
};
|
|
958
|
+
}).filter((cluster) => new Set(cluster.tools.map((tool) => tool.server)).size > 1).sort((a, b) => b.score - a.score || b.tools.length - a.tools.length).slice(0, 10);
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
// src/analysis/analyze.ts
|
|
963
|
+
function windowUsage(tokens, window) {
|
|
964
|
+
return Math.round(tokens / window * 100);
|
|
965
|
+
}
|
|
966
|
+
function serverMetadata(server) {
|
|
967
|
+
return stableStringify({
|
|
968
|
+
server: server.name,
|
|
969
|
+
transport: server.transport,
|
|
970
|
+
command: server.command,
|
|
971
|
+
args: server.args,
|
|
972
|
+
urlHost: server.urlHost
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
function toolPayload(server, tool) {
|
|
976
|
+
const payload = {
|
|
977
|
+
server: server.name,
|
|
978
|
+
transport: server.transport,
|
|
979
|
+
tool: {
|
|
980
|
+
name: tool.name,
|
|
981
|
+
description: tool.description,
|
|
982
|
+
inputSchema: tool.inputSchema,
|
|
983
|
+
annotations: tool.annotations,
|
|
984
|
+
outputSchema: tool.outputSchema,
|
|
985
|
+
metadata: tool.metadata
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
return stableStringify(payload);
|
|
989
|
+
}
|
|
990
|
+
async function analyzeServers(inspectedServers, tokenEstimator, options) {
|
|
991
|
+
const warnings = [...options.warnings ?? []];
|
|
992
|
+
const analyzedServers = [];
|
|
993
|
+
const liveToolsForOverlap = [];
|
|
994
|
+
for (const server of inspectedServers) {
|
|
995
|
+
const metadataEstimate = await tokenEstimator.count(serverMetadata(server));
|
|
996
|
+
let serverClaude = metadataEstimate.claude.tokens;
|
|
997
|
+
let serverOpenAi = metadataEstimate.openaiCl100k.tokens;
|
|
998
|
+
const tools = [];
|
|
999
|
+
for (const tool of server.toolDefinitions) {
|
|
1000
|
+
const estimates = await tokenEstimator.count(toolPayload(server, tool));
|
|
1001
|
+
serverClaude += estimates.claude.tokens;
|
|
1002
|
+
serverOpenAi += estimates.openaiCl100k.tokens;
|
|
1003
|
+
const analyzedTool = {
|
|
1004
|
+
server: server.name,
|
|
1005
|
+
name: tool.name,
|
|
1006
|
+
description: tool.description,
|
|
1007
|
+
inputSchema: tool.inputSchema,
|
|
1008
|
+
estimatedTokens: {
|
|
1009
|
+
claude: estimates.claude.tokens,
|
|
1010
|
+
openaiCl100k: estimates.openaiCl100k.tokens
|
|
1011
|
+
},
|
|
1012
|
+
hasInputSchema: tool.inputSchema !== void 0
|
|
1013
|
+
};
|
|
1014
|
+
tools.push({
|
|
1015
|
+
name: analyzedTool.name,
|
|
1016
|
+
description: analyzedTool.description,
|
|
1017
|
+
estimatedTokens: analyzedTool.estimatedTokens,
|
|
1018
|
+
hasInputSchema: analyzedTool.hasInputSchema
|
|
1019
|
+
});
|
|
1020
|
+
if (server.inspectionMode === "live") {
|
|
1021
|
+
liveToolsForOverlap.push(analyzedTool);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
analyzedServers.push({
|
|
1025
|
+
name: server.name,
|
|
1026
|
+
sourceConfigPath: server.sourceConfigPath,
|
|
1027
|
+
transport: server.transport,
|
|
1028
|
+
command: server.command,
|
|
1029
|
+
args: server.args,
|
|
1030
|
+
urlHost: server.urlHost,
|
|
1031
|
+
toolCount: tools.length,
|
|
1032
|
+
estimatedTokens: {
|
|
1033
|
+
claude: serverClaude,
|
|
1034
|
+
openaiCl100k: serverOpenAi
|
|
1035
|
+
},
|
|
1036
|
+
inspectionMode: server.inspectionMode,
|
|
1037
|
+
confidence: server.confidence,
|
|
1038
|
+
warnings: server.warnings,
|
|
1039
|
+
tools: tools.sort((a, b) => b.estimatedTokens.claude - a.estimatedTokens.claude)
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
analyzedServers.sort((a, b) => b.estimatedTokens.claude - a.estimatedTokens.claude);
|
|
1043
|
+
const totalClaude = analyzedServers.reduce(
|
|
1044
|
+
(sum, server) => sum + server.estimatedTokens.claude,
|
|
1045
|
+
0
|
|
1046
|
+
);
|
|
1047
|
+
const totalOpenAi = analyzedServers.reduce(
|
|
1048
|
+
(sum, server) => sum + server.estimatedTokens.openaiCl100k,
|
|
1049
|
+
0
|
|
1050
|
+
);
|
|
1051
|
+
const overlapClusters = new OverlapDetector().detect(liveToolsForOverlap);
|
|
1052
|
+
const report = {
|
|
1053
|
+
version: VERSION,
|
|
1054
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1055
|
+
summary: {
|
|
1056
|
+
configFiles: options.configFiles,
|
|
1057
|
+
servers: analyzedServers.length,
|
|
1058
|
+
tools: analyzedServers.reduce((sum, server) => sum + server.toolCount, 0),
|
|
1059
|
+
estimatedTokens: {
|
|
1060
|
+
claude: totalClaude,
|
|
1061
|
+
openaiCl100k: totalOpenAi
|
|
1062
|
+
},
|
|
1063
|
+
contextWindows: {
|
|
1064
|
+
"64000": {
|
|
1065
|
+
claude: windowUsage(totalClaude, 64e3),
|
|
1066
|
+
openaiCl100k: windowUsage(totalOpenAi, 64e3)
|
|
1067
|
+
},
|
|
1068
|
+
"128000": {
|
|
1069
|
+
claude: windowUsage(totalClaude, 128e3),
|
|
1070
|
+
openaiCl100k: windowUsage(totalOpenAi, 128e3)
|
|
1071
|
+
},
|
|
1072
|
+
"200000": {
|
|
1073
|
+
claude: windowUsage(totalClaude, 2e5),
|
|
1074
|
+
openaiCl100k: windowUsage(totalOpenAi, 2e5)
|
|
1075
|
+
}
|
|
1076
|
+
},
|
|
1077
|
+
insufficientServers: analyzedServers.filter((server) => server.inspectionMode !== "live").length
|
|
1078
|
+
},
|
|
1079
|
+
servers: analyzedServers,
|
|
1080
|
+
overlapClusters,
|
|
1081
|
+
recommendations: [],
|
|
1082
|
+
warnings,
|
|
1083
|
+
metadata: {
|
|
1084
|
+
staticOnly: options.staticOnly,
|
|
1085
|
+
inspectionMode: options.staticOnly ? "static-only" : "live default"
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
report.recommendations = buildRecommendations(report);
|
|
1089
|
+
return report;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// src/tokens/claudeEstimator.ts
|
|
1093
|
+
var LocalClaudeEstimator = class {
|
|
1094
|
+
constructor(openAiCount) {
|
|
1095
|
+
this.openAiCount = openAiCount;
|
|
1096
|
+
}
|
|
1097
|
+
openAiCount;
|
|
1098
|
+
async count(text) {
|
|
1099
|
+
const openAiEstimate = await this.openAiCount?.();
|
|
1100
|
+
if (openAiEstimate?.confidence !== "low") {
|
|
1101
|
+
return {
|
|
1102
|
+
tokenizer: "claude-estimate",
|
|
1103
|
+
tokens: Math.ceil((openAiEstimate?.tokens ?? Math.ceil(text.length / 4)) * 1.1),
|
|
1104
|
+
confidence: "medium",
|
|
1105
|
+
warning: "Using local Claude approximation. Use --claude-tokenizer api for API-backed token counting."
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
return {
|
|
1109
|
+
tokenizer: "claude-estimate",
|
|
1110
|
+
tokens: Math.ceil(text.length / 4),
|
|
1111
|
+
confidence: "low",
|
|
1112
|
+
warning: "Using low-confidence local Claude approximation because OpenAI cl100k estimate was unavailable."
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
// src/tokens/openaiCl100kCounter.ts
|
|
1118
|
+
import { encode } from "gpt-tokenizer";
|
|
1119
|
+
var OpenAICl100kCounter = class {
|
|
1120
|
+
async count(text) {
|
|
1121
|
+
try {
|
|
1122
|
+
return {
|
|
1123
|
+
tokenizer: "openai-cl100k",
|
|
1124
|
+
tokens: encode(text).length,
|
|
1125
|
+
confidence: "high"
|
|
1126
|
+
};
|
|
1127
|
+
} catch {
|
|
1128
|
+
return {
|
|
1129
|
+
tokenizer: "fallback-char-ratio",
|
|
1130
|
+
tokens: Math.ceil(text.length / 4),
|
|
1131
|
+
confidence: "low",
|
|
1132
|
+
warning: "OpenAI cl100k tokenizer failed; using character-ratio fallback."
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
// src/utils/hash.ts
|
|
1139
|
+
import { createHash } from "crypto";
|
|
1140
|
+
function hashText(text) {
|
|
1141
|
+
return createHash("sha256").update(text).digest("hex");
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// src/tokens/tokenCache.ts
|
|
1145
|
+
var TokenCache = class {
|
|
1146
|
+
cache = /* @__PURE__ */ new Map();
|
|
1147
|
+
getOrSet(keyPrefix, text, count) {
|
|
1148
|
+
const key = `${keyPrefix}:${hashText(text)}`;
|
|
1149
|
+
const existing = this.cache.get(key);
|
|
1150
|
+
if (existing) {
|
|
1151
|
+
return existing;
|
|
1152
|
+
}
|
|
1153
|
+
const estimate = count();
|
|
1154
|
+
this.cache.set(key, estimate);
|
|
1155
|
+
return estimate;
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
// src/tokens/countTokens.ts
|
|
1160
|
+
var PromiseQueue = class {
|
|
1161
|
+
constructor(concurrency) {
|
|
1162
|
+
this.concurrency = concurrency;
|
|
1163
|
+
}
|
|
1164
|
+
concurrency;
|
|
1165
|
+
active = 0;
|
|
1166
|
+
tasks = [];
|
|
1167
|
+
push(run) {
|
|
1168
|
+
return new Promise((resolve, reject) => {
|
|
1169
|
+
this.tasks.push({ run, resolve, reject });
|
|
1170
|
+
this.drain();
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
drain() {
|
|
1174
|
+
while (this.active < this.concurrency && this.tasks.length > 0) {
|
|
1175
|
+
const task = this.tasks.shift();
|
|
1176
|
+
if (!task) {
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
this.active += 1;
|
|
1180
|
+
void task.run().then(task.resolve).catch(task.reject).finally(() => {
|
|
1181
|
+
this.active -= 1;
|
|
1182
|
+
this.drain();
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
var TokenEstimator = class {
|
|
1188
|
+
constructor(options) {
|
|
1189
|
+
this.options = options;
|
|
1190
|
+
}
|
|
1191
|
+
options;
|
|
1192
|
+
cache = new TokenCache();
|
|
1193
|
+
openAiCounter = new OpenAICl100kCounter();
|
|
1194
|
+
anthropicQueue = new PromiseQueue(3);
|
|
1195
|
+
anthropicApiUnavailable = false;
|
|
1196
|
+
emittedMissingKeyWarning = false;
|
|
1197
|
+
emittedDisabledWarning = false;
|
|
1198
|
+
emittedApiFailureWarning = false;
|
|
1199
|
+
async count(text) {
|
|
1200
|
+
const openaiCl100k = await this.cache.getOrSet(
|
|
1201
|
+
"openai-cl100k",
|
|
1202
|
+
text,
|
|
1203
|
+
() => this.openAiCounter.count(text)
|
|
1204
|
+
);
|
|
1205
|
+
const claude = await this.cache.getOrSet(
|
|
1206
|
+
`claude:${this.options.claudeTokenizerMode}`,
|
|
1207
|
+
text,
|
|
1208
|
+
() => this.countClaude(text, openaiCl100k)
|
|
1209
|
+
);
|
|
1210
|
+
return { claude, openaiCl100k };
|
|
1211
|
+
}
|
|
1212
|
+
async countClaude(text, openAiEstimate) {
|
|
1213
|
+
if (this.options.claudeTokenizerMode !== "api") {
|
|
1214
|
+
return new LocalClaudeEstimator(async () => openAiEstimate).count(text);
|
|
1215
|
+
}
|
|
1216
|
+
if (this.options.anthropicDisabled) {
|
|
1217
|
+
if (!this.emittedDisabledWarning) {
|
|
1218
|
+
this.options.onWarning?.(
|
|
1219
|
+
"Claude API token counting requested but TARE_DISABLE_ANTHROPIC_TOKEN_API=1 is set. Falling back to local Claude approximation."
|
|
1220
|
+
);
|
|
1221
|
+
this.emittedDisabledWarning = true;
|
|
1222
|
+
}
|
|
1223
|
+
return new LocalClaudeEstimator(async () => openAiEstimate).count(text);
|
|
1224
|
+
}
|
|
1225
|
+
if (!this.options.anthropicApiKey) {
|
|
1226
|
+
if (!this.emittedMissingKeyWarning) {
|
|
1227
|
+
this.options.onWarning?.(
|
|
1228
|
+
"Claude API token counting requested but ANTHROPIC_API_KEY is not set. Falling back to local Claude approximation."
|
|
1229
|
+
);
|
|
1230
|
+
this.emittedMissingKeyWarning = true;
|
|
1231
|
+
}
|
|
1232
|
+
return new LocalClaudeEstimator(async () => openAiEstimate).count(text);
|
|
1233
|
+
}
|
|
1234
|
+
if (this.anthropicApiUnavailable) {
|
|
1235
|
+
return new LocalClaudeEstimator(async () => openAiEstimate).count(text);
|
|
1236
|
+
}
|
|
1237
|
+
try {
|
|
1238
|
+
return await this.anthropicQueue.push(() => this.countClaudeWithApi(text));
|
|
1239
|
+
} catch {
|
|
1240
|
+
this.anthropicApiUnavailable = true;
|
|
1241
|
+
if (!this.emittedApiFailureWarning) {
|
|
1242
|
+
this.options.onWarning?.(
|
|
1243
|
+
"Claude API token counting failed. Falling back to local Claude approximation."
|
|
1244
|
+
);
|
|
1245
|
+
this.emittedApiFailureWarning = true;
|
|
1246
|
+
}
|
|
1247
|
+
return new LocalClaudeEstimator(async () => openAiEstimate).count(text);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
async countClaudeWithApi(text) {
|
|
1251
|
+
const controller = new AbortController();
|
|
1252
|
+
const timeout = setTimeout(() => controller.abort(), this.options.timeoutMs ?? 5e3);
|
|
1253
|
+
try {
|
|
1254
|
+
const response = await fetch("https://api.anthropic.com/v1/messages/count_tokens", {
|
|
1255
|
+
method: "POST",
|
|
1256
|
+
signal: controller.signal,
|
|
1257
|
+
headers: {
|
|
1258
|
+
"x-api-key": this.options.anthropicApiKey ?? "",
|
|
1259
|
+
"anthropic-version": "2023-06-01",
|
|
1260
|
+
"content-type": "application/json"
|
|
1261
|
+
},
|
|
1262
|
+
body: JSON.stringify({
|
|
1263
|
+
model: this.options.anthropicModel ?? "claude-sonnet-4-6",
|
|
1264
|
+
messages: [
|
|
1265
|
+
{
|
|
1266
|
+
role: "user",
|
|
1267
|
+
content: text
|
|
1268
|
+
}
|
|
1269
|
+
]
|
|
1270
|
+
})
|
|
1271
|
+
});
|
|
1272
|
+
if (!response.ok) {
|
|
1273
|
+
throw new Error(`Anthropic count_tokens failed with ${response.status}`);
|
|
1274
|
+
}
|
|
1275
|
+
const body = await response.json();
|
|
1276
|
+
const tokens = body.input_tokens ?? body.inputTokens;
|
|
1277
|
+
if (typeof tokens !== "number" || !Number.isFinite(tokens)) {
|
|
1278
|
+
throw new Error("Anthropic count_tokens response did not include input_tokens.");
|
|
1279
|
+
}
|
|
1280
|
+
return {
|
|
1281
|
+
tokenizer: "claude-api",
|
|
1282
|
+
tokens: Math.ceil(tokens),
|
|
1283
|
+
confidence: "high"
|
|
1284
|
+
};
|
|
1285
|
+
} finally {
|
|
1286
|
+
clearTimeout(timeout);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
// src/reporters/humanReporter.ts
|
|
1292
|
+
import pc from "picocolors";
|
|
1293
|
+
function formatNumber(value) {
|
|
1294
|
+
return Math.round(value).toLocaleString("en-US");
|
|
1295
|
+
}
|
|
1296
|
+
function approx(value) {
|
|
1297
|
+
return `~${formatNumber(value)}`;
|
|
1298
|
+
}
|
|
1299
|
+
function padRight(text, length) {
|
|
1300
|
+
return text.padEnd(length, " ");
|
|
1301
|
+
}
|
|
1302
|
+
function allTools(report) {
|
|
1303
|
+
return report.servers.flatMap(
|
|
1304
|
+
(server) => server.tools.map((tool) => ({
|
|
1305
|
+
server: server.name,
|
|
1306
|
+
name: tool.name,
|
|
1307
|
+
estimatedTokens: tool.estimatedTokens
|
|
1308
|
+
}))
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
function renderHumanReport(report) {
|
|
1312
|
+
const lines = [];
|
|
1313
|
+
lines.push(pc.bold("tare-mcp \u2014 MCP context weight"));
|
|
1314
|
+
lines.push("");
|
|
1315
|
+
lines.push("MCP made tools easy to connect. It did not make them cheap to carry.");
|
|
1316
|
+
lines.push("");
|
|
1317
|
+
if (report.metadata.staticOnly) {
|
|
1318
|
+
lines.push(pc.yellow("Static-only mode: insufficient for packaged or hosted MCP servers."));
|
|
1319
|
+
lines.push("Run without --no-exec for actual tool/schema weight.");
|
|
1320
|
+
lines.push("");
|
|
1321
|
+
}
|
|
1322
|
+
for (const warning of report.warnings) {
|
|
1323
|
+
lines.push(pc.yellow(`\u26A0 ${warning}`));
|
|
1324
|
+
}
|
|
1325
|
+
if (report.warnings.length > 0) {
|
|
1326
|
+
lines.push("");
|
|
1327
|
+
}
|
|
1328
|
+
lines.push(`Config files found: ${report.summary.configFiles}`);
|
|
1329
|
+
lines.push(`Servers analyzed: ${report.summary.servers}`);
|
|
1330
|
+
lines.push(`Inspection mode: ${report.metadata.inspectionMode}`);
|
|
1331
|
+
lines.push(`Tools exposed: ${report.summary.tools}`);
|
|
1332
|
+
lines.push("");
|
|
1333
|
+
lines.push("Estimated context weight:");
|
|
1334
|
+
lines.push(`- Claude estimate: ${approx(report.summary.estimatedTokens.claude)} tokens`);
|
|
1335
|
+
lines.push(
|
|
1336
|
+
`- OpenAI cl100k estimate: ${approx(report.summary.estimatedTokens.openaiCl100k)} tokens`
|
|
1337
|
+
);
|
|
1338
|
+
lines.push("");
|
|
1339
|
+
lines.push("Context window usage:");
|
|
1340
|
+
lines.push(`- 200k window: ${report.summary.contextWindows["200000"].claude}%`);
|
|
1341
|
+
lines.push(`- 128k window: ${report.summary.contextWindows["128000"].claude}%`);
|
|
1342
|
+
lines.push(`- 64k window: ${report.summary.contextWindows["64000"].claude}%`);
|
|
1343
|
+
if (report.servers.length > 0) {
|
|
1344
|
+
lines.push("");
|
|
1345
|
+
lines.push("Worst servers:");
|
|
1346
|
+
for (const [index, server] of report.servers.slice(0, 5).entries()) {
|
|
1347
|
+
lines.push(
|
|
1348
|
+
`${index + 1}. ${padRight(server.name, 12)} ${padRight(
|
|
1349
|
+
`${approx(server.estimatedTokens.claude)} Claude tokens`,
|
|
1350
|
+
24
|
|
1351
|
+
)} ${server.toolCount.toString().padStart(4)} tools`
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
const tools = allTools(report).sort(
|
|
1356
|
+
(a, b) => b.estimatedTokens.claude - a.estimatedTokens.claude
|
|
1357
|
+
);
|
|
1358
|
+
if (tools.length > 0) {
|
|
1359
|
+
lines.push("");
|
|
1360
|
+
lines.push("Worst tools:");
|
|
1361
|
+
for (const [index, tool] of tools.slice(0, 5).entries()) {
|
|
1362
|
+
lines.push(
|
|
1363
|
+
`${index + 1}. ${padRight(`${tool.server}.${tool.name}`, 34)} ${approx(
|
|
1364
|
+
tool.estimatedTokens.claude
|
|
1365
|
+
)} Claude tokens`
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (report.overlapClusters.length > 0) {
|
|
1370
|
+
lines.push("");
|
|
1371
|
+
lines.push(`Overlap warnings: ${report.overlapClusters.length} clusters`);
|
|
1372
|
+
lines.push("");
|
|
1373
|
+
for (const [index, cluster] of report.overlapClusters.slice(0, 5).entries()) {
|
|
1374
|
+
lines.push(`${index + 1}. ${cluster.label}`);
|
|
1375
|
+
for (const tool of cluster.tools) {
|
|
1376
|
+
lines.push(` ${tool.server}.${tool.name}`);
|
|
1377
|
+
}
|
|
1378
|
+
lines.push(` \u2192 ${cluster.recommendation}`);
|
|
1379
|
+
if (index < Math.min(report.overlapClusters.length, 5) - 1) {
|
|
1380
|
+
lines.push("");
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
const insufficientServers = report.servers.filter((server) => server.inspectionMode !== "live");
|
|
1385
|
+
if (insufficientServers.length > 0) {
|
|
1386
|
+
lines.push("");
|
|
1387
|
+
lines.push("Insufficient data:");
|
|
1388
|
+
for (const server of insufficientServers) {
|
|
1389
|
+
lines.push(
|
|
1390
|
+
`- ${server.name}: ${server.warnings[0] ?? "static fallback cannot see actual tool schemas."}`
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
lines.push("");
|
|
1395
|
+
lines.push("Recommendations:");
|
|
1396
|
+
for (const recommendation of report.recommendations) {
|
|
1397
|
+
lines.push(`- ${recommendation.message}`);
|
|
1398
|
+
}
|
|
1399
|
+
return `${lines.join("\n")}
|
|
1400
|
+
`;
|
|
1401
|
+
}
|
|
1402
|
+
function budgetActual(report, tokenizer) {
|
|
1403
|
+
return tokenizer === "openai" ? report.summary.estimatedTokens.openaiCl100k : report.summary.estimatedTokens.claude;
|
|
1404
|
+
}
|
|
1405
|
+
function renderBudgetFailure(report, budget, tokenizer) {
|
|
1406
|
+
const actual = budgetActual(report, tokenizer);
|
|
1407
|
+
const label = tokenizer === "openai" ? "OpenAI cl100k-estimated tokens" : "Claude-estimated tokens";
|
|
1408
|
+
const lines = [];
|
|
1409
|
+
lines.push("");
|
|
1410
|
+
lines.push(pc.red(pc.bold("FAILED: MCP context budget exceeded.")));
|
|
1411
|
+
lines.push("");
|
|
1412
|
+
lines.push(`Budget: ${formatNumber(budget)} ${label}`);
|
|
1413
|
+
lines.push(`Actual: ${approx(actual)} ${label}`);
|
|
1414
|
+
lines.push(`Over by: ${approx(actual - budget)} tokens`);
|
|
1415
|
+
if (report.servers.length > 0) {
|
|
1416
|
+
lines.push("");
|
|
1417
|
+
lines.push("Top offenders:");
|
|
1418
|
+
for (const [index, server] of report.servers.slice(0, 3).entries()) {
|
|
1419
|
+
const tokens = tokenizer === "openai" ? server.estimatedTokens.openaiCl100k : server.estimatedTokens.claude;
|
|
1420
|
+
lines.push(`${index + 1}. ${padRight(server.name, 12)} ${approx(tokens)} tokens`);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
const largestCluster = report.overlapClusters[0];
|
|
1424
|
+
if (largestCluster) {
|
|
1425
|
+
lines.push("");
|
|
1426
|
+
lines.push("Largest overlap cluster:");
|
|
1427
|
+
lines.push(largestCluster.label);
|
|
1428
|
+
for (const tool of largestCluster.tools) {
|
|
1429
|
+
lines.push(`- ${tool.server}.${tool.name}`);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return `${lines.join("\n")}
|
|
1433
|
+
`;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// src/reporters/jsonReporter.ts
|
|
1437
|
+
function renderJsonReport(report) {
|
|
1438
|
+
return `${JSON.stringify(report, null, 2)}
|
|
1439
|
+
`;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// src/diff/loadReport.ts
|
|
1443
|
+
import { z as z3 } from "zod";
|
|
1444
|
+
var TokenTotalsSchema = z3.object({
|
|
1445
|
+
claude: z3.number(),
|
|
1446
|
+
openaiCl100k: z3.number()
|
|
1447
|
+
}).passthrough();
|
|
1448
|
+
var OptionalTokenTotalsSchema = z3.object({
|
|
1449
|
+
claude: z3.number().optional(),
|
|
1450
|
+
openaiCl100k: z3.number().optional()
|
|
1451
|
+
}).passthrough();
|
|
1452
|
+
var OverlapClusterSchema = z3.object({
|
|
1453
|
+
label: z3.string(),
|
|
1454
|
+
score: z3.number(),
|
|
1455
|
+
reason: z3.string(),
|
|
1456
|
+
signals: z3.array(z3.enum(["tfidf", "intent-heuristic"])),
|
|
1457
|
+
tools: z3.array(
|
|
1458
|
+
z3.object({
|
|
1459
|
+
server: z3.string(),
|
|
1460
|
+
name: z3.string(),
|
|
1461
|
+
description: z3.string().optional(),
|
|
1462
|
+
estimatedTokens: OptionalTokenTotalsSchema.optional()
|
|
1463
|
+
}).passthrough()
|
|
1464
|
+
),
|
|
1465
|
+
recommendation: z3.string()
|
|
1466
|
+
}).passthrough();
|
|
1467
|
+
var TareReportSchema = z3.object({
|
|
1468
|
+
version: z3.string(),
|
|
1469
|
+
generatedAt: z3.string(),
|
|
1470
|
+
summary: z3.object({
|
|
1471
|
+
configFiles: z3.number(),
|
|
1472
|
+
servers: z3.number(),
|
|
1473
|
+
tools: z3.number(),
|
|
1474
|
+
estimatedTokens: TokenTotalsSchema,
|
|
1475
|
+
contextWindows: z3.object({
|
|
1476
|
+
"64000": TokenTotalsSchema,
|
|
1477
|
+
"128000": TokenTotalsSchema,
|
|
1478
|
+
"200000": TokenTotalsSchema
|
|
1479
|
+
}).passthrough(),
|
|
1480
|
+
insufficientServers: z3.number()
|
|
1481
|
+
}).passthrough(),
|
|
1482
|
+
servers: z3.array(
|
|
1483
|
+
z3.object({
|
|
1484
|
+
name: z3.string(),
|
|
1485
|
+
sourceConfigPath: z3.string(),
|
|
1486
|
+
transport: z3.enum(["stdio", "streamable-http", "sse", "unknown"]),
|
|
1487
|
+
command: z3.string().optional(),
|
|
1488
|
+
args: z3.array(z3.string()).optional(),
|
|
1489
|
+
urlHost: z3.string().optional(),
|
|
1490
|
+
toolCount: z3.number(),
|
|
1491
|
+
estimatedTokens: TokenTotalsSchema,
|
|
1492
|
+
inspectionMode: z3.enum(["live", "static-insufficient", "fallback-static-insufficient"]),
|
|
1493
|
+
confidence: z3.enum(["high", "medium", "low"]),
|
|
1494
|
+
warnings: z3.array(z3.string()),
|
|
1495
|
+
tools: z3.array(
|
|
1496
|
+
z3.object({
|
|
1497
|
+
name: z3.string(),
|
|
1498
|
+
description: z3.string().optional(),
|
|
1499
|
+
estimatedTokens: TokenTotalsSchema,
|
|
1500
|
+
hasInputSchema: z3.boolean()
|
|
1501
|
+
}).passthrough()
|
|
1502
|
+
)
|
|
1503
|
+
}).passthrough()
|
|
1504
|
+
),
|
|
1505
|
+
overlapClusters: z3.array(OverlapClusterSchema),
|
|
1506
|
+
recommendations: z3.array(
|
|
1507
|
+
z3.object({
|
|
1508
|
+
type: z3.string(),
|
|
1509
|
+
message: z3.string()
|
|
1510
|
+
}).passthrough()
|
|
1511
|
+
),
|
|
1512
|
+
warnings: z3.array(z3.string()),
|
|
1513
|
+
metadata: z3.object({
|
|
1514
|
+
staticOnly: z3.boolean(),
|
|
1515
|
+
inspectionMode: z3.enum(["live default", "static-only"])
|
|
1516
|
+
}).passthrough()
|
|
1517
|
+
}).passthrough();
|
|
1518
|
+
var ReportLoadError = class extends Error {
|
|
1519
|
+
path;
|
|
1520
|
+
constructor(filePath, message) {
|
|
1521
|
+
super(message);
|
|
1522
|
+
this.name = "ReportLoadError";
|
|
1523
|
+
this.path = filePath;
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
async function loadReport(filePath) {
|
|
1527
|
+
const text = await readReportText(filePath);
|
|
1528
|
+
const parsed = parseJson(filePath, text);
|
|
1529
|
+
const result = TareReportSchema.safeParse(parsed);
|
|
1530
|
+
if (!result.success) {
|
|
1531
|
+
throw new ReportLoadError(filePath, formatReportIssue(filePath, result.error.issues[0]));
|
|
1532
|
+
}
|
|
1533
|
+
return {
|
|
1534
|
+
path: filePath,
|
|
1535
|
+
report: result.data
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
async function readReportText(filePath) {
|
|
1539
|
+
try {
|
|
1540
|
+
return await readUtf8(filePath);
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
1543
|
+
throw new ReportLoadError(filePath, `${filePath} was not found.`);
|
|
1544
|
+
}
|
|
1545
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1546
|
+
throw new ReportLoadError(filePath, `Could not read ${filePath}: ${reason}`);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
function parseJson(filePath, text) {
|
|
1550
|
+
try {
|
|
1551
|
+
return JSON.parse(text);
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1554
|
+
throw new ReportLoadError(filePath, `${filePath} is not valid JSON: ${reason}`);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
function formatReportIssue(filePath, issue) {
|
|
1558
|
+
if (!issue) {
|
|
1559
|
+
return `${filePath} is not a valid tare-mcp report.`;
|
|
1560
|
+
}
|
|
1561
|
+
const field = issue.path.length > 0 ? issue.path.join(".") : "report";
|
|
1562
|
+
return `${filePath} has invalid ${field}: ${issue.message}`;
|
|
1563
|
+
}
|
|
1564
|
+
function isNodeError(error) {
|
|
1565
|
+
return error instanceof Error && "code" in error;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// src/diff/diffReports.ts
|
|
1569
|
+
function diffReports(baseReport, headReport, options) {
|
|
1570
|
+
const baseServers = new Map(baseReport.servers.map((server) => [server.name, server]));
|
|
1571
|
+
const headServers = new Map(headReport.servers.map((server) => [server.name, server]));
|
|
1572
|
+
const baseTools = buildToolMap(baseReport);
|
|
1573
|
+
const headTools = buildToolMap(headReport);
|
|
1574
|
+
const baseClusters = buildClusterMap(baseReport);
|
|
1575
|
+
const headClusters = buildClusterMap(headReport);
|
|
1576
|
+
const report = {
|
|
1577
|
+
version: VERSION,
|
|
1578
|
+
generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1579
|
+
base: {
|
|
1580
|
+
path: options.basePath,
|
|
1581
|
+
reportVersion: baseReport.version,
|
|
1582
|
+
generatedAt: baseReport.generatedAt
|
|
1583
|
+
},
|
|
1584
|
+
head: {
|
|
1585
|
+
path: options.headPath,
|
|
1586
|
+
reportVersion: headReport.version,
|
|
1587
|
+
generatedAt: headReport.generatedAt
|
|
1588
|
+
},
|
|
1589
|
+
summary: {
|
|
1590
|
+
servers: numericDelta(baseReport.summary.servers, headReport.summary.servers),
|
|
1591
|
+
tools: numericDelta(baseReport.summary.tools, headReport.summary.tools),
|
|
1592
|
+
estimatedTokens: tokenDelta(
|
|
1593
|
+
baseReport.summary.estimatedTokens,
|
|
1594
|
+
headReport.summary.estimatedTokens
|
|
1595
|
+
),
|
|
1596
|
+
overlapClusters: numericDelta(
|
|
1597
|
+
baseReport.overlapClusters.length,
|
|
1598
|
+
headReport.overlapClusters.length
|
|
1599
|
+
)
|
|
1600
|
+
},
|
|
1601
|
+
servers: diffServers(baseServers, headServers),
|
|
1602
|
+
tools: diffTools(baseTools, headTools),
|
|
1603
|
+
overlapClusters: diffOverlapClusters(baseClusters, headClusters),
|
|
1604
|
+
thresholds: [],
|
|
1605
|
+
recommendations: [],
|
|
1606
|
+
warnings: buildWarnings(baseReport, headReport)
|
|
1607
|
+
};
|
|
1608
|
+
report.recommendations = buildDiffRecommendations(report);
|
|
1609
|
+
return report;
|
|
1610
|
+
}
|
|
1611
|
+
function overlapClusterIdentity(cluster) {
|
|
1612
|
+
return JSON.stringify(cluster.tools.map((tool) => `${tool.server}.${tool.name}`).sort());
|
|
1613
|
+
}
|
|
1614
|
+
function diffServers(baseServers, headServers) {
|
|
1615
|
+
const added = [];
|
|
1616
|
+
const removed = [];
|
|
1617
|
+
const changed = [];
|
|
1618
|
+
for (const [name, headServer] of headServers) {
|
|
1619
|
+
const baseServer = baseServers.get(name);
|
|
1620
|
+
if (!baseServer) {
|
|
1621
|
+
added.push(toDiffServer(headServer));
|
|
1622
|
+
continue;
|
|
1623
|
+
}
|
|
1624
|
+
const serverChange = diffExistingServer(baseServer, headServer);
|
|
1625
|
+
if (isServerChanged(serverChange)) {
|
|
1626
|
+
changed.push(serverChange);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
for (const [name, baseServer] of baseServers) {
|
|
1630
|
+
if (!headServers.has(name)) {
|
|
1631
|
+
removed.push(toDiffServer(baseServer));
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
return {
|
|
1635
|
+
added: added.sort(compareServersByTokens),
|
|
1636
|
+
removed: removed.sort(compareServersByTokens),
|
|
1637
|
+
changed: changed.sort(compareServerChanges)
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
function diffTools(baseTools, headTools) {
|
|
1641
|
+
const added = [];
|
|
1642
|
+
const removed = [];
|
|
1643
|
+
const changed = [];
|
|
1644
|
+
for (const [id, headTool] of headTools) {
|
|
1645
|
+
const baseTool = baseTools.get(id);
|
|
1646
|
+
if (!baseTool) {
|
|
1647
|
+
added.push(headTool);
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
const toolChange = diffExistingTool(baseTool, headTool);
|
|
1651
|
+
if (isToolChanged(toolChange)) {
|
|
1652
|
+
changed.push(toolChange);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
for (const [id, baseTool] of baseTools) {
|
|
1656
|
+
if (!headTools.has(id)) {
|
|
1657
|
+
removed.push(baseTool);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
return {
|
|
1661
|
+
added: added.sort(compareToolsByTokens),
|
|
1662
|
+
removed: removed.sort(compareToolsByTokens),
|
|
1663
|
+
changed: changed.sort(compareToolChanges)
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
function diffOverlapClusters(baseClusters, headClusters) {
|
|
1667
|
+
const added = [];
|
|
1668
|
+
const removed = [];
|
|
1669
|
+
for (const [id, headCluster] of headClusters) {
|
|
1670
|
+
if (!baseClusters.has(id)) {
|
|
1671
|
+
added.push(headCluster);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
for (const [id, baseCluster] of baseClusters) {
|
|
1675
|
+
if (!headClusters.has(id)) {
|
|
1676
|
+
removed.push(baseCluster);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
return {
|
|
1680
|
+
added: added.sort(compareClusters),
|
|
1681
|
+
removed: removed.sort(compareClusters)
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
function diffExistingServer(base, head) {
|
|
1685
|
+
return {
|
|
1686
|
+
name: head.name,
|
|
1687
|
+
toolCount: numericDelta(base.toolCount, head.toolCount),
|
|
1688
|
+
estimatedTokens: tokenDelta(base.estimatedTokens, head.estimatedTokens),
|
|
1689
|
+
transport: valueDelta(base.transport, head.transport),
|
|
1690
|
+
sourceConfigPath: valueDelta(base.sourceConfigPath, head.sourceConfigPath),
|
|
1691
|
+
command: valueDelta(base.command ?? null, head.command ?? null),
|
|
1692
|
+
args: valueDelta(base.args ?? null, head.args ?? null),
|
|
1693
|
+
urlHost: valueDelta(base.urlHost ?? null, head.urlHost ?? null),
|
|
1694
|
+
inspectionMode: valueDelta(base.inspectionMode, head.inspectionMode),
|
|
1695
|
+
confidence: valueDelta(base.confidence, head.confidence)
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
function diffExistingTool(base, head) {
|
|
1699
|
+
return {
|
|
1700
|
+
server: head.server,
|
|
1701
|
+
name: head.name,
|
|
1702
|
+
estimatedTokens: tokenDelta(base.estimatedTokens, head.estimatedTokens),
|
|
1703
|
+
descriptionChanged: (base.description ?? null) !== (head.description ?? null),
|
|
1704
|
+
inputSchemaPresenceChanged: base.hasInputSchema !== head.hasInputSchema
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
function isServerChanged(change) {
|
|
1708
|
+
return change.toolCount.delta !== 0 || change.estimatedTokens.delta.claude !== 0 || change.estimatedTokens.delta.openaiCl100k !== 0 || change.transport.changed || change.sourceConfigPath.changed || change.command.changed || change.args.changed || change.urlHost.changed || change.inspectionMode.changed || change.confidence.changed;
|
|
1709
|
+
}
|
|
1710
|
+
function isToolChanged(change) {
|
|
1711
|
+
return change.estimatedTokens.delta.claude !== 0 || change.estimatedTokens.delta.openaiCl100k !== 0 || change.descriptionChanged || change.inputSchemaPresenceChanged;
|
|
1712
|
+
}
|
|
1713
|
+
function toDiffServer(server) {
|
|
1714
|
+
return {
|
|
1715
|
+
name: server.name,
|
|
1716
|
+
sourceConfigPath: server.sourceConfigPath,
|
|
1717
|
+
transport: server.transport,
|
|
1718
|
+
command: server.command,
|
|
1719
|
+
args: server.args,
|
|
1720
|
+
urlHost: server.urlHost,
|
|
1721
|
+
toolCount: server.toolCount,
|
|
1722
|
+
estimatedTokens: server.estimatedTokens,
|
|
1723
|
+
inspectionMode: server.inspectionMode,
|
|
1724
|
+
confidence: server.confidence
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
function toDiffTool(server, tool) {
|
|
1728
|
+
return {
|
|
1729
|
+
server: server.name,
|
|
1730
|
+
name: tool.name,
|
|
1731
|
+
description: tool.description,
|
|
1732
|
+
estimatedTokens: tool.estimatedTokens,
|
|
1733
|
+
hasInputSchema: tool.hasInputSchema
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
function toDiffOverlapCluster(cluster) {
|
|
1737
|
+
return {
|
|
1738
|
+
id: overlapClusterIdentity(cluster),
|
|
1739
|
+
label: cluster.label,
|
|
1740
|
+
score: cluster.score,
|
|
1741
|
+
tools: cluster.tools.map((tool) => ({ server: tool.server, name: tool.name })),
|
|
1742
|
+
recommendation: cluster.recommendation
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
function buildToolMap(report) {
|
|
1746
|
+
const tools = /* @__PURE__ */ new Map();
|
|
1747
|
+
for (const server of report.servers) {
|
|
1748
|
+
for (const tool of server.tools) {
|
|
1749
|
+
tools.set(toolKey(server.name, tool.name), toDiffTool(server, tool));
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
return tools;
|
|
1753
|
+
}
|
|
1754
|
+
function buildClusterMap(report) {
|
|
1755
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
1756
|
+
for (const cluster of report.overlapClusters) {
|
|
1757
|
+
clusters.set(overlapClusterIdentity(cluster), toDiffOverlapCluster(cluster));
|
|
1758
|
+
}
|
|
1759
|
+
return clusters;
|
|
1760
|
+
}
|
|
1761
|
+
function buildWarnings(baseReport, headReport) {
|
|
1762
|
+
const warnings = [];
|
|
1763
|
+
if (baseReport.version !== headReport.version) {
|
|
1764
|
+
warnings.push(
|
|
1765
|
+
`Base report version ${baseReport.version} differs from head report version ${headReport.version}.`
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
if (baseReport.metadata.staticOnly || headReport.metadata.staticOnly) {
|
|
1769
|
+
warnings.push(
|
|
1770
|
+
"One or both reports were generated with --no-exec; live tool/schema regressions may be hidden."
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
return warnings;
|
|
1774
|
+
}
|
|
1775
|
+
function buildDiffRecommendations(report) {
|
|
1776
|
+
const recommendations = [];
|
|
1777
|
+
if (positiveIncrease(report.summary.estimatedTokens.delta.claude) > 0) {
|
|
1778
|
+
recommendations.push({
|
|
1779
|
+
type: "budget",
|
|
1780
|
+
message: "Review the largest token increases before merging this MCP config change."
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
if (report.servers.added.length > 0 || report.tools.added.length > 0) {
|
|
1784
|
+
recommendations.push({
|
|
1785
|
+
type: "profile",
|
|
1786
|
+
message: "Keep new MCP surfaces scoped to the workflows that actually need them."
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
if (report.overlapClusters.added.length > 0) {
|
|
1790
|
+
recommendations.push({
|
|
1791
|
+
type: "overlap",
|
|
1792
|
+
message: "Resolve new overlapping tool clusters or document why both tools should stay exposed."
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
if (recommendations.length === 0) {
|
|
1796
|
+
recommendations.push({
|
|
1797
|
+
type: "status",
|
|
1798
|
+
message: "No MCP context regression was detected in this diff."
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
return recommendations;
|
|
1802
|
+
}
|
|
1803
|
+
function tokenDelta(base, head) {
|
|
1804
|
+
return {
|
|
1805
|
+
base,
|
|
1806
|
+
head,
|
|
1807
|
+
delta: {
|
|
1808
|
+
claude: head.claude - base.claude,
|
|
1809
|
+
openaiCl100k: head.openaiCl100k - base.openaiCl100k
|
|
1810
|
+
}
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
function numericDelta(base, head) {
|
|
1814
|
+
return {
|
|
1815
|
+
base,
|
|
1816
|
+
head,
|
|
1817
|
+
delta: head - base
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
function valueDelta(base, head) {
|
|
1821
|
+
return {
|
|
1822
|
+
base,
|
|
1823
|
+
head,
|
|
1824
|
+
changed: JSON.stringify(base) !== JSON.stringify(head)
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
function toolKey(server, tool) {
|
|
1828
|
+
return `${server}\0${tool}`;
|
|
1829
|
+
}
|
|
1830
|
+
function positiveIncrease(value) {
|
|
1831
|
+
return Math.max(0, value);
|
|
1832
|
+
}
|
|
1833
|
+
function compareServersByTokens(a, b) {
|
|
1834
|
+
return b.estimatedTokens.claude - a.estimatedTokens.claude || a.name.localeCompare(b.name);
|
|
1835
|
+
}
|
|
1836
|
+
function compareServerChanges(a, b) {
|
|
1837
|
+
return Math.abs(b.estimatedTokens.delta.claude) - Math.abs(a.estimatedTokens.delta.claude) || a.name.localeCompare(b.name);
|
|
1838
|
+
}
|
|
1839
|
+
function compareToolsByTokens(a, b) {
|
|
1840
|
+
return b.estimatedTokens.claude - a.estimatedTokens.claude || `${a.server}.${a.name}`.localeCompare(`${b.server}.${b.name}`);
|
|
1841
|
+
}
|
|
1842
|
+
function compareToolChanges(a, b) {
|
|
1843
|
+
return Math.abs(b.estimatedTokens.delta.claude) - Math.abs(a.estimatedTokens.delta.claude) || `${a.server}.${a.name}`.localeCompare(`${b.server}.${b.name}`);
|
|
1844
|
+
}
|
|
1845
|
+
function compareClusters(a, b) {
|
|
1846
|
+
return b.score - a.score || a.id.localeCompare(b.id);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
// src/diff/thresholds.ts
|
|
1850
|
+
function evaluateDiffThresholds(report, options) {
|
|
1851
|
+
const results = [];
|
|
1852
|
+
addThreshold(results, {
|
|
1853
|
+
flag: "--max-token-increase",
|
|
1854
|
+
tokenizer: options.tokenizer,
|
|
1855
|
+
allowed: options.maxTokenIncrease,
|
|
1856
|
+
actual: options.tokenizer === "openai" ? report.summary.estimatedTokens.delta.openaiCl100k : report.summary.estimatedTokens.delta.claude
|
|
1857
|
+
});
|
|
1858
|
+
addThreshold(results, {
|
|
1859
|
+
flag: "--max-tool-increase",
|
|
1860
|
+
allowed: options.maxToolIncrease,
|
|
1861
|
+
actual: report.summary.tools.delta
|
|
1862
|
+
});
|
|
1863
|
+
addThreshold(results, {
|
|
1864
|
+
flag: "--max-server-increase",
|
|
1865
|
+
allowed: options.maxServerIncrease,
|
|
1866
|
+
actual: report.summary.servers.delta
|
|
1867
|
+
});
|
|
1868
|
+
addThreshold(results, {
|
|
1869
|
+
flag: "--max-overlap-increase",
|
|
1870
|
+
allowed: options.maxOverlapIncrease,
|
|
1871
|
+
actual: report.overlapClusters.added.length
|
|
1872
|
+
});
|
|
1873
|
+
return results;
|
|
1874
|
+
}
|
|
1875
|
+
function hasThresholdFailure(report) {
|
|
1876
|
+
return report.thresholds.some((threshold) => threshold.exceeded);
|
|
1877
|
+
}
|
|
1878
|
+
function addThreshold(results, input) {
|
|
1879
|
+
if (input.allowed === void 0) {
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
const actualIncrease = Math.max(0, input.actual);
|
|
1883
|
+
const result = {
|
|
1884
|
+
flag: input.flag,
|
|
1885
|
+
allowed: input.allowed,
|
|
1886
|
+
actual: actualIncrease,
|
|
1887
|
+
exceeded: actualIncrease > input.allowed
|
|
1888
|
+
};
|
|
1889
|
+
if (input.tokenizer !== void 0) {
|
|
1890
|
+
result.tokenizer = input.tokenizer;
|
|
1891
|
+
}
|
|
1892
|
+
results.push(result);
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// src/reporters/diffHumanReporter.ts
|
|
1896
|
+
import pc2 from "picocolors";
|
|
1897
|
+
function renderDiffHumanReport(report, options) {
|
|
1898
|
+
const lines = [];
|
|
1899
|
+
lines.push(pc2.bold("tare-mcp diff - MCP context regression"));
|
|
1900
|
+
lines.push("");
|
|
1901
|
+
lines.push(`Base: ${report.base.path} (${report.base.reportVersion})`);
|
|
1902
|
+
lines.push(`Head: ${report.head.path} (${report.head.reportVersion})`);
|
|
1903
|
+
lines.push("");
|
|
1904
|
+
for (const warning of report.warnings) {
|
|
1905
|
+
lines.push(pc2.yellow(`Warning: ${warning}`));
|
|
1906
|
+
}
|
|
1907
|
+
if (report.warnings.length > 0) {
|
|
1908
|
+
lines.push("");
|
|
1909
|
+
}
|
|
1910
|
+
lines.push("Summary:");
|
|
1911
|
+
lines.push(`- Servers: ${formatDeltaLine(report.summary.servers)}`);
|
|
1912
|
+
lines.push(`- Tools: ${formatDeltaLine(report.summary.tools)}`);
|
|
1913
|
+
lines.push(`- Claude tokens: ${formatTokenDeltaLine(report.summary.estimatedTokens, "claude")}`);
|
|
1914
|
+
lines.push(
|
|
1915
|
+
`- OpenAI cl100k tokens: ${formatTokenDeltaLine(report.summary.estimatedTokens, "openai")}`
|
|
1916
|
+
);
|
|
1917
|
+
lines.push(`- Overlap clusters: ${formatDeltaLine(report.summary.overlapClusters)}`);
|
|
1918
|
+
pushServerSection(lines, "New servers", report.servers.added, options.tokenizer);
|
|
1919
|
+
pushServerSection(lines, "Removed servers", report.servers.removed, options.tokenizer);
|
|
1920
|
+
pushServerChangeSection(lines, report.servers.changed, options.tokenizer);
|
|
1921
|
+
pushToolSection(lines, "New tools", report.tools.added, options.tokenizer);
|
|
1922
|
+
pushToolSection(lines, "Removed tools", report.tools.removed, options.tokenizer);
|
|
1923
|
+
pushToolChangeSection(lines, report.tools.changed, options.tokenizer);
|
|
1924
|
+
pushOverlapSection(lines, report);
|
|
1925
|
+
pushThresholdSection(lines, report, options.tokenizer);
|
|
1926
|
+
pushRecommendations(lines, report);
|
|
1927
|
+
return `${lines.join("\n")}
|
|
1928
|
+
`;
|
|
1929
|
+
}
|
|
1930
|
+
function renderDiffThresholdFailure(report, options) {
|
|
1931
|
+
const failures = report.thresholds.filter((threshold) => threshold.exceeded);
|
|
1932
|
+
if (failures.length === 0) {
|
|
1933
|
+
return "";
|
|
1934
|
+
}
|
|
1935
|
+
const lines = [];
|
|
1936
|
+
lines.push("");
|
|
1937
|
+
lines.push(pc2.red(pc2.bold("FAILED: MCP context regression threshold exceeded.")));
|
|
1938
|
+
lines.push("");
|
|
1939
|
+
for (const failure of failures) {
|
|
1940
|
+
const label = thresholdLabel(failure.flag, failure.tokenizer ?? options.tokenizer);
|
|
1941
|
+
const isToken = failure.flag === "--max-token-increase";
|
|
1942
|
+
const fmt = isToken ? approx2 : formatNumber2;
|
|
1943
|
+
lines.push(
|
|
1944
|
+
`${failure.flag}: allowed ${fmt(failure.allowed)}, actual ${fmt(failure.actual)} ${label}`
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
const serverOffenders = tokenServerOffenders(report, options.tokenizer);
|
|
1948
|
+
if (serverOffenders.length > 0) {
|
|
1949
|
+
lines.push("");
|
|
1950
|
+
lines.push("Top server increases:");
|
|
1951
|
+
for (const [index, offender] of serverOffenders.slice(0, 5).entries()) {
|
|
1952
|
+
lines.push(
|
|
1953
|
+
`${index + 1}. ${padRight2(offender.label, 20)} ${approxSigned(offender.tokens)} tokens (${offender.details})`
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
const toolOffenders = tokenToolOffenders(report, options.tokenizer);
|
|
1958
|
+
if (toolOffenders.length > 0) {
|
|
1959
|
+
lines.push("");
|
|
1960
|
+
lines.push("Top tool increases:");
|
|
1961
|
+
for (const [index, offender] of toolOffenders.slice(0, 5).entries()) {
|
|
1962
|
+
lines.push(
|
|
1963
|
+
`${index + 1}. ${padRight2(offender.label, 34)} ${approxSigned(offender.tokens)} tokens (${offender.details})`
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
return `${lines.join("\n")}
|
|
1968
|
+
`;
|
|
1969
|
+
}
|
|
1970
|
+
function pushServerSection(lines, title, servers, tokenizer) {
|
|
1971
|
+
if (servers.length === 0) {
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
lines.push("");
|
|
1975
|
+
lines.push(`${title}:`);
|
|
1976
|
+
const tokenizerLabel = tokenizer === "openai" ? "OpenAI cl100k" : "Claude";
|
|
1977
|
+
for (const server of servers.slice(0, 10)) {
|
|
1978
|
+
lines.push(
|
|
1979
|
+
`- ${server.name}: ${server.toolCount} tools, ${approx2(tokenValue(server.estimatedTokens, tokenizer))} ${tokenizerLabel} tokens`
|
|
1980
|
+
);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
function pushServerChangeSection(lines, servers, tokenizer) {
|
|
1984
|
+
const materialChanges = servers.filter(
|
|
1985
|
+
(server) => server.toolCount.delta !== 0 || tokenValue(server.estimatedTokens.delta, tokenizer) !== 0
|
|
1986
|
+
);
|
|
1987
|
+
if (materialChanges.length === 0) {
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
const tokenizerLabel = tokenizer === "openai" ? "OpenAI cl100k" : "Claude";
|
|
1991
|
+
lines.push("");
|
|
1992
|
+
lines.push("Largest changes to existing servers:");
|
|
1993
|
+
for (const server of materialChanges.slice(0, 10)) {
|
|
1994
|
+
lines.push(
|
|
1995
|
+
`- ${server.name}: ${formatDeltaLine(server.toolCount)} tools, ${approxSigned(
|
|
1996
|
+
tokenValue(server.estimatedTokens.delta, tokenizer)
|
|
1997
|
+
)} ${tokenizerLabel} tokens`
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
function pushToolSection(lines, title, tools, tokenizer) {
|
|
2002
|
+
if (tools.length === 0) {
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
lines.push("");
|
|
2006
|
+
lines.push(`${title}:`);
|
|
2007
|
+
const tokenizerLabel = tokenizer === "openai" ? "OpenAI cl100k" : "Claude";
|
|
2008
|
+
for (const tool of tools.slice(0, 10)) {
|
|
2009
|
+
lines.push(
|
|
2010
|
+
`- ${tool.server}.${tool.name}: ${approx2(tokenValue(tool.estimatedTokens, tokenizer))} ${tokenizerLabel} tokens`
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
function pushToolChangeSection(lines, tools, tokenizer) {
|
|
2015
|
+
const materialChanges = tools.filter(
|
|
2016
|
+
(tool) => tokenValue(tool.estimatedTokens.delta, tokenizer) !== 0 || tool.descriptionChanged
|
|
2017
|
+
);
|
|
2018
|
+
if (materialChanges.length === 0) {
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
const tokenizerLabel = tokenizer === "openai" ? "OpenAI cl100k" : "Claude";
|
|
2022
|
+
lines.push("");
|
|
2023
|
+
lines.push("Largest changes to existing tools:");
|
|
2024
|
+
for (const tool of materialChanges.slice(0, 10)) {
|
|
2025
|
+
const notes = [
|
|
2026
|
+
tool.descriptionChanged ? "description changed" : void 0,
|
|
2027
|
+
tool.inputSchemaPresenceChanged ? "schema presence changed" : void 0
|
|
2028
|
+
].filter(Boolean);
|
|
2029
|
+
const suffix = notes.length > 0 ? ` (${notes.join(", ")})` : "";
|
|
2030
|
+
lines.push(
|
|
2031
|
+
`- ${tool.server}.${tool.name}: ${approxSigned(
|
|
2032
|
+
tokenValue(tool.estimatedTokens.delta, tokenizer)
|
|
2033
|
+
)} ${tokenizerLabel} tokens${suffix}`
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
function pushOverlapSection(lines, report) {
|
|
2038
|
+
if (report.overlapClusters.added.length === 0 && report.overlapClusters.removed.length === 0) {
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
if (report.overlapClusters.added.length > 0) {
|
|
2042
|
+
lines.push("");
|
|
2043
|
+
lines.push("New overlap clusters:");
|
|
2044
|
+
for (const cluster of report.overlapClusters.added.slice(0, 5)) {
|
|
2045
|
+
lines.push(`- ${cluster.label}`);
|
|
2046
|
+
for (const tool of cluster.tools) {
|
|
2047
|
+
lines.push(` ${tool.server}.${tool.name}`);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
if (report.overlapClusters.removed.length > 0) {
|
|
2052
|
+
lines.push("");
|
|
2053
|
+
lines.push("Resolved overlap clusters:");
|
|
2054
|
+
for (const cluster of report.overlapClusters.removed.slice(0, 5)) {
|
|
2055
|
+
lines.push(`- ${cluster.label}`);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
function pushThresholdSection(lines, report, tokenizer) {
|
|
2060
|
+
if (report.thresholds.length === 0) {
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
lines.push("");
|
|
2064
|
+
lines.push("Thresholds:");
|
|
2065
|
+
for (const threshold of report.thresholds) {
|
|
2066
|
+
const status = threshold.exceeded ? pc2.red("fail") : pc2.green("pass");
|
|
2067
|
+
const label = thresholdLabel(threshold.flag, threshold.tokenizer ?? tokenizer);
|
|
2068
|
+
const isToken = threshold.flag === "--max-token-increase";
|
|
2069
|
+
const fmt = isToken ? approx2 : formatNumber2;
|
|
2070
|
+
lines.push(
|
|
2071
|
+
`- ${threshold.flag}: ${status} (${fmt(threshold.actual)} / ${fmt(threshold.allowed)} ${label})`
|
|
2072
|
+
);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
function pushRecommendations(lines, report) {
|
|
2076
|
+
if (report.recommendations.length === 0) {
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
lines.push("");
|
|
2080
|
+
lines.push("Recommendations:");
|
|
2081
|
+
for (const recommendation of report.recommendations) {
|
|
2082
|
+
lines.push(`- ${recommendation.message}`);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
function tokenServerOffenders(report, tokenizer) {
|
|
2086
|
+
const added = report.servers.added.map((server) => ({
|
|
2087
|
+
label: server.name,
|
|
2088
|
+
tokens: tokenValue(server.estimatedTokens, tokenizer),
|
|
2089
|
+
details: `new server, ${server.toolCount} tools`
|
|
2090
|
+
}));
|
|
2091
|
+
const changed = report.servers.changed.map((server) => ({
|
|
2092
|
+
label: server.name,
|
|
2093
|
+
tokens: tokenValue(server.estimatedTokens.delta, tokenizer),
|
|
2094
|
+
details: `${formatSignedNumber(server.toolCount.delta)} tools`
|
|
2095
|
+
})).filter((offender) => offender.tokens > 0);
|
|
2096
|
+
return [...added, ...changed].sort(compareOffenders);
|
|
2097
|
+
}
|
|
2098
|
+
function tokenToolOffenders(report, tokenizer) {
|
|
2099
|
+
const added = report.tools.added.map((tool) => ({
|
|
2100
|
+
label: `${tool.server}.${tool.name}`,
|
|
2101
|
+
tokens: tokenValue(tool.estimatedTokens, tokenizer),
|
|
2102
|
+
details: "new tool"
|
|
2103
|
+
}));
|
|
2104
|
+
const changed = report.tools.changed.map((tool) => ({
|
|
2105
|
+
label: `${tool.server}.${tool.name}`,
|
|
2106
|
+
tokens: tokenValue(tool.estimatedTokens.delta, tokenizer),
|
|
2107
|
+
details: tool.descriptionChanged ? "description changed" : "existing tool"
|
|
2108
|
+
})).filter((offender) => offender.tokens > 0);
|
|
2109
|
+
return [...added, ...changed].sort(compareOffenders);
|
|
2110
|
+
}
|
|
2111
|
+
function compareOffenders(a, b) {
|
|
2112
|
+
return b.tokens - a.tokens || a.label.localeCompare(b.label);
|
|
2113
|
+
}
|
|
2114
|
+
function formatDeltaLine(delta) {
|
|
2115
|
+
return `${formatNumber2(delta.base)} -> ${formatNumber2(delta.head)} (${formatSignedNumber(
|
|
2116
|
+
delta.delta
|
|
2117
|
+
)})`;
|
|
2118
|
+
}
|
|
2119
|
+
function formatTokenDeltaLine(delta, tokenizer) {
|
|
2120
|
+
return `${approx2(tokenValue(delta.base, tokenizer))} -> ${approx2(
|
|
2121
|
+
tokenValue(delta.head, tokenizer)
|
|
2122
|
+
)} (${approxSigned(tokenValue(delta.delta, tokenizer))})`;
|
|
2123
|
+
}
|
|
2124
|
+
function tokenValue(tokens, tokenizer) {
|
|
2125
|
+
return tokenizer === "openai" ? tokens.openaiCl100k : tokens.claude;
|
|
2126
|
+
}
|
|
2127
|
+
function thresholdLabel(flag, tokenizer) {
|
|
2128
|
+
if (flag === "--max-token-increase") {
|
|
2129
|
+
return tokenizer === "openai" ? "OpenAI cl100k tokens" : "Claude tokens";
|
|
2130
|
+
}
|
|
2131
|
+
if (flag === "--max-server-increase") {
|
|
2132
|
+
return "servers";
|
|
2133
|
+
}
|
|
2134
|
+
if (flag === "--max-overlap-increase") {
|
|
2135
|
+
return "new overlap clusters";
|
|
2136
|
+
}
|
|
2137
|
+
return "tools";
|
|
2138
|
+
}
|
|
2139
|
+
function formatNumber2(value) {
|
|
2140
|
+
return Math.round(value).toLocaleString("en-US");
|
|
2141
|
+
}
|
|
2142
|
+
function formatSignedNumber(value) {
|
|
2143
|
+
if (value > 0) {
|
|
2144
|
+
return `+${formatNumber2(value)}`;
|
|
2145
|
+
}
|
|
2146
|
+
return formatNumber2(value);
|
|
2147
|
+
}
|
|
2148
|
+
function approx2(value) {
|
|
2149
|
+
return `~${formatNumber2(value)}`;
|
|
2150
|
+
}
|
|
2151
|
+
function approxSigned(value) {
|
|
2152
|
+
if (value > 0) {
|
|
2153
|
+
return `~+${formatNumber2(value)}`;
|
|
2154
|
+
}
|
|
2155
|
+
if (value < 0) {
|
|
2156
|
+
return `~-${formatNumber2(Math.abs(value))}`;
|
|
2157
|
+
}
|
|
2158
|
+
return "~0";
|
|
2159
|
+
}
|
|
2160
|
+
function padRight2(text, length) {
|
|
2161
|
+
return text.padEnd(length, " ");
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
// src/reporters/diffJsonReporter.ts
|
|
2165
|
+
function renderDiffJsonReport(report) {
|
|
2166
|
+
return `${JSON.stringify(report, null, 2)}
|
|
2167
|
+
`;
|
|
2168
|
+
}
|
|
2169
|
+
export {
|
|
2170
|
+
LocalClaudeEstimator,
|
|
2171
|
+
OpenAICl100kCounter,
|
|
2172
|
+
OverlapDetector,
|
|
2173
|
+
ReportLoadError,
|
|
2174
|
+
TareReportSchema,
|
|
2175
|
+
TokenEstimator,
|
|
2176
|
+
VERSION,
|
|
2177
|
+
analyzeServers,
|
|
2178
|
+
buildRecommendations,
|
|
2179
|
+
buildServerEnv,
|
|
2180
|
+
createStaticInspection,
|
|
2181
|
+
diffReports,
|
|
2182
|
+
discoverConfigs,
|
|
2183
|
+
evaluateDiffThresholds,
|
|
2184
|
+
getDefaultConfigCandidates,
|
|
2185
|
+
hasThresholdFailure,
|
|
2186
|
+
inspectStdioServer,
|
|
2187
|
+
inspectStreamableHttpServer,
|
|
2188
|
+
loadReport,
|
|
2189
|
+
normalizeServer,
|
|
2190
|
+
overlapClusterIdentity,
|
|
2191
|
+
parseConfigFile,
|
|
2192
|
+
parseConfigText,
|
|
2193
|
+
renderBudgetFailure,
|
|
2194
|
+
renderDiffHumanReport,
|
|
2195
|
+
renderDiffJsonReport,
|
|
2196
|
+
renderDiffThresholdFailure,
|
|
2197
|
+
renderHumanReport,
|
|
2198
|
+
renderJsonReport
|
|
2199
|
+
};
|