@tokenzero/mcp 0.1.1 → 0.1.3
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 +132 -18
- 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
|
@@ -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;
|
|
@@ -253,6 +255,98 @@ function detectRepoKind(manifests2, files) {
|
|
|
253
255
|
return { kind: "unknown", reason: "no manifest signal" };
|
|
254
256
|
}
|
|
255
257
|
var DOWN = { high: "medium", medium: "low", low: "info", info: "info" };
|
|
258
|
+
var OPEN = "([{";
|
|
259
|
+
var CLOSE = ")]}";
|
|
260
|
+
function matchClose(m, from, limit = 4e4) {
|
|
261
|
+
let depth = 0;
|
|
262
|
+
for (let k = from; k < Math.min(m.length, from + limit); k++) {
|
|
263
|
+
const c = m[k];
|
|
264
|
+
if (OPEN.includes(c)) depth++;
|
|
265
|
+
else if (CLOSE.includes(c) && --depth === 0) return k;
|
|
266
|
+
}
|
|
267
|
+
return Math.min(m.length, from + limit) - 1;
|
|
268
|
+
}
|
|
269
|
+
function makeLocator(masked) {
|
|
270
|
+
const text = masked.join("\n");
|
|
271
|
+
const starts = [];
|
|
272
|
+
let o = 0;
|
|
273
|
+
for (const l of masked) {
|
|
274
|
+
starts.push(o);
|
|
275
|
+
o += l.length + 1;
|
|
276
|
+
}
|
|
277
|
+
const lineOf = (off) => {
|
|
278
|
+
let lo = 0, hi = starts.length - 1;
|
|
279
|
+
while (lo < hi) {
|
|
280
|
+
const mid = lo + hi + 1 >> 1;
|
|
281
|
+
if (starts[mid] <= off) lo = mid;
|
|
282
|
+
else hi = mid - 1;
|
|
283
|
+
}
|
|
284
|
+
return lo;
|
|
285
|
+
};
|
|
286
|
+
return { text, starts, lineOf };
|
|
287
|
+
}
|
|
288
|
+
var ASSIGN = (name) => new RegExp(`^\\s*(?:export\\s+)?(?:const|let|var|final|static|private|public|readonly|\\s)*\\s*(?:self\\.|this\\.)?${name}\\s*(?::[^=\\n]+)?=(?![=>])`);
|
|
289
|
+
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"]);
|
|
290
|
+
function assignmentScope(loc, masked, line) {
|
|
291
|
+
const eq = masked[line].search(/=(?![=>])/);
|
|
292
|
+
let end = line;
|
|
293
|
+
let k = loc.starts[line] + eq + 1;
|
|
294
|
+
for (; ; ) {
|
|
295
|
+
const lineEnd = loc.starts[end] + masked[end].length;
|
|
296
|
+
let opened = -1;
|
|
297
|
+
for (let q = k; q < lineEnd; q++) if (OPEN.includes(loc.text[q])) {
|
|
298
|
+
opened = q;
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
if (opened >= 0) {
|
|
302
|
+
const close = matchClose(loc.text, opened, 2e4);
|
|
303
|
+
end = loc.lineOf(close);
|
|
304
|
+
k = close + 1;
|
|
305
|
+
if (k < loc.starts[end] + masked[end].length) continue;
|
|
306
|
+
}
|
|
307
|
+
const tail = masked[end].trimEnd();
|
|
308
|
+
if (end + 1 < masked.length && (/[+\\,(]$/.test(tail) || /^\s*[+.]/.test(masked[end + 1] ?? "")) && end - line < 60) {
|
|
309
|
+
end++;
|
|
310
|
+
k = loc.starts[end];
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
return { start: line, end };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function identifiersIn(masked, sc) {
|
|
317
|
+
const ids = /* @__PURE__ */ new Set();
|
|
318
|
+
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]);
|
|
319
|
+
return ids;
|
|
320
|
+
}
|
|
321
|
+
function resolveScopes(loc, masked, from, depth = 2) {
|
|
322
|
+
const out = [], seen = /* @__PURE__ */ new Set();
|
|
323
|
+
let frontier = [from];
|
|
324
|
+
for (let d = 0; d < depth && frontier.length; d++) {
|
|
325
|
+
const next = [];
|
|
326
|
+
for (const sc of frontier) for (const id of identifiersIn(masked, sc)) {
|
|
327
|
+
if (seen.has(id)) continue;
|
|
328
|
+
seen.add(id);
|
|
329
|
+
const re = ASSIGN(id);
|
|
330
|
+
let found = -1;
|
|
331
|
+
for (let l = sc.start; l >= 0; l--) if (re.test(masked[l])) {
|
|
332
|
+
found = l;
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
if (found < 0) {
|
|
336
|
+
for (let l = sc.end + 1; l < masked.length; l++) if (re.test(masked[l])) {
|
|
337
|
+
found = l;
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (found < 0 || found >= from.start && found <= from.end) continue;
|
|
342
|
+
const a = assignmentScope(loc, masked, found);
|
|
343
|
+
out.push(a);
|
|
344
|
+
next.push(a);
|
|
345
|
+
}
|
|
346
|
+
frontier = next;
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
256
350
|
function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
257
351
|
const findings = [];
|
|
258
352
|
const callSites = [];
|
|
@@ -280,9 +374,16 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
280
374
|
const hasCacheControl = /cache_control|cacheControl|prompt_cache_key|promptCacheKey|cachedContent|cached_content/.test(src);
|
|
281
375
|
const illustrative = EXAMPLE_PATH.test(f.path);
|
|
282
376
|
let repeatFlagged = false;
|
|
377
|
+
const loc = makeLocator(masked);
|
|
378
|
+
const callLines = [];
|
|
283
379
|
masked.forEach((code, i) => {
|
|
284
|
-
const
|
|
285
|
-
if (!
|
|
380
|
+
const hit = CALL_RE.find(([re]) => re.test(code));
|
|
381
|
+
if (!hit) return;
|
|
382
|
+
const m = hit[0].exec(code);
|
|
383
|
+
const close = matchClose(loc.text, loc.starts[i] + m.index + m[0].length - 1);
|
|
384
|
+
callLines.push({ line: i, sdk: hit[1], scope: { start: i, end: Math.max(i, loc.lineOf(close)) } });
|
|
385
|
+
});
|
|
386
|
+
callLines.forEach(({ line: i, sdk, scope }, n) => {
|
|
286
387
|
const local = [];
|
|
287
388
|
const push = (fd) => {
|
|
288
389
|
let sev = fd.severity;
|
|
@@ -290,13 +391,22 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
290
391
|
if (illustrative) sev = DOWN[sev];
|
|
291
392
|
local.push({ ...fd, severity: sev });
|
|
292
393
|
};
|
|
293
|
-
const
|
|
294
|
-
const
|
|
394
|
+
const refs = resolveScopes(loc, masked, scope);
|
|
395
|
+
const inScopes = (l, scs) => scs.some((sc) => l.line - 1 >= sc.start && l.line - 1 <= sc.end);
|
|
396
|
+
let near = literals.filter((l) => inScopes(l, [scope, ...refs]));
|
|
397
|
+
if (!near.some((l) => l.text.trim().length > 20)) {
|
|
398
|
+
const prevEnd = n > 0 ? callLines[n - 1].scope.end : -1;
|
|
399
|
+
near = near.concat(literals.filter((l) => l.line - 1 > prevEnd && l.line - 1 >= i - 40 && l.line - 1 < i));
|
|
400
|
+
}
|
|
401
|
+
const spanText = lines.slice(scope.start, scope.end + 1).join("\n");
|
|
402
|
+
const nextStart = n + 1 < callLines.length ? callLines[n + 1].line : lines.length;
|
|
403
|
+
const fallbackWindow = lines.slice(i, Math.min(nextStart, i + 30)).join("\n");
|
|
404
|
+
const modelFrom = (t) => [...t.matchAll(MODEL_RE)].map((m) => m[1])[0] ?? null;
|
|
405
|
+
const model = modelFrom(spanText) ?? modelFrom(refs.map((r) => lines.slice(r.start, r.end + 1).join("\n")).join("\n")) ?? modelFrom(fallbackWindow);
|
|
295
406
|
callSites.push({ file: f.path, line: i + 1, model, sdk });
|
|
296
407
|
if (model) models[model] = (models[model] ?? 0) + 1;
|
|
297
408
|
const key = normalizeModel(model);
|
|
298
409
|
const price = key ? priceFor(key) : null;
|
|
299
|
-
const near = literals.filter((l) => l.line >= i + 1 - 40 && l.line <= i + 1 + 40);
|
|
300
410
|
const prompts = near.filter((l) => promptLike(l.text));
|
|
301
411
|
const codeCtx = masked.slice(Math.max(0, i - 25), i + 25).join("\n");
|
|
302
412
|
const simpleTask = near.some((l) => SIMPLE_TASK.test(l.text));
|
|
@@ -310,23 +420,25 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
310
420
|
severity: isAnthropic ? bigTok >= 1500 ? "high" : cacheable ? "medium" : "low" : cacheable ? "medium" : "low",
|
|
311
421
|
file: f.path,
|
|
312
422
|
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
|
|
423
|
+
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`,
|
|
424
|
+
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
425
|
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
426
|
// OpenAI/Gemini already cache stable prefixes automatically; the recoverable part
|
|
317
427
|
// is prefix hygiene and cache routing, not the whole cache discount.
|
|
318
428
|
est_savings_pct: !cacheable ? 0 : isAnthropic ? Math.min(85, Math.round(bigTok / (bigTok + 400) * readRate)) : 15
|
|
319
429
|
});
|
|
320
430
|
}
|
|
321
|
-
|
|
322
|
-
|
|
431
|
+
const cascadeTo = key ? CASCADE_TARGET[key] : void 0;
|
|
432
|
+
const ratio = cascadeTo ? costOf(cascadeTo, { input: 1500, output: 200 }) / costOf(key, { input: 1500, output: 200 }) : 1;
|
|
433
|
+
if (key && price && cascadeTo && simpleTask && (price.tier === "frontier" || ratio <= 0.5)) {
|
|
434
|
+
const to = cascadeTo;
|
|
323
435
|
const a = costOf(key, { input: 1500, output: 200 }), b = costOf(to, { input: 1500, output: 200 });
|
|
324
436
|
push({
|
|
325
|
-
severity: "high",
|
|
437
|
+
severity: price.tier === "frontier" || ratio <= 0.2 ? "high" : "medium",
|
|
326
438
|
file: f.path,
|
|
327
439
|
line: i + 1,
|
|
328
440
|
title: `${key} used for a task whose prompt reads like extraction/classification`,
|
|
329
|
-
detail:
|
|
441
|
+
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.`,
|
|
330
442
|
fix: `Opt into TokenZero cascade routing (\`X-TokenZero-Cascade: auto\`) so this call is routed to ${to} only when the classifier is confident, with parity reported in the ledger, or switch the model here behind an eval.`,
|
|
331
443
|
est_savings_pct: Math.round((1 - b / a) * 100)
|
|
332
444
|
});
|
|
@@ -343,10 +455,10 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
343
455
|
est_savings_pct: 25
|
|
344
456
|
});
|
|
345
457
|
}
|
|
346
|
-
if (simpleTask && !repeatFlagged && !/seed
|
|
458
|
+
if (simpleTask && !repeatFlagged && !/seed["']?\s*[:=]|temperature["']?\s*[:=]\s*0\b|X-TokenZero-Cache/i.test(spanText)) {
|
|
347
459
|
repeatFlagged = true;
|
|
348
460
|
push({
|
|
349
|
-
severity: "
|
|
461
|
+
severity: "low",
|
|
350
462
|
file: f.path,
|
|
351
463
|
line: i + 1,
|
|
352
464
|
title: "Repeat-prone task with no response-cache opt-in",
|
|
@@ -355,7 +467,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
355
467
|
est_savings_pct: 10
|
|
356
468
|
});
|
|
357
469
|
}
|
|
358
|
-
if (isAnthropic && /\btools\s*[:=]/.test(masked.slice(
|
|
470
|
+
if (isAnthropic && /\btools\s*[:=]/.test(masked.slice(scope.start, scope.end + 1).join("\n")) && !hasCacheControl) {
|
|
359
471
|
push({
|
|
360
472
|
severity: "low",
|
|
361
473
|
file: f.path,
|
|
@@ -528,8 +640,8 @@ function manifests(root) {
|
|
|
528
640
|
}
|
|
529
641
|
|
|
530
642
|
// mcp/src/main.ts
|
|
531
|
-
var VERSION = "0.1.
|
|
532
|
-
var BAKED_URL =
|
|
643
|
+
var VERSION = "0.1.3";
|
|
644
|
+
var BAKED_URL = true ? "https://tokenzero.sh" : "__TOKENZERO_DEFAULT_URL__";
|
|
533
645
|
var SUPPORTED = ["2025-11-25", "2025-06-18", "2025-03-26"];
|
|
534
646
|
var serviceUrl = () => (process.env.TOKENZERO_URL || (BAKED_URL.startsWith("http") ? BAKED_URL : "")).replace(/\/+$/, "");
|
|
535
647
|
var TOOLS = [
|
|
@@ -566,7 +678,9 @@ async function callTool(name, args) {
|
|
|
566
678
|
case "optimize_prompt_payload": {
|
|
567
679
|
const base = serviceUrl();
|
|
568
680
|
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 } }) })
|
|
681
|
+
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) => {
|
|
682
|
+
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`);
|
|
683
|
+
});
|
|
570
684
|
if (r.status >= 300 && r.status < 400) throw new Error(`${base} is not publicly reachable yet (redirected)`);
|
|
571
685
|
const j = await r.json();
|
|
572
686
|
if (j.error) throw new Error(j.error.message);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenzero/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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",
|