@tokenzero/mcp 0.1.3 → 0.1.5

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.
@@ -68,7 +68,18 @@ var PRICES = {
68
68
  "gemini-3-flash": P("gemini", "fast", 0.5, 3, 0.05),
69
69
  "gemini-2.5-pro": P("gemini", "frontier", 1.25, 10, 0.125, void 0, { longCtxAt: 2e5, longCtx: { input: 2.5, output: 15, cachedInput: 0.25 } }),
70
70
  "gemini-2.5-flash": P("gemini", "fast", 0.3, 2.5, 0.03),
71
- "gemini-2.5-flash-lite": P("gemini", "fast", 0.1, 0.4, 0.01)
71
+ "gemini-2.5-flash-lite": P("gemini", "fast", 0.1, 0.4, 0.01),
72
+ // ---- xAI Grok ----
73
+ "grok-4": P("xai", "frontier", 3, 15, 0.75),
74
+ "grok-4-fast": P("xai", "fast", 0.2, 0.8, 0.05),
75
+ "grok-4-mini": P("xai", "fast", 0.2, 0.8, 0.05),
76
+ "grok-3": P("xai", "mid", 3, 15, 0.75),
77
+ "grok-3-mini": P("xai", "fast", 0.3, 0.5, 0.075),
78
+ // ---- DeepSeek ----
79
+ "deepseek-v3": P("deepseek", "fast", 0.27, 1.1, 0.07),
80
+ "deepseek-chat": P("deepseek", "fast", 0.27, 1.1, 0.07),
81
+ "deepseek-r1": P("deepseek", "mid", 0.55, 2.19, 0.14),
82
+ "deepseek-reasoner": P("deepseek", "mid", 0.55, 2.19, 0.14)
72
83
  };
