mcp-context-cost 0.4.0 → 0.6.0

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.
Files changed (41) hide show
  1. package/README.md +101 -33
  2. package/dist/audit/audit.d.ts +48 -0
  3. package/dist/audit/audit.js +392 -7
  4. package/dist/audit/config.d.ts +46 -0
  5. package/dist/audit/config.js +86 -2
  6. package/dist/audit/deferral.d.ts +366 -0
  7. package/dist/audit/deferral.js +403 -0
  8. package/dist/audit/run.d.ts +22 -0
  9. package/dist/audit/run.js +17 -1
  10. package/dist/cli.js +11 -0
  11. package/dist/core/adoption.d.ts +226 -0
  12. package/dist/core/adoption.js +432 -0
  13. package/dist/core/canonical.d.ts +6 -0
  14. package/dist/core/canonical.js +3 -0
  15. package/dist/core/index.d.ts +1 -0
  16. package/dist/core/index.js +1 -0
  17. package/dist/core/session-start.d.ts +102 -0
  18. package/dist/core/session-start.js +186 -0
  19. package/dist/core/types.d.ts +8 -0
  20. package/dist/sweep/client.d.ts +6 -1
  21. package/dist/sweep/client.js +1 -0
  22. package/dist/sweep/dashboard.d.ts +10 -0
  23. package/dist/sweep/dashboard.js +32 -16
  24. package/dist/sweep/docker.d.ts +23 -0
  25. package/dist/sweep/docker.js +16 -12
  26. package/dist/sweep/harness-guard.d.ts +57 -0
  27. package/dist/sweep/harness-guard.js +144 -0
  28. package/dist/sweep/history.d.ts +33 -1
  29. package/dist/sweep/history.js +60 -5
  30. package/dist/sweep/regen.js +6 -1
  31. package/dist/sweep/report.d.ts +16 -0
  32. package/dist/sweep/report.js +79 -5
  33. package/dist/sweep/run.d.ts +41 -0
  34. package/dist/sweep/run.js +129 -36
  35. package/dist/sweep/server-pages.js +31 -6
  36. package/dist/sweep/session-start.d.ts +3 -0
  37. package/dist/sweep/session-start.js +103 -0
  38. package/dist/sweep/shard.d.ts +41 -0
  39. package/dist/sweep/shard.js +58 -0
  40. package/dist/sweep/sweep-all.js +56 -2
  41. package/package.json +3 -1
package/README.md CHANGED
@@ -24,8 +24,10 @@ claude-desktop ~/Library/Application Support/Claude/claude_desktop_config.json
24
24
  ────────────────────────────────────────────
25
25
  total 37 7,901
26
26
 
27
- Every request in this client carries 7,901 tokens of tool schemas — 4.0% of a
28
- 200,000-token context window, before you type anything.
27
+ 7,901 tokens of tool schemas — 4.0% of a 200,000-token context window.
28
+ No default deferral is on record for claude-desktop, so every request
29
+ carries these tokens before you type anything — an absence of a record
30
+ about the client, not a measurement of it.
29
31
 
30
32
  heaviest tools
31
33
  sequential-thinking · sequentialthinking 990
@@ -46,11 +48,71 @@ Totals are reported per config file, never merged: a context window belongs to o
46
48
  session, so summing Cursor's servers into Claude Desktop's total would describe a session
47
49
  nobody runs.
48
50
 
