@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.
@@ -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(`^\\s*(?:export\\s+)?(?:const|let|var|final|static|private|public|readonly|\\s)*\\s*(?:self\\.|this\\.)?${name}\\s*(?::[^=\\n]+)?=(?![=>])`);
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.9";
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 INSTRUCTIONS = "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.";
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.9",
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",