@tokenzero/mcp 0.1.3 → 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.
@@ -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
@@ -223,6 +223,7 @@ function promptLike(t) {
223
223
  const words = (t.match(/[A-Za-z]{2,}/g) ?? []).length;
224
224
  return tags < 4 && cssRules < 3 && codeChars / t.length < 0.03 && words / (t.length / 6) > 0.5;
225
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;
226
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;
227
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"]);
228
229
  var LLM_LIB_FAMILY = /^(@langchain\/[\w-]+|langchain[-_][\w-]+|langgraph[-_][\w-]+|llama[-_]index[-_][\w-]+|@ai-sdk\/[\w-]+|openai[-_]agents[-_][\w-]+)$/i;
@@ -358,7 +359,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
358
359
  const { kind, reason } = detectRepoKind(manifests2, sources);
359
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.`);
360
361
  const promptLibrary = [];
361
- 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));
362
363
  const PROMPT_FILE = /(^|\/)[^/]*(prompt|instruction|template|persona)s?[^/]*\.[a-z]+$|(^|\/)prompts?\//i;
363
364
  const PROMPT_NAME = /\b[\w.]*(prompt|system|instruction|template|persona)\w*\s*(:\s*[\w[\]|. ]+)?\s*[:=]\s*(f|r|rf|fr|b)?$/i;
364
365
  for (const f of sources) {
@@ -371,7 +372,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
371
372
  const lead = (masked[l.line - 1] ?? "").slice(0, Math.max(0, (masked[l.line - 1] ?? "").search(/["'`]|$/))).trimEnd();
372
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 });
373
374
  }
374
- 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);
375
376
  const illustrative = EXAMPLE_PATH.test(f.path);
376
377
  let repeatFlagged = false;
377
378
  const loc = makeLocator(masked);
@@ -392,6 +393,16 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
392
393
  local.push({ ...fd, severity: sev });
393
394
  };
394
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
+ }
395
406
  const inScopes = (l, scs) => scs.some((sc) => l.line - 1 >= sc.start && l.line - 1 <= sc.end);
396
407
  let near = literals.filter((l) => inScopes(l, [scope, ...refs]));
397
408
  if (!near.some((l) => l.text.trim().length > 20)) {
@@ -410,6 +421,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
410
421
  const prompts = near.filter((l) => promptLike(l.text));
411
422
  const codeCtx = masked.slice(Math.max(0, i - 25), i + 25).join("\n");
412
423
  const simpleTask = near.some((l) => SIMPLE_TASK.test(l.text));
424
+ const repeatTask = near.some((l) => REPEAT_TASK.test(l.text));
413
425
  const isAnthropic = sdk.includes("anthropic") || (model?.startsWith("claude") ?? false);
414
426
  const bigTok = prompts.reduce((a, l) => a + countTokens(l.text), 0);
415
427
  if (bigTok >= 300 && !hasCacheControl) {
@@ -439,7 +451,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
439
451
  line: i + 1,
440
452
  title: `${key} used for a task whose prompt reads like extraction/classification`,
441
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.`,
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.`,
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.`,
443
455
  est_savings_pct: Math.round((1 - b / a) * 100)
444
456
  });
445
457
  }
@@ -450,12 +462,12 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
450
462
  file: f.path,
451
463
  line: i + 1,
452
464
  title: "LLM call inside a loop",
453
- 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.",
454
- 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).',
455
467
  est_savings_pct: 25
456
468
  });
457
469
  }
458
- if (simpleTask && !repeatFlagged && !/seed["']?\s*[:=]|temperature["']?\s*[:=]\s*0\b|X-TokenZero-Cache/i.test(spanText)) {
470
+ if (repeatTask && !repeatFlagged && !/seed["']?\s*[:=]|temperature["']?\s*[:=]\s*0\b|X-TokenZero-Cache/i.test(spanText)) {
459
471
  repeatFlagged = true;
460
472
  push({
461
473
  severity: "low",
@@ -525,17 +537,32 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
525
537
  function filesFromDiff(diff) {
526
538
  const files = [];
527
539
  let cur = null;
540
+ let lineNo = 0;
528
541
  for (const line of diff.split("\n")) {
529
542
  const m = /^\+\+\+ b\/(.+)$/.exec(line);
530
543
  if (m) {
531
544
  cur = { path: m[1], content: "" };
532
545
  files.push(cur);
546
+ lineNo = 0;
533
547
  continue;
534
548
  }
535
549
  if (!cur) continue;
536
- if (line.startsWith("+") && !line.startsWith("+++")) cur.content += line.slice(1) + "\n";
537
- else if (line.startsWith(" ")) cur.content += line.slice(1) + "\n";
538
- 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;
539
566
  }
540
567
  return files;
541
568
  }
@@ -640,9 +667,9 @@ function manifests(root) {
640
667
  }
641
668
 
642
669
  // mcp/src/main.ts
643
- var VERSION = "0.1.3";
670
+ var VERSION = "0.1.4";
644
671
  var BAKED_URL = true ? "https://tokenzero.sh" : "__TOKENZERO_DEFAULT_URL__";
645
- var SUPPORTED = ["2025-11-25", "2025-06-18", "2025-03-26"];
672
+ var SUPPORTED = ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26"];
646
673
  var serviceUrl = () => (process.env.TOKENZERO_URL || (BAKED_URL.startsWith("http") ? BAKED_URL : "")).replace(/\/+$/, "");
647
674
  var TOOLS = [
648
675
  {
@@ -702,6 +729,16 @@ async function handle(m) {
702
729
  const want = m.params?.protocolVersion;
703
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." });
704
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
+ });
705
742
  case "ping":
706
743
  return reply({});
707
744
  case "tools/list":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenzero/mcp",
3
- "version": "0.1.3",
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",