@tokenzero/mcp 0.1.1 → 0.1.4
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/README.md +2 -2
- package/bin/tokenzero-mcp.mjs +180 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,8 +14,8 @@ claude mcp add tokenzero -- npx -y @tokenzero/mcp # Claude Code
|
|
|
14
14
|
|---|---|---|
|
|
15
15
|
| `audit_repo_token_waste` | locally | Static audit of a local directory (default: the working directory), a public GitHub repo, or a diff. Nothing from a local directory is uploaded. |
|
|
16
16
|
| `calculate_model_arbitrage` | locally | Prices a workload across models at list prices, with and without prompt caching. |
|
|
17
|
-
| `optimize_prompt_payload` | TokenZero service | Rewrites a request body so its static prefix is cacheable. Sends the body you pass to the service
|
|
17
|
+
| `optimize_prompt_payload` | TokenZero service | Rewrites a request body so its static prefix is cacheable. Sends the body you pass to the TokenZero service (`https://tokenzero.sh` unless `TOKENZERO_URL` is set); do not include secrets. |
|
|
18
18
|
|
|
19
|
-
Optional environment: `TOKENZERO_URL` (
|
|
19
|
+
Optional environment: `TOKENZERO_URL` (another gateway for `optimize_prompt_payload`; default `https://tokenzero.sh`), `GITHUB_TOKEN` (raises GitHub rate limits for repo audits).
|
|
20
20
|
|
|
21
21
|
Audits are static heuristics: estimates at list prices, not measurements. Node >= 18. Single file, no dependencies. Apache-2.0.
|
package/bin/tokenzero-mcp.mjs
CHANGED
|
@@ -117,7 +117,7 @@ function priceFor(model) {
|
|
|
117
117
|
const key = normalizeModel(model);
|
|
118
118
|
return key ? { key, ...PRICES[key] } : null;
|
|
119
119
|
}
|
|
120
|
-
function costOf(model, u) {
|
|
120
|
+
function costOf(model, u, factor = 1) {
|
|
121
121
|
const p = priceFor(model);
|
|
122
122
|
if (!p) return null;
|
|
123
123
|
const tier = p.longCtx && p.longCtxAt && u.input > p.longCtxAt ? { ...p, ...p.longCtx } : p;
|
|
@@ -125,7 +125,7 @@ function costOf(model, u) {
|
|
|
125
125
|
const written = u.cacheWrite ?? 0;
|
|
126
126
|
const fresh = Math.max(0, u.input - cached - written);
|
|
127
127
|
const M = 1e6;
|
|
128
|
-
return fresh * tier.input / M + cached * (tier.cachedInput ?? tier.input) / M + written * (tier.cacheWrite ?? tier.input) / M + u.output * tier.output / M;
|
|
128
|
+
return factor * (fresh * tier.input / M + cached * (tier.cachedInput ?? tier.input) / M + written * (tier.cacheWrite ?? tier.input) / M + u.output * tier.output / M);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
// cli/src/tokens-lite.ts
|
|
@@ -202,7 +202,9 @@ function lex(src, path3) {
|
|
|
202
202
|
j++;
|
|
203
203
|
}
|
|
204
204
|
const end = Math.min(j, src.length);
|
|
205
|
-
|
|
205
|
+
const rep = (py ? /^\s*\*\s*(\d{1,6})\b/ : /^\.repeat\(\s*(\d{1,6})\s*\)/).exec(src.slice(end + 1, end + 40));
|
|
206
|
+
const body = src.slice(i + 1, end);
|
|
207
|
+
literals.push({ text: rep ? body.repeat(Math.max(1, Math.min(Number(rep[1]), Math.floor(2e5 / Math.max(1, body.length))))) : body, line });
|
|
206
208
|
blank(i + 1, end);
|
|
207
209
|
advance(i, end + 1);
|
|
208
210
|
i = end + 1;
|
|
@@ -221,6 +223,7 @@ function promptLike(t) {
|
|
|
221
223
|
const words = (t.match(/[A-Za-z]{2,}/g) ?? []).length;
|
|
222
224
|
return tags < 4 && cssRules < 3 && codeChars / t.length < 0.03 && words / (t.length / 6) > 0.5;
|
|
223
225
|
}
|
|
226
|
+
var REPEAT_TASK = /\b(classify|classification|categori[sz]e|sentiment|label (this|the|each)|is (this|it) spam|yes or no|true or false)\b/i;
|
|
224
227
|
var SIMPLE_TASK = /\b(classify|classification|categori[sz]e|sentiment|extract (the|all|every|any)|return (only |just )?(valid |a )?json|respond (only )?(with|in) (valid )?json|output (only )?(valid )?json|translate (this|the following|into)|summari[sz]e (this|the following|in \d+)|label (this|the|each)|is (this|it) spam|yes or no|true or false)\b/i;
|
|
225
228
|
var LLM_LIB_EXACT = /* @__PURE__ */ new Set(["openai", "anthropic", "@anthropic-ai/sdk", "ai", "litellm", "openai-agents", "@openai/agents", "instructor", "dspy", "dspy-ai", "guidance", "semantic-kernel", "haystack-ai", "autogen", "pyautogen", "autogen-agentchat", "crewai", "google-genai", "@google/genai", "mistralai", "@mistralai/mistralai", "cohere", "cohere-ai", "llama-index", "llama_index", "llamaindex", "langchain", "langgraph", "@langchain/core", "groq-sdk", "together-ai", "ollama"]);
|
|
226
229
|
var LLM_LIB_FAMILY = /^(@langchain\/[\w-]+|langchain[-_][\w-]+|langgraph[-_][\w-]+|llama[-_]index[-_][\w-]+|@ai-sdk\/[\w-]+|openai[-_]agents[-_][\w-]+)$/i;
|
|
@@ -253,6 +256,98 @@ function detectRepoKind(manifests2, files) {
|
|
|
253
256
|
return { kind: "unknown", reason: "no manifest signal" };
|
|
254
257
|
}
|
|
255
258
|
var DOWN = { high: "medium", medium: "low", low: "info", info: "info" };
|
|
259
|
+
var OPEN = "([{";
|
|
260
|
+
var CLOSE = ")]}";
|
|
261
|
+
function matchClose(m, from, limit = 4e4) {
|
|
262
|
+
let depth = 0;
|
|
263
|
+
for (let k = from; k < Math.min(m.length, from + limit); k++) {
|
|
264
|
+
const c = m[k];
|
|
265
|
+
if (OPEN.includes(c)) depth++;
|
|
266
|
+
else if (CLOSE.includes(c) && --depth === 0) return k;
|
|
267
|
+
}
|
|
268
|
+
return Math.min(m.length, from + limit) - 1;
|
|
269
|
+
}
|
|
270
|
+
function makeLocator(masked) {
|
|
271
|
+
const text = masked.join("\n");
|
|
272
|
+
const starts = [];
|
|
273
|
+
let o = 0;
|
|
274
|
+
for (const l of masked) {
|
|
275
|
+
starts.push(o);
|
|
276
|
+
o += l.length + 1;
|
|
277
|
+
}
|
|
278
|
+
const lineOf = (off) => {
|
|
279
|
+
let lo = 0, hi = starts.length - 1;
|
|
280
|
+
while (lo < hi) {
|
|
281
|
+
const mid = lo + hi + 1 >> 1;
|
|
282
|
+
if (starts[mid] <= off) lo = mid;
|
|
283
|
+
else hi = mid - 1;
|
|
284
|
+
}
|
|
285
|
+
return lo;
|
|
286
|
+
};
|
|
287
|
+
return { text, starts, lineOf };
|
|
288
|
+
}
|
|
289
|
+
var ASSIGN = (name) => new RegExp(`^\\s*(?:export\\s+)?(?:const|let|var|final|static|private|public|readonly|\\s)*\\s*(?:self\\.|this\\.)?${name}\\s*(?::[^=\\n]+)?=(?![=>])`);
|
|
290
|
+
var KEYWORDS = /* @__PURE__ */ new Set(["self", "this", "await", "async", "return", "model", "messages", "role", "content", "system", "user", "true", "false", "True", "False", "None", "null", "undefined", "new", "max_tokens", "temperature", "client", "resp", "response"]);
|
|
291
|
+
function assignmentScope(loc, masked, line) {
|
|
292
|
+
const eq = masked[line].search(/=(?![=>])/);
|
|
293
|
+
let end = line;
|
|
294
|
+
let k = loc.starts[line] + eq + 1;
|
|
295
|
+
for (; ; ) {
|
|
296
|
+
const lineEnd = loc.starts[end] + masked[end].length;
|
|
297
|
+
let opened = -1;
|
|
298
|
+
for (let q = k; q < lineEnd; q++) if (OPEN.includes(loc.text[q])) {
|
|
299
|
+
opened = q;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
if (opened >= 0) {
|
|
303
|
+
const close = matchClose(loc.text, opened, 2e4);
|
|
304
|
+
end = loc.lineOf(close);
|
|
305
|
+
k = close + 1;
|
|
306
|
+
if (k < loc.starts[end] + masked[end].length) continue;
|
|
307
|
+
}
|
|
308
|
+
const tail = masked[end].trimEnd();
|
|
309
|
+
if (end + 1 < masked.length && (/[+\\,(]$/.test(tail) || /^\s*[+.]/.test(masked[end + 1] ?? "")) && end - line < 60) {
|
|
310
|
+
end++;
|
|
311
|
+
k = loc.starts[end];
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
return { start: line, end };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function identifiersIn(masked, sc) {
|
|
318
|
+
const ids = /* @__PURE__ */ new Set();
|
|
319
|
+
for (let l = sc.start; l <= sc.end; l++) for (const m of (masked[l] ?? "").matchAll(/\b([A-Za-z_]\w{1,60})\b/g)) if (!KEYWORDS.has(m[1])) ids.add(m[1]);
|
|
320
|
+
return ids;
|
|
321
|
+
}
|
|
322
|
+
function resolveScopes(loc, masked, from, depth = 2) {
|
|
323
|
+
const out = [], seen = /* @__PURE__ */ new Set();
|
|
324
|
+
let frontier = [from];
|
|
325
|
+
for (let d = 0; d < depth && frontier.length; d++) {
|
|
326
|
+
const next = [];
|
|
327
|
+
for (const sc of frontier) for (const id of identifiersIn(masked, sc)) {
|
|
328
|
+
if (seen.has(id)) continue;
|
|
329
|
+
seen.add(id);
|
|
330
|
+
const re = ASSIGN(id);
|
|
331
|
+
let found = -1;
|
|
332
|
+
for (let l = sc.start; l >= 0; l--) if (re.test(masked[l])) {
|
|
333
|
+
found = l;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
if (found < 0) {
|
|
337
|
+
for (let l = sc.end + 1; l < masked.length; l++) if (re.test(masked[l])) {
|
|
338
|
+
found = l;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (found < 0 || found >= from.start && found <= from.end) continue;
|
|
343
|
+
const a = assignmentScope(loc, masked, found);
|
|
344
|
+
out.push(a);
|
|
345
|
+
next.push(a);
|
|
346
|
+
}
|
|
347
|
+
frontier = next;
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
256
351
|
function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
257
352
|
const findings = [];
|
|
258
353
|
const callSites = [];
|
|
@@ -264,7 +359,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
264
359
|
const { kind, reason } = detectRepoKind(manifests2, sources);
|
|
265
360
|
if (kind === "library") notes.push(`Library/SDK layout detected (${reason}): call sites are most likely pass-through wrappers where the caller chooses the model and prompt, so findings are downgraded one level and the estimate is discounted.`);
|
|
266
361
|
const promptLibrary = [];
|
|
267
|
-
const repoCaches = sources.some((f) => /cache_control|cacheControl|prompt_cache_key|promptCacheKey|cachedContent|cached_content/.test(f.content));
|
|
362
|
+
const repoCaches = sources.some((f) => /cache_control|cacheControl|prompt_cache_key|promptCacheKey|prompt_cache_options|promptCacheOptions|prompt_cache_breakpoint|promptCacheBreakpoint|prompt_cache_retention|promptCacheRetention|cachedContent|cached_content/.test(f.content));
|
|
268
363
|
const PROMPT_FILE = /(^|\/)[^/]*(prompt|instruction|template|persona)s?[^/]*\.[a-z]+$|(^|\/)prompts?\//i;
|
|
269
364
|
const PROMPT_NAME = /\b[\w.]*(prompt|system|instruction|template|persona)\w*\s*(:\s*[\w[\]|. ]+)?\s*[:=]\s*(f|r|rf|fr|b)?$/i;
|
|
270
365
|
for (const f of sources) {
|
|
@@ -277,12 +372,19 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
277
372
|
const lead = (masked[l.line - 1] ?? "").slice(0, Math.max(0, (masked[l.line - 1] ?? "").search(/["'`]|$/))).trimEnd();
|
|
278
373
|
if (PROMPT_FILE.test(f.path) || PROMPT_NAME.test(lead) || PROMPT_NAME.test((masked[l.line - 2] ?? "").trimEnd())) promptLibrary.push({ file: f.path, line: l.line, tokens });
|
|
279
374
|
}
|
|
280
|
-
const hasCacheControl = /cache_control|cacheControl|prompt_cache_key|promptCacheKey|cachedContent|cached_content/.test(src);
|
|
375
|
+
const hasCacheControl = /cache_control|cacheControl|prompt_cache_key|promptCacheKey|prompt_cache_options|promptCacheOptions|prompt_cache_breakpoint|promptCacheBreakpoint|prompt_cache_retention|promptCacheRetention|cachedContent|cached_content/.test(src);
|
|
281
376
|
const illustrative = EXAMPLE_PATH.test(f.path);
|
|
282
377
|
let repeatFlagged = false;
|
|
378
|
+
const loc = makeLocator(masked);
|
|
379
|
+
const callLines = [];
|
|
283
380
|
masked.forEach((code, i) => {
|
|
284
|
-
const
|
|
285
|
-
if (!
|
|
381
|
+
const hit = CALL_RE.find(([re]) => re.test(code));
|
|
382
|
+
if (!hit) return;
|
|
383
|
+
const m = hit[0].exec(code);
|
|
384
|
+
const close = matchClose(loc.text, loc.starts[i] + m.index + m[0].length - 1);
|
|
385
|
+
callLines.push({ line: i, sdk: hit[1], scope: { start: i, end: Math.max(i, loc.lineOf(close)) } });
|
|
386
|
+
});
|
|
387
|
+
callLines.forEach(({ line: i, sdk, scope }, n) => {
|
|
286
388
|
const local = [];
|
|
287
389
|
const push = (fd) => {
|
|
288
390
|
let sev = fd.severity;
|
|
@@ -290,16 +392,36 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
290
392
|
if (illustrative) sev = DOWN[sev];
|
|
291
393
|
local.push({ ...fd, severity: sev });
|
|
292
394
|
};
|
|
293
|
-
const
|
|
294
|
-
|
|
395
|
+
const refs = resolveScopes(loc, masked, scope);
|
|
396
|
+
if (sdk.startsWith("langchain")) {
|
|
397
|
+
const assigned = /^\s*(?:const|let|var|self\.|this\.)?\s*([A-Za-z_]\w*)\s*(?::[^=\n]+)?=/.exec(masked[i])?.[1];
|
|
398
|
+
if (assigned) for (let l = 0; l < masked.length; l++) {
|
|
399
|
+
const inv = new RegExp(`\\b${assigned}\\.(invoke|ainvoke|stream|astream|batch|abatch|call|predict|generate)\\s*\\(`).exec(masked[l]);
|
|
400
|
+
if (!inv) continue;
|
|
401
|
+
const close = matchClose(loc.text, loc.starts[l] + inv.index + inv[0].length - 1);
|
|
402
|
+
const sc = { start: l, end: Math.max(l, loc.lineOf(close)) };
|
|
403
|
+
refs.push(sc, ...resolveScopes(loc, masked, sc));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const inScopes = (l, scs) => scs.some((sc) => l.line - 1 >= sc.start && l.line - 1 <= sc.end);
|
|
407
|
+
let near = literals.filter((l) => inScopes(l, [scope, ...refs]));
|
|
408
|
+
if (!near.some((l) => l.text.trim().length > 20)) {
|
|
409
|
+
const prevEnd = n > 0 ? callLines[n - 1].scope.end : -1;
|
|
410
|
+
near = near.concat(literals.filter((l) => l.line - 1 > prevEnd && l.line - 1 >= i - 40 && l.line - 1 < i));
|
|
411
|
+
}
|
|
412
|
+
const spanText = lines.slice(scope.start, scope.end + 1).join("\n");
|
|
413
|
+
const nextStart = n + 1 < callLines.length ? callLines[n + 1].line : lines.length;
|
|
414
|
+
const fallbackWindow = lines.slice(i, Math.min(nextStart, i + 30)).join("\n");
|
|
415
|
+
const modelFrom = (t) => [...t.matchAll(MODEL_RE)].map((m) => m[1])[0] ?? null;
|
|
416
|
+
const model = modelFrom(spanText) ?? modelFrom(refs.map((r) => lines.slice(r.start, r.end + 1).join("\n")).join("\n")) ?? modelFrom(fallbackWindow);
|
|
295
417
|
callSites.push({ file: f.path, line: i + 1, model, sdk });
|
|
296
418
|
if (model) models[model] = (models[model] ?? 0) + 1;
|
|
297
419
|
const key = normalizeModel(model);
|
|
298
420
|
const price = key ? priceFor(key) : null;
|
|
299
|
-
const near = literals.filter((l) => l.line >= i + 1 - 40 && l.line <= i + 1 + 40);
|
|
300
421
|
const prompts = near.filter((l) => promptLike(l.text));
|
|
301
422
|
const codeCtx = masked.slice(Math.max(0, i - 25), i + 25).join("\n");
|
|
302
423
|
const simpleTask = near.some((l) => SIMPLE_TASK.test(l.text));
|
|
424
|
+
const repeatTask = near.some((l) => REPEAT_TASK.test(l.text));
|
|
303
425
|
const isAnthropic = sdk.includes("anthropic") || (model?.startsWith("claude") ?? false);
|
|
304
426
|
const bigTok = prompts.reduce((a, l) => a + countTokens(l.text), 0);
|
|
305
427
|
if (bigTok >= 300 && !hasCacheControl) {
|
|
@@ -310,24 +432,26 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
310
432
|
severity: isAnthropic ? bigTok >= 1500 ? "high" : cacheable ? "medium" : "low" : cacheable ? "medium" : "low",
|
|
311
433
|
file: f.path,
|
|
312
434
|
line: prompts[0].line,
|
|
313
|
-
title: `~${bigTok.toLocaleString()} static prompt tokens re-billed at full price on every call`,
|
|
314
|
-
detail: isAnthropic ? `This call sends a large fixed prompt but never marks a cache breakpoint, so Anthropic bills all ${bigTok} tokens at the full input rate each time instead of the cache-read rate (${readRate}% cheaper).` : `A fixed
|
|
435
|
+
title: isAnthropic ? `~${bigTok.toLocaleString()} static prompt tokens re-billed at full price on every call` : `~${bigTok.toLocaleString()} static prompt tokens sent on every call; only a stable prefix gets the cache discount`,
|
|
436
|
+
detail: (isAnthropic ? `This call sends a large fixed prompt but never marks a cache breakpoint, so Anthropic bills all ${bigTok} tokens at the full input rate each time instead of the cache-read rate (${readRate}% cheaper).` : `A fixed block of ~${bigTok} tokens goes out with every call. ${model ? model : "This provider"} discounts cached input ${readRate}%, but only for a byte-identical prefix of at least ${min} tokens that is routed to the same cache; anything volatile (dates, ids, user text) placed before or inside it, or traffic spread across cache shards, means paying full price.`) + (prompts[0].line !== i + 1 ? ` Sent by the call at ${f.path}:${i + 1}.` : ""),
|
|
315
437
|
fix: isAnthropic ? 'Route through TokenZero (breakpoints are injected automatically) or add `cache_control: {type: "ephemeral"}` to the last system block and the last tool.' : "Route through TokenZero to get a stable `prompt_cache_key`, or move dynamic content below the static prefix and set `prompt_cache_key` yourself.",
|
|
316
438
|
// OpenAI/Gemini already cache stable prefixes automatically; the recoverable part
|
|
317
439
|
// is prefix hygiene and cache routing, not the whole cache discount.
|
|
318
440
|
est_savings_pct: !cacheable ? 0 : isAnthropic ? Math.min(85, Math.round(bigTok / (bigTok + 400) * readRate)) : 15
|
|
319
441
|
});
|
|
320
442
|
}
|
|
321
|
-
|
|
322
|
-
|
|
443
|
+
const cascadeTo = key ? CASCADE_TARGET[key] : void 0;
|
|
444
|
+
const ratio = cascadeTo ? costOf(cascadeTo, { input: 1500, output: 200 }) / costOf(key, { input: 1500, output: 200 }) : 1;
|
|
445
|
+
if (key && price && cascadeTo && simpleTask && (price.tier === "frontier" || ratio <= 0.5)) {
|
|
446
|
+
const to = cascadeTo;
|
|
323
447
|
const a = costOf(key, { input: 1500, output: 200 }), b = costOf(to, { input: 1500, output: 200 });
|
|
324
448
|
push({
|
|
325
|
-
severity: "high",
|
|
449
|
+
severity: price.tier === "frontier" || ratio <= 0.2 ? "high" : "medium",
|
|
326
450
|
file: f.path,
|
|
327
451
|
line: i + 1,
|
|
328
452
|
title: `${key} used for a task whose prompt reads like extraction/classification`,
|
|
329
|
-
detail:
|
|
330
|
-
fix: `Opt into TokenZero cascade routing (\`X-TokenZero-Cascade: auto\`) so this call is routed to ${to} only when the classifier is confident
|
|
453
|
+
detail: `${price.tier === "frontier" ? "Frontier" : "Full-size"} pricing ($${price.input}/M in, $${price.output}/M out) on a constrained task. ${to} costs ~${Math.round((1 - b / a) * 100)}% less per call at this shape.`,
|
|
454
|
+
fix: `Opt into TokenZero cascade routing (\`X-TokenZero-Cascade: auto\`) so this call is routed to ${to} only when the classifier is confident (an opt-in shadow parity monitor is coming; nothing is promised), or switch the model here behind an eval.`,
|
|
331
455
|
est_savings_pct: Math.round((1 - b / a) * 100)
|
|
332
456
|
});
|
|
333
457
|
}
|
|
@@ -338,15 +462,15 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
338
462
|
file: f.path,
|
|
339
463
|
line: i + 1,
|
|
340
464
|
title: "LLM call inside a loop",
|
|
341
|
-
detail:
|
|
342
|
-
fix:
|
|
465
|
+
detail: 'Per-item calls repeat the full prompt prefix for every element. If items are independent and latency-tolerant, the provider Batch API is 50% cheaper, and OpenAI Flex processing (`service_tier: "flex"`) gives the same 50% on ordinary calls without restructuring; if the prefix is shared, caching pays off immediately.',
|
|
466
|
+
fix: 'Batch where possible, or set `service_tier: "flex"` on OpenAI calls that can wait; otherwise keep the shared prefix cache-formatted (TokenZero does this automatically).',
|
|
343
467
|
est_savings_pct: 25
|
|
344
468
|
});
|
|
345
469
|
}
|
|
346
|
-
if (
|
|
470
|
+
if (repeatTask && !repeatFlagged && !/seed["']?\s*[:=]|temperature["']?\s*[:=]\s*0\b|X-TokenZero-Cache/i.test(spanText)) {
|
|
347
471
|
repeatFlagged = true;
|
|
348
472
|
push({
|
|
349
|
-
severity: "
|
|
473
|
+
severity: "low",
|
|
350
474
|
file: f.path,
|
|
351
475
|
line: i + 1,
|
|
352
476
|
title: "Repeat-prone task with no response-cache opt-in",
|
|
@@ -355,7 +479,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
355
479
|
est_savings_pct: 10
|
|
356
480
|
});
|
|
357
481
|
}
|
|
358
|
-
if (isAnthropic && /\btools\s*[:=]/.test(masked.slice(
|
|
482
|
+
if (isAnthropic && /\btools\s*[:=]/.test(masked.slice(scope.start, scope.end + 1).join("\n")) && !hasCacheControl) {
|
|
359
483
|
push({
|
|
360
484
|
severity: "low",
|
|
361
485
|
file: f.path,
|
|
@@ -413,17 +537,32 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
413
537
|
function filesFromDiff(diff) {
|
|
414
538
|
const files = [];
|
|
415
539
|
let cur = null;
|
|
540
|
+
let lineNo = 0;
|
|
416
541
|
for (const line of diff.split("\n")) {
|
|
417
542
|
const m = /^\+\+\+ b\/(.+)$/.exec(line);
|
|
418
543
|
if (m) {
|
|
419
544
|
cur = { path: m[1], content: "" };
|
|
420
545
|
files.push(cur);
|
|
546
|
+
lineNo = 0;
|
|
421
547
|
continue;
|
|
422
548
|
}
|
|
423
549
|
if (!cur) continue;
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
550
|
+
const h = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
|
|
551
|
+
if (h) {
|
|
552
|
+
const start = Number(h[1]);
|
|
553
|
+
while (lineNo < start - 1) {
|
|
554
|
+
cur.content += "\n";
|
|
555
|
+
lineNo++;
|
|
556
|
+
}
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
560
|
+
cur.content += line.slice(1) + "\n";
|
|
561
|
+
lineNo++;
|
|
562
|
+
} else if (line.startsWith(" ")) {
|
|
563
|
+
cur.content += line.slice(1) + "\n";
|
|
564
|
+
lineNo++;
|
|
565
|
+
} else if (line === "\") continue;
|
|
427
566
|
}
|
|
428
567
|
return files;
|
|
429
568
|
}
|
|
@@ -528,9 +667,9 @@ function manifests(root) {
|
|
|
528
667
|
}
|
|
529
668
|
|
|
530
669
|
// mcp/src/main.ts
|
|
531
|
-
var VERSION = "0.1.
|
|
532
|
-
var BAKED_URL =
|
|
533
|
-
var SUPPORTED = ["2025-11-25", "2025-06-18", "2025-03-26"];
|
|
670
|
+
var VERSION = "0.1.4";
|
|
671
|
+
var BAKED_URL = true ? "https://tokenzero.sh" : "__TOKENZERO_DEFAULT_URL__";
|
|
672
|
+
var SUPPORTED = ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26"];
|
|
534
673
|
var serviceUrl = () => (process.env.TOKENZERO_URL || (BAKED_URL.startsWith("http") ? BAKED_URL : "")).replace(/\/+$/, "");
|
|
535
674
|
var TOOLS = [
|
|
536
675
|
{
|
|
@@ -566,7 +705,9 @@ async function callTool(name, args) {
|
|
|
566
705
|
case "optimize_prompt_payload": {
|
|
567
706
|
const base = serviceUrl();
|
|
568
707
|
if (!base) throw new Error("set TOKENZERO_URL to use this tool (the cache formatter runs in the TokenZero service)");
|
|
569
|
-
const r = await fetch(base + "/mcp", { method: "POST", redirect: "manual", headers: { "content-type": "application/json", "user-agent": `tokenzero-mcp/${VERSION}` }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } }) })
|
|
708
|
+
const r = await fetch(base + "/mcp", { method: "POST", redirect: "manual", signal: AbortSignal.timeout(2e4), headers: { "content-type": "application/json", "user-agent": `tokenzero-mcp/${VERSION}` }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } }) }).catch((e) => {
|
|
709
|
+
throw new Error(`could not reach the TokenZero service at ${base} (${e?.cause?.code ?? e?.message ?? e}); set TOKENZERO_URL to use a different one`);
|
|
710
|
+
});
|
|
570
711
|
if (r.status >= 300 && r.status < 400) throw new Error(`${base} is not publicly reachable yet (redirected)`);
|
|
571
712
|
const j = await r.json();
|
|
572
713
|
if (j.error) throw new Error(j.error.message);
|
|
@@ -588,6 +729,16 @@ async function handle(m) {
|
|
|
588
729
|
const want = m.params?.protocolVersion;
|
|
589
730
|
return reply({ protocolVersion: SUPPORTED.includes(want) ? want : SUPPORTED[0], capabilities: { tools: { listChanged: false } }, serverInfo: { name: "tokenzero", version: VERSION }, instructions: "Use audit_repo_token_waste to find LLM cost waste in the current project (local, nothing uploaded), calculate_model_arbitrage to price model alternatives, optimize_prompt_payload to make a request body cache-friendly." });
|
|
590
731
|
}
|
|
732
|
+
case "server/discover":
|
|
733
|
+
return reply({
|
|
734
|
+
resultType: "complete",
|
|
735
|
+
supportedVersions: ["2026-07-28"],
|
|
736
|
+
capabilities: { tools: { listChanged: false } },
|
|
737
|
+
instructions: "Use audit_repo_token_waste to find LLM cost waste in the current project (local, nothing uploaded), calculate_model_arbitrage to price model alternatives, optimize_prompt_payload to make a request body cache-friendly.",
|
|
738
|
+
ttlMs: 36e5,
|
|
739
|
+
cacheScope: "public",
|
|
740
|
+
_meta: { "io.modelcontextprotocol/serverInfo": { name: "tokenzero", version: VERSION } }
|
|
741
|
+
});
|
|
591
742
|
case "ping":
|
|
592
743
|
return reply({});
|
|
593
744
|
case "tools/list":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenzero/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "TokenZero MCP server (stdio): audit LLM token waste in your project locally, price model alternatives, and make prompts cache-friendly from Claude Code, Cursor, Windsurf or Cline.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|