73
84
  var CASCADE_TARGET = {
74
85
  "gpt-6-astra": "gpt-6.1-sol",
@@ -99,13 +110,19 @@ var CASCADE_TARGET = {
99
110
  "gemini-3.1-pro": "gemini-3.8-flash",
100
111
  "gemini-3.8-flash": "gemini-3.5-flash-lite",
101
112
  "gemini-2.5-pro": "gemini-2.5-flash",
102
- "gemini-2.5-flash": "gemini-2.5-flash-lite"
113
+ "gemini-2.5-flash": "gemini-2.5-flash-lite",
114
+ "grok-4": "grok-4-fast",
115
+ "grok-3": "grok-3-mini",
116
+ "deepseek-r1": "deepseek-v3",
117
+ "deepseek-reasoner": "deepseek-chat"
103
118
  };
104
119
  var KEYS_BY_LENGTH = Object.keys(PRICES).sort((a, b) => b.length - a.length);
105
120
  function normalizeModel(model) {
106
121
  if (typeof model !== "string" || !model) return null;
107
122
  let m = model.toLowerCase().trim();
108
- m = m.replace(/^(openai|anthropic|google|models|vertex_ai)\//, "");
123
+ m = m.replace(/^(openai|anthropic|google|gemini|x-ai|xai|deepseek|bedrock|models|vertex_ai)\//, "");
124
+ m = m.replace(/^(?:us\.|eu\.|apac\.)?anthropic\./, "");
125
+ m = m.replace(/-v\d+:\d+$/, "");
109
126
  m = m.replace(/@\d{8}$/, "");
110
127
  m = m.replace(/-(\d{8}|\d{4}-\d{2}-\d{2})$/, "");
111
128
  m = m.replace(/-(latest|preview|customtools|thinking)$/, "");
@@ -117,7 +134,7 @@ function priceFor(model) {
117
134
  const key = normalizeModel(model);
118
135
  return key ? { key, ...PRICES[key] } : null;
119
136
  }
120
- function costOf(model, u) {
137
+ function costOf(model, u, factor = 1) {
121
138
  const p = priceFor(model);
122
139
  if (!p) return null;
123
140
  const tier = p.longCtx && p.longCtxAt && u.input > p.longCtxAt ? { ...p, ...p.longCtx } : p;
@@ -125,7 +142,7 @@ function costOf(model, u) {
125
142
  const written = u.cacheWrite ?? 0;
126
143
  const fresh = Math.max(0, u.input - cached - written);
127
144
  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;
145
+ return factor * (fresh * tier.input / M + cached * (tier.cachedInput ?? tier.input) / M + written * (tier.cacheWrite ?? tier.input) / M + u.output * tier.output / M);
129
146
  }
130
147
 
131
148
  // cli/src/tokens-lite.ts
@@ -223,6 +240,7 @@ function promptLike(t) {
223
240
  const words = (t.match(/[A-Za-z]{2,}/g) ?? []).length;
224
241
  return tags < 4 && cssRules < 3 && codeChars / t.length < 0.03 && words / (t.length / 6) > 0.5;
225
242
  }
243
+ 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
244
  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
245
  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
246
  var LLM_LIB_FAMILY = /^(@langchain\/[\w-]+|langchain[-_][\w-]+|langgraph[-_][\w-]+|llama[-_]index[-_][\w-]+|@ai-sdk\/[\w-]+|openai[-_]agents[-_][\w-]+)$/i;
@@ -358,7 +376,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
358
376
  const { kind, reason } = detectRepoKind(manifests2, sources);
359
377
  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
378
  const promptLibrary = [];
361
- const repoCaches = sources.some((f) => /cache_control|cacheControl|prompt_cache_key|promptCacheKey|cachedContent|cached_content/.test(f.content));
379
+ 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
380
  const PROMPT_FILE = /(^|\/)[^/]*(prompt|instruction|template|persona)s?[^/]*\.[a-z]+$|(^|\/)prompts?\//i;
363
381
  const PROMPT_NAME = /\b[\w.]*(prompt|system|instruction|template|persona)\w*\s*(:\s*[\w[\]|. ]+)?\s*[:=]\s*(f|r|rf|fr|b)?$/i;
364
382
  for (const f of sources) {
@@ -371,7 +389,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
371
389
  const lead = (masked[l.line - 1] ?? "").slice(0, Math.max(0, (masked[l.line - 1] ?? "").search(/["'`]|$/))).trimEnd();
372
390
  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
391
  }
374
- const hasCacheControl = /cache_control|cacheControl|prompt_cache_key|promptCacheKey|cachedContent|cached_content/.test(src);
392
+ 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
393
  const illustrative = EXAMPLE_PATH.test(f.path);
376
394
  let repeatFlagged = false;
377
395
  const loc = makeLocator(masked);
@@ -392,6 +410,16 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
392
410
  local.push({ ...fd, severity: sev });
393
411
  };
394
412
  const refs = resolveScopes(loc, masked, scope);
413
+ if (sdk.startsWith("langchain")) {
414
+ const assigned = /^\s*(?:const|let|var|self\.|this\.)?\s*([A-Za-z_]\w*)\s*(?::[^=\n]+)?=/.exec(masked[i])?.[1];
415
+ if (assigned) for (let l = 0; l < masked.length; l++) {
416
+ const inv = new RegExp(`\\b${assigned}\\.(invoke|ainvoke|stream|astream|batch|abatch|call|predict|generate)\\s*\\(`).exec(masked[l]);
417
+ if (!inv) continue;
418
+ const close = matchClose(loc.text, loc.starts[l] + inv.index + inv[0].length - 1);
419
+ const sc = { start: l, end: Math.max(l, loc.lineOf(close)) };
420
+ refs.push(sc, ...resolveScopes(loc, masked, sc));
421
+ }
422
+ }
395
423
  const inScopes = (l, scs) => scs.some((sc) => l.line - 1 >= sc.start && l.line - 1 <= sc.end);
396
424
  let near = literals.filter((l) => inScopes(l, [scope, ...refs]));
397
425
  if (!near.some((l) => l.text.trim().length > 20)) {
@@ -410,6 +438,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
410
438
  const prompts = near.filter((l) => promptLike(l.text));
411
439
  const codeCtx = masked.slice(Math.max(0, i - 25), i + 25).join("\n");
412
440
  const simpleTask = near.some((l) => SIMPLE_TASK.test(l.text));
441
+ const repeatTask = near.some((l) => REPEAT_TASK.test(l.text));
413
442
  const isAnthropic = sdk.includes("anthropic") || (model?.startsWith("claude") ?? false);
414
443
  const bigTok = prompts.reduce((a, l) => a + countTokens(l.text), 0);
415
444
  if (bigTok >= 300 && !hasCacheControl) {
@@ -439,7 +468,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
439
468
  line: i + 1,
440
469
  title: `${key} used for a task whose prompt reads like extraction/classification`,
441
470
  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.`,
471
+ 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
472
  est_savings_pct: Math.round((1 - b / a) * 100)
444
473
  });
445
474
  }
@@ -450,12 +479,12 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
450
479
  file: f.path,
451
480
  line: i + 1,
452
481
  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).",
482
+ 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.',
483
+ 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
484
  est_savings_pct: 25
456
485
  });
457
486
  }
458
- if (simpleTask && !repeatFlagged && !/seed["']?\s*[:=]|temperature["']?\s*[:=]\s*0\b|X-TokenZero-Cache/i.test(spanText)) {
487
+ if (repeatTask && !repeatFlagged && !/seed["']?\s*[:=]|temperature["']?\s*[:=]\s*0\b|X-TokenZero-Cache/i.test(spanText)) {
459
488
  repeatFlagged = true;
460
489
  push({
461
490
  severity: "low",
@@ -525,17 +554,32 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
525
554
  function filesFromDiff(diff) {
526
555
  const files = [];
527
556
  let cur = null;
557
+ let lineNo = 0;
528
558
  for (const line of diff.split("\n")) {
529
559
  const m = /^\+\+\+ b\/(.+)$/.exec(line);
530
560
  if (m) {
531
561
  cur = { path: m[1], content: "" };
532
562
  files.push(cur);
563
+ lineNo = 0;
533
564
  continue;
534
565
  }
535
566
  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";
567
+ const h = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
568
+ if (h) {
569
+ const start = Number(h[1]);
570
+ while (lineNo < start - 1) {
571
+ cur.content += "\n";
572
+ lineNo++;
573
+ }
574
+ continue;
575
+ }
576
+ if (line.startsWith("+") && !line.startsWith("+++")) {
577
+ cur.content += line.slice(1) + "\n";
578
+ lineNo++;
579
+ } else if (line.startsWith(" ")) {
580
+ cur.content += line.slice(1) + "\n";
581
+ lineNo++;
582
+ } else if (line === "\") continue;
539
583
  }
540
584
  return files;
541
585
  }
@@ -640,9 +684,9 @@ function manifests(root) {
640
684
  }
641
685
 
642
686
  // mcp/src/main.ts
643
- var VERSION = "0.1.3";
687
+ var VERSION = "0.1.5";
644
688
  var BAKED_URL = true ? "https://tokenzero.sh" : "__TOKENZERO_DEFAULT_URL__";
645
- var SUPPORTED = ["2025-11-25", "2025-06-18", "2025-03-26"];
689
+ var SUPPORTED = ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26"];
646
690
  var serviceUrl = () => (process.env.TOKENZERO_URL || (BAKED_URL.startsWith("http") ? BAKED_URL : "")).replace(/\/+$/, "");
647
691
  var TOOLS = [
648
692
  {
@@ -702,6 +746,16 @@ async function handle(m) {
702
746
  const want = m.params?.protocolVersion;
703
747
  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
748
  }
749
+ case "server/discover":
750
+ return reply({
751
+ resultType: "complete",
752
+ supportedVersions: ["2026-07-28"],
753
+ capabilities: { tools: { listChanged: false } },
754
+ 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.",
755
+ ttlMs: 36e5,
756
+ cacheScope: "public",
757
+ _meta: { "io.modelcontextprotocol/serverInfo": { name: "tokenzero", version: VERSION } }
758
+ });
705
759
  case "ping":
706
760
  return reply({});
707
761
  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.5",
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",