mcp-context-cost 0.15.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
@@ -294,7 +294,7 @@ answers a question no client asks: **what did this server cost last month?**
294
294
  [results/regressions.md](results/regressions.md) reports each server's most recent movement —
295
295
  dated to when it happened, separated into *shipped more tools* versus *same tools, rewritten*,
296
296
  and compared only within one isolation. The ecosystem ratchets upward: of the servers whose
297
- 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:
298
298
  [cost movement](docs/METHODOLOGY.md#cost-movement).
299
299
 
300
300
  If you publish a server, the same measurement is available as a badge, so your users can see
@@ -400,8 +400,8 @@ repository), then in your README:
400
400
  A badge says what your server costs today; it does nothing about the release
401
401
  that adds 1,200 tokens to every user's context next month. Across the servers
402
402
  measured here most costs hold steady from sweep to sweep, but when a cost does
403
- move it usually moves up: the [movement report](results/regressions.md) has 11
404
- 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
405
405
  maintainers had a check that would have said so first. `measure` takes the same
406
406
  gate flags `audit` does, so your own CI can be that check:
407
407
 
@@ -477,7 +477,7 @@ says as a whole is written up, dated, in
477
477
  [The State of MCP Context Cost](https://athakur3.github.io/mcp-context-cost/state-of-mcp-context-cost)
478
478
  (September 2026). Two
479
479
  weekly jobs re-measure the set — the `memory` reference server on Mondays, and a rotating
480
- 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
481
481
  date as the date it means, and don't take the cadence on trust — the build history is
482
482
  public, one click each:
483
483
  [re-sweep runs](https://github.com/athakur3/mcp-context-cost/actions/workflows/resweep.yml)
@@ -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;
@@ -493,6 +493,40 @@ function sharedMeasurementLines(d, skippedNames, consequence) {
493
493
  }
494
494
  return lines;
495
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
+ }
496
530
  /** Where a threshold is in play, the unknown size is the whole verdict. */
497
531
  const SIDE_UNKNOWN = [
498
532
  ' either direction — which side of the threshold this stack falls on cannot be',
@@ -608,6 +642,7 @@ function deferralLines(d, skippedNames) {
608
642
  lines.push(` ${d.client} defers tool definitions above a threshold here (${d.mechanism}):`);
609
643
  lines.push(` ${settingPhrase(d)}, so deferral activates once the`);
610
644
  lines.push(` definitions reach ${n(t)} tokens — ${pct(d.thresholdShare ?? 0)} of the context window.`);
645
+ lines.push(...thresholdAssumptionLines(d));
611
646
  lines.push(...postureSourceLines(d));
612
647
  const c = d.clientTokens;
613
648
  if (!c) {
@@ -662,7 +697,7 @@ function deferralLines(d, skippedNames) {
662
697
  /** "0.20×–1.92×", from whichever band the verdict was actually computed against. */
663
698
  function ratioBand(d) {
664
699
  const r = d.ratio ?? PUBLISHED_WIRE_TO_CLIENT_RATIO;
665
- return `${r.low.toFixed(2)}×–${r.high.toFixed(2)}×`;
700
+ return `${r.low.toFixed(BAND_PRECISION)}×–${r.high.toFixed(BAND_PRECISION)}×`;
666
701
  }
667
702
  /** Human output. JSON output is the report object itself. */
668
703
  export function formatReport(report) {
@@ -861,8 +896,8 @@ export function formatReport(report) {
861
896
  }
862
897
  lines.push('');
863
898
  lines.push('These are wire tokens — what the server puts on the wire, counted with o200k_base. What your model is billed');
864
- lines.push(`differs per provider: measured ratios run ${PUBLISHED_WIRE_TO_CLIENT_RATIO.low.toFixed(2)}×–` +
865
- `${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.`);
866
901
  return lines.map((l) => l.replace(/\s+$/, '')).join('\n');
867
902
  }
868
903
  /** Top-level tool list across every config — used by nothing yet, handy for --json consumers. */
@@ -286,6 +286,41 @@ export interface WireToClientRatio {
286
286
  * level down. The fields are the record; `source` dates them.
287
287
  */
288
288
  export declare const PUBLISHED_WIRE_TO_CLIENT_RATIO: WireToClientRatio;
289
+ /**
290
+ * The decimal places the band is published at.
291
+ *
292
+ * Not a display choice: it is the precision a snapshot of the band has to stay
293
+ * accurate to, because two numbers that round the same way decide the same
294
+ * above/below verdict against a client threshold, and two that do not, do not.
295
+ * The pages print the band to this many places (`published-stats.ts`) and
296
+ * `bandSnapshotProblem` judges a snapshot at the same width, so "still right"
297
+ * means the same thing in both places.
298
+ */
299
+ export declare const BAND_PRECISION = 2;
300
+ /**
301
+ * Is a snapshot of the band still an honest description of `derived`? The
302
+ * problem in words, or `null` when the snapshot may lag but does not mislead.
303
+ *
304
+ * Two callers, one rule. The suite asks it of `PUBLISHED_WIRE_TO_CLIENT_RATIO`
305
+ * against the run committed beside it. The release readiness gate asks it of
306
+ * the constant the *last release* shipped against the run on trunk — that band
307
+ * is what an installed package states offline, and nothing except cutting a
308
+ * release can move it, so a release being due is exactly what a finding there
309
+ * means.
310
+ *
311
+ * Deliberately not an equality check, and the first version of this rule was
312
+ * one: it demanded agreement and went red on the next bot commit, which had
313
+ * done nothing but measure a server for the first time. The run grows whenever
314
+ * a sweep reaches a new server, and the bot that commits it cannot edit a
315
+ * TypeScript constant. A snapshot is allowed to lag. It is not allowed to be
316
+ * wrong:
317
+ *
318
+ * - the **band** must still be right to the precision it is published at,
319
+ * because it decides an above/below verdict against a client threshold;
320
+ * - the **count** must never exceed the run, because a snapshot claiming more
321
+ * servers than were measured is a fabricated number rather than an old one.
322
+ */
323
+ export declare function bandSnapshotProblem(snapshot: Pick<WireToClientRatio, 'low' | 'high' | 'servers'>, derived: Pick<WireToClientRatio, 'low' | 'high' | 'servers'>): string | null;
289
324
  /** Derive the band from a supplied divergence run, falling back to the published one. */
290
325
  export declare function wireToClientRatio(run?: DivergenceRun | null): WireToClientRatio;
291
326
  /** One measured server, as the deferral arithmetic needs it. */
@@ -258,6 +258,51 @@ export const PUBLISHED_WIRE_TO_CLIENT_RATIO = {
258
258
  servers: 86,
259
259
  source: 'the published claude-opus-5 divergence run',
260
260
  };
261
+ /**
262
+ * The decimal places the band is published at.
263
+ *
264
+ * Not a display choice: it is the precision a snapshot of the band has to stay
265
+ * accurate to, because two numbers that round the same way decide the same
266
+ * above/below verdict against a client threshold, and two that do not, do not.
267
+ * The pages print the band to this many places (`published-stats.ts`) and
268
+ * `bandSnapshotProblem` judges a snapshot at the same width, so "still right"
269
+ * means the same thing in both places.
270
+ */
271
+ export const BAND_PRECISION = 2;
272
+ /**
273
+ * Is a snapshot of the band still an honest description of `derived`? The
274
+ * problem in words, or `null` when the snapshot may lag but does not mislead.
275
+ *
276
+ * Two callers, one rule. The suite asks it of `PUBLISHED_WIRE_TO_CLIENT_RATIO`
277
+ * against the run committed beside it. The release readiness gate asks it of
278
+ * the constant the *last release* shipped against the run on trunk — that band
279
+ * is what an installed package states offline, and nothing except cutting a
280
+ * release can move it, so a release being due is exactly what a finding there
281
+ * means.
282
+ *
283
+ * Deliberately not an equality check, and the first version of this rule was
284
+ * one: it demanded agreement and went red on the next bot commit, which had
285
+ * done nothing but measure a server for the first time. The run grows whenever
286
+ * a sweep reaches a new server, and the bot that commits it cannot edit a
287
+ * TypeScript constant. A snapshot is allowed to lag. It is not allowed to be
288
+ * wrong:
289
+ *
290
+ * - the **band** must still be right to the precision it is published at,
291
+ * because it decides an above/below verdict against a client threshold;
292
+ * - the **count** must never exceed the run, because a snapshot claiming more
293
+ * servers than were measured is a fabricated number rather than an old one.
294
+ */
295
+ export function bandSnapshotProblem(snapshot, derived) {
296
+ const at = (n) => n.toFixed(BAND_PRECISION);
297
+ if (snapshot.servers > derived.servers) {
298
+ return `it is measured across ${snapshot.servers} servers where the run holds ${derived.servers}`;
299
+ }
300
+ if (at(snapshot.low) !== at(derived.low) || at(snapshot.high) !== at(derived.high)) {
301
+ return (`it states ${at(snapshot.low)}×–${at(snapshot.high)}× where the run derives ` +
302
+ `${at(derived.low)}×–${at(derived.high)}×`);
303
+ }
304
+ return null;
305
+ }
261
306
  /** Derive the band from a supplied divergence run, falling back to the published one. */
262
307
  export function wireToClientRatio(run) {
263
308
  if (!run)
@@ -28,7 +28,7 @@
28
28
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
29
29
  import { join } from 'node:path';
30
30
  import { DEFAULT_CONTEXT_WINDOW } from '../audit/audit.js';
31
- import { wireToClientRatio } from '../audit/deferral.js';
31
+ import { BAND_PRECISION, wireToClientRatio } from '../audit/deferral.js';
32
32
  import { fieldSelectionShare, isCurrent } from '../core/divergence.js';
33
33
  import { sessionStartLoad } from '../core/session-start.js';
34
34
  import { isGood } from './harness-guard.js';
@@ -240,7 +240,11 @@ export const PAGE_CLAIMS = [
240
240
  // page-number guard: three numbers written by hand beside the two
241
241
  // sentences regen already kept true.
242
242
  template: 'measured at {f}×–{f}× across {n} servers)',
243
- values: (s) => [s.claude.ratioMin.toFixed(2), s.claude.ratioMax.toFixed(2), fmt(s.claude.ratioServers)],
243
+ values: (s) => [
244
+ s.claude.ratioMin.toFixed(BAND_PRECISION),
245
+ s.claude.ratioMax.toFixed(BAND_PRECISION),
246
+ fmt(s.claude.ratioServers),
247
+ ],
244
248
  },
245
249
  {
246
250
  file: 'README.md',
@@ -358,13 +362,21 @@ export const PAGE_CLAIMS = [
358
362
  // different sources for one number is the drift this file exists to end, so
359
363
  // both pages state the run and the constant is guarded separately.
360
364
  template: 'band ({f}×–{f}×\nacross {n} servers)',
361
- values: (s) => [s.claude.ratioMin.toFixed(2), s.claude.ratioMax.toFixed(2), fmt(s.claude.ratioServers)],
365
+ values: (s) => [
366
+ s.claude.ratioMin.toFixed(BAND_PRECISION),
367
+ s.claude.ratioMax.toFixed(BAND_PRECISION),
368
+ fmt(s.claude.ratioServers),
369
+ ],
362
370
  },
363
371
  {
364
372
  file: 'docs/METHODOLOGY.md',
365
373
  id: 'divergence:ratio-range',
366
374
  template: 'it ranged from {f}× to {f}× across the {n} servers in the run,',
367
- values: (s) => [s.claude.ratioMin.toFixed(2), s.claude.ratioMax.toFixed(2), fmt(s.claude.ratioServers)],
375
+ values: (s) => [
376
+ s.claude.ratioMin.toFixed(BAND_PRECISION),
377
+ s.claude.ratioMax.toFixed(BAND_PRECISION),
378
+ fmt(s.claude.ratioServers),
379
+ ],
368
380
  },
369
381
  {
370
382
  file: 'docs/METHODOLOGY.md',
@@ -4,7 +4,7 @@
4
4
  * `history.csv` only grows for servers that get measured again, and the only
5
5
  * scheduled measurement this project runs is the weekly self-badge job — one
6
6
  * server, `memory`. Every other row's trend line therefore stops at whatever
7
- * date a maintenance run last swept by hand. Re-measuring all 82 on a schedule
7
+ * date a maintenance run last swept by hand. Re-measuring every one of them on a schedule
8
8
  * is not the fix: a cold runner pays a full image pull and package install per
9
9
  * server with no cache volume to carry over, so the whole set does not fit in
10
10
  * one job's budget comfortably or cheaply.
@@ -4,7 +4,7 @@
4
4
  * `history.csv` only grows for servers that get measured again, and the only
5
5
  * scheduled measurement this project runs is the weekly self-badge job — one
6
6
  * server, `memory`. Every other row's trend line therefore stops at whatever
7
- * date a maintenance run last swept by hand. Re-measuring all 82 on a schedule
7
+ * date a maintenance run last swept by hand. Re-measuring every one of them on a schedule
8
8
  * is not the fix: a cold runner pays a full image pull and package install per
9
9
  * server with no cache volume to carry over, so the whole set does not fit in
10
10
  * one job's budget comfortably or cheaply.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-context-cost",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Measure what your MCP servers cost in context tokens — audit your own config, or badge the server you publish",
5
5
  "type": "module",
6
6
  "license": "MIT",