@tokenzero/mcp 0.2.9 → 0.2.10
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 +79 -4
- package/package.json +1 -1
package/bin/tokenzero-mcp.mjs
CHANGED
|
@@ -137,8 +137,15 @@ function normalizeModel(model) {
|
|
|
137
137
|
for (const k of KEYS_BY_LENGTH) if (m.startsWith(k + "-") || m === k) return k;
|
|
138
138
|
return null;
|
|
139
139
|
}
|
|
140
|
+
var isFreeModel = (model) => typeof model === "string" && (/:free$/i.test(model.trim()) || /^openrouter\/free$/i.test(model.trim()));
|
|
141
|
+
var dynamicPrice = null;
|
|
142
|
+
var FREE_PRICE = { provider: "openrouter", tier: "fast", input: 0, output: 0, cachedInput: 0 };
|
|
140
143
|
function priceFor(model) {
|
|
144
|
+
if (isFreeModel(model)) return { key: String(model).trim().toLowerCase(), ...FREE_PRICE };
|
|
141
145
|
const key = normalizeModel(model);
|
|
146
|
+
if (key && PRICES[key]) return { key, ...PRICES[key] };
|
|
147
|
+
const dyn = typeof model === "string" && dynamicPrice ? dynamicPrice(model) : null;
|
|
148
|
+
if (dyn) return { key: String(model).trim().toLowerCase(), ...dyn };
|
|
142
149
|
return key ? { key, ...PRICES[key] } : null;
|
|
143
150
|
}
|
|
144
151
|
function costOf(model, u, factor = 1) {
|
|
@@ -338,7 +345,7 @@ function makeLocator(masked) {
|
|
|
338
345
|
};
|
|
339
346
|
return { text, starts, lineOf };
|
|
340
347
|
}
|
|
341
|
-
var ASSIGN = (name) => new RegExp(
|
|
348
|
+
var ASSIGN = (name) => new RegExp(`^[ \\t]*(?:export[ \\t]+)?(?:(?:const|let|var|final|static|private|public|readonly)[ \\t]+)*(?:self\\.|this\\.)?${name}[ \\t]*(?::[^=\\n]{0,200})?=(?![=>])`);
|
|
342
349
|
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"]);
|
|
343
350
|
function assignmentScope(loc, masked, line) {
|
|
344
351
|
const eq = masked[line].search(/=(?![=>])/);
|
|
@@ -407,6 +414,10 @@ var MODEL_NAME_TEST = /\b(?:model|model_?id|model_?name|modelId|modelName|deploy
|
|
|
407
414
|
var VOLATILE_CALL = /\b(?:datetime\.(?:now|utcnow)\(|date\.today\(|time\.time\(|time\.strftime\(|Date\.now\(|new Date\(\s*\)|\.toISOString\(|\.isoformat\(|\.toLocale(?:Date|Time)?String\(|uuid\.?uuid4\(|uuid4\(|randomUUID\(|nanoid\(|Math\.random\(|secrets\.token_hex\()/;
|
|
408
415
|
var INTERPOLATES = /\bf["']|\$\{|\.format\(|%\s*\(|["']\s*\+|\+\s*["']|`/;
|
|
409
416
|
var SYSTEM_CTX = /\bsystem(?:_?prompt|Prompt|_?message|Message|_?instructions?)?\b\s*[:=(]|["']role["']\s*:\s*["']system["']|\brole\s*:\s*["']system["']|\binstructions\s*[:=]/i;
|
|
417
|
+
var HARDCODED_KEY = /["'`]((?:sk-ant-api\d\d-|sk-ant-|sk-proj-|sk-or-v1-|sk-svcacct-)[A-Za-z0-9_-]{20,200}|sk-[A-Za-z0-9]{40,200}|AIza[0-9A-Za-z_-]{35}|xai-[A-Za-z0-9]{40,200}|gsk_[A-Za-z0-9]{40,200}|csk-[A-Za-z0-9]{30,200}|tz_live_[A-Za-z0-9_-]{24,200})["'`]/;
|
|
418
|
+
var KEY_PLACEHOLDER = /x{6,}|X{6,}|0{8,}|1234567|abcdef|your|example|placeholder|dummy|fake|test|redacted|\.\.\./i;
|
|
419
|
+
var keyProvider = (k) => k.startsWith("sk-ant-") ? "Anthropic" : k.startsWith("sk-or-") ? "OpenRouter" : k.startsWith("AIza") ? "Google" : k.startsWith("xai-") ? "xAI" : k.startsWith("gsk_") ? "Groq" : k.startsWith("csk-") ? "Cerebras" : k.startsWith("tz_live_") ? "TokenZero" : "OpenAI";
|
|
420
|
+
var PREFIX_DYNAMIC = /\bf(?:"""|'''|"|')\s{0,4}\{[A-Za-z_][\w.\[\]'"]{0,60}\}|`\s{0,4}\$\{[^}\n]{1,80}\}/;
|
|
410
421
|
var RANDOM_ID = /(?:\b(?:tool_call_id|toolCallId|call_id|callId|tool_use_id|toolUseId)\b|["']id["'])\s*[:=]\s*[^,\n]*?(?:uuid\.?uuid4\(|uuid4\(|randomUUID\(|nanoid\(|Math\.random\(|secrets\.token_hex\(|shortuuid)/;
|
|
411
422
|
var SET_TOOLS = /\btools?\s*=\s*\[[^\]\n]*\bfor\s+\w+\s+in\s+(?:set\(|\w*_?set\b|\w+\.keys\(\)\s*\|)/;
|
|
412
423
|
var OPENAI_COMPAT = /\bfrom\s+openai\s+import|\bimport\s+openai\b|from\s+["']openai["']|require\(\s*["']openai["']\s*\)|\bOpenAI\(|openrouter\.ai|chat\.completions\.create|litellm\.completion/;
|
|
@@ -418,6 +429,26 @@ var CACHE_USAGE_FIELD = /cache_read_input_tokens|cache_creation_input_tokens|cac
|
|
|
418
429
|
var COST_MATH = /\b(?:input_tokens|prompt_tokens|promptTokenCount|inputTokens|promptTokens|prompt_token_count)\b[^;\n]*(?:\*\s*[\w.[\]'"]*(?:price|rate|cost|per_?m|perM|usd)|\/\s*1[_,]?000[_,]?000|\/\s*1e6)|(?:price|rate|cost|per_?m|perM|usd)\w*\s*\*\s*[\w.[\]'"]*\b(?:input_tokens|prompt_tokens|promptTokenCount|inputTokens|promptTokens)\b/i;
|
|
419
430
|
var ZEROED_CACHE_USAGE = /\b(?:cache_creation_input_tokens|cache_read_input_tokens|cacheCreationInputTokens|cacheReadInputTokens|cached_tokens|cachedTokens)["']?\s*[:=]\s*0\b(?!\s*(?:if|\?|\|\||or\b|&&))/;
|
|
420
431
|
var isComment = (raw) => /^\s*(?:#|\/\/|\*|\/\*)/.test(raw);
|
|
432
|
+
function secretLint(path3, lines) {
|
|
433
|
+
const out = [];
|
|
434
|
+
for (let i = 0; i < lines.length && out.length < 5; i++) {
|
|
435
|
+
const raw = lines[i];
|
|
436
|
+
if (raw.length > 2e4) continue;
|
|
437
|
+
const km = HARDCODED_KEY.exec(raw);
|
|
438
|
+
if (!km || KEY_PLACEHOLDER.test(km[1])) continue;
|
|
439
|
+
const who = keyProvider(km[1]);
|
|
440
|
+
out.push({
|
|
441
|
+
file: path3,
|
|
442
|
+
severity: "high",
|
|
443
|
+
line: i + 1,
|
|
444
|
+
title: `Hard-coded ${who} API key in source`,
|
|
445
|
+
detail: `A string that looks like a live ${who} key (${km[1].slice(0, 7)}\u2026) is committed in this file. Anyone who can read the repository or its history can bill your account; a key committed to a public repo and never revoked is how the October 2026 LiteLLM supply-chain compromise began.`,
|
|
446
|
+
fix: "Revoke the key at the provider now (removing it from the file does not remove it from git history), then load it from the environment or a secret manager. TokenZero never needs your key in code: SDKs read OPENAI_API_KEY / ANTHROPIC_API_KEY as usual.",
|
|
447
|
+
est_savings_pct: 0
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return out;
|
|
451
|
+
}
|
|
421
452
|
function cacheLint(path3, lines, masked) {
|
|
422
453
|
const out = [];
|
|
423
454
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -472,6 +503,15 @@ function cacheLint(path3, lines, masked) {
|
|
|
472
503
|
fix: "Keep the system prompt static. Put the current time or request id in the last user message (or round it to the day if it must stay in the system prompt).",
|
|
473
504
|
est_savings_pct: 15
|
|
474
505
|
});
|
|
506
|
+
const pm = PREFIX_DYNAMIC.exec(raw);
|
|
507
|
+
if (pm && SYSTEM_CTX.test(raw.slice(0, pm.index + 1)) && raw.length - pm.index >= 160) add("prefix-order", {
|
|
508
|
+
severity: "medium",
|
|
509
|
+
line: i + 1,
|
|
510
|
+
title: "Per-request value at the start of the system prompt (everything after it misses the cache)",
|
|
511
|
+
detail: "The system prompt opens with an interpolated value and continues with a long fixed block. Provider caches match byte-for-byte from the first token, so when the opening varies per request the fixed block behind it is re-billed at the full input rate every time. Moving per-job details below the shared prompt took a published AI code-review pipeline from $2.43 to $0.77 per run.",
|
|
512
|
+
fix: "Put the fixed instructions first and the per-request details (names, ids, dates, file lists) at the end, ideally in the last user message.",
|
|
513
|
+
est_savings_pct: 20
|
|
514
|
+
});
|
|
475
515
|
if (RANDOM_ID.test(raw) || SET_TOOLS.test(raw)) add("serialization", {
|
|
476
516
|
severity: "low",
|
|
477
517
|
line: i + 1,
|
|
@@ -678,6 +718,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
678
718
|
findings.push(...local);
|
|
679
719
|
siteFindings.push(local);
|
|
680
720
|
});
|
|
721
|
+
findings.push(...secretLint(f.path, lines));
|
|
681
722
|
if (callLines.length || hasCacheControl || USAGE_FIELD.test(src)) {
|
|
682
723
|
for (const fd of cacheLint(f.path, lines, masked)) findings.push({ ...fd, severity: kind === "library" || illustrative ? DOWN[fd.severity] : fd.severity });
|
|
683
724
|
}
|
|
@@ -880,14 +921,39 @@ function manifests(root) {
|
|
|
880
921
|
}
|
|
881
922
|
|
|
882
923
|
// mcp/src/main.ts
|
|
883
|
-
var VERSION = "0.2.
|
|
924
|
+
var VERSION = "0.2.10";
|
|
884
925
|
var BAKED_URL = true ? "https://tokenzero.sh" : "__TOKENZERO_DEFAULT_URL__";
|
|
885
926
|
var SUPPORTED = [...MCP_LEGACY_VERSIONS];
|
|
886
927
|
var SERVER = { name: "tokenzero", version: VERSION };
|
|
887
|
-
var CAPABILITIES = { tools: { listChanged: false } };
|
|
888
|
-
var
|
|
928
|
+
var CAPABILITIES = { tools: { listChanged: false }, prompts: { listChanged: false } };
|
|
929
|
+
var PROMPTS = [
|
|
930
|
+
{ name: "cut-my-llm-bill", title: "Cut my LLM bill", description: "Find token waste in this project (scanned locally, nothing uploaded) and show the top fixes with estimated savings. No API key needed.", arguments: [{ name: "repo", description: "owner/name of a public GitHub repo instead of the local project", required: false }] },
|
|
931
|
+
{ name: "price-my-workload", title: "Price my LLM workload", description: "Price a workload across models at list prices, with the cheaper sibling and the effect of prompt caching. No API key needed.", arguments: [{ name: "model", description: "model id, e.g. gpt-6.1-sol or claude-sonnet-5-5", required: true }, { name: "requests_per_month", description: "monthly request volume (default 10000)", required: false }] },
|
|
932
|
+
{ name: "try-tokenzero", title: "Try TokenZero on one call", description: "Run one priced call through TokenZero with no key or setup and show the saving and its signed receipt.", arguments: [] }
|
|
933
|
+
];
|
|
934
|
+
function promptMessages(name, args = {}) {
|
|
935
|
+
const p = PROMPTS.find((x) => x.name === name);
|
|
936
|
+
if (!p) throw new Error(`unknown prompt ${name}; available: ${PROMPTS.map((x) => x.name).join(", ")}`);
|
|
937
|
+
const say = (text) => ({ description: p.description, messages: [{ role: "user", content: { type: "text", text } }] });
|
|
938
|
+
const repo = typeof args?.repo === "string" && /^[\w.-]+\/[\w.-]+$/.test(args.repo.trim()) ? args.repo.trim() : "";
|
|
939
|
+
if (name === "cut-my-llm-bill") return say(`Use the TokenZero MCP tools to find where this project wastes LLM tokens. Call audit_repo_token_waste with ${repo ? `repo "${repo}"` : 'path "." (it scans this machine; nothing is uploaded)'}. Then list the top 3 findings with file:line, the estimated saving and the fix, and finish with the one-line change that applies the fixes automatically (base_url ${serviceUrl() || "https://api.tokenzero.sh"}/v1, my provider key stays mine). If nothing calls an LLM here, call try_tokenzero instead and show what it measured.`);
|
|
940
|
+
if (name === "price-my-workload") {
|
|
941
|
+
const model = typeof args?.model === "string" && args.model.trim() ? args.model.trim().slice(0, 80) : "gpt-6.1-sol";
|
|
942
|
+
const n = Number(args?.requests_per_month) > 0 ? Math.min(1e9, Math.round(Number(args.requests_per_month))) : 1e4;
|
|
943
|
+
return say(`Call the TokenZero tool calculate_model_arbitrage with {"model": "${model}", "requests_per_month": ${n}}. Show the monthly cost on ${model}, on its cheaper sibling, and with prompt caching, as a short table, then one sentence on which change saves the most.`);
|
|
944
|
+
}
|
|
945
|
+
return say("Call the TokenZero tool try_tokenzero with no arguments. Show what happened, the saving it measured, and the signed receipt id, then the keyless curl it returns so I can try a real call.");
|
|
946
|
+
}
|
|
947
|
+
var INSTRUCTIONS = "Start with try_tokenzero (no key, no setup). Use provision_workspace to create a TokenZero workspace and key with no form; 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.";
|
|
889
948
|
var serviceUrl = () => (process.env.TOKENZERO_URL || (BAKED_URL.startsWith("http") ? BAKED_URL : "")).replace(/\/+$/, "");
|
|
890
949
|
var TOOLS = [
|
|
950
|
+
{
|
|
951
|
+
name: "try_tokenzero",
|
|
952
|
+
title: "Try TokenZero (no key needed)",
|
|
953
|
+
annotations: { title: "Try TokenZero (no key needed)", readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
954
|
+
description: 'Call this first: no API key, no signup, no setup. Shows what TokenZero would do to one of your LLM calls: it runs the four savings engines (prompt-cache formatting, edge cache, cascade advice, signed receipt) on a sample or your own prompt and model, and returns the list-price saving, the signed receipt and a copy-paste keyless curl that makes a real call on a free model. Pass {} for the default, or {"model": "gpt-6.1-sol", "system": "...", "prompt": "..."}. Runs in the TokenZero service; send no secrets in the prompt.',
|
|
955
|
+
inputSchema: { type: "object", properties: { model: { type: "string", description: "a priced model id; default gpt-6.1-sol" }, system: { type: "string", description: "your system prompt (optional)" }, prompt: { type: "string", description: "your user message (optional)" } }, additionalProperties: false }
|
|
956
|
+
},
|
|
891
957
|
{
|
|
892
958
|
name: "audit_repo_token_waste",
|
|
893
959
|
title: "Audit a repo or directory for LLM token waste",
|
|
@@ -938,6 +1004,7 @@ async function callTool(name, args) {
|
|
|
938
1004
|
}
|
|
939
1005
|
case "calculate_model_arbitrage":
|
|
940
1006
|
return modelArbitrage(args);
|
|
1007
|
+
case "try_tokenzero":
|
|
941
1008
|
case "optimize_prompt_payload": {
|
|
942
1009
|
const base = serviceUrl();
|
|
943
1010
|
if (!base) throw new Error("set TOKENZERO_URL to use this tool (the cache formatter runs in the TokenZero service)");
|
|
@@ -1019,6 +1086,10 @@ async function handle(m) {
|
|
|
1019
1086
|
return reply(discoverResult(SERVER, CAPABILITIES, INSTRUCTIONS));
|
|
1020
1087
|
case "tools/list":
|
|
1021
1088
|
return reply(modernResult({ tools: TOOLS }, SERVER, true));
|
|
1089
|
+
case "prompts/list":
|
|
1090
|
+
return reply(modernResult({ prompts: PROMPTS }, SERVER, true));
|
|
1091
|
+
case "prompts/get":
|
|
1092
|
+
return reply(modernResult(promptMessages(String(m.params?.name ?? ""), m.params?.arguments), SERVER));
|
|
1022
1093
|
case "tools/call":
|
|
1023
1094
|
return reply(modernResult(await call(), SERVER));
|
|
1024
1095
|
default:
|
|
@@ -1038,6 +1109,10 @@ async function handle(m) {
|
|
|
1038
1109
|
return reply(discoverResult(SERVER, CAPABILITIES, INSTRUCTIONS));
|
|
1039
1110
|
case "tools/list":
|
|
1040
1111
|
return reply({ tools: TOOLS });
|
|
1112
|
+
case "prompts/list":
|
|
1113
|
+
return reply({ prompts: PROMPTS });
|
|
1114
|
+
case "prompts/get":
|
|
1115
|
+
return reply(promptMessages(String(m.params?.name ?? ""), m.params?.arguments));
|
|
1041
1116
|
case "tools/call":
|
|
1042
1117
|
return reply(await call());
|
|
1043
1118
|
default:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenzero/mcp",
|
|
3
3
|
"mcpName": "sh.tokenzero/mcp",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.10",
|
|
5
5
|
"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.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "Apache-2.0",
|