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