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/config.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the servers are declared, and what they are.
|
|
3
|
+
*
|
|
4
|
+
* Every client keeps its own file in its own shape, and most people have the
|
|
5
|
+
* same handful of servers in two or three of them. This finds the files, reads
|
|
6
|
+
* whichever exist, and folds identical servers together — a server is the same
|
|
7
|
+
* server when it is the same command or the same URL, whatever each config
|
|
8
|
+
* chose to call it.
|
|
9
|
+
*/
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { readFile } from 'node:fs/promises';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { parseToml } from "./toml.js";
|
|
14
|
+
/**
|
|
15
|
+
* The files worth looking at, in the order a report should mention them.
|
|
16
|
+
*
|
|
17
|
+
* Project files come after the user ones for each client so that a project
|
|
18
|
+
* entry overrides the machine-wide one of the same name, which is how every
|
|
19
|
+
* client resolves it.
|
|
20
|
+
*/
|
|
21
|
+
export function candidates(cwd, home = homedir()) {
|
|
22
|
+
return [
|
|
23
|
+
{ client: 'claude-code', scope: 'user', path: join(home, '.claude.json'), format: 'json' },
|
|
24
|
+
{ client: 'claude-code', scope: 'project', path: join(cwd, '.mcp.json'), format: 'json' },
|
|
25
|
+
{ client: 'cursor', scope: 'user', path: join(home, '.cursor', 'mcp.json'), format: 'json' },
|
|
26
|
+
{ client: 'cursor', scope: 'project', path: join(cwd, '.cursor', 'mcp.json'), format: 'json' },
|
|
27
|
+
{ client: 'vscode', scope: 'project', path: join(cwd, '.vscode', 'mcp.json'), format: 'json' },
|
|
28
|
+
{ client: 'windsurf', scope: 'user', path: join(home, '.codeium', 'windsurf', 'mcp_config.json'), format: 'json' },
|
|
29
|
+
{ client: 'codex', scope: 'user', path: join(home, '.codex', 'config.toml'), format: 'toml' },
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
const asRecord = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : {};
|
|
33
|
+
const asStrings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === 'string') : [];
|
|
34
|
+
const asStringMap = (v) => {
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const [k, val] of Object.entries(asRecord(v)))
|
|
37
|
+
if (typeof val === 'string')
|
|
38
|
+
out[k] = val;
|
|
39
|
+
return out;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* One entry from a config, whatever the client called its fields.
|
|
43
|
+
*
|
|
44
|
+
* `disabled: true`, `enabled: false` and Claude Code's `disabledMcpjsonServers`
|
|
45
|
+
* all mean the same thing and all end up here as `enabled: false` — a switched
|
|
46
|
+
* off server is still listed, because knowing what you would pay to switch it
|
|
47
|
+
* back on is half the question.
|
|
48
|
+
*/
|
|
49
|
+
function toSpec(name, raw, at, offSwitch) {
|
|
50
|
+
const entry = asRecord(raw);
|
|
51
|
+
const url = typeof entry['url'] === 'string' ? entry['url'] : undefined;
|
|
52
|
+
const command = typeof entry['command'] === 'string' ? entry['command'] : undefined;
|
|
53
|
+
if (!url && !command)
|
|
54
|
+
return null;
|
|
55
|
+
const declared = typeof entry['type'] === 'string' ? entry['type'] : undefined;
|
|
56
|
+
const transport = url || declared === 'http' || declared === 'sse' ? 'http' : 'stdio';
|
|
57
|
+
const enabled = entry['disabled'] !== true && entry['enabled'] !== false && !offSwitch.has(name);
|
|
58
|
+
const spec = {
|
|
59
|
+
name,
|
|
60
|
+
client: at.client,
|
|
61
|
+
source: at.path,
|
|
62
|
+
scope: at.scope,
|
|
63
|
+
transport,
|
|
64
|
+
enabled,
|
|
65
|
+
carriedBy: [at.client],
|
|
66
|
+
};
|
|
67
|
+
if (command)
|
|
68
|
+
spec.command = command;
|
|
69
|
+
const args = asStrings(entry['args']);
|
|
70
|
+
if (args.length)
|
|
71
|
+
spec.args = args;
|
|
72
|
+
const env = asStringMap(entry['env']);
|
|
73
|
+
if (Object.keys(env).length)
|
|
74
|
+
spec.env = env;
|
|
75
|
+
if (typeof entry['cwd'] === 'string')
|
|
76
|
+
spec.cwd = entry['cwd'];
|
|
77
|
+
if (url)
|
|
78
|
+
spec.url = url;
|
|
79
|
+
const headers = asStringMap(entry['headers']);
|
|
80
|
+
if (Object.keys(headers).length)
|
|
81
|
+
spec.headers = headers;
|
|
82
|
+
return spec;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The servers one file declares.
|
|
86
|
+
*
|
|
87
|
+
* Exported and taking text rather than a path so that it can be tested against
|
|
88
|
+
* every client's shape without a filesystem: `mcpServers` for Claude Code,
|
|
89
|
+
* Cursor and Windsurf, `servers` for VS Code, `mcp_servers` for Codex.
|
|
90
|
+
*/
|
|
91
|
+
export function parseConfig(text, at, cwd) {
|
|
92
|
+
let root;
|
|
93
|
+
try {
|
|
94
|
+
root = at.format === 'toml' ? parseToml(text) : asRecord(JSON.parse(text));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const offSwitch = new Set(asStrings(root['disabledMcpjsonServers']));
|
|
100
|
+
// Claude Code's user config keeps per-project servers under the project's own
|
|
101
|
+
// path, and those are the ones that apply when you are standing in it.
|
|
102
|
+
const scoped = cwd ? asRecord(asRecord(root['projects'])[cwd]) : {};
|
|
103
|
+
const tables = [root['mcpServers'], root['servers'], root['mcp_servers'], scoped['mcpServers']];
|
|
104
|
+
const specs = [];
|
|
105
|
+
for (const table of tables) {
|
|
106
|
+
for (const [name, raw] of Object.entries(asRecord(table))) {
|
|
107
|
+
const spec = toSpec(name, raw, at, offSwitch);
|
|
108
|
+
if (spec)
|
|
109
|
+
specs.push(spec);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return specs;
|
|
113
|
+
}
|
|
114
|
+
/** What makes two entries the same server: the thing that gets run, not the name. */
|
|
115
|
+
export function identity(spec) {
|
|
116
|
+
if (spec.transport === 'http')
|
|
117
|
+
return `http:${spec.url}`;
|
|
118
|
+
return `stdio:${spec.command} ${(spec.args ?? []).join(' ')}`.trim();
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Folds duplicates together.
|
|
122
|
+
*
|
|
123
|
+
* The first sighting wins the name and the file shown, later ones only add
|
|
124
|
+
* their client to `carriedBy` — except that a project-scoped entry replaces a
|
|
125
|
+
* user-scoped one, which is the override every client applies.
|
|
126
|
+
*/
|
|
127
|
+
export function merge(specs) {
|
|
128
|
+
const byIdentity = new Map();
|
|
129
|
+
for (const spec of specs) {
|
|
130
|
+
const id = identity(spec);
|
|
131
|
+
const held = byIdentity.get(id);
|
|
132
|
+
if (!held) {
|
|
133
|
+
byIdentity.set(id, { ...spec, carriedBy: [...spec.carriedBy] });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (!held.carriedBy.includes(spec.client))
|
|
137
|
+
held.carriedBy.push(spec.client);
|
|
138
|
+
if (spec.scope === 'project' && held.scope === 'user') {
|
|
139
|
+
byIdentity.set(id, { ...spec, carriedBy: held.carriedBy });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return [...byIdentity.values()];
|
|
143
|
+
}
|
|
144
|
+
/** Every server declared on this machine, for this directory. */
|
|
145
|
+
export async function discover(cwd, home) {
|
|
146
|
+
const files = [];
|
|
147
|
+
const found = [];
|
|
148
|
+
for (const at of candidates(cwd, home)) {
|
|
149
|
+
let text;
|
|
150
|
+
try {
|
|
151
|
+
text = await readFile(at.path, 'utf8');
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const specs = parseConfig(text, at, cwd);
|
|
157
|
+
if (specs.length)
|
|
158
|
+
files.push(at.path);
|
|
159
|
+
found.push(...specs);
|
|
160
|
+
}
|
|
161
|
+
return { specs: merge(found), files };
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/exact.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The exact count, when an estimate is not good enough.
|
|
3
|
+
*
|
|
4
|
+
* Anthropic's `count_tokens` endpoint takes the same `tools` array a real
|
|
5
|
+
* request would and answers with the number the model would actually be
|
|
6
|
+
* charged — framing, separators and all. It bills nothing and needs no
|
|
7
|
+
* inference, so `--exact` costs an API key and a second, not money.
|
|
8
|
+
*
|
|
9
|
+
* A baseline call with no tools is subtracted, because the endpoint counts the
|
|
10
|
+
* whole request and the question here is only what the tools added.
|
|
11
|
+
*/
|
|
12
|
+
import type { Inventory } from './types.ts';
|
|
13
|
+
/**
|
|
14
|
+
* Exact totals, one server at a time.
|
|
15
|
+
*
|
|
16
|
+
* Per server rather than per tool: a server with forty tools would be forty
|
|
17
|
+
* round trips for a breakdown nobody acts on, and the decision this informs —
|
|
18
|
+
* keep it or switch it off — is made at the server.
|
|
19
|
+
*
|
|
20
|
+
* Returns a map of server name to tokens. A server that errors is left out,
|
|
21
|
+
* and the caller keeps its estimate.
|
|
22
|
+
*/
|
|
23
|
+
export declare function exactly(inventories: readonly Inventory[], apiKey: string): Promise<Map<string, number>>;
|
package/dist/exact.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The exact count, when an estimate is not good enough.
|
|
3
|
+
*
|
|
4
|
+
* Anthropic's `count_tokens` endpoint takes the same `tools` array a real
|
|
5
|
+
* request would and answers with the number the model would actually be
|
|
6
|
+
* charged — framing, separators and all. It bills nothing and needs no
|
|
7
|
+
* inference, so `--exact` costs an API key and a second, not money.
|
|
8
|
+
*
|
|
9
|
+
* A baseline call with no tools is subtracted, because the endpoint counts the
|
|
10
|
+
* whole request and the question here is only what the tools added.
|
|
11
|
+
*/
|
|
12
|
+
import { qualify } from "./weigh.js";
|
|
13
|
+
const ENDPOINT = 'https://api.anthropic.com/v1/messages/count_tokens';
|
|
14
|
+
/** Any model prices the same count; this one is picked for being cheap to name. */
|
|
15
|
+
const MODEL = 'claude-sonnet-5';
|
|
16
|
+
const toApi = (tool, server) => ({
|
|
17
|
+
name: qualify(server, tool.name),
|
|
18
|
+
description: tool.description ?? '',
|
|
19
|
+
input_schema: tool.inputSchema ?? { type: 'object', properties: {} },
|
|
20
|
+
});
|
|
21
|
+
async function count(tools, apiKey) {
|
|
22
|
+
const response = await fetch(ENDPOINT, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: {
|
|
25
|
+
'content-type': 'application/json',
|
|
26
|
+
'x-api-key': apiKey,
|
|
27
|
+
'anthropic-version': '2023-06-01',
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
model: MODEL,
|
|
31
|
+
messages: [{ role: 'user', content: 'x' }],
|
|
32
|
+
...(tools.length ? { tools } : {}),
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(`count_tokens: HTTP ${response.status} ${await response.text().catch(() => '')}`.trim());
|
|
37
|
+
}
|
|
38
|
+
const body = (await response.json());
|
|
39
|
+
if (typeof body.input_tokens !== 'number')
|
|
40
|
+
throw new Error('count_tokens: no input_tokens in the answer');
|
|
41
|
+
return body.input_tokens;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Exact totals, one server at a time.
|
|
45
|
+
*
|
|
46
|
+
* Per server rather than per tool: a server with forty tools would be forty
|
|
47
|
+
* round trips for a breakdown nobody acts on, and the decision this informs —
|
|
48
|
+
* keep it or switch it off — is made at the server.
|
|
49
|
+
*
|
|
50
|
+
* Returns a map of server name to tokens. A server that errors is left out,
|
|
51
|
+
* and the caller keeps its estimate.
|
|
52
|
+
*/
|
|
53
|
+
export async function exactly(inventories, apiKey) {
|
|
54
|
+
const out = new Map();
|
|
55
|
+
const baseline = await count([], apiKey);
|
|
56
|
+
for (const inv of inventories) {
|
|
57
|
+
if (inv.error || inv.tools.length === 0)
|
|
58
|
+
continue;
|
|
59
|
+
try {
|
|
60
|
+
const withTools = await count(inv.tools.map((t) => toApi(t, inv.server.name)), apiKey);
|
|
61
|
+
out.set(inv.server.name, Math.max(0, withTools - baseline));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Keep the estimate for this one; one refused server is not a failed run.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=exact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exact.js","sourceRoot":"","sources":["../src/exact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAGrC,MAAM,QAAQ,GAAG,oDAAoD,CAAC;AAEtE,mFAAmF;AACnF,MAAM,KAAK,GAAG,iBAAiB,CAAC;AAIhC,MAAM,KAAK,GAAG,CAAC,IAAa,EAAE,MAAc,EAAW,EAAE,CAAC,CAAC;IACzD,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC;IAChC,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,EAAE;IACnC,YAAY,EAAE,IAAI,CAAC,WAAW,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE;CACrE,CAAC,CAAC;AAEH,KAAK,UAAU,KAAK,CAAC,KAAgB,EAAE,MAAc;IACnD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,QAAQ,EAAE;QACrC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,cAAc,EAAE,kBAAkB;YAClC,WAAW,EAAE,MAAM;YACnB,mBAAmB,EAAE,YAAY;SAClC;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,KAAK,EAAE,KAAK;YACZ,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;YAC1C,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACnC,CAAC;KACH,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,sBAAsB,QAAQ,CAAC,MAAM,IAAI,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;IAC3G,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA8B,CAAC;IAClE,IAAI,OAAO,IAAI,CAAC,YAAY,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IAC1G,OAAO,IAAI,CAAC,YAAY,CAAC;AAC3B,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,WAAiC,EAAE,MAAc;IAC7E,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IAEzC,KAAK,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;QAC9B,IAAI,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAClD,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,MAAM,KAAK,CAC3B,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAC/C,MAAM,CACP,CAAC;YACF,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC,CAAC,CAAC;QAC9D,CAAC;QAAC,MAAM,CAAC;YACP,0EAA0E;QAC5E,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The library, for anything that wants the figures without the table.
|
|
3
|
+
*
|
|
4
|
+
* A CI check that fails when a config grows past a budget is the obvious one,
|
|
5
|
+
* and it needs `scan()` and a number — not a report.
|
|
6
|
+
*/
|
|
7
|
+
import type { WeighedServer } from './types.ts';
|
|
8
|
+
export type ScanOptions = {
|
|
9
|
+
cwd?: string;
|
|
10
|
+
/** Days of transcripts to read for usage; `0` skips them and leaves usage unknown. */
|
|
11
|
+
days?: number;
|
|
12
|
+
timeout?: number;
|
|
13
|
+
/** Include servers the config has switched off. */
|
|
14
|
+
all?: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type Scan = {
|
|
17
|
+
servers: WeighedServer[];
|
|
18
|
+
/** Tokens every request carries, across every server that answered. */
|
|
19
|
+
tokens: number;
|
|
20
|
+
/** The config files that had something in them. */
|
|
21
|
+
files: string[];
|
|
22
|
+
};
|
|
23
|
+
/** Find every server, weigh it, and say what it costs. */
|
|
24
|
+
export declare function scan(options?: ScanOptions): Promise<Scan>;
|
|
25
|
+
export { candidates, discover, identity, merge, parseConfig } from './config.ts';
|
|
26
|
+
export { exactly } from './exact.ts';
|
|
27
|
+
export { inspect, inspectAll } from './mcp.ts';
|
|
28
|
+
export { asJson, breakdown, compact, DEFAULT_WINDOW, idleness, origin, report, shorten } from './report.ts';
|
|
29
|
+
export { estimate, TOOL_FRAMING } from './tokens.ts';
|
|
30
|
+
export { callsInLine, lookup, readUsage } from './usage.ts';
|
|
31
|
+
export { heaviestFirst, qualify, serialize, totalTokens, unused, weigh } from './weigh.ts';
|
|
32
|
+
export type * from './types.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The library, for anything that wants the figures without the table.
|
|
3
|
+
*
|
|
4
|
+
* A CI check that fails when a config grows past a budget is the obvious one,
|
|
5
|
+
* and it needs `scan()` and a number — not a report.
|
|
6
|
+
*/
|
|
7
|
+
import { discover } from "./config.js";
|
|
8
|
+
import { inspectAll } from "./mcp.js";
|
|
9
|
+
import { lookup, readUsage } from "./usage.js";
|
|
10
|
+
import { heaviestFirst, totalTokens, weigh } from "./weigh.js";
|
|
11
|
+
/** Find every server, weigh it, and say what it costs. */
|
|
12
|
+
export async function scan(options = {}) {
|
|
13
|
+
const cwd = options.cwd ?? process.cwd();
|
|
14
|
+
const { specs, files } = await discover(cwd);
|
|
15
|
+
const wanted = specs.filter((s) => options.all || s.enabled);
|
|
16
|
+
const inventories = await inspectAll(wanted, { timeout: options.timeout });
|
|
17
|
+
const usage = options.days === 0 ? null : await readUsage(options.days);
|
|
18
|
+
const callsOf = lookup(usage);
|
|
19
|
+
const servers = heaviestFirst(inventories.map((inv) => weigh(inv, callsOf)));
|
|
20
|
+
return { servers, tokens: totalTokens(servers.filter((s) => !s.error)), files };
|
|
21
|
+
}
|
|
22
|
+
export { candidates, discover, identity, merge, parseConfig } from "./config.js";
|
|
23
|
+
export { exactly } from "./exact.js";
|
|
24
|
+
export { inspect, inspectAll } from "./mcp.js";
|
|
25
|
+
export { asJson, breakdown, compact, DEFAULT_WINDOW, idleness, origin, report, shorten } from "./report.js";
|
|
26
|
+
export { estimate, TOOL_FRAMING } from "./tokens.js";
|
|
27
|
+
export { callsInLine, lookup, readUsage } from "./usage.js";
|
|
28
|
+
export { heaviestFirst, qualify, serialize, totalTokens, unused, weigh } from "./weigh.js";
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAoB/D,0DAA0D;AAC1D,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,UAAuB,EAAE;IAClD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IACzC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC;IAE7D,MAAM,WAAW,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAC3E,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAE9B,MAAM,OAAO,GAAG,aAAa,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;AAClF,CAAC;AAED,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC5G,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal MCP client — enough to ask a server what it carries, and no more.
|
|
3
|
+
*
|
|
4
|
+
* The official SDK would do this, and would also pull a dependency tree into a
|
|
5
|
+
* tool whose whole subject is weight. What is needed here is four messages:
|
|
6
|
+
* `initialize`, the `initialized` notification, and then the three listings.
|
|
7
|
+
* No tool is ever called, nothing is written, and the connection is closed as
|
|
8
|
+
* soon as the listing is in hand.
|
|
9
|
+
*
|
|
10
|
+
* Both transports are spoken because both are in people's configs: stdio,
|
|
11
|
+
* where the server is a process this spawns, and streamable HTTP, where it is
|
|
12
|
+
* a URL. A server that fails is reported as a failed row rather than taken as
|
|
13
|
+
* a reason to stop — one broken entry in a config should not cost the run.
|
|
14
|
+
*/
|
|
15
|
+
import type { Inventory, ServerSpec } from './types.ts';
|
|
16
|
+
/** How long one server gets, all in, before it is written off. */
|
|
17
|
+
export declare const DEFAULT_TIMEOUT = 20000;
|
|
18
|
+
/**
|
|
19
|
+
* How to start the process.
|
|
20
|
+
*
|
|
21
|
+
* Almost every server in a config is `npx` or `uvx`, which on Windows are
|
|
22
|
+
* `.cmd` shims that `CreateProcess` will not run — the shell has to. So on
|
|
23
|
+
* Windows the whole thing becomes one quoted command line handed to `cmd`,
|
|
24
|
+
* rather than a command plus an argument array: passing both is what Node
|
|
25
|
+
* deprecated, because the arguments would be concatenated unescaped.
|
|
26
|
+
*/
|
|
27
|
+
export declare function stdioOptions(spec: ServerSpec): {
|
|
28
|
+
command: string;
|
|
29
|
+
args: string[];
|
|
30
|
+
shell: boolean;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Ask one server what it carries.
|
|
34
|
+
*
|
|
35
|
+
* Never throws: a server that cannot be reached comes back as an inventory
|
|
36
|
+
* with an `error` and no tools, which is a row in the report like any other.
|
|
37
|
+
*/
|
|
38
|
+
export declare function inspect(spec: ServerSpec, timeout?: number): Promise<Inventory>;
|
|
39
|
+
/**
|
|
40
|
+
* Every server, a few at a time.
|
|
41
|
+
*
|
|
42
|
+
* Each one is a process to spawn, so they are not all started at once; four in
|
|
43
|
+
* flight keeps a machine with a dozen servers responsive and still finishes in
|
|
44
|
+
* about the time the slowest handful take.
|
|
45
|
+
*/
|
|
46
|
+
export declare function inspectAll(specs: readonly ServerSpec[], options?: {
|
|
47
|
+
timeout?: number;
|
|
48
|
+
lanes?: number;
|
|
49
|
+
onDone?: (inv: Inventory) => void;
|
|
50
|
+
}): Promise<Inventory[]>;
|