@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 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 at `TOKENZERO_URL`; do not include secrets. |
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` (needed only for `optimize_prompt_payload`), `GITHUB_TOKEN` (raises GitHub rate limits for repo audits).
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.
@@ -202,7 +202,9 @@ function lex(src, path3) {
202
202
  j++;
203
203
  }
204
204
  const end = Math.min(j, src.length);
205
- literals.push({ text: src.slice(i + 1, end), line });
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 sdk = CALL_RE.find(([re]) => re.test(code))?.[1];
285
- if (!sdk) return;
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 window = lines.slice(i, Math.min(lines.length, i + 30)).join("\n");
294
- const model = [...window.matchAll(MODEL_RE)].map((m) => m[1])[0] ?? null;
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 prefix of ~${bigTok} tokens is sent on every call. Prefix caching applies only to stable prefixes of at least ${min} tokens on this model; keep the static part first and nothing volatile (timestamps, ids) interpolated into it.`,
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
- if (key && price?.tier === "frontier" && CASCADE_TARGET[key] && simpleTask) {
322
- const to = CASCADE_TARGET[key];
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: `Frontier 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.`,
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\s*[:=]|temperature\s*[:=]\s*0\b/.test(codeCtx)) {
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: "info",
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(i, i + 30).join("\n")) && !hasCacheControl) {
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.1";
532
- var BAKED_URL = typeof __TOKENZERO_BAKED__ === "string" ? __TOKENZERO_BAKED__ : "__TOKENZERO_DEFAULT_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.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",