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 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`), and Windsurf or pass `--config <path>`. Servers are measured
64
- by the same path as the published leaderboard (dual `tools/list` capture, `o200k_base` over
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 example above. That sentence is an
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 server pinned with "alwaysLoad": true, whose tools load at session start regardless
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 four discovered clients with no default on record — `claude-desktop`, `cursor`, `vscode`, `windsurf` — the tokens are counted as
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, 11 moved up against 6 that moved down. Method:
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 (5 clients, JSONC-tolerant), the per-stack report, and the baseline diff |
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 11
391
- servers ratcheting upward against 6 that got cheaper, and none of those
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
- sixth of the list on Wednesdays, so every row comes round within six weeks. Read each row's
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)
@@ -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
- export type AuditStatus = MeasurementStatus | 'remote-not-measurable';
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;
@@ -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
- return JSON.stringify(Object.keys(env).sort().map((k) => [k, env[k]]));
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) => ({ tokens: s.tokens ?? 0, claudeTokens: s.claudeTokens }))),
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
- skipped.push({
225
- ...base,
226
- status: 'remote-not-measurable',
227
- tokens: null,
228
- toolCount: null,
229
- share: null,
230
- notes: `remote endpoint (${s.url ?? 'url'}) stdio measurement does not apply`,
231
- });
232
- continue;
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
- const unestablished = results.flatMap((c) => c.skipped.filter((s) => s.status !== 'remote-not-measurable').map((s) => `${c.source}: ${s.name} (${s.status})`));
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(2)}×–${r.high.toFixed(2)}×`;
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(2)}×–` +
811
- `${PUBLISHED_WIRE_TO_CLIENT_RATIO.high.toFixed(2)}× on Anthropic requests. See docs/METHODOLOGY.md §claude-divergence.`);
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. */
@@ -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 — recorded so the report can say why it was skipped. */
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;