@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.
- package/bin/tokenzero-mcp.mjs +70 -16
- package/package.json +1 -1
package/bin/tokenzero-mcp.mjs
CHANGED
|
@@ -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
|
|
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:
|
|
454
|
-
fix:
|
|
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 (
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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.
|
|
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
|
+
"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",
|