mcp-baggage 0.1.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/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/bin.d.ts +9 -0
- package/dist/bin.js +14 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.js +215 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.js +163 -0
- package/dist/config.js.map +1 -0
- package/dist/exact.d.ts +23 -0
- package/dist/exact.js +69 -0
- package/dist/exact.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +50 -0
- package/dist/mcp.js +288 -0
- package/dist/mcp.js.map +1 -0
- package/dist/report.d.ts +52 -0
- package/dist/report.js +166 -0
- package/dist/report.js.map +1 -0
- package/dist/tokens.d.ts +31 -0
- package/dist/tokens.js +67 -0
- package/dist/tokens.js.map +1 -0
- package/dist/toml.d.ts +11 -0
- package/dist/toml.js +145 -0
- package/dist/toml.js.map +1 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/usage.d.ts +43 -0
- package/dist/usage.js +119 -0
- package/dist/usage.js.map +1 -0
- package/dist/weigh.d.ts +41 -0
- package/dist/weigh.js +79 -0
- package/dist/weigh.js.map +1 -0
- package/package.json +50 -0
package/dist/toml.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Just enough TOML to read Codex's config. [PURE]
|
|
3
|
+
*
|
|
4
|
+
* Codex keeps its servers in `~/.codex/config.toml`, and a dependency would be
|
|
5
|
+
* a poor trade for the handful of shapes that file actually uses: table
|
|
6
|
+
* headers, strings, integers, booleans, string arrays and inline tables. What
|
|
7
|
+
* this cannot parse it skips rather than guesses at, and an unreadable file
|
|
8
|
+
* costs one client, not the run.
|
|
9
|
+
*/
|
|
10
|
+
function parseValue(raw) {
|
|
11
|
+
const text = raw.trim();
|
|
12
|
+
if (!text)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (text.startsWith('"'))
|
|
15
|
+
return unquote(text, '"');
|
|
16
|
+
if (text.startsWith("'"))
|
|
17
|
+
return unquote(text, "'");
|
|
18
|
+
if (text === 'true')
|
|
19
|
+
return true;
|
|
20
|
+
if (text === 'false')
|
|
21
|
+
return false;
|
|
22
|
+
if (text.startsWith('[')) {
|
|
23
|
+
const inner = text.slice(1, text.endsWith(']') ? -1 : undefined);
|
|
24
|
+
return splitTop(inner).map(parseValue).filter((v) => v !== undefined);
|
|
25
|
+
}
|
|
26
|
+
if (text.startsWith('{')) {
|
|
27
|
+
const inner = text.slice(1, text.endsWith('}') ? -1 : undefined);
|
|
28
|
+
const table = {};
|
|
29
|
+
for (const pair of splitTop(inner)) {
|
|
30
|
+
const eq = pair.indexOf('=');
|
|
31
|
+
if (eq === -1)
|
|
32
|
+
continue;
|
|
33
|
+
const value = parseValue(pair.slice(eq + 1));
|
|
34
|
+
if (value !== undefined)
|
|
35
|
+
table[key(pair.slice(0, eq))] = value;
|
|
36
|
+
}
|
|
37
|
+
return table;
|
|
38
|
+
}
|
|
39
|
+
const n = Number(text);
|
|
40
|
+
return Number.isFinite(n) ? n : text;
|
|
41
|
+
}
|
|
42
|
+
function unquote(text, quote) {
|
|
43
|
+
const end = text.indexOf(quote, 1);
|
|
44
|
+
const body = end === -1 ? text.slice(1) : text.slice(1, end);
|
|
45
|
+
return quote === '"' ? body.replace(/\\(.)/g, (_, c) => (c === 'n' ? '\n' : c === 't' ? '\t' : c)) : body;
|
|
46
|
+
}
|
|
47
|
+
const key = (raw) => raw.trim().replace(/^["']|["']$/g, '');
|
|
48
|
+
/** Splits on commas that are not inside a string, an array or an inline table. */
|
|
49
|
+
function splitTop(text) {
|
|
50
|
+
const parts = [];
|
|
51
|
+
let depth = 0;
|
|
52
|
+
let quote = '';
|
|
53
|
+
let start = 0;
|
|
54
|
+
for (let i = 0; i < text.length; i++) {
|
|
55
|
+
const c = text[i];
|
|
56
|
+
if (quote) {
|
|
57
|
+
if (c === quote && text[i - 1] !== '\\')
|
|
58
|
+
quote = '';
|
|
59
|
+
}
|
|
60
|
+
else if (c === '"' || c === "'")
|
|
61
|
+
quote = c;
|
|
62
|
+
else if (c === '[' || c === '{')
|
|
63
|
+
depth++;
|
|
64
|
+
else if (c === ']' || c === '}')
|
|
65
|
+
depth--;
|
|
66
|
+
else if (c === ',' && depth === 0) {
|
|
67
|
+
parts.push(text.slice(start, i));
|
|
68
|
+
start = i + 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
parts.push(text.slice(start));
|
|
72
|
+
return parts.map((p) => p.trim()).filter(Boolean);
|
|
73
|
+
}
|
|
74
|
+
/** Splits a dotted table header into its segments, respecting quotes. */
|
|
75
|
+
function path(header) {
|
|
76
|
+
return splitDots(header).map(key);
|
|
77
|
+
}
|
|
78
|
+
function splitDots(text) {
|
|
79
|
+
const parts = [];
|
|
80
|
+
let quote = '';
|
|
81
|
+
let start = 0;
|
|
82
|
+
for (let i = 0; i < text.length; i++) {
|
|
83
|
+
const c = text[i];
|
|
84
|
+
if (quote) {
|
|
85
|
+
if (c === quote)
|
|
86
|
+
quote = '';
|
|
87
|
+
}
|
|
88
|
+
else if (c === '"' || c === "'")
|
|
89
|
+
quote = c;
|
|
90
|
+
else if (c === '.') {
|
|
91
|
+
parts.push(text.slice(start, i));
|
|
92
|
+
start = i + 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
parts.push(text.slice(start));
|
|
96
|
+
return parts;
|
|
97
|
+
}
|
|
98
|
+
/** The whole file, as nested plain objects. Arrays of tables are not supported. */
|
|
99
|
+
export function parseToml(text) {
|
|
100
|
+
const root = {};
|
|
101
|
+
let table = root;
|
|
102
|
+
for (const line of text.split(/\r?\n/)) {
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
105
|
+
continue;
|
|
106
|
+
if (trimmed.startsWith('[')) {
|
|
107
|
+
const header = trimmed.slice(1, trimmed.indexOf(']') === -1 ? undefined : trimmed.indexOf(']'));
|
|
108
|
+
if (header.startsWith('['))
|
|
109
|
+
continue; // array of tables: not needed here
|
|
110
|
+
table = root;
|
|
111
|
+
for (const segment of path(header)) {
|
|
112
|
+
const next = table[segment];
|
|
113
|
+
if (next && typeof next === 'object' && !Array.isArray(next))
|
|
114
|
+
table = next;
|
|
115
|
+
else
|
|
116
|
+
table = (table[segment] = {});
|
|
117
|
+
}
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const eq = trimmed.indexOf('=');
|
|
121
|
+
if (eq === -1)
|
|
122
|
+
continue;
|
|
123
|
+
const value = parseValue(stripComment(trimmed.slice(eq + 1)));
|
|
124
|
+
if (value !== undefined)
|
|
125
|
+
table[key(trimmed.slice(0, eq))] = value;
|
|
126
|
+
}
|
|
127
|
+
return root;
|
|
128
|
+
}
|
|
129
|
+
/** Drops a trailing `# comment`, unless the `#` is inside a string. */
|
|
130
|
+
function stripComment(text) {
|
|
131
|
+
let quote = '';
|
|
132
|
+
for (let i = 0; i < text.length; i++) {
|
|
133
|
+
const c = text[i];
|
|
134
|
+
if (quote) {
|
|
135
|
+
if (c === quote && text[i - 1] !== '\\')
|
|
136
|
+
quote = '';
|
|
137
|
+
}
|
|
138
|
+
else if (c === '"' || c === "'")
|
|
139
|
+
quote = c;
|
|
140
|
+
else if (c === '#')
|
|
141
|
+
return text.slice(0, i);
|
|
142
|
+
}
|
|
143
|
+
return text;
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=toml.js.map
|
package/dist/toml.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toml.js","sourceRoot":"","sources":["../src/toml.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shapes everything else passes around. [PURE]
|
|
3
|
+
*
|
|
4
|
+
* Deliberately looser than the MCP schema: a server is free to send fields
|
|
5
|
+
* this tool has never heard of, and the point is to weigh what arrives, not
|
|
6
|
+
* to validate it. Anything unrecognised still counts toward the bill.
|
|
7
|
+
*/
|
|
8
|
+
/** How a server is reached. */
|
|
9
|
+
export type Transport = 'stdio' | 'http';
|
|
10
|
+
/** One server, as some client's config file describes it. */
|
|
11
|
+
export type ServerSpec = {
|
|
12
|
+
/** The name the config gave it — this is what tool names are prefixed with. */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Which client's config this came from: `claude-code`, `cursor`, `codex`, `vscode`, `windsurf`. */
|
|
15
|
+
client: string;
|
|
16
|
+
/** Where that config lives, so a report can say which file to edit. */
|
|
17
|
+
source: string;
|
|
18
|
+
/** `user` for a machine-wide config, `project` for one inside the repo. */
|
|
19
|
+
scope: 'user' | 'project';
|
|
20
|
+
transport: Transport;
|
|
21
|
+
/** stdio only. */
|
|
22
|
+
command?: string;
|
|
23
|
+
args?: string[];
|
|
24
|
+
env?: Record<string, string>;
|
|
25
|
+
cwd?: string;
|
|
26
|
+
/** http only. */
|
|
27
|
+
url?: string;
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
/** Some clients can carry a server while leaving it switched off. */
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Every client whose config carries this same server.
|
|
33
|
+
*
|
|
34
|
+
* The same command appears in two or three configs more often than not, and
|
|
35
|
+
* it costs the same in each, so it is weighed once and the report names the
|
|
36
|
+
* files rather than billing it twice.
|
|
37
|
+
*/
|
|
38
|
+
carriedBy: string[];
|
|
39
|
+
};
|
|
40
|
+
/** A tool as the server declares it, and as the model will be shown it. */
|
|
41
|
+
export type ToolDef = {
|
|
42
|
+
name: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
/** JSON Schema. Kept as-is: its size is the whole subject. */
|
|
45
|
+
inputSchema?: unknown;
|
|
46
|
+
outputSchema?: unknown;
|
|
47
|
+
annotations?: unknown;
|
|
48
|
+
};
|
|
49
|
+
export type PromptDef = {
|
|
50
|
+
name: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
arguments?: unknown;
|
|
53
|
+
};
|
|
54
|
+
export type ResourceDef = {
|
|
55
|
+
uri?: string;
|
|
56
|
+
name?: string;
|
|
57
|
+
description?: string;
|
|
58
|
+
mimeType?: string;
|
|
59
|
+
};
|
|
60
|
+
/** What one server turned out to be carrying. */
|
|
61
|
+
export type Inventory = {
|
|
62
|
+
server: ServerSpec;
|
|
63
|
+
tools: ToolDef[];
|
|
64
|
+
prompts: PromptDef[];
|
|
65
|
+
resources: ResourceDef[];
|
|
66
|
+
/** Server name and version from `initialize`, when it gave one. */
|
|
67
|
+
title?: string;
|
|
68
|
+
/** How long the handshake and listing took, in milliseconds. */
|
|
69
|
+
ms: number;
|
|
70
|
+
/** Set when the server could not be reached or refused to speak. */
|
|
71
|
+
error?: string;
|
|
72
|
+
};
|
|
73
|
+
/** A tool weighed: what it costs, and whether anyone has used it. */
|
|
74
|
+
export type WeighedTool = {
|
|
75
|
+
/** The bare name the server gave. */
|
|
76
|
+
name: string;
|
|
77
|
+
/** The name a client shows the model, e.g. `mcp__github__create_issue`. */
|
|
78
|
+
qualified: string;
|
|
79
|
+
tokens: number;
|
|
80
|
+
/** Times it was called in the transcripts that were read, or `null` when none were. */
|
|
81
|
+
calls: number | null;
|
|
82
|
+
};
|
|
83
|
+
export type WeighedServer = {
|
|
84
|
+
server: ServerSpec;
|
|
85
|
+
title?: string;
|
|
86
|
+
tools: WeighedTool[];
|
|
87
|
+
/** Tokens for the tool definitions alone. */
|
|
88
|
+
tokens: number;
|
|
89
|
+
prompts: number;
|
|
90
|
+
resources: number;
|
|
91
|
+
ms: number;
|
|
92
|
+
error?: string;
|
|
93
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shapes everything else passes around. [PURE]
|
|
3
|
+
*
|
|
4
|
+
* Deliberately looser than the MCP schema: a server is free to send fields
|
|
5
|
+
* this tool has never heard of, and the point is to weigh what arrives, not
|
|
6
|
+
* to validate it. Anything unrecognised still counts toward the bill.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|
package/dist/usage.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which tools anyone actually called.
|
|
3
|
+
*
|
|
4
|
+
* The token cost of a tool definition is paid on every request whether or not
|
|
5
|
+
* the tool is ever used, and that is the whole argument of this program — but
|
|
6
|
+
* it is only an argument once you can see the other half of the ledger. So the
|
|
7
|
+
* session transcripts are read for `tool_use` blocks, and each MCP tool gets a
|
|
8
|
+
* count of the times it was reached for.
|
|
9
|
+
*
|
|
10
|
+
* Only Claude Code keeps transcripts in a documented place, so only its
|
|
11
|
+
* sessions are read. When there are none, every count is `null` — unknown,
|
|
12
|
+
* which the report is careful never to print as zero.
|
|
13
|
+
*/
|
|
14
|
+
/** What the transcripts said, or nothing at all. */
|
|
15
|
+
export type Usage = {
|
|
16
|
+
/** Qualified tool name (`mcp__server__tool`) to times called. */
|
|
17
|
+
calls: Map<string, number>;
|
|
18
|
+
/** Transcript files read. */
|
|
19
|
+
sessions: number;
|
|
20
|
+
/** How far back the read went, in days. */
|
|
21
|
+
days: number;
|
|
22
|
+
};
|
|
23
|
+
/** How many days of transcripts to read by default. */
|
|
24
|
+
export declare const DEFAULT_DAYS = 30;
|
|
25
|
+
/**
|
|
26
|
+
* A tool call, from one line of a transcript.
|
|
27
|
+
*
|
|
28
|
+
* Exported for its own sake: the line is a whole assistant turn and can be
|
|
29
|
+
* megabytes of text, so it is only parsed when the marker is present, and
|
|
30
|
+
* `tool_use` blocks are the only thing taken from it. A `tool_result` echoing
|
|
31
|
+
* the same name must not count as a second call.
|
|
32
|
+
*/
|
|
33
|
+
export declare function callsInLine(line: string): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Read the transcripts and count the calls.
|
|
36
|
+
*
|
|
37
|
+
* Files untouched since the cutoff are skipped by their timestamp rather than
|
|
38
|
+
* opened — a year of sessions is a lot of lines, and the last month is what a
|
|
39
|
+
* decision about today's config rests on.
|
|
40
|
+
*/
|
|
41
|
+
export declare function readUsage(days?: number, home?: string): Promise<Usage | null>;
|
|
42
|
+
/** A lookup that answers `null` — unknown — rather than zero when nothing was read. */
|
|
43
|
+
export declare function lookup(usage: Usage | null): (qualified: string) => number | null;
|
package/dist/usage.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which tools anyone actually called.
|
|
3
|
+
*
|
|
4
|
+
* The token cost of a tool definition is paid on every request whether or not
|
|
5
|
+
* the tool is ever used, and that is the whole argument of this program — but
|
|
6
|
+
* it is only an argument once you can see the other half of the ledger. So the
|
|
7
|
+
* session transcripts are read for `tool_use` blocks, and each MCP tool gets a
|
|
8
|
+
* count of the times it was reached for.
|
|
9
|
+
*
|
|
10
|
+
* Only Claude Code keeps transcripts in a documented place, so only its
|
|
11
|
+
* sessions are read. When there are none, every count is `null` — unknown,
|
|
12
|
+
* which the report is careful never to print as zero.
|
|
13
|
+
*/
|
|
14
|
+
import { createReadStream } from 'node:fs';
|
|
15
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { createInterface } from 'node:readline';
|
|
19
|
+
/** How many days of transcripts to read by default. */
|
|
20
|
+
export const DEFAULT_DAYS = 30;
|
|
21
|
+
/**
|
|
22
|
+
* A tool call, from one line of a transcript.
|
|
23
|
+
*
|
|
24
|
+
* Exported for its own sake: the line is a whole assistant turn and can be
|
|
25
|
+
* megabytes of text, so it is only parsed when the marker is present, and
|
|
26
|
+
* `tool_use` blocks are the only thing taken from it. A `tool_result` echoing
|
|
27
|
+
* the same name must not count as a second call.
|
|
28
|
+
*/
|
|
29
|
+
export function callsInLine(line) {
|
|
30
|
+
if (!line.includes('mcp__'))
|
|
31
|
+
return [];
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(line);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const found = [];
|
|
40
|
+
const walk = (node, depth) => {
|
|
41
|
+
if (depth > 8 || !node || typeof node !== 'object')
|
|
42
|
+
return;
|
|
43
|
+
if (Array.isArray(node)) {
|
|
44
|
+
for (const item of node)
|
|
45
|
+
walk(item, depth + 1);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const record = node;
|
|
49
|
+
if (record['type'] === 'tool_use' && typeof record['name'] === 'string' && record['name'].startsWith('mcp__')) {
|
|
50
|
+
found.push(record['name']);
|
|
51
|
+
}
|
|
52
|
+
for (const value of Object.values(record))
|
|
53
|
+
walk(value, depth + 1);
|
|
54
|
+
};
|
|
55
|
+
walk(parsed, 0);
|
|
56
|
+
return found;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Every `.jsonl` under a directory, one level of nesting or ten.
|
|
60
|
+
*
|
|
61
|
+
* The directory a `Dirent` came from is `parentPath` on Node 20.12 and after,
|
|
62
|
+
* and was `path` before it. Both are read, because getting this wrong does not
|
|
63
|
+
* fail — it silently reads nothing and reports every tool as never called.
|
|
64
|
+
*/
|
|
65
|
+
async function transcripts(root) {
|
|
66
|
+
try {
|
|
67
|
+
const entries = await readdir(root, { recursive: true, withFileTypes: true });
|
|
68
|
+
return entries
|
|
69
|
+
.filter((e) => e.isFile() && e.name.endsWith('.jsonl'))
|
|
70
|
+
.map((e) => join(e.parentPath ?? e.path ?? root, e.name));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Read the transcripts and count the calls.
|
|
78
|
+
*
|
|
79
|
+
* Files untouched since the cutoff are skipped by their timestamp rather than
|
|
80
|
+
* opened — a year of sessions is a lot of lines, and the last month is what a
|
|
81
|
+
* decision about today's config rests on.
|
|
82
|
+
*/
|
|
83
|
+
export async function readUsage(days = DEFAULT_DAYS, home = homedir()) {
|
|
84
|
+
const files = await transcripts(join(home, '.claude', 'projects'));
|
|
85
|
+
if (files.length === 0)
|
|
86
|
+
return null;
|
|
87
|
+
const cutoff = Date.now() - days * 86_400_000;
|
|
88
|
+
const calls = new Map();
|
|
89
|
+
let sessions = 0;
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
try {
|
|
92
|
+
const info = await stat(file);
|
|
93
|
+
if (info.mtimeMs < cutoff)
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
sessions++;
|
|
100
|
+
try {
|
|
101
|
+
const lines = createInterface({ input: createReadStream(file, 'utf8'), crlfDelay: Infinity });
|
|
102
|
+
for await (const line of lines) {
|
|
103
|
+
for (const name of callsInLine(line))
|
|
104
|
+
calls.set(name, (calls.get(name) ?? 0) + 1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// A half-written transcript from a live session; what was read still counts.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { calls, sessions, days };
|
|
112
|
+
}
|
|
113
|
+
/** A lookup that answers `null` — unknown — rather than zero when nothing was read. */
|
|
114
|
+
export function lookup(usage) {
|
|
115
|
+
if (!usage)
|
|
116
|
+
return () => null;
|
|
117
|
+
return (qualified) => usage.calls.get(qualified) ?? 0;
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=usage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"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"}
|
package/dist/weigh.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a server's listing is worth, in tokens. [PURE]
|
|
3
|
+
*
|
|
4
|
+
* A tool does not reach the model as the server wrote it. The client renames
|
|
5
|
+
* it — `mcp__github__create_issue` rather than `create_issue` — and sends the
|
|
6
|
+
* name, the description and the input schema as one JSON object per tool, on
|
|
7
|
+
* every single request. That serialized form is what this weighs, because
|
|
8
|
+
* that is what is actually paid for, turn after turn, whether or not the tool
|
|
9
|
+
* is ever called.
|
|
10
|
+
*/
|
|
11
|
+
import type { Inventory, ToolDef, WeighedServer, WeighedTool } from './types.ts';
|
|
12
|
+
/** The name a client shows the model. The separator is `__` across all of them. */
|
|
13
|
+
export declare function qualify(server: string, tool: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* A tool as the request carries it.
|
|
16
|
+
*
|
|
17
|
+
* `input_schema` rather than `inputSchema`: the wire format between client and
|
|
18
|
+
* server is camelCase, the one between client and model is snake_case, and the
|
|
19
|
+
* second is the one being billed.
|
|
20
|
+
*/
|
|
21
|
+
export declare function serialize(tool: ToolDef, qualified: string): string;
|
|
22
|
+
export declare function weighTool(tool: ToolDef, serverName: string, calls: number | null): WeighedTool;
|
|
23
|
+
/**
|
|
24
|
+
* The whole listing, weighed.
|
|
25
|
+
*
|
|
26
|
+
* Prompts and resources are counted but kept apart from the total: most
|
|
27
|
+
* clients list them lazily and only a few put them in the system prompt, so
|
|
28
|
+
* folding them into one number would overstate what every turn costs. The
|
|
29
|
+
* report shows them as a separate line.
|
|
30
|
+
*/
|
|
31
|
+
export declare function weigh(inv: Inventory, callsOf: (qualified: string) => number | null): WeighedServer;
|
|
32
|
+
/** Servers first by what they cost, then by name, so a report is stable run to run. */
|
|
33
|
+
export declare function heaviestFirst(servers: WeighedServer[]): WeighedServer[];
|
|
34
|
+
export declare const totalTokens: (servers: readonly WeighedServer[]) => number;
|
|
35
|
+
/**
|
|
36
|
+
* Tools that were paid for and never called.
|
|
37
|
+
*
|
|
38
|
+
* `calls === null` means no transcript was read, which is not the same as zero
|
|
39
|
+
* and is never reported as waste.
|
|
40
|
+
*/
|
|
41
|
+
export declare function unused(servers: readonly WeighedServer[]): WeighedTool[];
|
package/dist/weigh.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a server's listing is worth, in tokens. [PURE]
|
|
3
|
+
*
|
|
4
|
+
* A tool does not reach the model as the server wrote it. The client renames
|
|
5
|
+
* it — `mcp__github__create_issue` rather than `create_issue` — and sends the
|
|
6
|
+
* name, the description and the input schema as one JSON object per tool, on
|
|
7
|
+
* every single request. That serialized form is what this weighs, because
|
|
8
|
+
* that is what is actually paid for, turn after turn, whether or not the tool
|
|
9
|
+
* is ever called.
|
|
10
|
+
*/
|
|
11
|
+
import { estimate, TOOL_FRAMING } from "./tokens.js";
|
|
12
|
+
/** The name a client shows the model. The separator is `__` across all of them. */
|
|
13
|
+
export function qualify(server, tool) {
|
|
14
|
+
return `mcp__${server.replace(/[^A-Za-z0-9_-]/g, '_')}__${tool}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A tool as the request carries it.
|
|
18
|
+
*
|
|
19
|
+
* `input_schema` rather than `inputSchema`: the wire format between client and
|
|
20
|
+
* server is camelCase, the one between client and model is snake_case, and the
|
|
21
|
+
* second is the one being billed.
|
|
22
|
+
*/
|
|
23
|
+
export function serialize(tool, qualified) {
|
|
24
|
+
return JSON.stringify({
|
|
25
|
+
name: qualified,
|
|
26
|
+
description: tool.description ?? '',
|
|
27
|
+
input_schema: tool.inputSchema ?? { type: 'object', properties: {} },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function weighTool(tool, serverName, calls) {
|
|
31
|
+
const qualified = qualify(serverName, tool.name);
|
|
32
|
+
return {
|
|
33
|
+
name: tool.name,
|
|
34
|
+
qualified,
|
|
35
|
+
tokens: estimate(serialize(tool, qualified)) + TOOL_FRAMING,
|
|
36
|
+
calls,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The whole listing, weighed.
|
|
41
|
+
*
|
|
42
|
+
* Prompts and resources are counted but kept apart from the total: most
|
|
43
|
+
* clients list them lazily and only a few put them in the system prompt, so
|
|
44
|
+
* folding them into one number would overstate what every turn costs. The
|
|
45
|
+
* report shows them as a separate line.
|
|
46
|
+
*/
|
|
47
|
+
export function weigh(inv, callsOf) {
|
|
48
|
+
const tools = inv.tools.map((t) => weighTool(t, inv.server.name, callsOf(qualify(inv.server.name, t.name))));
|
|
49
|
+
return {
|
|
50
|
+
server: inv.server,
|
|
51
|
+
title: inv.title,
|
|
52
|
+
tools: tools.sort((a, b) => b.tokens - a.tokens),
|
|
53
|
+
tokens: tools.reduce((n, t) => n + t.tokens, 0),
|
|
54
|
+
prompts: inv.prompts.length,
|
|
55
|
+
resources: inv.resources.length,
|
|
56
|
+
ms: inv.ms,
|
|
57
|
+
error: inv.error,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Servers first by what they cost, then by name, so a report is stable run to run. */
|
|
61
|
+
export function heaviestFirst(servers) {
|
|
62
|
+
return servers
|
|
63
|
+
.slice()
|
|
64
|
+
.sort((a, b) => b.tokens - a.tokens || a.server.name.localeCompare(b.server.name));
|
|
65
|
+
}
|
|
66
|
+
export const totalTokens = (servers) => servers.reduce((n, s) => n + s.tokens, 0);
|
|
67
|
+
/**
|
|
68
|
+
* Tools that were paid for and never called.
|
|
69
|
+
*
|
|
70
|
+
* `calls === null` means no transcript was read, which is not the same as zero
|
|
71
|
+
* and is never reported as waste.
|
|
72
|
+
*/
|
|
73
|
+
export function unused(servers) {
|
|
74
|
+
return servers
|
|
75
|
+
.flatMap((s) => s.tools)
|
|
76
|
+
.filter((t) => t.calls === 0)
|
|
77
|
+
.sort((a, b) => b.tokens - a.tokens);
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=weigh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weigh.js","sourceRoot":"","sources":["../src/weigh.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-baggage",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "What each MCP server carries into your context window, and how much of it you actually use.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"context-window",
|
|
9
|
+
"tokens",
|
|
10
|
+
"claude-code",
|
|
11
|
+
"cursor",
|
|
12
|
+
"codex",
|
|
13
|
+
"cli"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/mtalhasahin/mcp-baggage#readme",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/mtalhasahin/mcp-baggage.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/mtalhasahin/mcp-baggage/issues"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "mtalhasahin",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"bin": {
|
|
27
|
+
"mcp-baggage": "dist/bin.js"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20.10"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"check": "tsc --noEmit",
|
|
43
|
+
"test": "node --test \"tests/*.test.ts\"",
|
|
44
|
+
"prepublishOnly": "npm run build"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^22.10.0",
|
|
48
|
+
"typescript": "^5.7.0"
|
|
49
|
+
}
|
|
50
|
+
}
|