@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 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.
@@ -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
- 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;
@@ -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 sdk = CALL_RE.find(([re]) => re.test(code))?.[1];
285
- if (!sdk) return;
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 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;
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 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.`,
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
- if (key && price?.tier === "frontier" && CASCADE_TARGET[key] && simpleTask) {
322
- const to = CASCADE_TARGET[key];
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: `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.`,
330
- 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.`,
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: "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; if the prefix is shared, caching pays off immediately.",
342
- fix: "Batch where possible; otherwise keep the shared prefix cache-formatted (TokenZero does this automatically).",
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 (simpleTask && !repeatFlagged && !/seed\s*[:=]|temperature\s*[:=]\s*0\b/.test(codeCtx)) {
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: "info",
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(i, i + 30).join("\n")) && !hasCacheControl) {
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
- if (line.startsWith("+") && !line.startsWith("+++")) cur.content += line.slice(1) + "\n";
425
- else if (line.startsWith(" ")) cur.content += line.slice(1) + "\n";
426
- else if (line.startsWith("@@")) cur.content += "\n";
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.1";
532
- var BAKED_URL = typeof __TOKENZERO_BAKED__ === "string" ? __TOKENZERO_BAKED__ : "__TOKENZERO_DEFAULT_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.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",