49
- One nuance: Claude Code's tool search (default-on in recent versions) defers full MCP
50
- schemas until used, loading only tool names at session start. Audit totals are the weight
51
- of the schema surface itself what loads upfront in clients without deferral (Claude
52
- Desktop, Cursor, VS Code, Windsurf today), and what Claude Code's documented fallback
53
- modes still load. Deferral-aware reporting is on the roadmap.
51
+ ### Where this cost is paid in full, and where it is deferred away
52
+
53
+ Not every client puts every tool definition in context on every request, so the total above
54
+ is not automatically your bill. Which client reads the config, and how that client is
55
+ configured **on this machine**, decides it and `audit` reads that rather than assuming it.
56
+
57
+ **Clients with no default deferral on record** — Claude Desktop, Cursor, VS Code, Windsurf.
58
+ The total is what every request carries, as in the example above. That sentence is an
59
+ absence of a record about those clients, not a measurement of them, and the report says so
60
+ in those words.
61
+
62
+ **Claude Code defers MCP tool definitions by default** (its **tool search**): they are not
63
+ in context at session start, and load when the model reaches for one. Three variables move
64
+ that, and `audit` reads all three — from the shell it runs in *and* from the `env` block of
65
+ Claude Code's own settings files (managed, `<cwd>/.claude/settings.local.json`,
66
+ `<cwd>/.claude/settings.json`, `~/.claude/settings.json`), because a machine that switched
67
+ deferral off in a settings file is not a machine running the default:
68
+
69
+ | setting | what the audit reports |
70
+ |---|---|
71
+ | nothing set (the default) | every definition deferred, at any size — no threshold applies |
72
+ | `ENABLE_TOOL_SEARCH=true` | same: every definition deferred |
73
+ | `ENABLE_TOOL_SEARCH=false` | deferral off — every request carries the full total |
74
+ | `ENABLE_TOOL_SEARCH=auto` / `auto:N` | deferred only once definitions reach 10% / N% of the context window |
75
+ | `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` set | tool search off — read first, because `ENABLE_TOOL_SEARCH` cannot override it |
76
+ | `ANTHROPIC_BASE_URL` off `api.anthropic.com` | falls back to loading up front — consulted only while `ENABLE_TOOL_SEARCH` is unset |
77
+ | anything else in `ENABLE_TOOL_SEARCH` | not a documented value, so nothing is claimed from it |
78
+
79
+ On a machine where none of them is set, the same stack reads:
80
+
81
+ ```
82
+ 7,901 tokens of tool schemas — 4.0% of a 200,000-token context window.
83
+ claude-code defers every MCP tool definition (tool search), with no threshold —
84
+ ENABLE_TOOL_SEARCH is unset here, which is the documented default. These tokens are NOT loaded
85
+ up front at any size; they load when the model reaches for a tool. Size
86
+ decides nothing here, so none of the arithmetic above changes the answer.
87
+ Where this was read — Claude Code takes these variables from the shell it
88
+ starts in and from the env block of its own settings files:
89
+ this shell — sets none of them
90
+ 4 other settings file(s) it reads are not on this machine
91
+ The full number is paid where deferral does not apply:
92
+ a Microsoft Foundry deployment hosted on Azure, which rejects tool search server-side
93
+ Google Cloud's Agent Platform on a model earlier than the Claude 4.5 generation
94
+ a model without support for tool_reference blocks (before Sonnet 4.5 / Haiku 4.5 / Opus 4.5)
95
+ a server pinned with "alwaysLoad": true, whose tools load at session start regardless
96
+ ```
97
+
98
+ Set `ENABLE_TOOL_SEARCH=false` in that shell and the same config reports the opposite —
99
+ `loads every tool definition up front here`, naming the variable and the place it was read
100
+ from. Deferring is also not free: what a deferring client *does* load at session start —
101
+ tool names plus the server's `instructions` — is measured per server and published in the
102
+ leaderboard's `session start` column, and for at least one server in the published set it
103
+ costs **more** than loading the definitions would.
104
+
105
+ Three things the report will not do: it will not convert between units silently (in
106
+ threshold mode the stack is compared as a range, because the audit counts wire bytes and the
107
+ threshold is counted in what the client sends to the API — measured at 0.20×–1.92× across 20
108
+ servers); it will not pick a winner when two places on the machine set the same variable to
109
+ different values, or when a settings file exists and cannot be read; and it will not pass an
110
+ absence of a record off as a measurement. The first two print as unanswered questions. The
111
+ third prints as an answer that names itself: for the four discovered clients with no default
112
+ on record — `claude-desktop`, `cursor`, `vscode`, `windsurf` — the tokens are counted as
113
+ loaded up front, and the report says so in those words, "an absence of a record about the
114
+ client, not a measurement of it".
115
+ Full model, sources and dates: [METHODOLOGY §who pays the number](docs/METHODOLOGY.md#who-pays).
54
116
 
55
117
  **In CI**, make it a gate — the bundlesize move for agents:
56
118
 
@@ -83,12 +145,12 @@ INCREASE FAIL:
83
145
  .mcp.json: +2,823 tokens per request, over the 2,000 allowed
84
146
  ```
85
147
 
86
- > **Version note.** `--baseline` and `--max-increase` are **not in the published 0.3.0**
87
- > they are on `main` and ship in the next release. This matters more than a normal
88
- > unreleased-feature note: 0.3.0 ignores flags it does not recognise, so running the command
89
- > above against it produces a plain audit and **exit 0** a passing CI check on a gate that
90
- > never ran. Builds after 0.3.0 reject unknown flags with exit 2 instead. Until the next
91
- > release, pin the gate to a version that has it, or it is not gating anything.
148
+ > **Version note.** `--baseline` and `--max-increase` shipped in **0.4.0** (published
149
+ > 2026-08-18), so the command above gates on
150
+ > `npx -y mcp-context-cost@latest`. Pinning to **0.3.0 or earlier** does not gate, and fails
151
+ > quietly: those builds ignore flags they do not recognise, so the same command produces a
152
+ > plain audit and **exit 0** a passing CI check on a gate that never ran. 0.4.0 rejects
153
+ > unknown flags with exit 2 instead. Pin at or above 0.4.0, or do not pin.
92
154
 
93
155
  A baseline is just a stored `audit --json` report, so any artifact store works. Without
94
156
  `--max-increase` the diff is informational and the exit code is unchanged.
@@ -118,8 +180,10 @@ INCREASE FAIL:
118
180
  Add `--claude` to annotate each server with its Anthropic-request cost from the published
119
181
  [Claude divergence](docs/METHODOLOGY.md#claude-divergence) run — an exact number when the
120
182
  published capture hash matches what you have installed, `—` (silence, not a stale guess)
121
- when it doesn't (today the run covers the top 15 measured servers, so most installs will
122
- show a mix):
183
+ when it doesn't. The run holds 20 rows — the top 20 measured servers by tokens; 19 of them
184
+ still match the capture on disk here, which is why
185
+ [results/leaderboard.md](results/leaderboard.md) prints a claude number for 19 and leaves the
186
+ twentieth blank rather than stale. Most installs will show a mix:
123
187
 
124
188
  ```
125
189
  server tools tokens share claude
@@ -135,21 +199,22 @@ Flags: `--json` (full report on stdout, progress on stderr), `--budget N`,
135
199
 
136
200
  The number `audit` gives you is the same measurement, run across a curated set of public
137
201
  servers — which is how you can tell it is a measurement and not this tool's opinion. It also
138
- shows what you are choosing between: across the 65 servers measured, cost spans **1,700×**,
139
- from the 32-token `postgres` reference server to github's 54,422. The table below starts at
140
- markitdown's 64 tokens, an 850× spread; the full range is in
202
+ shows what you are choosing between: across the 69 servers measured, cost spans **1,700×**,
203
+ from the 32-token `postgres` reference server to github's 54,422. The table below is a
204
+ sample of that range; the full range is in
141
205
  [results/leaderboard.md](results/leaderboard.md).
142
206
 
143
207
  | server | context cost | tools |
144
208
  |---|---:|---:|
145
209
  | github (official) | **54,422 tokens** | 44 |
210
+ | xcodebuildmcp | 26,594 | 24 |
146
211
  | brave-search | 25,456 | 8 |
147
212
  | notion | 17,500 | 24 |
148
213
  | playwright *(4.8M installs/week)* | 4,024 | 24 |
149
214
  | filesystem (reference) | 2,823 | 14 |
150
215
  | markitdown | 64 | 1 |
151
216
 
152
- *(65 of 82 popular servers measured, 2026-08-18 sweep — full table in
217
+ *(69 of 82 popular servers measured, sweeps of 2026-08-18 and 2026-08-19 — full table in
153
218
  [results/leaderboard.md](results/leaderboard.md); every failure is listed with its reason.
154
219
  Each measured server also has a [detail page](https://athakur3.github.io/mcp-context-cost/servers/)
155
220
  showing which tools its tokens are in.)*
@@ -212,7 +277,7 @@ number is *not*, config policy, failure taxonomy, frozen color bands, known dive
212
277
  | `src/cli.ts` | `audit` (measure your own stack), `verify` (re-derive any published number), `measure` |
213
278
  | `spec/fixtures/` | golden vectors shared by the TypeScript and bash implementations |
214
279
  | `tools/` | the one script that calls a network API (Claude divergence); kept out of the package so the library stays offline |
215
- | `upstream/` | staged contribution to [sd2k/mcp-tokens-action](https://github.com/sd2k/mcp-tokens-action): `badge.sh` + action patch + tests |
280
+ | `upstream/` | `badge.sh` + composite-action patch + bash tests — the self-serve badge recipe, carried here |
216
281
  | `servers.yaml` | 82 curated candidates with live install metrics and provenance |
217
282
  | `results/` · `badges/` | measurements, leaderboard, history series, shields endpoint JSONs |
218
283
  | `docs/` | methodology, dashboard, and a generated page per measured server |
@@ -235,32 +300,35 @@ Point the link at the measurement behind the number — for servers in this swee
235
300
  `https://athakur3.github.io/mcp-context-cost/servers/<name>.html`; otherwise the
236
301
  methodology page. A badge nobody can audit is decoration.
237
302
 
238
- Or self-serve from CI via the (staged) mcp-tokens-action badge inputs see
239
- [upstream/action-patch.md](upstream/action-patch.md).
303
+ How many projects outside this repository actually display it is a dated reading rather
304
+ than a guess — [docs/adoption.md](docs/adoption.md), regenerated by `npm run adoption`,
305
+ which publishes the queries it ran and every file it examined. A zero there means the
306
+ search ran and found none; if it could not run, it says that instead of publishing a zero.
240
307
 
241
308
  ## Development
242
309
 
243
310
  ```bash
244
- npm test # 158 TS tests incl. golden fixtures + dispute drills
311
+ npm test # TS suite incl. golden fixtures + dispute drills
245
312
  npx tsc --noEmit # typecheck
246
- ./upstream/tests/badge-test.sh # 21 bash tests — byte-identical to the TS reference
313
+ ./upstream/tests/badge-test.sh # bash suite — byte-identical to the TS reference
247
314
  npm run sweep:all -- --docker # full curated sweep (Docker isolation)
248
315
  ```
249
316
 
250
- Notable engineering choices: the MCP client is a deliberate ~150-line raw-wire
317
+ Notable engineering choices: the MCP client is a deliberate ~220-line raw-wire
251
318
  implementation (SDK schema-parsing can reorder keys, which would corrupt canonical bytes);
252
319
  sweep servers run in credential-free Docker containers with recorded isolation; the badge
253
320
  color bands are frozen against the observed distribution of the first full sweep.
254
321
 
255
322
  ## Status
256
323
 
257
- Active. 56 of the 65 numbers come from a single sweep on 2026-08-16, 3 from 2026-08-17, and
258
- 6 from 2026-08-18 (an upstream `mcp` package bump broke the old low-level-`Server` API these
259
- six relied on; pinning `mcp<2` in their launch commands fixed startup, not this project's code);
260
- the weekly job currently re-measures one server (`memory`), so treat the leaderboard as a
261
- dated snapshot rather than a live feed. Badge PRs are open across the ecosystem and
262
- [sd2k/mcp-tokens-action#5](https://github.com/sd2k/mcp-tokens-action/pull/5) proposes the
263
- self-serve badge path upstream. See [ROADMAP.md](ROADMAP.md) for what's next —
264
- contributions welcome, especially new `servers.yaml` entries.
324
+ Active. 46 of the 69 numbers come from the sweep of 2026-08-19 and 23 from 2026-08-18. Two
325
+ weekly jobs re-measure the set — the `memory` reference server on Mondays, and a rotating
326
+ sixth of the list on Wednesdays, so every row comes round within six weeks. Read each row's
327
+ date as the date it means, and don't take the cadence on trust — the build history is
328
+ public, one click each:
329
+ [re-sweep runs](https://github.com/athakur3/mcp-context-cost/actions/workflows/resweep.yml)
330
+ and [self-badge runs](https://github.com/athakur3/mcp-context-cost/actions/workflows/self-badge.yml).
331
+ See [ROADMAP.md](ROADMAP.md) for what's next — contributions welcome, especially new
332
+ `servers.yaml` entries.
265
333
 
266
334
  MIT © 2026
@@ -1,6 +1,7 @@
1
1
  import { type DivergenceRun } from '../core/divergence.js';
2
2
  import type { Measurement, MeasurementStatus, ToolMeasurement } from '../core/types.js';
3
3
  import type { ConfiguredServer, LoadedConfig } from './config.js';
4
+ import { type DeferralVerdict, type ToolSearchEnv, type ToolSearchSource } from './deferral.js';
4
5
  import { type AuditDiff, type IncreaseGate } from './diff.js';
5
6
  export declare const DEFAULT_CONTEXT_WINDOW = 200000;
6
7
  export type AuditStatus = MeasurementStatus | 'remote-not-measurable';
@@ -53,6 +54,16 @@ export interface AuditConfigResult {
53
54
  skipped: AuditServerResult[];
54
55
  heaviestTools: HeaviestTool[];
55
56
  trimAdvice: TrimAdvice | null;
57
+ /**
58
+ * Whether this client loads the total up front or defers it, and — when the
59
+ * client decides that by a threshold — which side of it this stack is on.
60
+ * Every config carries one: the answer "no deferral is on record for this
61
+ * client" is a reading, not a gap.
62
+ *
63
+ * Configs that one session loads together share a single verdict object, so
64
+ * `deferral.sources` can name more files than this config's own `source`.
65
+ */
66
+ deferral: DeferralVerdict;
56
67
  }
57
68
  export interface BudgetFitStep {
58
69
  name: string;
@@ -89,6 +100,16 @@ export interface AuditReport {
89
100
  generatedAt: string;
90
101
  contextWindow: number;
91
102
  configs: AuditConfigResult[];
103
+ /**
104
+ * Client configs that were read and parsed but declare no servers. They get
105
+ * no report line — there is nothing to total — but they are the record that a
106
+ * client is installed here, which is not the same machine as one with no
107
+ * client at all.
108
+ */
109
+ emptyConfigs: {
110
+ client: string;
111
+ source: string;
112
+ }[];
92
113
  budget?: {
93
114
  limit: number;
94
115
  worstTotal: number;
@@ -110,6 +131,19 @@ export interface AuditReport {
110
131
  }
111
132
  /** Cache key for measurement reuse: the exact argv two configs would spawn. */
112
133
  export declare function serverKey(s: ConfiguredServer): string;
134
+ /**
135
+ * The measurement keys that stand for more than one distinct server.
136
+ *
137
+ * `serverKey` is the argv alone, so two entries running the same command under
138
+ * different environments are measured once and both are given that one number.
139
+ * Environment decides what a server serves — `GITHUB_TOOLSETS` on
140
+ * `github-mcp-server` selects which toolsets it lists — so for entries under one
141
+ * of these keys, the number reported is one entry's, not each one's.
142
+ *
143
+ * Same argv AND same environment is not collapsed: two clients pointing at an
144
+ * identical server are one measurement, which is the reuse this key is for.
145
+ */
146
+ export declare function collapsedKeys(configs: LoadedConfig[]): Set<string>;
113
147
  /**
114
148
  * Assemble the report from configs + measurements. Pure: `runAudit` does the
115
149
  * spawning, this does the arithmetic, so totals and shares are testable without
@@ -121,6 +155,20 @@ export declare function buildReport(configs: LoadedConfig[], measured: Map<strin
121
155
  generatedAt?: string;
122
156
  /** Published `tools-delta/v1` run to join against (`--claude`); omit to skip the join. */
123
157
  divergence?: DivergenceRun | null;
158
+ /**
159
+ * The audited machine's SHELL tool-search variables. Passed in rather than
160
+ * read here so this stays pure and a report is reproducible from its
161
+ * inputs; `runAudit` supplies the real environment. Omitted means the shell
162
+ * set nothing.
163
+ */
164
+ env?: ToolSearchEnv;
165
+ /**
166
+ * The other place those variables come from: Claude Code's own settings
167
+ * files, highest precedence first, as `loadSettingsSources` read them.
168
+ * `runAudit` supplies these. Omitted means they were not read here — which
169
+ * the report says, rather than reporting a default it did not establish.
170
+ */
171
+ settings?: ToolSearchSource[];
124
172
  }): AuditReport;
125
173
  /** Human output. JSON output is the report object itself. */
126
174
  export declare function formatReport(report: AuditReport): string;