mcp-context-cost 0.7.0 → 0.9.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 -16
- package/dist/cli.js +27 -12
- package/dist/core/cross-check.d.ts +93 -0
- package/dist/core/cross-check.js +176 -0
- package/dist/sweep/cross-check.d.ts +31 -0
- package/dist/sweep/cross-check.js +342 -0
- package/dist/sweep/docker.d.ts +55 -0
- package/dist/sweep/docker.js +94 -1
- package/dist/sweep/published-stats.d.ts +114 -0
- package/dist/sweep/published-stats.js +383 -0
- package/dist/sweep/regen.js +13 -0
- package/dist/sweep/report.d.ts +9 -0
- package/dist/sweep/report.js +50 -4
- package/dist/sweep/run.js +35 -6
- package/dist/sweep/sweep-all.js +41 -9
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -70,11 +70,12 @@ deferral off in a settings file is not a machine running the default:
|
|
|
70
70
|
|---|---|
|
|
71
71
|
| nothing set (the default) | every definition deferred, at any size — no threshold applies |
|
|
72
72
|
| `ENABLE_TOOL_SEARCH=true` | same: every definition deferred |
|
|
73
|
-
| `ENABLE_TOOL_SEARCH=false` | deferral off — every request carries the full total |
|
|
73
|
+
| `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 |
|
|
74
74
|
| `ENABLE_TOOL_SEARCH=auto` / `auto:N` | deferred only once definitions reach 10% / N% of the context window |
|
|
75
75
|
| `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` set | tool search off — read first, because `ENABLE_TOOL_SEARCH` cannot override it |
|
|
76
76
|
| `ANTHROPIC_BASE_URL` off `api.anthropic.com` | falls back to loading up front — consulted only while `ENABLE_TOOL_SEARCH` is unset |
|
|
77
77
|
| anything else in `ENABLE_TOOL_SEARCH` | not a documented value, so nothing is claimed from it |
|
|
78
|
+
| 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 |
|
|
78
79
|
|
|
79
80
|
On a machine where none of them is set, the same stack reads:
|
|
80
81
|
|
|
@@ -105,11 +106,13 @@ costs **more** than loading the definitions would.
|
|
|
105
106
|
Three things the report will not do: it will not convert between units silently (in
|
|
106
107
|
threshold mode the stack is compared as a range, because the audit counts wire bytes and the
|
|
107
108
|
threshold is counted in what the client sends to the API — measured at 0.20×–1.92× across 20
|
|
108
|
-
servers); it will not
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
servers); it will not claim a posture the machine did not state readably, which is four
|
|
110
|
+
refusals and not one — when two places set the same variable to different values, when a
|
|
111
|
+
settings file exists and cannot be read, when the place that would decide sets the variable
|
|
112
|
+
to something that is not a string, and when `ENABLE_TOOL_SEARCH` holds a value Claude Code
|
|
113
|
+
does not document; and it will not pass an absence of a record off as a measurement. The
|
|
114
|
+
first two print as unanswered questions. The third prints as an answer that names
|
|
115
|
+
itself: for the four discovered clients with no default on record — `claude-desktop`, `cursor`, `vscode`, `windsurf` — the tokens are counted as
|
|
113
116
|
loaded up front, and the report says so in those words, "an absence of a record about the
|
|
114
117
|
client, not a measurement of it".
|
|
115
118
|
Full model, sources and dates: [METHODOLOGY §who pays the number](docs/METHODOLOGY.md#who-pays).
|
|
@@ -180,10 +183,9 @@ INCREASE FAIL:
|
|
|
180
183
|
Add `--claude` to annotate each server with its Anthropic-request cost from the published
|
|
181
184
|
[Claude divergence](docs/METHODOLOGY.md#claude-divergence) run — an exact number when the
|
|
182
185
|
published capture hash matches what you have installed, `—` (silence, not a stale guess)
|
|
183
|
-
when it doesn't. The run holds 20 rows — the top 20 measured servers by tokens
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
twentieth blank rather than stale. Most installs will show a mix:
|
|
186
|
+
when it doesn't. The run holds 20 rows — the top 20 measured servers by tokens when it ran —
|
|
187
|
+
and [results/leaderboard.md](results/leaderboard.md) prints a claude number for the 16 that
|
|
188
|
+
still match today and silence for the rest. Most installs will show a mix:
|
|
187
189
|
|
|
188
190
|
```
|
|
189
191
|
server tools tokens share claude
|
|
@@ -199,14 +201,14 @@ Flags: `--json` (full report on stdout, progress on stderr), `--budget N`,
|
|
|
199
201
|
|
|
200
202
|
The number `audit` gives you is the same measurement, run across a curated set of public
|
|
201
203
|
servers — which is how you can tell it is a measurement and not this tool's opinion. It also
|
|
202
|
-
shows what you are choosing between: across the
|
|
203
|
-
from
|
|
204
|
+
shows what you are choosing between: across the 81 servers measured, cost spans **1,700×**,
|
|
205
|
+
from `postgres` at 32 tokens to `github` at 54,622. The table below is a
|
|
204
206
|
sample of that range; the full range is in
|
|
205
207
|
[results/leaderboard.md](results/leaderboard.md).
|
|
206
208
|
|
|
207
209
|
| server | context cost | tools |
|
|
208
210
|
|---|---:|---:|
|
|
209
|
-
| github (official) | **54,
|
|
211
|
+
| github (official) | **54,622 tokens** | 44 |
|
|
210
212
|
| xcodebuildmcp | 26,594 | 24 |
|
|
211
213
|
| brave-search | 25,456 | 8 |
|
|
212
214
|
| notion | 17,500 | 24 |
|
|
@@ -214,7 +216,7 @@ sample of that range; the full range is in
|
|
|
214
216
|
| filesystem (reference) | 2,823 | 14 |
|
|
215
217
|
| markitdown | 64 | 1 |
|
|
216
218
|
|
|
217
|
-
*(
|
|
219
|
+
*(81 of 106 popular servers measured, each row dated by its own most recent sweep — full table in
|
|
218
220
|
[results/leaderboard.md](results/leaderboard.md); every failure is listed with its reason.
|
|
219
221
|
Each measured server also has a [detail page](https://athakur3.github.io/mcp-context-cost/servers/)
|
|
220
222
|
showing which tools its tokens are in.)*
|
|
@@ -249,7 +251,7 @@ exact launch command. Disputes reduce to a byte-level diff:
|
|
|
249
251
|
|
|
250
252
|
```bash
|
|
251
253
|
npx -y mcp-context-cost verify results/github/measurement.json
|
|
252
|
-
# OK github-mcp-server:
|
|
254
|
+
# OK github-mcp-server: 54622 tokens (o200k_base, methodology 1.0) — capture, hash, and count all agree
|
|
253
255
|
|
|
254
256
|
# or point it at a published measurement.json directly, no clone required
|
|
255
257
|
npx -y mcp-context-cost verify --remote https://raw.githubusercontent.com/athakur3/mcp-context-cost/main/results/github/measurement.json
|
|
@@ -264,6 +266,12 @@ failed, `2` usage error.
|
|
|
264
266
|
to the one that was published — which is exactly what `--claude` uses to decide whether it is
|
|
265
267
|
allowed to show you a number.
|
|
266
268
|
|
|
269
|
+
The number is also cross-checked against the other CLI that measures this,
|
|
270
|
+
[`sd2k/mcp-tokens`](https://github.com/sd2k/mcp-tokens): the leaderboard's **mcp-tokens**
|
|
271
|
+
column publishes its count beside ours wherever both tools saw the same tool set — same
|
|
272
|
+
o200k encoding, differences documented, the divergence published rather than left to be
|
|
273
|
+
discovered. Method: [CLI cross-check](docs/METHODOLOGY.md#cli-cross-check).
|
|
274
|
+
|
|
267
275
|
Full definition: [docs/METHODOLOGY.md](docs/METHODOLOGY.md) — what is counted, what the
|
|
268
276
|
number is *not*, config policy, failure taxonomy, frozen color bands, known divergences.
|
|
269
277
|
|
|
@@ -321,7 +329,7 @@ color bands are frozen against the observed distribution of the first full sweep
|
|
|
321
329
|
|
|
322
330
|
## Status
|
|
323
331
|
|
|
324
|
-
Active.
|
|
332
|
+
Active. Every row carries the date of its own most recent measurement. Two
|
|
325
333
|
weekly jobs re-measure the set — the `memory` reference server on Mondays, and a rotating
|
|
326
334
|
sixth of the list on Wednesdays, so every row comes round within six weeks. Read each row's
|
|
327
335
|
date as the date it means, and don't take the cadence on trust — the build history is
|
package/dist/cli.js
CHANGED
|
@@ -159,18 +159,33 @@ if (cmd === 'audit') {
|
|
|
159
159
|
}
|
|
160
160
|
const { runAudit } = await import('./audit/run.js');
|
|
161
161
|
const { formatReport } = await import('./audit/audit.js');
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
162
|
+
const { DockerHarnessFault } = await import('./sweep/docker.js');
|
|
163
|
+
let report;
|
|
164
|
+
try {
|
|
165
|
+
report = await runAudit({
|
|
166
|
+
configPaths: all('config'),
|
|
167
|
+
budget,
|
|
168
|
+
contextWindow: numeric('context'),
|
|
169
|
+
timeoutMs: numeric('timeout'),
|
|
170
|
+
concurrency: numeric('concurrency'),
|
|
171
|
+
docker: rest.includes('--docker'),
|
|
172
|
+
claude: rest.includes('--claude'),
|
|
173
|
+
divergenceUrl: argOf('divergence-url'),
|
|
174
|
+
// Progress goes to stderr so `--json` stdout stays a single parseable object.
|
|
175
|
+
onProgress: json ? undefined : (name, done, total) => process.stderr.write(` [${done}/${total}] ${name}\n`),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
// Docker failing as docker means every measurement through it would be a
|
|
180
|
+
// statement about this machine, so the audit refuses whole rather than
|
|
181
|
+
// reporting each server as broken.
|
|
182
|
+
if (e instanceof DockerHarnessFault) {
|
|
183
|
+
console.error(`audit --docker cannot answer for this machine: ${e.message}`);
|
|
184
|
+
console.error('Fix Docker here, or run without --docker.');
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
throw e;
|
|
188
|
+
}
|
|
174
189
|
if (report.configs.length === 0) {
|
|
175
190
|
const where = report.problems.length ? `\n${report.problems.map((p) => ` ${p}`).join('\n')}` : '';
|
|
176
191
|
const empty = report.emptyConfigs ?? [];
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Measurement } from './types.js';
|
|
2
|
+
/** Method identifier, versioned independently of METHODOLOGY_VERSION. */
|
|
3
|
+
export declare const CROSS_CHECK_METHOD = "cli-cross-check/v1";
|
|
4
|
+
/** The CLI being cross-checked against, and the release this run pins. */
|
|
5
|
+
export declare const CROSS_CHECK_CLI = "sd2k/mcp-tokens";
|
|
6
|
+
export declare const CROSS_CHECK_CLI_VERSION = "v0.2.5";
|
|
7
|
+
/**
|
|
8
|
+
* The exact analyze invocation, recorded in every run. `--model gpt-4o` is
|
|
9
|
+
* load-bearing: tiktoken-rs maps it to o200k_base, and without a model the CLI
|
|
10
|
+
* falls back to cl100k_base — a systematic difference that would swamp the one
|
|
11
|
+
* being measured (spec/upstream-notes.md, finding 1).
|
|
12
|
+
*/
|
|
13
|
+
export declare const CROSS_CHECK_CLI_ARGS: readonly ['analyze', '--provider', 'tiktoken', '--model', 'gpt-4o', '--format', 'json'];
|
|
14
|
+
export interface CrossCheckRow {
|
|
15
|
+
/** o200k count of our fresh canonical capture from this run — the headline. */
|
|
16
|
+
ourTokens: number;
|
|
17
|
+
/**
|
|
18
|
+
* o200k count of the same capture's name/description/input_schema
|
|
19
|
+
* projection — what the CLI's structs actually model, and therefore the
|
|
20
|
+
* number its count is compared against.
|
|
21
|
+
*/
|
|
22
|
+
ourMappedTokens: number;
|
|
23
|
+
/** The CLI's `.tools.total` for the same server, launched by the CLI itself. */
|
|
24
|
+
cliTokens: number;
|
|
25
|
+
ourToolCount: number;
|
|
26
|
+
cliToolCount: number;
|
|
27
|
+
/** Both tools saw the same tool names (order-insensitive) — the comparison is between like and like. */
|
|
28
|
+
toolSetMatches: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Our fresh measurement listed dynamically (tools/list differed between its
|
|
31
|
+
* own two captures). The CLI's launch is a third capture, so even with
|
|
32
|
+
* matching names its residual mixes content drift with counter disagreement
|
|
33
|
+
* — recorded, never printed.
|
|
34
|
+
*/
|
|
35
|
+
dynamic: boolean;
|
|
36
|
+
/** canonicalSha256 of our capture in this run; null when our measurement failed. */
|
|
37
|
+
capturedSha256: string | null;
|
|
38
|
+
/** Set when either side could not produce a number; nothing is published from the row. */
|
|
39
|
+
error?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface CrossCheckRun {
|
|
42
|
+
method: string;
|
|
43
|
+
cli: string;
|
|
44
|
+
cliVersion: string;
|
|
45
|
+
/** The analyze invocation, verbatim, so the encoding choice is auditable. */
|
|
46
|
+
cliArgs: string[];
|
|
47
|
+
/** UTC day the run was taken (YYYY-MM-DD). */
|
|
48
|
+
measuredAt: string;
|
|
49
|
+
isolation: string;
|
|
50
|
+
servers: Record<string, CrossCheckRow>;
|
|
51
|
+
}
|
|
52
|
+
export declare function parseCrossCheck(text: string): CrossCheckRun | null;
|
|
53
|
+
/** What the CLI's JSON report contributes to a row. */
|
|
54
|
+
export interface CliReport {
|
|
55
|
+
total: number;
|
|
56
|
+
count: number;
|
|
57
|
+
names: string[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Read the CLI's `--format json` report. The report is that CLI's contract, not
|
|
61
|
+
* ours, so this is deliberately narrow: `.tools.total` (the number the CLI's
|
|
62
|
+
* own action publishes), `.tools.count`, and the per-tool names — and a report
|
|
63
|
+
* that does not carry a usable total is a named problem, never a zero.
|
|
64
|
+
*/
|
|
65
|
+
export declare function parseCliReport(text: string): {
|
|
66
|
+
report?: CliReport;
|
|
67
|
+
problem?: string;
|
|
68
|
+
};
|
|
69
|
+
/** Order-insensitive equality of the two tool-name lists, repeats included. */
|
|
70
|
+
export declare function sameToolSet(ours: string[], theirs: string[]): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Build a row from our fresh measurement and the CLI's report. The caller
|
|
73
|
+
* decides whether to run the CLI at all; a failed side arrives here as an
|
|
74
|
+
* `error`, and the row keeps whatever the other side established.
|
|
75
|
+
*/
|
|
76
|
+
export declare function toCrossCheckRow(m: Measurement, cli: {
|
|
77
|
+
report?: CliReport;
|
|
78
|
+
problem?: string;
|
|
79
|
+
}): CrossCheckRow;
|
|
80
|
+
/**
|
|
81
|
+
* A row is only printable while it compares like with like: no error on either
|
|
82
|
+
* side, the same tool names seen by both tools, and our capture still the one
|
|
83
|
+
* published — the exact staleness rule the claude column follows.
|
|
84
|
+
*/
|
|
85
|
+
export declare function isComparable(row: CrossCheckRow | undefined, canonicalSha256: string | null): row is CrossCheckRow;
|
|
86
|
+
/**
|
|
87
|
+
* Signed divergence of the CLI's count from our count of the projection it
|
|
88
|
+
* models, in percent. Against the mapped number and not the headline: the gap
|
|
89
|
+
* to the headline is field selection, published separately per server, and
|
|
90
|
+
* folding it in here would bury the number this column exists to check —
|
|
91
|
+
* whether two independent counters agree on the fields both count.
|
|
92
|
+
*/
|
|
93
|
+
export declare function divergencePct(row: CrossCheckRow): number | null;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI cross-check — the other tool's number, published beside ours.
|
|
3
|
+
*
|
|
4
|
+
* `sd2k/mcp-tokens` measures the same thing this project measures, differently
|
|
5
|
+
* in two documented ways (spec/upstream-notes.md): its tiktoken provider picks
|
|
6
|
+
* the encoding from a `--model` argument with a cl100k_base fallback, and it
|
|
7
|
+
* counts a `serde_json` re-serialization of deserialized tool structs rather
|
|
8
|
+
* than the wire bytes. Measured (2026-09-03, addendum in the notes), the
|
|
9
|
+
* second difference is nearly all field modeling: the CLI's count lands within
|
|
10
|
+
* a fraction of a percent of our o200k count of the three-field
|
|
11
|
+
* name/description/input_schema projection — the same projection the Claude
|
|
12
|
+
* divergence starts from — not of the full capture. So the published
|
|
13
|
+
* comparison is against `mappedTokens` of the same fresh capture: like counted
|
|
14
|
+
* against like, with the field-selection gap already published separately as
|
|
15
|
+
* each server's field-selection share. The honest move is the one the notes
|
|
16
|
+
* chose on day one: publish the divergence ourselves rather than leave it to
|
|
17
|
+
* be discovered by critics.
|
|
18
|
+
*
|
|
19
|
+
* Every row is filed under the `canonicalSha256` of a capture taken by OUR
|
|
20
|
+
* client in the same run, minutes from the CLI's own launch — the session-start
|
|
21
|
+
* discipline: a re-sweep that moves the published capture marks the row stale
|
|
22
|
+
* instead of leaving a comparison against bytes that no longer exist. And a
|
|
23
|
+
* comparison is only printed when both tools counted the same tool set: the CLI
|
|
24
|
+
* launches the server itself, so on a server that changed between the two
|
|
25
|
+
* launches (or lists dynamically) the two numbers describe different schemas,
|
|
26
|
+
* and publishing their difference as "divergence" would be a category error.
|
|
27
|
+
* Those rows keep their data in the run file and print silence.
|
|
28
|
+
*
|
|
29
|
+
* Versioned independently of the o200k methodology, like `tools-delta/v1` and
|
|
30
|
+
* `deferred-load/v1`: a new published number, no change to the definition of
|
|
31
|
+
* the first. No `totalTokens` and no canonical hash moves.
|
|
32
|
+
*/
|
|
33
|
+
import { mappedTokens } from './divergence.js';
|
|
34
|
+
import { toolNames } from './session-start.js';
|
|
35
|
+
/** Method identifier, versioned independently of METHODOLOGY_VERSION. */
|
|
36
|
+
export const CROSS_CHECK_METHOD = 'cli-cross-check/v1';
|
|
37
|
+
/** The CLI being cross-checked against, and the release this run pins. */
|
|
38
|
+
export const CROSS_CHECK_CLI = 'sd2k/mcp-tokens';
|
|
39
|
+
export const CROSS_CHECK_CLI_VERSION = 'v0.2.5';
|
|
40
|
+
/**
|
|
41
|
+
* The exact analyze invocation, recorded in every run. `--model gpt-4o` is
|
|
42
|
+
* load-bearing: tiktoken-rs maps it to o200k_base, and without a model the CLI
|
|
43
|
+
* falls back to cl100k_base — a systematic difference that would swamp the one
|
|
44
|
+
* being measured (spec/upstream-notes.md, finding 1).
|
|
45
|
+
*/
|
|
46
|
+
export const CROSS_CHECK_CLI_ARGS = ['analyze', '--provider', 'tiktoken', '--model', 'gpt-4o', '--format', 'json'];
|
|
47
|
+
export function parseCrossCheck(text) {
|
|
48
|
+
let run;
|
|
49
|
+
try {
|
|
50
|
+
run = JSON.parse(text);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const r = run;
|
|
56
|
+
if (!r || typeof r.cliVersion !== 'string' || typeof r.measuredAt !== 'string')
|
|
57
|
+
return null;
|
|
58
|
+
if (!r.servers || typeof r.servers !== 'object')
|
|
59
|
+
return null;
|
|
60
|
+
return {
|
|
61
|
+
method: typeof r.method === 'string' ? r.method : CROSS_CHECK_METHOD,
|
|
62
|
+
cli: typeof r.cli === 'string' ? r.cli : CROSS_CHECK_CLI,
|
|
63
|
+
cliVersion: r.cliVersion,
|
|
64
|
+
cliArgs: Array.isArray(r.cliArgs) ? r.cliArgs.map(String) : [...CROSS_CHECK_CLI_ARGS],
|
|
65
|
+
measuredAt: r.measuredAt,
|
|
66
|
+
isolation: typeof r.isolation === 'string' ? r.isolation : 'not recorded',
|
|
67
|
+
servers: r.servers,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Read the CLI's `--format json` report. The report is that CLI's contract, not
|
|
72
|
+
* ours, so this is deliberately narrow: `.tools.total` (the number the CLI's
|
|
73
|
+
* own action publishes), `.tools.count`, and the per-tool names — and a report
|
|
74
|
+
* that does not carry a usable total is a named problem, never a zero.
|
|
75
|
+
*/
|
|
76
|
+
export function parseCliReport(text) {
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(text);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// `--format json` puts the report on stdout, but a launcher inside the same
|
|
83
|
+
// pipe can precede it with noise; the report is the outermost JSON object.
|
|
84
|
+
const start = text.indexOf('{');
|
|
85
|
+
const end = text.lastIndexOf('}');
|
|
86
|
+
if (start < 0 || end <= start)
|
|
87
|
+
return { problem: 'CLI stdout carried no JSON report' };
|
|
88
|
+
try {
|
|
89
|
+
parsed = JSON.parse(text.slice(start, end + 1));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { problem: 'CLI stdout carried no parseable JSON report' };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const tools = parsed.tools;
|
|
96
|
+
if (!tools || typeof tools !== 'object')
|
|
97
|
+
return { problem: 'CLI report has no .tools object' };
|
|
98
|
+
if (typeof tools.total !== 'number' || !Number.isFinite(tools.total) || tools.total < 0) {
|
|
99
|
+
return { problem: 'CLI report has no usable .tools.total' };
|
|
100
|
+
}
|
|
101
|
+
const items = Array.isArray(tools.items) ? tools.items : [];
|
|
102
|
+
const names = [];
|
|
103
|
+
for (const item of items) {
|
|
104
|
+
const name = (item ?? {}).name;
|
|
105
|
+
if (typeof name === 'string' && name !== '')
|
|
106
|
+
names.push(name);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
report: {
|
|
110
|
+
total: tools.total,
|
|
111
|
+
count: typeof tools.count === 'number' ? tools.count : items.length,
|
|
112
|
+
names,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** Order-insensitive equality of the two tool-name lists, repeats included. */
|
|
117
|
+
export function sameToolSet(ours, theirs) {
|
|
118
|
+
if (ours.length !== theirs.length)
|
|
119
|
+
return false;
|
|
120
|
+
const a = [...ours].sort();
|
|
121
|
+
const b = [...theirs].sort();
|
|
122
|
+
return a.every((name, i) => name === b[i]);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Build a row from our fresh measurement and the CLI's report. The caller
|
|
126
|
+
* decides whether to run the CLI at all; a failed side arrives here as an
|
|
127
|
+
* `error`, and the row keeps whatever the other side established.
|
|
128
|
+
*/
|
|
129
|
+
export function toCrossCheckRow(m, cli) {
|
|
130
|
+
const measured = m.status === 'measured' || m.status === 'dynamic';
|
|
131
|
+
const raw = measured && Array.isArray(m.rawToolsCapture) ? m.rawToolsCapture : [];
|
|
132
|
+
const ours = toolNames(raw);
|
|
133
|
+
const row = {
|
|
134
|
+
ourTokens: measured && typeof m.totalTokens === 'number' ? m.totalTokens : 0,
|
|
135
|
+
ourMappedTokens: raw.length > 0 ? mappedTokens(raw) : 0,
|
|
136
|
+
cliTokens: cli.report?.total ?? 0,
|
|
137
|
+
ourToolCount: measured && typeof m.toolCount === 'number' ? m.toolCount : 0,
|
|
138
|
+
cliToolCount: cli.report?.count ?? 0,
|
|
139
|
+
toolSetMatches: cli.report !== undefined && measured ? sameToolSet(ours, cli.report.names) : false,
|
|
140
|
+
dynamic: m.status === 'dynamic',
|
|
141
|
+
capturedSha256: measured ? m.canonicalSha256 : null,
|
|
142
|
+
};
|
|
143
|
+
if (!measured)
|
|
144
|
+
row.error = `our measurement: ${m.status}: ${(m.notes ?? '').slice(0, 200)}`;
|
|
145
|
+
else if (cli.problem)
|
|
146
|
+
row.error = `cli: ${cli.problem.slice(0, 200)}`;
|
|
147
|
+
return row;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* A row is only printable while it compares like with like: no error on either
|
|
151
|
+
* side, the same tool names seen by both tools, and our capture still the one
|
|
152
|
+
* published — the exact staleness rule the claude column follows.
|
|
153
|
+
*/
|
|
154
|
+
export function isComparable(row, canonicalSha256) {
|
|
155
|
+
return (row !== undefined &&
|
|
156
|
+
row.error === undefined &&
|
|
157
|
+
row.toolSetMatches &&
|
|
158
|
+
!row.dynamic &&
|
|
159
|
+
row.capturedSha256 !== null &&
|
|
160
|
+
canonicalSha256 !== null &&
|
|
161
|
+
row.capturedSha256 === canonicalSha256 &&
|
|
162
|
+
row.ourTokens > 0 &&
|
|
163
|
+
row.ourMappedTokens > 0);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Signed divergence of the CLI's count from our count of the projection it
|
|
167
|
+
* models, in percent. Against the mapped number and not the headline: the gap
|
|
168
|
+
* to the headline is field selection, published separately per server, and
|
|
169
|
+
* folding it in here would bury the number this column exists to check —
|
|
170
|
+
* whether two independent counters agree on the fields both count.
|
|
171
|
+
*/
|
|
172
|
+
export function divergencePct(row) {
|
|
173
|
+
if (row.ourMappedTokens <= 0)
|
|
174
|
+
return null;
|
|
175
|
+
return ((row.cliTokens - row.ourMappedTokens) / row.ourMappedTokens) * 100;
|
|
176
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ServerEntry } from './report.js';
|
|
2
|
+
import { type CrossCheckRun } from '../core/cross-check.js';
|
|
3
|
+
export declare function loadCrossCheck(root?: string): CrossCheckRun | null;
|
|
4
|
+
export declare function writeCrossCheck(run: CrossCheckRun, root?: string): void;
|
|
5
|
+
/** Release-asset triple for where the CLI will actually run. */
|
|
6
|
+
export declare function cliTriple(docker: boolean, platform?: NodeJS.Platform, arch?: NodeJS.Architecture): string;
|
|
7
|
+
/**
|
|
8
|
+
* Fetch the pinned CLI release for `triple` into a host cache, verifying the
|
|
9
|
+
* archive against the release's own `.sha256` before anything is extracted or
|
|
10
|
+
* executed. `MCP_TOKENS_BIN` overrides the whole dance — that is how the tests
|
|
11
|
+
* substitute a shim, and how an airgapped machine supplies its own copy.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ensureCliBinary(triple: string): Promise<string>;
|
|
14
|
+
interface CliOutcome {
|
|
15
|
+
code: number | null;
|
|
16
|
+
stdout: string;
|
|
17
|
+
stderr: string;
|
|
18
|
+
timedOut: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Run the CLI against one server, host or containerized. In docker mode the
|
|
22
|
+
* binary is bind-mounted read-only and the CLI launches the server inside the
|
|
23
|
+
* container — same image, limits, dummy env and shared package caches as the
|
|
24
|
+
* measurement that just ran, so the npx install it pays is already warm.
|
|
25
|
+
*/
|
|
26
|
+
export declare function runCli(binPath: string, entry: ServerEntry, opts: {
|
|
27
|
+
docker: boolean;
|
|
28
|
+
timeoutMs: number;
|
|
29
|
+
graceMs?: number;
|
|
30
|
+
}): Promise<CliOutcome>;
|
|
31
|
+
export {};
|