mcp-context-cost 0.18.0 → 0.20.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.
- package/README.md +82 -137
- package/dist/audit/audit.d.ts +79 -28
- package/dist/audit/audit.js +261 -52
- package/dist/audit/config.d.ts +29 -8
- package/dist/audit/config.js +82 -12
- package/dist/audit/deferral.d.ts +20 -13
- package/dist/audit/deferral.js +35 -26
- package/dist/audit/diff.js +35 -9
- package/dist/audit/mcp-policy.d.ts +134 -0
- package/dist/audit/mcp-policy.js +229 -0
- package/dist/audit/remote.d.ts +2 -2
- package/dist/audit/remote.js +12 -3
- package/dist/audit/run.d.ts +18 -18
- package/dist/audit/run.js +18 -7
- package/dist/cli.d.ts +0 -49
- package/dist/cli.js +34 -118
- package/dist/core/bands.d.ts +4 -3
- package/dist/core/bands.js +4 -3
- package/dist/core/canonical.d.ts +6 -6
- package/dist/core/capture-index.js +6 -1
- package/dist/core/cross-check.js +11 -3
- package/dist/core/format.d.ts +34 -0
- package/dist/core/format.js +37 -0
- package/dist/core/index.d.ts +39 -7
- package/dist/core/index.js +38 -7
- package/dist/core/protocol.d.ts +25 -3
- package/dist/core/protocol.js +14 -3
- package/dist/core/regression.js +7 -3
- package/dist/core/server-diff.d.ts +2 -2
- package/dist/core/server-diff.js +6 -3
- package/dist/core/session-start.d.ts +7 -50
- package/dist/core/session-start.js +7 -65
- package/dist/core/tool-shape.js +17 -5
- package/dist/core/types.d.ts +27 -22
- package/dist/flags.d.ts +61 -0
- package/dist/flags.js +123 -0
- package/dist/sweep/client.d.ts +12 -4
- package/dist/sweep/client.js +26 -10
- package/dist/sweep/cross-check.d.ts +1 -2
- package/dist/sweep/cross-check.js +35 -25
- package/dist/sweep/dashboard.js +28 -15
- package/dist/sweep/docker.d.ts +8 -8
- package/dist/sweep/docker.js +1 -1
- package/dist/sweep/harness-guard.d.ts +27 -3
- package/dist/sweep/harness-guard.js +30 -6
- package/dist/sweep/history.js +10 -7
- package/dist/sweep/pr-check.js +22 -14
- package/dist/sweep/published-stats.d.ts +8 -10
- package/dist/sweep/published-stats.js +120 -49
- package/dist/sweep/regen.js +8 -10
- package/dist/sweep/registry-scan.d.ts +1 -2
- package/dist/sweep/registry-scan.js +29 -8
- package/dist/sweep/regressions.js +7 -8
- package/dist/sweep/report.d.ts +1 -5
- package/dist/sweep/report.js +13 -28
- package/dist/sweep/run.d.ts +28 -13
- package/dist/sweep/run.js +58 -11
- package/dist/sweep/server-pages.js +13 -8
- package/dist/sweep/servers-schema.d.ts +1 -1
- package/dist/sweep/servers-schema.js +8 -4
- package/dist/sweep/sweep-all.js +24 -15
- package/dist/sweep/tool-shape.js +1 -1
- package/package.json +18 -6
- package/dist/core/adoption.d.ts +0 -403
- package/dist/core/adoption.js +0 -695
- package/dist/sweep/session-start.d.ts +0 -3
- package/dist/sweep/session-start.js +0 -127
package/README.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
[](https://github.com/athakur3/mcp-context-cost/actions/workflows/ci.yml)
|
|
5
5
|
[](https://github.com/marketplace/actions/mcp-context-cost)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
cost than you meant to ship:
|
|
7
|
+
Three entry points: `audit`, for the config you run (below); a GitHub Action, for the server
|
|
8
|
+
you publish; and the measurement itself, as a library, for anything else. The Action is five
|
|
9
|
+
lines in a workflow that fail a pull request adding more context cost than you meant to ship:
|
|
10
10
|
|
|
11
11
|
```yaml
|
|
12
12
|
- uses: athakur3/mcp-context-cost@v1
|
|
@@ -24,6 +24,18 @@ gate passed or not, so a later step can comment the number on the PR or publish
|
|
|
24
24
|
[how the gate decides](#defend-the-number-dont-just-display-it) ·
|
|
25
25
|
[what the number is](docs/METHODOLOGY.md).
|
|
26
26
|
|
|
27
|
+
As a library, if you want the measurement without either wrapper — the same canonical form,
|
|
28
|
+
the same tokenizer, the same badge:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
import { measureTools, canonicalString, toBadge, METHODOLOGY_VERSION } from "mcp-context-cost";
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
It is the definition rather than the harness: measurement and canonical bytes, the color
|
|
35
|
+
bands, the badge JSON, the Claude field-selection mapping, and the session-start load. It
|
|
36
|
+
does not launch servers or read your config — that is what `audit` is for. The surface is
|
|
37
|
+
pinned by a test, so a name is added or removed on purpose and says so in the changelog.
|
|
38
|
+
|
|
27
39
|
**What do the MCP servers in your config cost you before you type anything — and what did
|
|
28
40
|
that last config change add to every session you will ever run?**
|
|
29
41
|
|
|
@@ -69,16 +81,16 @@ VS Code (`.vscode/mcp.json`), Windsurf, Codex CLI (`~/.codex/config.toml`), Gemi
|
|
|
69
81
|
Goose (`~/.config/goose/config.yaml`) — or pass `--config <path>`. Servers are measured by
|
|
70
82
|
the same path as the published leaderboard (dual `tools/list` capture, `o200k_base` over
|
|
71
83
|
canonical JSON), so a server in both places gets the same number. Nothing is written to your
|
|
72
|
-
project, and
|
|
84
|
+
project, and no **value** an entry carries — an env var, a header — is ever read into the
|
|
85
|
+
output. Only their names are.
|
|
73
86
|
|
|
74
|
-
A remote entry — `url`, or the client's own spelling of it — is
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
and nothing here ever opens a browser.
|
|
87
|
+
A remote entry — `url`, or the client's own spelling of it — is probed before anything is
|
|
88
|
+
launched, and reported as measured, **auth-walled**, **protocol-mismatch** or
|
|
89
|
+
**unreachable** depending on what it answers; an auth-walled or protocol-mismatch row makes
|
|
90
|
+
the total above it a floor rather than a number. The probe,
|
|
91
|
+
what each verdict quotes, and why it exists at all are in
|
|
92
|
+
[METHODOLOGY §who pays the number](docs/METHODOLOGY.md#who-pays). Nothing here opens a
|
|
93
|
+
browser.
|
|
82
94
|
|
|
83
95
|
Totals are reported per config file, never merged: a context window belongs to one client
|
|
84
96
|
session, so summing Cursor's servers into Claude Desktop's total would describe a session
|
|
@@ -88,45 +100,21 @@ nobody runs.
|
|
|
88
100
|
|
|
89
101
|
Not every client puts every tool definition in context on every request, so the total above
|
|
90
102
|
is not automatically your bill. Which client reads the config decides it, and for Claude Code
|
|
91
|
-
so does how that client is configured **on this machine** —
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
**Clients with no default deferral on record** — Claude Desktop, Windsurf, Gemini CLI, Zed,
|
|
107
|
-
Kiro, Goose. The total is what every request carries, as in the example above. That sentence is
|
|
108
|
-
an absence of a record about those clients, not a measurement of them, and the report says so
|
|
109
|
-
in those words.
|
|
110
|
-
|
|
111
|
-
**Claude Code defers MCP tool definitions by default** (its **tool search**): they are not
|
|
112
|
-
in context at session start, and load when the model reaches for one. Three variables move
|
|
113
|
-
that, and `audit` reads all three — from the shell it runs in *and* from the `env` block of
|
|
114
|
-
Claude Code's own settings files (managed, `<cwd>/.claude/settings.local.json`,
|
|
115
|
-
`<cwd>/.claude/settings.json`, `~/.claude/settings.json`), because a machine that switched
|
|
116
|
-
deferral off in a settings file is not a machine running the default:
|
|
117
|
-
|
|
118
|
-
| setting | what the audit reports |
|
|
119
|
-
|---|---|
|
|
120
|
-
| nothing set (the default) | every definition deferred, at any size — no threshold applies |
|
|
121
|
-
| `ENABLE_TOOL_SEARCH=true` | same: every definition deferred |
|
|
122
|
-
| `ENABLE_TOOL_SEARCH=false` | deferral off — every request carries the full total. In a settings `env` block that is the **string** `"false"`; the JSON boolean `false` is the last row, not this one |
|
|
123
|
-
| `ENABLE_TOOL_SEARCH=auto` / `auto:N` | deferred only once definitions reach 10% / N% of the context window |
|
|
124
|
-
| `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` set to `1`, `true`, `yes` or `on` | tool search off — read first, because `ENABLE_TOOL_SEARCH` cannot override it |
|
|
125
|
-
| `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` set to `0`, `false`, `no` or `off` | it turned nothing off, so the read moves on and the rows above decide. It is a boolean flag in the client, not a marker whose presence is the signal |
|
|
126
|
-
| `ANTHROPIC_BASE_URL` off `api.anthropic.com` | falls back to loading up front — consulted only while `ENABLE_TOOL_SEARCH` is unset |
|
|
127
|
-
| anything else in `ENABLE_TOOL_SEARCH` | not a documented value, so nothing is claimed from it |
|
|
128
|
-
| any of the three set, in a settings `env` block, to something that is not a string — a JSON boolean, a number, `null` | it is set there and what it is set to is unknown, so no posture is claimed: the report says whether these tokens are deferred cannot be said from it |
|
|
129
|
-
| a server pinned `"alwaysLoad": true` in its entry | loads at session start whatever the setting says — read from the entry, named with its tokens, and left out of any threshold comparison |
|
|
103
|
+
so does how that client is configured **on this machine** — three environment variables and a
|
|
104
|
+
per-server `alwaysLoad` pin, which `audit` reads rather than assumes, from the shell it runs
|
|
105
|
+
in *and* from the `env` block of Claude Code's own settings files. It also reads what an
|
|
106
|
+
organisation deploys: the managed MCP file that takes exclusive control of the server list
|
|
107
|
+
where it exists, and the allow/deny lists that filter what loads — a clean deny is applied to
|
|
108
|
+
the session's claims, an allowlist is reported and never subtracted. No other client's posture
|
|
109
|
+
is readable from a file this opens, so for those the report gives what the vendor is on record
|
|
110
|
+
with, or says there is nothing on record, and claims neither as a measurement.
|
|
111
|
+
|
|
112
|
+
**The model is written down once, on the methodology page**, because it gets corrected often
|
|
113
|
+
and a second copy here would be a second place to correct:
|
|
114
|
+
[METHODOLOGY §who pays the number](docs/METHODOLOGY.md#who-pays) carries every variable and
|
|
115
|
+
what it resolves to, the four kinds of first-party record the rule admits, the questions the
|
|
116
|
+
report refuses to answer rather than guess, and the address and date behind every claim. What
|
|
117
|
+
follows is what that model looks like when you run it.
|
|
130
118
|
|
|
131
119
|
On a machine where none of them is set, the same stack reads:
|
|
132
120
|
|
|
@@ -147,7 +135,7 @@ On a machine where none of them is set, the same stack reads:
|
|
|
147
135
|
a tool whose _meta carries "anthropic/alwaysLoad": true, which this audit does not read from a capture
|
|
148
136
|
```
|
|
149
137
|
|
|
150
|
-
**Do not take that
|
|
138
|
+
**Do not take that model on trust — your own client will tell you.** All of it is read
|
|
151
139
|
from Anthropic's documentation, and documentation about someone else's product is exactly the
|
|
152
140
|
kind of claim this project refuses to leave unchecked elsewhere. Claude Code writes its own
|
|
153
141
|
decision to a debug log, before it sends anything, so you can check your machine rather than
|
|
@@ -164,7 +152,7 @@ Three line shapes answer three different questions:
|
|
|
164
152
|
|---|---|
|
|
165
153
|
| `[ToolSearch:optimistic] mode=…, ENABLE_TOOL_SEARCH=…, result=…` | which mode it picked at startup, and the value it read. **Optimistic is its own word for a guess** — it can be revised below |
|
|
166
154
|
| `Dynamic tool loading: 0/N deferred tools included` | the one that settles it: how many of the `N` deferrable tools went into the request. `0/N` is deferral actually happening |
|
|
167
|
-
| `[ToolSearch:optimistic] disabled: ANTHROPIC_BASE_URL=… is not a first-party Anthropic host` | the fallback
|
|
155
|
+
| `[ToolSearch:optimistic] disabled: ANTHROPIC_BASE_URL=… is not a first-party Anthropic host` | the base-URL fallback, firing, in the client's own words |
|
|
168
156
|
|
|
169
157
|
Read the *later* requests, not the first. A stdio server can finish connecting after the first
|
|
170
158
|
request has already gone, so an early low count is a race rather than a finding.
|
|
@@ -177,8 +165,8 @@ a proportionally larger one — pass `--context` to `audit` to compare against t
|
|
|
177
165
|
actually run.
|
|
178
166
|
|
|
179
167
|
None of this is free, and it is one trivial request. These are Claude Code's own debug lines
|
|
180
|
-
rather than a documented interface, so they can change; the
|
|
181
|
-
holds to a dated re-read. No other client discovered by `audit` writes anything comparable,
|
|
168
|
+
rather than a documented interface, so they can change; the methodology page is what this
|
|
169
|
+
project holds to a dated re-read. No other client discovered by `audit` writes anything comparable,
|
|
182
170
|
which is why no other row here is a measurement: three of them get their vendor's own record,
|
|
183
171
|
dated and addressed, and the rest an absence of one.
|
|
184
172
|
|
|
@@ -189,22 +177,6 @@ tool names plus the server's `instructions` — is measured per server and publi
|
|
|
189
177
|
leaderboard's `session start` column, and for at least one server in the published set it
|
|
190
178
|
costs **more** than loading the definitions would.
|
|
191
179
|
|
|
192
|
-
Three things the report will not do: it will not convert between units silently (in
|
|
193
|
-
threshold mode the stack is compared as a range, because the audit counts wire bytes and the
|
|
194
|
-
threshold is counted in what the client sends to the API — measured at 0.19×–1.93× across 86
|
|
195
|
-
servers); it will not claim a posture the machine did not state readably, which is four
|
|
196
|
-
refusals and not one — when two places set the same variable to different values, when a
|
|
197
|
-
settings file exists and cannot be read, when the place that would decide sets the variable
|
|
198
|
-
to something that is not a string, and when `ENABLE_TOOL_SEARCH` holds a value Claude Code
|
|
199
|
-
does not document; and it will not pass an absence of a record off as a measurement, or a
|
|
200
|
-
vendor's record off as one either. The first two print as unanswered questions. The third
|
|
201
|
-
prints as an answer that names itself: for the six discovered clients with no default on
|
|
202
|
-
record — `claude-desktop`, `windsurf`, `gemini`, `zed`, `kiro`, `goose` — the tokens are
|
|
203
|
-
counted as loaded up front, and the report says so in those words, "an absence of a record
|
|
204
|
-
about the client, not a measurement of it". For `cursor`, `codex` and `vscode` it prints the
|
|
205
|
-
vendor's record with its dates and everything it leaves open, and claims no side.
|
|
206
|
-
Full model, sources and dates: [METHODOLOGY §who pays the number](docs/METHODOLOGY.md#who-pays).
|
|
207
|
-
|
|
208
180
|
**In CI**, make it a gate — the bundlesize move for agents:
|
|
209
181
|
|
|
210
182
|
```bash
|
|
@@ -212,6 +184,13 @@ npx -y mcp-context-cost audit --config .mcp.json --budget 20000
|
|
|
212
184
|
# exits 1 when the stack exceeds the budget, so a PR adding a 25K-token server fails
|
|
213
185
|
```
|
|
214
186
|
|
|
187
|
+
The budget is denominated in **sessions**, because a context window belongs to one session:
|
|
188
|
+
for claude-code that is the files one session loads together — both its configs, or the
|
|
189
|
+
managed file alone where one is deployed, minus anything `deniedMcpServers` removes — and for
|
|
190
|
+
every other client one file is one session. Per-file totals are still reported as file facts;
|
|
191
|
+
the gate reads the costliest session. The baseline diff below stays per file, because a
|
|
192
|
+
baseline pins a file artifact.
|
|
193
|
+
|
|
215
194
|
The budget is an absolute ceiling. What a reviewer actually wants to know is what *this pull
|
|
216
195
|
request* did, so record a baseline and diff against it:
|
|
217
196
|
|
|
@@ -277,19 +256,18 @@ still match today and silence for the rest. Most installs will show a mix:
|
|
|
277
256
|
|
|
278
257
|
```
|
|
279
258
|
server tools tokens share claude
|
|
280
|
-
|
|
281
|
-
|
|
259
|
+
my-server 18 9,400 79.8% 3,120
|
|
260
|
+
my-fork 6 2,380 20.2% —
|
|
282
261
|
```
|
|
283
262
|
|
|
284
|
-
Add `--suggest` to place each of your tools in the measured set's tool-shape distribution
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
1,430 measured tools:
|
|
263
|
+
Add `--suggest` to place each of your tools in the measured set's tool-shape distribution and
|
|
264
|
+
get advice only where the data can point at something. What draws a suggestion, what never
|
|
265
|
+
does, and the percentile that decides are in
|
|
266
|
+
[METHODOLOGY §tool shape](docs/METHODOLOGY.md#tool-shape):
|
|
289
267
|
|
|
290
268
|
```
|
|
291
269
|
suggest — descriptions at or above the 90th percentile of measured tools
|
|
292
|
-
(baseline 2026-09-
|
|
270
|
+
(baseline 2026-09-09: 1,433 tools across 87 measured servers):
|
|
293
271
|
stub · wordy — 345 tokens: description 321 (p92), schema 14
|
|
294
272
|
rewriting the description toward the measured median (27) would recover ≈294 tokens on every request
|
|
295
273
|
1 of 2 tools sit inside the distribution — no advice where nothing is measurably unusual.
|
|
@@ -305,8 +283,8 @@ are not:
|
|
|
305
283
|
|
|
306
284
|
```
|
|
307
285
|
changed — published versions of your servers that have moved since
|
|
308
|
-
(index 2026-09-
|
|
309
|
-
notes (published as
|
|
286
|
+
(index 2026-09-05, 99 published captures; matched by canonical hash, never by name):
|
|
287
|
+
notes (published as my-server) — you have the capture published 2026-08-19 at 1,132 tokens;
|
|
310
288
|
the current one is 2,062 (+930, 2026-08-26)
|
|
311
289
|
updating all 1 would add 930 tokens to every request in this client.
|
|
312
290
|
```
|
|
@@ -349,7 +327,7 @@ answers a question no client asks: **what did this server cost last month?**
|
|
|
349
327
|
[results/regressions.md](results/regressions.md) reports each server's most recent movement —
|
|
350
328
|
dated to when it happened, separated into *shipped more tools* versus *same tools, rewritten*,
|
|
351
329
|
and compared only within one isolation. The ecosystem ratchets upward: of the servers whose
|
|
352
|
-
cost has moved at all,
|
|
330
|
+
cost has moved at all, 15 moved up against 5 that moved down. Method:
|
|
353
331
|
[cost movement](docs/METHODOLOGY.md#cost-movement).
|
|
354
332
|
|
|
355
333
|
If you publish a server, the same measurement is available as a badge, so your users can see
|
|
@@ -378,9 +356,10 @@ the method is [Claude divergence](docs/METHODOLOGY.md#claude-divergence).
|
|
|
378
356
|
|
|
379
357
|
## Why trust the number?
|
|
380
358
|
|
|
381
|
-
Every published number is backed by a `measurement.json
|
|
382
|
-
|
|
383
|
-
|
|
359
|
+
Every published number is backed by a `measurement.json`. What it holds, and the five lines
|
|
360
|
+
that re-derive the number from it, are in
|
|
361
|
+
[METHODOLOGY §reproduce it](docs/METHODOLOGY.md#reproduce-it). Disputes reduce to a byte-level
|
|
362
|
+
diff, and the CLI does that for you:
|
|
384
363
|
|
|
385
364
|
```bash
|
|
386
365
|
npx -y mcp-context-cost verify results/github/measurement.json
|
|
@@ -417,8 +396,8 @@ number is *not*, config policy, failure taxonomy, frozen color bands, known dive
|
|
|
417
396
|
| `src/audit/` | client-config discovery (10 clients; JSON with comments, TOML, YAML), the remote probe, the per-stack report, and the baseline diff |
|
|
418
397
|
| `src/cli.ts` | `audit` (measure your own stack), `verify` (re-derive any published number), `measure` |
|
|
419
398
|
| `spec/fixtures/` | golden vectors shared by the TypeScript and bash implementations |
|
|
420
|
-
| `tools/` | the scripts that call a network API — the Claude divergence run, the
|
|
421
|
-
| `upstream/` | `badge.sh` +
|
|
399
|
+
| `tools/` | the scripts that call a network API — the Claude divergence run, the registry scan — kept out of the package so the library stays offline |
|
|
400
|
+
| `upstream/` | `badge.sh` + bash tests — the self-serve badge recipe, carried here |
|
|
422
401
|
| `servers.yaml` | 107 curated candidates with live install metrics and provenance |
|
|
423
402
|
| `results/` · `badges/` | measurements, leaderboard, history series, shields endpoint JSONs |
|
|
424
403
|
| `docs/` | methodology, dashboard, and a generated page per measured server |
|
|
@@ -434,14 +413,12 @@ That prints the number and writes nothing. Published records
|
|
|
434
413
|
(`results/<name>/measurement.json`, `badges/<name>.json`, the `history.csv`
|
|
435
414
|
row) come from CI: a developer machine is a different architecture under
|
|
436
415
|
different load, and a measurement taken there describes it rather than the
|
|
437
|
-
server.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
on that PR measures the entry read-only, and the rotation publishes it after
|
|
444
|
-
merge.
|
|
416
|
+
server. Which machine a number applies to, and the failed record that made it
|
|
417
|
+
a rule, are in
|
|
418
|
+
[METHODOLOGY §which machine a number applies to](docs/METHODOLOGY.md#which-machine-a-number-applies-to).
|
|
419
|
+
To get your server into the leaderboard, add an entry to `servers.yaml` and
|
|
420
|
+
open a pull request; the check on that PR measures the entry read-only, and the
|
|
421
|
+
rotation publishes it after merge.
|
|
445
422
|
|
|
446
423
|
For a badge on your own README, run the published CLI in your server's own CI
|
|
447
424
|
(the [gate](#defend-the-number-dont-just-display-it) below writes
|
|
@@ -457,7 +434,7 @@ repository), then in your README:
|
|
|
457
434
|
A badge says what your server costs today; it does nothing about the release
|
|
458
435
|
that adds 1,200 tokens to every user's context next month. Across the servers
|
|
459
436
|
measured here most costs hold steady from sweep to sweep, but when a cost does
|
|
460
|
-
move it usually moves up: the [movement report](results/regressions.md) has
|
|
437
|
+
move it usually moves up: the [movement report](results/regressions.md) has 15
|
|
461
438
|
servers ratcheting upward against 5 that got cheaper, and none of those
|
|
462
439
|
maintainers had a check that would have said so first. `measure` takes the same
|
|
463
440
|
gate flags `audit` does, so your own CI can be that check:
|
|
@@ -469,7 +446,7 @@ cp results/my-server/measurement.json .context-cost/baseline.json
|
|
|
469
446
|
|
|
470
447
|
# on every pull request
|
|
471
448
|
npx -y mcp-context-cost measure --name my-server --command "node dist/index.js" \
|
|
472
|
-
--baseline .context-cost/baseline.json --max-increase
|
|
449
|
+
--baseline .context-cost/baseline.json --max-increase 100
|
|
473
450
|
```
|
|
474
451
|
|
|
475
452
|
```
|
|
@@ -483,50 +460,18 @@ INCREASE FAIL: +121 tokens, over the 100 allowed — this change adds that to ev
|
|
|
483
460
|
|
|
484
461
|
Both sides are single measurements carrying per-tool counts, so an established
|
|
485
462
|
change is attributed exactly: which tools arrived, which grew, and by how much.
|
|
486
|
-
And `--max-increase` fails on more than the number
|
|
487
|
-
|
|
488
|
-
improvement is the one mistake a gate like this must not make, so a change that
|
|
489
|
-
could not be established fails too.
|
|
463
|
+
And `--max-increase` fails on more than the number here too: a change that could
|
|
464
|
+
not be established fails the gate, for the reason the `audit` gate gives above.
|
|
490
465
|
|
|
491
|
-
As a GitHub Action
|
|
492
|
-
([full example](examples/server-author-ci.yml))
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
- uses: athakur3/mcp-context-cost@v1
|
|
496
|
-
with:
|
|
497
|
-
name: my-server
|
|
498
|
-
command: node dist/index.js
|
|
499
|
-
baseline: .context-cost/baseline.json
|
|
500
|
-
max-increase: 500
|
|
501
|
-
```
|
|
502
|
-
|
|
503
|
-
It exposes `tokens`, `tools`, `status`, `measurement` and `badge` as outputs —
|
|
504
|
-
available whether the gate passed or not — so a later step can comment the
|
|
505
|
-
number on the pull request or publish the badge.
|
|
466
|
+
As a GitHub Action that whole workflow is the five lines at the top of this page
|
|
467
|
+
([full example](examples/server-author-ci.yml)) — the same inputs, and the same
|
|
468
|
+
outputs written whether the gate passed or not, for a later step to comment on
|
|
469
|
+
the pull request or publish as a badge.
|
|
506
470
|
|
|
507
471
|
Point the link at the measurement behind the number — for servers in this sweep that is
|
|
508
472
|
`https://athakur3.github.io/mcp-context-cost/servers/<name>.html`; otherwise the
|
|
509
473
|
methodology page. A badge nobody can audit is decoration.
|
|
510
474
|
|
|
511
|
-
How many projects outside this repository actually display it is a dated reading rather
|
|
512
|
-
than a guess — [docs/adoption.md](docs/adoption.md), regenerated by `npm run adoption`,
|
|
513
|
-
which publishes the queries it ran and every file it examined. A zero there means the
|
|
514
|
-
search ran and found none; if it could not run, it says that instead of publishing a zero.
|
|
515
|
-
|
|
516
|
-
## Development
|
|
517
|
-
|
|
518
|
-
```bash
|
|
519
|
-
npm test # TS suite incl. golden fixtures + dispute drills
|
|
520
|
-
npx tsc --noEmit # typecheck
|
|
521
|
-
./upstream/tests/badge-test.sh # bash suite — byte-identical to the TS reference
|
|
522
|
-
npm run sweep:all -- --docker # full curated sweep (Docker isolation)
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
Notable engineering choices: the MCP client is a deliberate ~220-line raw-wire
|
|
526
|
-
implementation (SDK schema-parsing can reorder keys, which would corrupt canonical bytes);
|
|
527
|
-
sweep servers run in credential-free Docker containers with recorded isolation; the badge
|
|
528
|
-
color bands are frozen against the observed distribution of the first full sweep.
|
|
529
|
-
|
|
530
475
|
## Status
|
|
531
476
|
|
|
532
477
|
Active. Every row carries the date of its own most recent measurement. Two
|
|
@@ -536,8 +481,8 @@ date as the date it means, and don't take the cadence on trust — the build his
|
|
|
536
481
|
public, one click each:
|
|
537
482
|
[re-sweep runs](https://github.com/athakur3/mcp-context-cost/actions/workflows/resweep.yml)
|
|
538
483
|
and [self-badge runs](https://github.com/athakur3/mcp-context-cost/actions/workflows/self-badge.yml).
|
|
539
|
-
|
|
540
|
-
`servers.yaml` entries
|
|
484
|
+
What changed, and why, is in [CHANGELOG.md](CHANGELOG.md); contributions welcome, especially
|
|
485
|
+
new `servers.yaml` entries — [CONTRIBUTING.md](CONTRIBUTING.md) says what an entry needs, the
|
|
541
486
|
order of steps that leaves a pull request green, and where its number comes from.
|
|
542
487
|
|
|
543
488
|
MIT © 2026
|
package/dist/audit/audit.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { ConfiguredServer, LoadedConfig } from './config.js';
|
|
|
6
6
|
import type { RemoteProbe } from './remote.js';
|
|
7
7
|
import { type DeferralVerdict, type ToolSearchEnv, type ToolSearchSource } from './deferral.js';
|
|
8
8
|
import { type AuditDiff, type IncreaseGate } from './diff.js';
|
|
9
|
+
import { type DenyVerdict, type McpPolicyEvaluation, type UnevaluatedEntry } from './mcp-policy.js';
|
|
9
10
|
export declare const DEFAULT_CONTEXT_WINDOW = 200000;
|
|
10
11
|
/**
|
|
11
12
|
* A server's status in a report: the measurement statuses, plus two that only
|
|
@@ -25,23 +26,23 @@ export interface AuditServerResult {
|
|
|
25
26
|
toolCount: number | null;
|
|
26
27
|
/** Share of this config's measured total, 0–1. */
|
|
27
28
|
share: number | null;
|
|
28
|
-
command?: string;
|
|
29
|
-
url?: string;
|
|
29
|
+
command?: string | undefined;
|
|
30
|
+
url?: string | undefined;
|
|
30
31
|
/** Names only — a server's env values never enter a report. */
|
|
31
32
|
envVarNames: string[];
|
|
32
33
|
/** Names only, and only for a remote entry that carries any — values never enter a report. */
|
|
33
|
-
headerNames?: string[];
|
|
34
|
+
headerNames?: string[] | undefined;
|
|
34
35
|
/** Claude Code's `alwaysLoad: true`, read from the entry: loads at session start whatever the setting. */
|
|
35
|
-
alwaysLoad?: true;
|
|
36
|
-
canonicalSha256?: string | null;
|
|
36
|
+
alwaysLoad?: true | undefined;
|
|
37
|
+
canonicalSha256?: string | null | undefined;
|
|
37
38
|
/**
|
|
38
39
|
* Anthropic-request cost from the published Claude divergence run, only when
|
|
39
40
|
* its captured hash matches this install (`--claude`). `null` means the
|
|
40
41
|
* install doesn't match what was published — silence, not a stale guess.
|
|
41
42
|
* `undefined` means `--claude` wasn't requested at all.
|
|
42
43
|
*/
|
|
43
|
-
claudeTokens?: number | null;
|
|
44
|
-
notes?: string;
|
|
44
|
+
claudeTokens?: number | null | undefined;
|
|
45
|
+
notes?: string | undefined;
|
|
45
46
|
}
|
|
46
47
|
export interface HeaviestTool {
|
|
47
48
|
server: string;
|
|
@@ -96,9 +97,18 @@ export interface AuditConfigResult {
|
|
|
96
97
|
heaviestTools: HeaviestTool[];
|
|
97
98
|
trimAdvice: TrimAdvice | null;
|
|
98
99
|
/** Present only when `--suggest` ran with a usable baseline. */
|
|
99
|
-
suggestions?: ConfigSuggestions;
|
|
100
|
+
suggestions?: ConfigSuggestions | undefined;
|
|
100
101
|
/** Present only when `--changed` ran with a usable capture index. */
|
|
101
|
-
captureVerdicts?: ServerCaptureVerdict[];
|
|
102
|
+
captureVerdicts?: ServerCaptureVerdict[] | undefined;
|
|
103
|
+
/** Set on the managed MCP file's own row (see managedMcpPath in config.ts). */
|
|
104
|
+
managed?: true | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* Set on a claude-code config that is measured here but does not load in a
|
|
107
|
+
* session, because a managed MCP file has exclusive control. The value is
|
|
108
|
+
* that file's path. The measurement stands — it is a fact about this file —
|
|
109
|
+
* and every session-level claim belongs to the managed file instead.
|
|
110
|
+
*/
|
|
111
|
+
suppressedByManagedMcp?: string | undefined;
|
|
102
112
|
/**
|
|
103
113
|
* Whether this client loads the total up front or defers it, and — when the
|
|
104
114
|
* client decides that by a threshold — which side of it this stack is on.
|
|
@@ -138,7 +148,7 @@ export interface BudgetFit {
|
|
|
138
148
|
* need, and dropping by weight will sometimes name the one you cannot live without. That
|
|
139
149
|
* caveat is printed with the result rather than left implied.
|
|
140
150
|
*/
|
|
141
|
-
export declare function planBudgetFit(config: AuditConfigResult, limit: number): BudgetFit;
|
|
151
|
+
export declare function planBudgetFit(config: Pick<AuditConfigResult, 'servers' | 'totalTokens'>, limit: number): BudgetFit;
|
|
142
152
|
export interface AuditReport {
|
|
143
153
|
methodologyVersion: string;
|
|
144
154
|
encoding: 'o200k_base';
|
|
@@ -171,30 +181,73 @@ export interface AuditReport {
|
|
|
171
181
|
* against the whole stack and the verdict fails rather than passing on a
|
|
172
182
|
* total that understates by an unknown amount.
|
|
173
183
|
*/
|
|
174
|
-
unestablished?: string[];
|
|
184
|
+
unestablished?: string[] | undefined;
|
|
175
185
|
/** Present only when over budget: the arithmetic of getting back under it. */
|
|
176
|
-
fit?: BudgetFit;
|
|
186
|
+
fit?: BudgetFit | undefined;
|
|
177
187
|
};
|
|
188
|
+
/**
|
|
189
|
+
* What each session loads, one row per session scope — for claude-code the
|
|
190
|
+
* files one session reads together (the managed file alone where deployed),
|
|
191
|
+
* with denylist removals already applied and named. Config rows above stay
|
|
192
|
+
* file facts; this is the session fact, and it is what `--budget` gates.
|
|
193
|
+
*/
|
|
194
|
+
sessions?: {
|
|
195
|
+
client: string;
|
|
196
|
+
sources: string[];
|
|
197
|
+
totalTokens: number;
|
|
198
|
+
contextShare: number;
|
|
199
|
+
deniedTokens?: number | undefined;
|
|
200
|
+
}[] | undefined;
|
|
201
|
+
/**
|
|
202
|
+
* Present when the managed MCP file exists on this machine. `exclusive`
|
|
203
|
+
* means a claude-code session loads only its servers; `disabled` that it is
|
|
204
|
+
* deployed with an empty server map, so no MCP server loads at all;
|
|
205
|
+
* `unreadable` that it exists and could not be read, so which servers a
|
|
206
|
+
* session loads cannot be said. `suppressed` names the claude-code configs
|
|
207
|
+
* measured here that do not load under it. Source:
|
|
208
|
+
* code.claude.com/docs/en/managed-mcp.md, read 2026-09-09.
|
|
209
|
+
*/
|
|
210
|
+
managedMcp?: {
|
|
211
|
+
path: string;
|
|
212
|
+
state: 'exclusive' | 'disabled' | 'unreadable';
|
|
213
|
+
error?: string | undefined;
|
|
214
|
+
suppressed: string[];
|
|
215
|
+
} | undefined;
|
|
216
|
+
/**
|
|
217
|
+
* Claude Code's MCP allowlist/denylist, as read from the settings files this
|
|
218
|
+
* audit opens, evaluated over the claude-code session's servers. Deny
|
|
219
|
+
* matches are applied — those servers are left out of the session-level
|
|
220
|
+
* sums, never out of a file's own total; allow verdicts are reported and
|
|
221
|
+
* never applied (mcp-policy.ts says why the two differ). Absent when no read
|
|
222
|
+
* settings file sets either list.
|
|
223
|
+
*/
|
|
224
|
+
mcpPolicy?: {
|
|
225
|
+
denied: (DenyVerdict & {
|
|
226
|
+
tokens: number | null;
|
|
227
|
+
})[];
|
|
228
|
+
denyUnevaluated: UnevaluatedEntry[];
|
|
229
|
+
allow?: McpPolicyEvaluation['allow'];
|
|
230
|
+
} | undefined;
|
|
178
231
|
/** Present only when a divergence run was supplied (`--claude`). */
|
|
179
232
|
claudeDivergence?: {
|
|
180
233
|
model: string;
|
|
181
234
|
measuredAt: string;
|
|
182
|
-
};
|
|
235
|
+
} | undefined;
|
|
183
236
|
/** Which published tool-shape baseline `--suggest` read its percentiles from. */
|
|
184
237
|
toolShape?: {
|
|
185
238
|
generatedAt: string;
|
|
186
239
|
toolCount: number;
|
|
187
240
|
serverCount: number;
|
|
188
|
-
};
|
|
241
|
+
} | undefined;
|
|
189
242
|
/** Which published capture index `--changed` joined against. */
|
|
190
243
|
captureIndex?: {
|
|
191
244
|
generatedAt: string;
|
|
192
245
|
captureCount: number;
|
|
193
|
-
};
|
|
246
|
+
} | undefined;
|
|
194
247
|
/** Present only when a baseline report was supplied (`--baseline`). */
|
|
195
|
-
diff?: AuditDiff;
|
|
248
|
+
diff?: AuditDiff | undefined;
|
|
196
249
|
/** Present only when `--max-increase` was supplied alongside a baseline. */
|
|
197
|
-
increaseGate?: IncreaseGate;
|
|
250
|
+
increaseGate?: IncreaseGate | undefined;
|
|
198
251
|
problems: string[];
|
|
199
252
|
}
|
|
200
253
|
/** Cache key for measurement reuse: the exact argv two configs would spawn. */
|
|
@@ -218,38 +271,36 @@ export declare function collapsedKeys(configs: LoadedConfig[]): Set<string>;
|
|
|
218
271
|
* launching a single server.
|
|
219
272
|
*/
|
|
220
273
|
export declare function buildReport(configs: LoadedConfig[], measured: Map<string, Measurement>, opts?: {
|
|
221
|
-
contextWindow?: number;
|
|
222
|
-
budget?: number;
|
|
223
|
-
generatedAt?: string;
|
|
274
|
+
contextWindow?: number | undefined;
|
|
275
|
+
budget?: number | undefined;
|
|
276
|
+
generatedAt?: string | undefined;
|
|
224
277
|
/** Published `tools-delta/v1` run to join against (`--claude`); omit to skip the join. */
|
|
225
|
-
divergence?: DivergenceRun | null;
|
|
278
|
+
divergence?: DivergenceRun | null | undefined;
|
|
226
279
|
/** Published `tool-shape/v1` baseline (`--suggest`); omit to skip suggestions. */
|
|
227
|
-
toolShape?: ToolShapeBaseline | null;
|
|
280
|
+
toolShape?: ToolShapeBaseline | null | undefined;
|
|
228
281
|
/** Published `capture-index/v1` (`--changed`); omit to skip the version join. */
|
|
229
|
-
captureIndex?: CaptureIndex | null;
|
|
282
|
+
captureIndex?: CaptureIndex | null | undefined;
|
|
230
283
|
/**
|
|
231
284
|
* The audited machine's SHELL tool-search variables. Passed in rather than
|
|
232
285
|
* read here so this stays pure and a report is reproducible from its
|
|
233
286
|
* inputs; `runAudit` supplies the real environment. Omitted means the shell
|
|
234
287
|
* set nothing.
|
|
235
288
|
*/
|
|
236
|
-
env?: ToolSearchEnv;
|
|
289
|
+
env?: ToolSearchEnv | undefined;
|
|
237
290
|
/**
|
|
238
291
|
* The other place those variables come from: Claude Code's own settings
|
|
239
292
|
* files, highest precedence first, as `loadSettingsSources` read them.
|
|
240
293
|
* `runAudit` supplies these. Omitted means they were not read here — which
|
|
241
294
|
* the report says, rather than reporting a default it did not establish.
|
|
242
295
|
*/
|
|
243
|
-
settings?: ToolSearchSource[];
|
|
296
|
+
settings?: ToolSearchSource[] | undefined;
|
|
244
297
|
/**
|
|
245
298
|
* What each remote endpoint said to an unauthenticated `initialize`, keyed
|
|
246
299
|
* by `serverKey`. `runAudit` supplies it from `probeRemotes`; omitted, a
|
|
247
300
|
* remote entry is reported as not probed rather than as anything else.
|
|
248
301
|
*/
|
|
249
|
-
remotes?: Map<string, RemoteProbe
|
|
302
|
+
remotes?: Map<string, RemoteProbe> | undefined;
|
|
250
303
|
}): AuditReport;
|
|
251
304
|
/** Human output. JSON output is the report object itself. */
|
|
252
305
|
export declare function formatReport(report: AuditReport): string;
|
|
253
|
-
/** Top-level tool list across every config — used by nothing yet, handy for --json consumers. */
|
|
254
|
-
export declare function allHeaviestTools(report: AuditReport, limit?: number): HeaviestTool[];
|
|
255
306
|
export type { ToolMeasurement };
|