mcp-context-cost 0.3.0 → 0.4.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 +131 -38
- package/dist/audit/audit.d.ts +63 -0
- package/dist/audit/audit.js +120 -9
- package/dist/audit/diff.d.ts +124 -0
- package/dist/audit/diff.js +318 -0
- package/dist/audit/run.d.ts +12 -0
- package/dist/audit/run.js +28 -1
- package/dist/cli.d.ts +21 -0
- package/dist/cli.js +141 -7
- package/dist/sweep/dashboard.d.ts +8 -0
- package/dist/sweep/dashboard.js +53 -5
- package/dist/sweep/docker.d.ts +8 -0
- package/dist/sweep/docker.js +7 -2
- package/dist/sweep/report.d.ts +2 -0
- package/dist/sweep/run.d.ts +2 -0
- package/dist/sweep/run.js +6 -1
- package/dist/sweep/sweep-all.js +1 -0
- package/package.json +1 -1
package/dist/sweep/dashboard.js
CHANGED
|
@@ -7,6 +7,38 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
|
7
7
|
import { dirname, join } from 'node:path';
|
|
8
8
|
import { parse } from 'yaml';
|
|
9
9
|
import { bandColor, BAND_META } from '../core/bands.js';
|
|
10
|
+
import { parseHistory } from './history.js';
|
|
11
|
+
/** Longest series a sparkline plots — a stat-tile trend, not a full chart. */
|
|
12
|
+
const SPARK_MAX_POINTS = 12;
|
|
13
|
+
/**
|
|
14
|
+
* A 12-point-max inline trend line, oldest to newest. Muted stroke (this is
|
|
15
|
+
* texture, not a headline number) with the current value picked out as an
|
|
16
|
+
* accent dot, per the sparkline spec: de-emphasis hue for the line, accent
|
|
17
|
+
* for "now". Flat series still draw a level line rather than faking a zero
|
|
18
|
+
* baseline. Returns '' when there's nothing to trend (0-1 points).
|
|
19
|
+
*/
|
|
20
|
+
export function renderSparkline(tokens) {
|
|
21
|
+
const points = tokens.slice(-SPARK_MAX_POINTS);
|
|
22
|
+
if (points.length < 2)
|
|
23
|
+
return '';
|
|
24
|
+
const w = 56;
|
|
25
|
+
const h = 18;
|
|
26
|
+
const pad = 3;
|
|
27
|
+
const min = Math.min(...points);
|
|
28
|
+
const max = Math.max(...points);
|
|
29
|
+
const range = max - min;
|
|
30
|
+
const stepX = (w - pad * 2) / (points.length - 1);
|
|
31
|
+
const coords = points.map((v, i) => {
|
|
32
|
+
const x = pad + i * stepX;
|
|
33
|
+
const y = range === 0 ? h / 2 : pad + (h - pad * 2) * (1 - (v - min) / range);
|
|
34
|
+
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
|
35
|
+
});
|
|
36
|
+
const [lastX, lastY] = coords[coords.length - 1].split(',');
|
|
37
|
+
return `<svg class="spark" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" aria-hidden="true" focusable="false">
|
|
38
|
+
<polyline points="${coords.join(' ')}" fill="none" stroke="var(--muted)" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
|
39
|
+
<circle cx="${lastX}" cy="${lastY}" r="2.2" fill="var(--accent)" stroke="var(--surface)" stroke-width="1"/>
|
|
40
|
+
</svg>`;
|
|
41
|
+
}
|
|
10
42
|
const esc = (s) => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
11
43
|
export function generateDashboard(root = process.cwd()) {
|
|
12
44
|
const doc = parse(readFileSync(join(root, 'servers.yaml'), 'utf8'));
|
|
@@ -15,6 +47,12 @@ export function generateDashboard(root = process.cwd()) {
|
|
|
15
47
|
? JSON.parse(readFileSync(divergencePath, 'utf8'))
|
|
16
48
|
: {};
|
|
17
49
|
const dSrv = divergence.servers ?? {};
|
|
50
|
+
const historyPath = join(root, 'results', 'history.csv');
|
|
51
|
+
const history = existsSync(historyPath) ? parseHistory(readFileSync(historyPath, 'utf8')) : [];
|
|
52
|
+
const seriesFor = (name) => history
|
|
53
|
+
.filter((h) => h.server === name)
|
|
54
|
+
.sort((a, b) => a.date.localeCompare(b.date))
|
|
55
|
+
.map((h) => h.tokens);
|
|
18
56
|
const rows = doc.servers.map((entry) => {
|
|
19
57
|
const p = join(root, 'results', entry.name, 'measurement.json');
|
|
20
58
|
return { entry, m: existsSync(p) ? JSON.parse(readFileSync(p, 'utf8')) : null };
|
|
@@ -39,11 +77,15 @@ export function generateDashboard(root = process.cwd()) {
|
|
|
39
77
|
const pct = Math.max(1.2, (t / max) * 100);
|
|
40
78
|
const div = dSrv[r.entry.name];
|
|
41
79
|
const claudeTip = div ? ` · in a Claude request: ${fmt(div.claudeDelta)} tok` : '';
|
|
80
|
+
const series = seriesFor(r.entry.name);
|
|
81
|
+
const spark = renderSparkline(series);
|
|
82
|
+
const trendTip = series.length > 1 ? ` · ${series.length}-sweep trend: ${series[0].toLocaleString('en-US')} → ${series[series.length - 1].toLocaleString('en-US')}` : '';
|
|
42
83
|
// The whole row is the link to the server's detail page (docs/servers/).
|
|
43
|
-
return `<a class="row" href="servers/${encodeURIComponent(r.entry.name)}.html" data-tip="${esc(m.toolCount)} tools · largest: ${esc(largest?.name)} (${fmt(largest?.tokens ?? 0)} tok)${claudeTip} · ${esc(r.entry.category)} · ${esc(m.status)}${m.serverVersion ? ' · v' + esc(String(m.serverVersion).replace(/^v/, '')) : ''}">
|
|
84
|
+
return `<a class="row" href="servers/${encodeURIComponent(r.entry.name)}.html" data-tip="${esc(m.toolCount)} tools · largest: ${esc(largest?.name)} (${fmt(largest?.tokens ?? 0)} tok)${claudeTip}${trendTip} · ${esc(r.entry.category)} · ${esc(m.status)}${m.serverVersion ? ' · v' + esc(String(m.serverVersion).replace(/^v/, '')) : ''}">
|
|
44
85
|
<span class="rank">${i + 1}</span>
|
|
45
86
|
<span class="name">${esc(r.entry.name)}</span>
|
|
46
87
|
<span class="track"><span class="bar" style="width:${pct.toFixed(1)}%"></span></span>
|
|
88
|
+
<span class="spark-cell">${spark}</span>
|
|
47
89
|
<span class="val"><span class="dot dot-${band}" aria-hidden="true"></span>${fmt(t)}<span class="bandname">${meta.label}</span></span>
|
|
48
90
|
</a>`;
|
|
49
91
|
})
|
|
@@ -59,7 +101,11 @@ export function generateDashboard(root = process.cwd()) {
|
|
|
59
101
|
.map((r, i) => {
|
|
60
102
|
const m = r.m;
|
|
61
103
|
const div = dSrv[r.entry.name];
|
|
62
|
-
|
|
104
|
+
const series = seriesFor(r.entry.name);
|
|
105
|
+
const trend = series.length > 1
|
|
106
|
+
? `${series[series.length - 1] - series[0] >= 0 ? '+' : ''}${fmt(series[series.length - 1] - series[0])} / ${series.length}d`
|
|
107
|
+
: '—';
|
|
108
|
+
return `<tr><td>${i + 1}</td><td>${esc(r.entry.name)}</td><td class="num">${fmt(m.totalTokens)}</td><td class="num">${div ? fmt(div.claudeDelta) : '—'}</td><td class="num">${esc(m.toolCount)}</td><td>${esc(BAND_META[bandColor(m.totalTokens)].label)}</td><td>${esc(r.entry.category)}</td><td class="num">${trend}</td></tr>`;
|
|
63
109
|
})
|
|
64
110
|
.join('\n');
|
|
65
111
|
const specimens = [measured[measured.length - 1], measured[0]]
|
|
@@ -117,13 +163,15 @@ export function generateDashboard(root = process.cwd()) {
|
|
|
117
163
|
.stat .l { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
|
|
118
164
|
|
|
119
165
|
.board { background: var(--surface); border: 1px solid var(--line); border-radius: 8px; padding: 14px 16px; }
|
|
120
|
-
.row { display: grid; grid-template-columns: 2ch minmax(120px, 190px) 1fr max-content; gap: 10px; align-items: center; padding: 3px 4px; border-radius: 4px; outline: none; color: inherit; text-decoration: none; }
|
|
166
|
+
.row { display: grid; grid-template-columns: 2ch minmax(120px, 190px) 1fr 56px max-content; gap: 10px; align-items: center; padding: 3px 4px; border-radius: 4px; outline: none; color: inherit; text-decoration: none; }
|
|
121
167
|
.row:hover, .row:focus-visible { background: var(--accent-soft); }
|
|
122
168
|
.row:hover .name, .row:focus-visible .name { text-decoration: underline; }
|
|
123
169
|
.rank { font-family: ui-monospace, Menlo, monospace; font-size: 11px; color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
|
|
124
170
|
.name { font-size: 0.86rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
125
171
|
.track { background: var(--track); border-radius: 3px; height: 12px; overflow: hidden; }
|
|
126
172
|
.bar { display: block; height: 100%; background: var(--accent); border-radius: 3px 3px 3px 3px; min-width: 3px; }
|
|
173
|
+
.spark-cell { display: flex; align-items: center; justify-content: center; height: 18px; }
|
|
174
|
+
.spark-cell:empty { visibility: hidden; }
|
|
127
175
|
.val { font-family: ui-monospace, Menlo, monospace; font-variant-numeric: tabular-nums; font-size: 0.82rem; display: flex; align-items: center; gap: 6px; }
|
|
128
176
|
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; box-shadow: 0 0 0 2px var(--surface); }
|
|
129
177
|
.dot-brightgreen { background: var(--b-brightgreen); } .dot-green { background: var(--b-green); }
|
|
@@ -172,7 +220,7 @@ export function generateDashboard(root = process.cwd()) {
|
|
|
172
220
|
</div>
|
|
173
221
|
|
|
174
222
|
<h2>Leaderboard</h2>
|
|
175
|
-
<p class="h2sub">Tokens = o200k_base count of the canonical <code>tools/list</code> bytes — the wire payload. What a <em>Claude request</em> actually carries can differ sharply (github: 54,422 on the wire, ${dSrv['github'] ? fmt(dSrv['github'].claudeDelta) : '…'} in a request — 80% of its schema bytes are fields no Anthropic request sends). Hover a row for
|
|
223
|
+
<p class="h2sub">Tokens = o200k_base count of the canonical <code>tools/list</code> bytes — the wire payload. What a <em>Claude request</em> actually carries can differ sharply (github: 54,422 on the wire, ${dSrv['github'] ? fmt(dSrv['github'].claudeDelta) : '…'} in a request — 80% of its schema bytes are fields no Anthropic request sends). The trend line plots tokens across every sweep date on record (oldest→newest, dot = current); servers with one sweep so far show no line yet. Hover a row for exact numbers; <a href="METHODOLOGY.html#claude-divergence">method</a>.</p>
|
|
176
224
|
<div class="board">
|
|
177
225
|
${barRows || '<p class="h2sub">Sweep in progress — first results land shortly.</p>'}
|
|
178
226
|
</div>
|
|
@@ -198,7 +246,7 @@ ${barRows || '<p class="h2sub">Sweep in progress — first results land shortly.
|
|
|
198
246
|
|
|
199
247
|
<details><summary>Full data table</summary>
|
|
200
248
|
<div class="tablewrap" style="margin-top:10px"><table>
|
|
201
|
-
<thead><tr><th>#</th><th>server</th><th>tokens (o200k)</th><th>claude req</th><th>tools</th><th>band</th><th>category</th></tr></thead>
|
|
249
|
+
<thead><tr><th>#</th><th>server</th><th>tokens (o200k)</th><th>claude req</th><th>tools</th><th>band</th><th>category</th><th>trend</th></tr></thead>
|
|
202
250
|
<tbody>${tableRows}</tbody>
|
|
203
251
|
</table></div>
|
|
204
252
|
</details>
|
package/dist/sweep/docker.d.ts
CHANGED
|
@@ -12,6 +12,14 @@ export interface DockerOptions {
|
|
|
12
12
|
dummyEnv?: string[];
|
|
13
13
|
/** Container name, so a timed-out container can be force-removed. */
|
|
14
14
|
containerName?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Install `git` inside the container before launch. The slim base images
|
|
17
|
+
* carry no VCS, so `uvx --from git+...` installs fail with "Git executable
|
|
18
|
+
* not found" — this only prefixes the containerized invocation, never the
|
|
19
|
+
* recorded `launchCommand`, so the published command stays what a user with
|
|
20
|
+
* git already on PATH would actually run.
|
|
21
|
+
*/
|
|
22
|
+
needsGit?: boolean;
|
|
15
23
|
}
|
|
16
24
|
export declare const DEFAULT_NODE_IMAGE = "public.ecr.aws/docker/library/node:22-slim";
|
|
17
25
|
export declare const DEFAULT_PYTHON_IMAGE = "ghcr.io/astral-sh/uv:python3.12-bookworm-slim";
|
package/dist/sweep/docker.js
CHANGED
|
@@ -35,7 +35,10 @@ export function dockerize(commandLine, opts = {}) {
|
|
|
35
35
|
for (const name of opts.dummyEnv ?? []) {
|
|
36
36
|
argv.push('-e', `${name}=dummy`);
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
const gitPrefix = opts.needsGit
|
|
39
|
+
? 'command -v git >/dev/null 2>&1 || (apt-get update -qq && apt-get install -y -qq --no-install-recommends git >/dev/null 2>&1); '
|
|
40
|
+
: '';
|
|
41
|
+
argv.push(image, 'sh', '-lc', gitPrefix + commandLine);
|
|
39
42
|
return {
|
|
40
43
|
command: 'docker',
|
|
41
44
|
argv,
|
|
@@ -43,7 +46,9 @@ export function dockerize(commandLine, opts = {}) {
|
|
|
43
46
|
docker: true,
|
|
44
47
|
image,
|
|
45
48
|
network: 'bridge',
|
|
46
|
-
note:
|
|
49
|
+
note: opts.needsGit
|
|
50
|
+
? 'network enabled for package fetch; clean FS, no host credentials, git installed'
|
|
51
|
+
: 'network enabled for package fetch; clean FS, no host credentials',
|
|
47
52
|
},
|
|
48
53
|
};
|
|
49
54
|
}
|
package/dist/sweep/report.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface ServerEntry {
|
|
|
11
11
|
remote?: boolean;
|
|
12
12
|
dockerImage?: string;
|
|
13
13
|
timeoutSeconds?: number;
|
|
14
|
+
/** Launch needs the `git` binary (e.g. `uvx --from git+...`) — absent from the slim isolation images. */
|
|
15
|
+
needsGit?: boolean;
|
|
14
16
|
}
|
|
15
17
|
/** Neutralize markdown/table syntax in third-party strings (tool names, notes). */
|
|
16
18
|
export declare function mdCell(s: unknown): string;
|
package/dist/sweep/run.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface MeasureOptions {
|
|
|
7
7
|
dockerImage?: string;
|
|
8
8
|
/** env var NAMES to provide as dummy values (docker mode). */
|
|
9
9
|
dummyEnv?: string[];
|
|
10
|
+
/** Install `git` in the container before launch (docker mode) — see docker.ts. */
|
|
11
|
+
needsGit?: boolean;
|
|
10
12
|
/**
|
|
11
13
|
* Exact argv, when the caller already has it (client configs store command and
|
|
12
14
|
* args separately). Avoids re-splitting a joined string on spaces, which would
|
package/dist/sweep/run.js
CHANGED
|
@@ -32,7 +32,12 @@ export async function measureServer(name, command, opts = {}) {
|
|
|
32
32
|
}
|
|
33
33
|
else if (opts.docker) {
|
|
34
34
|
containerName = `mcp-ctx-${name}-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
35
|
-
const d = dockerize(command, {
|
|
35
|
+
const d = dockerize(command, {
|
|
36
|
+
image: opts.dockerImage,
|
|
37
|
+
dummyEnv: opts.dummyEnv,
|
|
38
|
+
containerName,
|
|
39
|
+
needsGit: opts.needsGit,
|
|
40
|
+
});
|
|
36
41
|
spec = { command: d.command, argv: d.argv };
|
|
37
42
|
isolation = d.isolation;
|
|
38
43
|
}
|
package/dist/sweep/sweep-all.js
CHANGED
package/package.json
CHANGED