mcp-context-cost 0.14.0 → 0.16.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 +24 -11
- package/dist/audit/audit.d.ts +21 -1
- package/dist/audit/audit.js +110 -21
- package/dist/audit/config.d.ts +19 -2
- package/dist/audit/config.js +140 -24
- package/dist/audit/deferral.d.ts +80 -4
- package/dist/audit/deferral.js +72 -4
- package/dist/audit/remote.d.ts +56 -0
- package/dist/audit/remote.js +126 -0
- package/dist/audit/run.d.ts +32 -2
- package/dist/audit/run.js +60 -5
- package/dist/cli.js +2 -1
- package/dist/sweep/published-stats.js +16 -4
- package/dist/sweep/shard.d.ts +1 -1
- package/dist/sweep/shard.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -60,11 +60,22 @@ claude-desktop ~/Library/Application Support/Claude/claude_desktop_config.json
|
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
It finds configs for Claude Desktop, Claude Code (`~/.claude.json`, `.mcp.json`), Cursor,
|
|
63
|
-
VS Code (`.vscode/mcp.json`),
|
|
64
|
-
|
|
63
|
+
VS Code (`.vscode/mcp.json`), Windsurf, Codex CLI (`~/.codex/config.toml`), Gemini CLI
|
|
64
|
+
(`~/.gemini/settings.json`), Zed (`context_servers`), Kiro (`~/.kiro/settings/mcp.json`) and
|
|
65
|
+
Goose (`~/.config/goose/config.yaml`) — or pass `--config <path>`. Servers are measured by
|
|
66
|
+
the same path as the published leaderboard (dual `tools/list` capture, `o200k_base` over
|
|
65
67
|
canonical JSON), so a server in both places gets the same number. Nothing is written to your
|
|
66
68
|
project, and env var **values** are never read into the output — only their names.
|
|
67
69
|
|
|
70
|
+
A remote entry — `url`, or the client's own spelling of it — is first asked what it says to
|
|
71
|
+
an unauthenticated `initialize`. An endpoint that answers is measured through the
|
|
72
|
+
`mcp-remote` bridge, the path the leaderboard's remote rows already take. One that answers
|
|
73
|
+
`401` or `403` is reported **auth-walled**, quoting the status and the `WWW-Authenticate`
|
|
74
|
+
header it sent, with the URL: a working server this audit holds no credential for, so the
|
|
75
|
+
total above it is a floor. One that answers nothing usable is **unreachable**, with the
|
|
76
|
+
reason. Header values an entry carries are sent and never printed — only their names are —
|
|
77
|
+
and nothing here ever opens a browser.
|
|
78
|
+
|
|
68
79
|
Totals are reported per config file, never merged: a context window belongs to one client
|
|
69
80
|
session, so summing Cursor's servers into Claude Desktop's total would describe a session
|
|
70
81
|
nobody runs.
|
|
@@ -75,8 +86,9 @@ Not every client puts every tool definition in context on every request, so the
|
|
|
75
86
|
is not automatically your bill. Which client reads the config, and how that client is
|
|
76
87
|
configured **on this machine**, decides it — and `audit` reads that rather than assuming it.
|
|
77
88
|
|
|
78
|
-
**Clients with no default deferral on record** — Claude Desktop, Cursor, VS Code, Windsurf
|
|
79
|
-
The total is what every request carries, as in the
|
|
89
|
+
**Clients with no default deferral on record** — Claude Desktop, Cursor, VS Code, Windsurf,
|
|
90
|
+
Codex CLI, Gemini CLI, Zed, Kiro, Goose. The total is what every request carries, as in the
|
|
91
|
+
example above. That sentence is an
|
|
80
92
|
absence of a record about those clients, not a measurement of them, and the report says so
|
|
81
93
|
in those words.
|
|
82
94
|
|
|
@@ -97,6 +109,7 @@ deferral off in a settings file is not a machine running the default:
|
|
|
97
109
|
| `ANTHROPIC_BASE_URL` off `api.anthropic.com` | falls back to loading up front — consulted only while `ENABLE_TOOL_SEARCH` is unset |
|
|
98
110
|
| anything else in `ENABLE_TOOL_SEARCH` | not a documented value, so nothing is claimed from it |
|
|
99
111
|
| 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 |
|
|
112
|
+
| 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 |
|
|
100
113
|
|
|
101
114
|
On a machine where none of them is set, the same stack reads:
|
|
102
115
|
|
|
@@ -114,7 +127,7 @@ On a machine where none of them is set, the same stack reads:
|
|
|
114
127
|
a Microsoft Foundry deployment hosted on Azure, which rejects tool search server-side
|
|
115
128
|
Google Cloud's Agent Platform on a model earlier than the Claude 4.5 generation
|
|
116
129
|
a model without support for tool_reference blocks (before Sonnet 4.5 / Haiku 4.5 / Opus 4.5)
|
|
117
|
-
a
|
|
130
|
+
a tool whose _meta carries "anthropic/alwaysLoad": true, which this audit does not read from a capture
|
|
118
131
|
```
|
|
119
132
|
|
|
120
133
|
Set `ENABLE_TOOL_SEARCH=false` in that shell and the same config reports the opposite —
|
|
@@ -133,7 +146,7 @@ settings file exists and cannot be read, when the place that would decide sets t
|
|
|
133
146
|
to something that is not a string, and when `ENABLE_TOOL_SEARCH` holds a value Claude Code
|
|
134
147
|
does not document; and it will not pass an absence of a record off as a measurement. The
|
|
135
148
|
first two print as unanswered questions. The third prints as an answer that names
|
|
136
|
-
itself: for the
|
|
149
|
+
itself: for the nine discovered clients with no default on record — `claude-desktop`, `cursor`, `vscode`, `windsurf`, `codex`, `gemini`, `zed`, `kiro`, `goose` — the tokens are counted as
|
|
137
150
|
loaded up front, and the report says so in those words, "an absence of a record about the
|
|
138
151
|
client, not a measurement of it".
|
|
139
152
|
Full model, sources and dates: [METHODOLOGY §who pays the number](docs/METHODOLOGY.md#who-pays).
|
|
@@ -281,7 +294,7 @@ answers a question no client asks: **what did this server cost last month?**
|
|
|
281
294
|
[results/regressions.md](results/regressions.md) reports each server's most recent movement —
|
|
282
295
|
dated to when it happened, separated into *shipped more tools* versus *same tools, rewritten*,
|
|
283
296
|
and compared only within one isolation. The ecosystem ratchets upward: of the servers whose
|
|
284
|
-
cost has moved at all,
|
|
297
|
+
cost has moved at all, 13 moved up against 5 that moved down. Method:
|
|
285
298
|
[cost movement](docs/METHODOLOGY.md#cost-movement).
|
|
286
299
|
|
|
287
300
|
If you publish a server, the same measurement is available as a badge, so your users can see
|
|
@@ -344,7 +357,7 @@ number is *not*, config policy, failure taxonomy, frozen color bands, known dive
|
|
|
344
357
|
|---|---|
|
|
345
358
|
| `src/core/` | the measurement spec, executable — canonical form, tokenizer, bands, badge JSON |
|
|
346
359
|
| `src/sweep/` | raw-wire MCP stdio client + Dockerized batch sweep + leaderboard/dashboard generators |
|
|
347
|
-
| `src/audit/` | client-config discovery (
|
|
360
|
+
| `src/audit/` | client-config discovery (10 clients; JSON with comments, TOML, YAML), the remote probe, the per-stack report, and the baseline diff |
|
|
348
361
|
| `src/cli.ts` | `audit` (measure your own stack), `verify` (re-derive any published number), `measure` |
|
|
349
362
|
| `spec/fixtures/` | golden vectors shared by the TypeScript and bash implementations |
|
|
350
363
|
| `tools/` | the scripts that call a network API — the Claude divergence run, the adoption reading, the registry scan — kept out of the package so the library stays offline |
|
|
@@ -387,8 +400,8 @@ repository), then in your README:
|
|
|
387
400
|
A badge says what your server costs today; it does nothing about the release
|
|
388
401
|
that adds 1,200 tokens to every user's context next month. Across the servers
|
|
389
402
|
measured here most costs hold steady from sweep to sweep, but when a cost does
|
|
390
|
-
move it usually moves up: the [movement report](results/regressions.md) has
|
|
391
|
-
servers ratcheting upward against
|
|
403
|
+
move it usually moves up: the [movement report](results/regressions.md) has 13
|
|
404
|
+
servers ratcheting upward against 5 that got cheaper, and none of those
|
|
392
405
|
maintainers had a check that would have said so first. `measure` takes the same
|
|
393
406
|
gate flags `audit` does, so your own CI can be that check:
|
|
394
407
|
|
|
@@ -464,7 +477,7 @@ says as a whole is written up, dated, in
|
|
|
464
477
|
[The State of MCP Context Cost](https://athakur3.github.io/mcp-context-cost/state-of-mcp-context-cost)
|
|
465
478
|
(September 2026). Two
|
|
466
479
|
weekly jobs re-measure the set — the `memory` reference server on Mondays, and a rotating
|
|
467
|
-
|
|
480
|
+
third of the list on Wednesdays, so every row comes round within three weeks. Read each row's
|
|
468
481
|
date as the date it means, and don't take the cadence on trust — the build history is
|
|
469
482
|
public, one click each:
|
|
470
483
|
[re-sweep runs](https://github.com/athakur3/mcp-context-cost/actions/workflows/resweep.yml)
|
package/dist/audit/audit.d.ts
CHANGED
|
@@ -3,10 +3,20 @@ import { type ToolShapeBaseline, type ToolSuggestion } from '../core/tool-shape.
|
|
|
3
3
|
import { type CaptureIndex, type CaptureVerdict } from '../core/capture-index.js';
|
|
4
4
|
import type { Measurement, MeasurementStatus, ToolMeasurement } from '../core/types.js';
|
|
5
5
|
import type { ConfiguredServer, LoadedConfig } from './config.js';
|
|
6
|
+
import type { RemoteProbe } from './remote.js';
|
|
6
7
|
import { type DeferralVerdict, type ToolSearchEnv, type ToolSearchSource } from './deferral.js';
|
|
7
8
|
import { type AuditDiff, type IncreaseGate } from './diff.js';
|
|
8
9
|
export declare const DEFAULT_CONTEXT_WINDOW = 200000;
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
* A server's status in a report: the measurement statuses, plus two that only
|
|
12
|
+
* a remote endpoint can have. `auth-walled` — it answered the unauthenticated
|
|
13
|
+
* `initialize` with 401 or 403: it works, and it wants a credential this audit
|
|
14
|
+
* does not hold. `unreachable` — no MCP answer came back at all: a connection
|
|
15
|
+
* failure, a timeout, or a status that is neither an answer nor a wall. Before
|
|
16
|
+
* 2026-09-06 every `url` entry was `remote-not-measurable`, which said nothing
|
|
17
|
+
* about the endpoint and under-counted exactly the stacks the audit is for.
|
|
18
|
+
*/
|
|
19
|
+
export type AuditStatus = MeasurementStatus | 'auth-walled' | 'unreachable';
|
|
10
20
|
export interface AuditServerResult {
|
|
11
21
|
name: string;
|
|
12
22
|
transport: 'stdio' | 'remote';
|
|
@@ -19,6 +29,10 @@ export interface AuditServerResult {
|
|
|
19
29
|
url?: string;
|
|
20
30
|
/** Names only — a server's env values never enter a report. */
|
|
21
31
|
envVarNames: string[];
|
|
32
|
+
/** Names only, and only for a remote entry that carries any — values never enter a report. */
|
|
33
|
+
headerNames?: string[];
|
|
34
|
+
/** Claude Code's `alwaysLoad: true`, read from the entry: loads at session start whatever the setting. */
|
|
35
|
+
alwaysLoad?: true;
|
|
22
36
|
canonicalSha256?: string | null;
|
|
23
37
|
/**
|
|
24
38
|
* Anthropic-request cost from the published Claude divergence run, only when
|
|
@@ -227,6 +241,12 @@ export declare function buildReport(configs: LoadedConfig[], measured: Map<strin
|
|
|
227
241
|
* the report says, rather than reporting a default it did not establish.
|
|
228
242
|
*/
|
|
229
243
|
settings?: ToolSearchSource[];
|
|
244
|
+
/**
|
|
245
|
+
* What each remote endpoint said to an unauthenticated `initialize`, keyed
|
|
246
|
+
* by `serverKey`. `runAudit` supplies it from `probeRemotes`; omitted, a
|
|
247
|
+
* remote entry is reported as not probed rather than as anything else.
|
|
248
|
+
*/
|
|
249
|
+
remotes?: Map<string, RemoteProbe>;
|
|
230
250
|
}): AuditReport;
|
|
231
251
|
/** Human output. JSON output is the report object itself. */
|
|
232
252
|
export declare function formatReport(report: AuditReport): string;
|
package/dist/audit/audit.js
CHANGED
|
@@ -16,7 +16,7 @@ import { METHODOLOGY_VERSION } from '../core/canonical.js';
|
|
|
16
16
|
import { isCurrent } from '../core/divergence.js';
|
|
17
17
|
import { SUGGEST_DESCRIPTION_PERCENTILE, suggestFor, } from '../core/tool-shape.js';
|
|
18
18
|
import { identify } from '../core/capture-index.js';
|
|
19
|
-
import { evaluateDeferral, PUBLISHED_WIRE_TO_CLIENT_RATIO, SHELL_SOURCE, } from './deferral.js';
|
|
19
|
+
import { evaluateDeferral, BAND_PRECISION, PUBLISHED_WIRE_TO_CLIENT_RATIO, SHELL_SOURCE, } from './deferral.js';
|
|
20
20
|
import { formatDiff, formatGate } from './diff.js';
|
|
21
21
|
export const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
22
22
|
const TRIM_TOOL_COUNT = 3;
|
|
@@ -85,7 +85,30 @@ export function serverKey(s) {
|
|
|
85
85
|
*/
|
|
86
86
|
function envSignature(s) {
|
|
87
87
|
const env = s.env ?? {};
|
|
88
|
-
|
|
88
|
+
const headers = s.headers ?? {};
|
|
89
|
+
return JSON.stringify([
|
|
90
|
+
Object.keys(env).sort().map((k) => [k, env[k]]),
|
|
91
|
+
// A remote's headers decide what it serves the way env decides for a
|
|
92
|
+
// process: a bearer token selects an account, and an account its tools.
|
|
93
|
+
Object.keys(headers).sort().map((k) => [k, headers[k]]),
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Every value an entry would spawn or send — env values, header values — so
|
|
98
|
+
* that none of them reaches a report by way of a server's own stderr. Values
|
|
99
|
+
* shorter than four characters are left alone: replacing every "1" in a
|
|
100
|
+
* message is not redaction.
|
|
101
|
+
*/
|
|
102
|
+
function secrets(s) {
|
|
103
|
+
return [...Object.values(s.env ?? {}), ...Object.values(s.headers ?? {})].filter((v) => v.length >= 4);
|
|
104
|
+
}
|
|
105
|
+
function redact(text, values) {
|
|
106
|
+
if (!text)
|
|
107
|
+
return text;
|
|
108
|
+
let out = text;
|
|
109
|
+
for (const v of values)
|
|
110
|
+
out = out.split(v).join('<redacted>');
|
|
111
|
+
return out;
|
|
89
112
|
}
|
|
90
113
|
/**
|
|
91
114
|
* The measurement keys that stand for more than one distinct server.
|
|
@@ -105,8 +128,6 @@ export function collapsedKeys(configs) {
|
|
|
105
128
|
if (cfg.error)
|
|
106
129
|
continue;
|
|
107
130
|
for (const s of cfg.servers) {
|
|
108
|
-
if (s.transport !== 'stdio')
|
|
109
|
-
continue;
|
|
110
131
|
const key = serverKey(s);
|
|
111
132
|
const seen = envs.get(key);
|
|
112
133
|
if (seen)
|
|
@@ -160,7 +181,12 @@ function attachDeferral(configs, contextWindow, opts) {
|
|
|
160
181
|
evaluateDeferral({
|
|
161
182
|
client: group[0].client,
|
|
162
183
|
sources: group.map((c) => c.source),
|
|
163
|
-
servers: group.flatMap((c) => c.servers.map((s) => ({
|
|
184
|
+
servers: group.flatMap((c) => c.servers.map((s) => ({
|
|
185
|
+
name: s.name,
|
|
186
|
+
tokens: s.tokens ?? 0,
|
|
187
|
+
claudeTokens: s.claudeTokens,
|
|
188
|
+
...(s.alwaysLoad ? { alwaysLoad: true } : {}),
|
|
189
|
+
}))),
|
|
164
190
|
skippedCount: group.reduce((a, c) => a + c.skipped.length, 0),
|
|
165
191
|
sharedMeasurements: group.reduce((a, c) => a + (opts.shared.get(c) ?? 0), 0),
|
|
166
192
|
}, { contextWindow, env: opts.env, settings: opts.settings, divergence: opts.divergence }));
|
|
@@ -219,17 +245,34 @@ export function buildReport(configs, measured, opts = {}) {
|
|
|
219
245
|
command: s.command,
|
|
220
246
|
url: s.url,
|
|
221
247
|
envVarNames: s.envVarNames,
|
|
248
|
+
...(s.headerNames?.length ? { headerNames: s.headerNames } : {}),
|
|
249
|
+
...(s.alwaysLoad ? { alwaysLoad: true } : {}),
|
|
222
250
|
};
|
|
251
|
+
const none = { tokens: null, toolCount: null, share: null };
|
|
223
252
|
if (s.transport === 'remote') {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
status: '
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
253
|
+
const probe = opts.remotes?.get(serverKey(s));
|
|
254
|
+
if (!probe) {
|
|
255
|
+
skipped.push({ ...base, ...none, status: 'unreachable', notes: `${s.url ?? 'url'} — not probed` });
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (probe.kind === 'auth-walled') {
|
|
259
|
+
// The server's own words: the status it sent and the header it named
|
|
260
|
+
// its authorization server with. A wall is a working server this
|
|
261
|
+
// audit holds no credential for; the session that holds one pays
|
|
262
|
+
// this server on top of the total below, which is therefore a floor.
|
|
263
|
+
skipped.push({
|
|
264
|
+
...base,
|
|
265
|
+
...none,
|
|
266
|
+
status: 'auth-walled',
|
|
267
|
+
notes: `${s.url} answered ${probe.detail} — wants a credential this audit does not hold`,
|
|
268
|
+
});
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (probe.kind === 'unreachable') {
|
|
272
|
+
skipped.push({ ...base, ...none, status: 'unreachable', notes: `${s.url}: ${probe.detail}` });
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
// Open: measured through the bridge, and read below like any launch.
|
|
233
276
|
}
|
|
234
277
|
const m = measured.get(serverKey(s));
|
|
235
278
|
if (!m) {
|
|
@@ -243,7 +286,7 @@ export function buildReport(configs, measured, opts = {}) {
|
|
|
243
286
|
tokens: null,
|
|
244
287
|
toolCount: null,
|
|
245
288
|
share: null,
|
|
246
|
-
notes: m.notes?.split('\n')[0]?.slice(0, 200),
|
|
289
|
+
notes: redact(m.notes?.split('\n')[0]?.slice(0, 200), secrets(s)),
|
|
247
290
|
});
|
|
248
291
|
continue;
|
|
249
292
|
}
|
|
@@ -258,7 +301,7 @@ export function buildReport(configs, measured, opts = {}) {
|
|
|
258
301
|
share: null, // filled once the total is known
|
|
259
302
|
canonicalSha256: m.canonicalSha256,
|
|
260
303
|
claudeTokens: opts.divergence ? (isCurrent(divRow, m.canonicalSha256 ?? null) ? divRow.claudeDelta : null) : undefined,
|
|
261
|
-
notes: m.status === 'dynamic' ? m.notes : undefined,
|
|
304
|
+
notes: m.status === 'dynamic' ? redact(m.notes, secrets(s)) : undefined,
|
|
262
305
|
});
|
|
263
306
|
for (const t of m.tools) {
|
|
264
307
|
tools.push({ server: s.name, tool: t.name, tokens: t.tokens });
|
|
@@ -330,8 +373,11 @@ export function buildReport(configs, measured, opts = {}) {
|
|
|
330
373
|
// server that failed to start contributes 0, so the stack reads lighter
|
|
331
374
|
// than it is and the budget passes on a number that is missing a server —
|
|
332
375
|
// exactly the PR the README says this gate catches. The server-level gate
|
|
333
|
-
// (core/server-diff.ts) already refuses this; so does this one now.
|
|
334
|
-
|
|
376
|
+
// (core/server-diff.ts) already refuses this; so does this one now. Every
|
|
377
|
+
// skipped row counts: an auth-walled endpoint is a working server the
|
|
378
|
+
// session pays for with its credential, and an unreachable one is a cost
|
|
379
|
+
// this could not establish, not a cost of zero.
|
|
380
|
+
const unestablished = results.flatMap((c) => c.skipped.map((s) => `${c.source}: ${s.name} (${s.status})`));
|
|
335
381
|
const over = (worst?.totalTokens ?? 0) > opts.budget;
|
|
336
382
|
report.budget = {
|
|
337
383
|
limit: opts.budget,
|
|
@@ -447,6 +493,40 @@ function sharedMeasurementLines(d, skippedNames, consequence) {
|
|
|
447
493
|
}
|
|
448
494
|
return lines;
|
|
449
495
|
}
|
|
496
|
+
/**
|
|
497
|
+
* What the threshold below is a share *of*, said where the threshold is said.
|
|
498
|
+
*
|
|
499
|
+
* The setting is a percentage of the context window, so the token figure it
|
|
500
|
+
* resolves to is only as right as the window this audit assumed. That window is
|
|
501
|
+
* a property of the model the client runs, which no config file states and this
|
|
502
|
+
* audit therefore cannot read: it uses 200,000 unless `--context` overrides it.
|
|
503
|
+
* A model with a larger window puts the threshold proportionally higher, and a
|
|
504
|
+
* stack that clears a 20,000-token threshold does not clear a 100,000-token one.
|
|
505
|
+
*
|
|
506
|
+
* This is stated rather than silently assumed because the error has a direction.
|
|
507
|
+
* A window assumed too small makes the threshold too small, which pushes the
|
|
508
|
+
* verdict toward "at or above" — toward telling a reader their tool definitions
|
|
509
|
+
* are deferred, and therefore not charged, when the client's own larger
|
|
510
|
+
* threshold would have loaded every one of them up front. Of the two ways to be
|
|
511
|
+
* wrong here, that is the one that costs somebody tokens they were told they
|
|
512
|
+
* would not pay, so it does not get to be a footnote.
|
|
513
|
+
*
|
|
514
|
+
* Only threshold mode reaches this. The default defers everything at any size,
|
|
515
|
+
* where no window and no arithmetic enter the answer at all.
|
|
516
|
+
*/
|
|
517
|
+
function thresholdAssumptionLines(d) {
|
|
518
|
+
const share = d.thresholdShare ?? 0;
|
|
519
|
+
if (!(share > 0) || d.thresholdTokens === null || d.thresholdTokens === undefined)
|
|
520
|
+
return [];
|
|
521
|
+
const window = Math.round(d.thresholdTokens / share);
|
|
522
|
+
return [
|
|
523
|
+
` That token figure assumes a ${n(window)}-token context window, which is a`,
|
|
524
|
+
' property of the model in use and not of your config, so this audit cannot',
|
|
525
|
+
' read it — pass --context to set it. A model with a larger window has a',
|
|
526
|
+
' proportionally larger threshold, and the same stack can fall on either',
|
|
527
|
+
' side of it depending on which model the client runs.',
|
|
528
|
+
];
|
|
529
|
+
}
|
|
450
530
|
/** Where a threshold is in play, the unknown size is the whole verdict. */
|
|
451
531
|
const SIDE_UNKNOWN = [
|
|
452
532
|
' either direction — which side of the threshold this stack falls on cannot be',
|
|
@@ -480,6 +560,14 @@ function deferralLines(d, skippedNames) {
|
|
|
480
560
|
lines.push(` ${s}`);
|
|
481
561
|
lines.push(' so they face the question below together, as their sum.');
|
|
482
562
|
}
|
|
563
|
+
// Read from the entries, so it is stated up front rather than listed among
|
|
564
|
+
// the conditions a reader has to check: whatever the setting says, these load.
|
|
565
|
+
if (d.mechanism === 'tool search' && d.alwaysLoad.servers.length) {
|
|
566
|
+
const n = d.alwaysLoad.servers.length;
|
|
567
|
+
lines.push(` ${n} server${n === 1 ? ' is' : 's are'} pinned "alwaysLoad": true and load${n === 1 ? 's' : ''} at session start whatever`);
|
|
568
|
+
lines.push(` the setting says: ${d.alwaysLoad.servers.join(', ')} — ${d.alwaysLoad.tokens.toLocaleString()} wire tokens,`);
|
|
569
|
+
lines.push(' left out of any threshold comparison below.');
|
|
570
|
+
}
|
|
483
571
|
if (d.mode === 'client-unknown') {
|
|
484
572
|
lines.push(' Which client reads this config is not known here, so whether it defers');
|
|
485
573
|
lines.push(' tool definitions by default is not known either. Read as loaded up front.');
|
|
@@ -554,6 +642,7 @@ function deferralLines(d, skippedNames) {
|
|
|
554
642
|
lines.push(` ${d.client} defers tool definitions above a threshold here (${d.mechanism}):`);
|
|
555
643
|
lines.push(` ${settingPhrase(d)}, so deferral activates once the`);
|
|
556
644
|
lines.push(` definitions reach ${n(t)} tokens — ${pct(d.thresholdShare ?? 0)} of the context window.`);
|
|
645
|
+
lines.push(...thresholdAssumptionLines(d));
|
|
557
646
|
lines.push(...postureSourceLines(d));
|
|
558
647
|
const c = d.clientTokens;
|
|
559
648
|
if (!c) {
|
|
@@ -608,7 +697,7 @@ function deferralLines(d, skippedNames) {
|
|
|
608
697
|
/** "0.20×–1.92×", from whichever band the verdict was actually computed against. */
|
|
609
698
|
function ratioBand(d) {
|
|
610
699
|
const r = d.ratio ?? PUBLISHED_WIRE_TO_CLIENT_RATIO;
|
|
611
|
-
return `${r.low.toFixed(
|
|
700
|
+
return `${r.low.toFixed(BAND_PRECISION)}×–${r.high.toFixed(BAND_PRECISION)}×`;
|
|
612
701
|
}
|
|
613
702
|
/** Human output. JSON output is the report object itself. */
|
|
614
703
|
export function formatReport(report) {
|
|
@@ -807,8 +896,8 @@ export function formatReport(report) {
|
|
|
807
896
|
}
|
|
808
897
|
lines.push('');
|
|
809
898
|
lines.push('These are wire tokens — what the server puts on the wire, counted with o200k_base. What your model is billed');
|
|
810
|
-
lines.push(`differs per provider: measured ratios run ${PUBLISHED_WIRE_TO_CLIENT_RATIO.low.toFixed(
|
|
811
|
-
`${PUBLISHED_WIRE_TO_CLIENT_RATIO.high.toFixed(
|
|
899
|
+
lines.push(`differs per provider: measured ratios run ${PUBLISHED_WIRE_TO_CLIENT_RATIO.low.toFixed(BAND_PRECISION)}×–` +
|
|
900
|
+
`${PUBLISHED_WIRE_TO_CLIENT_RATIO.high.toFixed(BAND_PRECISION)}× on Anthropic requests. See docs/METHODOLOGY.md §claude-divergence.`);
|
|
812
901
|
return lines.map((l) => l.replace(/\s+$/, '')).join('\n');
|
|
813
902
|
}
|
|
814
903
|
/** Top-level tool list across every config — used by nothing yet, handy for --json consumers. */
|
package/dist/audit/config.d.ts
CHANGED
|
@@ -13,8 +13,19 @@ export interface ConfiguredServer {
|
|
|
13
13
|
envVarNames: string[];
|
|
14
14
|
/** Values, needed to spawn the server. NEVER serialize this. */
|
|
15
15
|
env?: Record<string, string>;
|
|
16
|
-
/** Remote endpoint —
|
|
16
|
+
/** Remote endpoint — probed, then measured through the bridge or reported as walled. */
|
|
17
17
|
url?: string;
|
|
18
|
+
/** Names only — a remote entry's header values never enter a report. Absent means none. */
|
|
19
|
+
headerNames?: string[];
|
|
20
|
+
/** Values, sent with the probe and the bridge. NEVER serialize this. */
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* Claude Code's `alwaysLoad: true`: this server's tools load at session
|
|
24
|
+
* start whatever the tool-search setting says (its MCP documentation, §"Exempt
|
|
25
|
+
* a server from deferral", read 2026-09-06). Read from the entry, so the
|
|
26
|
+
* deferral verdict can count it rather than list it as a condition.
|
|
27
|
+
*/
|
|
28
|
+
alwaysLoad?: true;
|
|
18
29
|
}
|
|
19
30
|
/**
|
|
20
31
|
* JSON with comments and trailing commas — VS Code's mcp.json allows both, and
|
|
@@ -31,6 +42,7 @@ export declare function extractServers(doc: unknown, meta: {
|
|
|
31
42
|
client: string;
|
|
32
43
|
source: string;
|
|
33
44
|
cwd?: string;
|
|
45
|
+
env?: Record<string, string | undefined>;
|
|
34
46
|
}): ConfiguredServer[];
|
|
35
47
|
/**
|
|
36
48
|
* What one config document declares, servers and switched-off entries both.
|
|
@@ -45,6 +57,7 @@ export declare function extractDeclaration(doc: unknown, meta: {
|
|
|
45
57
|
client: string;
|
|
46
58
|
source: string;
|
|
47
59
|
cwd?: string;
|
|
60
|
+
env?: Record<string, string | undefined>;
|
|
48
61
|
}): {
|
|
49
62
|
servers: ConfiguredServer[];
|
|
50
63
|
disabled: string[];
|
|
@@ -52,6 +65,8 @@ export declare function extractDeclaration(doc: unknown, meta: {
|
|
|
52
65
|
export interface ConfigCandidate {
|
|
53
66
|
client: string;
|
|
54
67
|
path: string;
|
|
68
|
+
/** How the file is written. Absent means JSON, with comments and trailing commas tolerated. */
|
|
69
|
+
format?: 'json' | 'toml' | 'yaml';
|
|
55
70
|
}
|
|
56
71
|
/** Every place a client config is known to live, whether or not it exists. */
|
|
57
72
|
export declare function configCandidates(env: {
|
|
@@ -60,6 +75,8 @@ export declare function configCandidates(env: {
|
|
|
60
75
|
platform: NodeJS.Platform;
|
|
61
76
|
appData?: string;
|
|
62
77
|
}): ConfigCandidate[];
|
|
78
|
+
/** Parse one config file's text in the format its candidate declares. */
|
|
79
|
+
export declare function parseConfigText(text: string, format?: ConfigCandidate['format']): unknown;
|
|
63
80
|
export interface LoadedConfig {
|
|
64
81
|
client: string;
|
|
65
82
|
source: string;
|
|
@@ -84,7 +101,7 @@ export interface LoadedConfig {
|
|
|
84
101
|
allDisabled?: string[];
|
|
85
102
|
}
|
|
86
103
|
/** Read + parse the candidates that exist. Unreadable files are reported, not thrown. */
|
|
87
|
-
export declare function loadConfigs(candidates: ConfigCandidate[], cwd: string): LoadedConfig[];
|
|
104
|
+
export declare function loadConfigs(candidates: ConfigCandidate[], cwd: string, processEnv?: Record<string, string | undefined>): LoadedConfig[];
|
|
88
105
|
/** One file Claude Code reads its `env` block from. */
|
|
89
106
|
export interface SettingsCandidate {
|
|
90
107
|
scope: ToolSearchScope;
|