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 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 pick a winner when two places on the machine set the same variable to
109
- different values, or when a settings file exists and cannot be read; and it will not pass an
110
- absence of a record off as a measurement. The first two print as unanswered questions. The
111
- third prints as an answer that names itself: for the four discovered clients with no default
112
- on record `claude-desktop`, `cursor`, `vscode`, `windsurf` the tokens are counted as
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; 19 of them
184
- still match the capture on disk here, which is why
185
- [results/leaderboard.md](results/leaderboard.md) prints a claude number for 19 and leaves the
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 69 servers measured, cost spans **1,700×**,
203
- from the 32-token `postgres` reference server to github's 54,422. The table below is a
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,422 tokens** | 44 |
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
- *(69 of 82 popular servers measured, sweeps of 2026-08-18 and 2026-08-19 — full table in
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: 54422 tokens (o200k_base, methodology 1.0) — capture, hash, and count all agree
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. 46 of the 69 numbers come from the sweep of 2026-08-19 and 23 from 2026-08-18. Two
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 report = await runAudit({
163
- configPaths: all('config'),
164
- budget,
165
- contextWindow: numeric('context'),
166
- timeoutMs: numeric('timeout'),
167
- concurrency: numeric('concurrency'),
168
- docker: rest.includes('--docker'),
169
- claude: rest.includes('--claude'),
170
- divergenceUrl: argOf('divergence-url'),
171
- // Progress goes to stderr so `--json` stdout stays a single parseable object.
172
- onProgress: json ? undefined : (name, done, total) => process.stderr.write(` [${done}/${total}] ${name}\n`),
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 {};