@mcpspend/proxy 0.1.5 → 0.2.2
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 +51 -30
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +105 -12
- package/dist/clients.d.ts +62 -0
- package/dist/clients.js +226 -0
- package/dist/config.d.ts +9 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +17 -1
- package/dist/ingest.d.ts +26 -0
- package/dist/init.d.ts +45 -0
- package/dist/init.js +214 -0
- package/dist/proxy.d.ts +7 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,52 +1,70 @@
|
|
|
1
1
|
# @mcpspend/proxy
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
One-command observability for [MCP](https://modelcontextprotocol.io) (Model Context Protocol) servers.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Detects every MCP client installed on your machine — Claude Desktop, Cursor, Windsurf, VS Code, Claude Code — and transparently wraps each configured server so MCPSpend can attribute tool calls, latency and cost. The proxy never blocks the MCP wire: if our API is unreachable, your agent keeps working.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Zero-friction setup — no install needed
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
```sh
|
|
10
|
+
npx @mcpspend/proxy init --key mcps_live_xxx
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
That single command:
|
|
14
|
+
|
|
15
|
+
1. Saves your API key to `~/.mcpspend/config.json` (mode `0600`).
|
|
16
|
+
2. Discovers every supported MCP client config on your machine.
|
|
17
|
+
3. Rewrites each `mcpServers` entry to be wrapped by `npx -y @mcpspend/proxy wrap --` (so the moment you restart your MCP client, it auto-fetches the proxy if missing).
|
|
18
|
+
4. Leaves a `.mcpspend.bak` backup next to every file it touches.
|
|
19
|
+
|
|
20
|
+
> Prefer a global binary? `npm install -g @mcpspend/proxy` then use `mcpspend init` — everything below works the same way.
|
|
21
|
+
|
|
22
|
+
Restart your clients (Claude Desktop, Cursor, Windsurf, etc.) and head to [mcpspend.com](https://mcpspend.com) — calls start showing up within a minute.
|
|
23
|
+
|
|
24
|
+
Get an API key at [mcpspend.com/dashboard/keys](https://mcpspend.com/dashboard/keys).
|
|
25
|
+
|
|
26
|
+
## Verify
|
|
10
27
|
|
|
11
28
|
```sh
|
|
12
|
-
|
|
29
|
+
npx @mcpspend/proxy doctor
|
|
13
30
|
```
|
|
14
31
|
|
|
15
|
-
|
|
32
|
+
Reports API key status, endpoint reachability, and for every detected client: how many MCP servers are configured and how many are wrapped.
|
|
16
33
|
|
|
17
|
-
|
|
18
|
-
2. Configure once:
|
|
19
|
-
```sh
|
|
20
|
-
mcpspend config set apiKey mcps_live_xxx
|
|
21
|
-
```
|
|
22
|
-
3. Wrap any MCP server you already use:
|
|
23
|
-
```sh
|
|
24
|
-
mcpspend wrap -- npx @modelcontextprotocol/server-filesystem /path
|
|
25
|
-
```
|
|
34
|
+
## Undo
|
|
26
35
|
|
|
27
|
-
|
|
36
|
+
```sh
|
|
37
|
+
npx @mcpspend/proxy init --unwrap
|
|
38
|
+
```
|
|
28
39
|
|
|
29
|
-
|
|
40
|
+
Restores every wrapped entry to its original `command` + `args`. The `.mcpspend.bak` files are left in place for paranoia.
|
|
30
41
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
## Supported clients
|
|
43
|
+
|
|
44
|
+
| Client | Config path (detected automatically) |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Claude Desktop | `%APPDATA%\Claude\claude_desktop_config.json` · `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
47
|
+
| Cursor | `~/.cursor/mcp.json` |
|
|
48
|
+
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
|
|
49
|
+
| VS Code (user) | `%APPDATA%\Code\User\mcp.json` · `~/.config/Code/User/mcp.json` |
|
|
50
|
+
| Claude Code | `~/.claude.json` |
|
|
51
|
+
|
|
52
|
+
## Manual single-server wrap
|
|
53
|
+
|
|
54
|
+
If you want to wrap one specific invocation without touching client configs:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
npx @mcpspend/proxy wrap --key mcps_live_xxx -- npx @modelcontextprotocol/server-filesystem /data
|
|
41
58
|
```
|
|
42
59
|
|
|
43
|
-
|
|
60
|
+
Or paste this into a client config yourself:
|
|
61
|
+
|
|
44
62
|
```json
|
|
45
63
|
{
|
|
46
64
|
"mcpServers": {
|
|
47
65
|
"filesystem": {
|
|
48
|
-
"command": "
|
|
49
|
-
"args": ["
|
|
66
|
+
"command": "npx",
|
|
67
|
+
"args": ["-y", "@mcpspend/proxy", "wrap", "--", "npx", "@modelcontextprotocol/server-filesystem", "/Users/me"]
|
|
50
68
|
}
|
|
51
69
|
}
|
|
52
70
|
}
|
|
@@ -64,9 +82,12 @@ CLI flags, environment variables, and `~/.mcpspend/config.json` are merged in th
|
|
|
64
82
|
| Agent name | `--agent` | `MCPSPEND_AGENT_NAME` | `agentName` |
|
|
65
83
|
| Disable tracking | `--disable` | `MCPSPEND_DISABLED=1` | `disabled: true` |
|
|
66
84
|
|
|
85
|
+
The API key is **never** written into client config files (which often land in dotfile repos). It lives only in `~/.mcpspend/config.json` (mode `0600`) or `MCPSPEND_API_KEY`.
|
|
86
|
+
|
|
67
87
|
## Privacy
|
|
68
88
|
|
|
69
89
|
The proxy reports:
|
|
90
|
+
|
|
70
91
|
- Tool name (e.g. `read_file`)
|
|
71
92
|
- Server name (e.g. `filesystem`)
|
|
72
93
|
- Latency, success, error codes
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
CHANGED
|
@@ -3,37 +3,62 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const config_js_1 = require("./config.js");
|
|
5
5
|
const proxy_js_1 = require("./proxy.js");
|
|
6
|
-
const
|
|
6
|
+
const init_js_1 = require("./init.js");
|
|
7
|
+
const VERSION = '0.2.2';
|
|
7
8
|
const HELP = `mcpspend — observability proxy for MCP servers (v${VERSION})
|
|
8
9
|
|
|
9
10
|
USAGE
|
|
10
|
-
mcpspend
|
|
11
|
+
mcpspend init [options] Auto-detect MCP clients and wrap their servers
|
|
12
|
+
mcpspend doctor Diagnose setup (clients, API key, endpoint)
|
|
13
|
+
mcpspend wrap [options] -- <cmd>... Manually wrap a single MCP server invocation
|
|
11
14
|
mcpspend config set <key> <value>
|
|
12
15
|
mcpspend config show
|
|
13
|
-
mcpspend --version
|
|
14
|
-
|
|
16
|
+
mcpspend --version | --help
|
|
17
|
+
|
|
18
|
+
INIT OPTIONS
|
|
19
|
+
--key <value> Save API key to ~/.mcpspend/config.json before patching
|
|
20
|
+
--project <id> Attribute calls to this project (baked into wrapped args)
|
|
21
|
+
--endpoint <url> Override API endpoint (default: https://api.mcpspend.com)
|
|
22
|
+
--agent <name> Agent name reported in dashboards
|
|
23
|
+
--client <id> Only patch a specific client (repeatable). IDs:
|
|
24
|
+
claude-desktop, cursor, windsurf, vscode, claude-code
|
|
25
|
+
--dry-run Show what would change without writing
|
|
26
|
+
--unwrap Restore original (non-wrapped) MCP server configs
|
|
15
27
|
|
|
16
28
|
WRAP OPTIONS
|
|
17
29
|
--key <value> API key (overrides config + MCPSPEND_API_KEY)
|
|
18
|
-
--endpoint <url> API endpoint
|
|
30
|
+
--endpoint <url> API endpoint
|
|
19
31
|
--project <id> Attribute calls to this project
|
|
20
|
-
--agent <name> Agent name
|
|
32
|
+
--agent <name> Agent name
|
|
21
33
|
--model <name> Model name for cost attribution (default: mcp-stdio)
|
|
22
34
|
--disable Run in passthrough mode (no tracking)
|
|
23
35
|
|
|
24
36
|
EXAMPLES
|
|
25
|
-
#
|
|
26
|
-
mcpspend
|
|
37
|
+
# First-time setup: paste your API key, patch every installed MCP client
|
|
38
|
+
mcpspend init --key mcps_live_xxx
|
|
39
|
+
|
|
40
|
+
# See what init would change without writing
|
|
41
|
+
mcpspend init --dry-run
|
|
42
|
+
|
|
43
|
+
# Restore original configs (removes mcpspend wrapping)
|
|
44
|
+
mcpspend init --unwrap
|
|
45
|
+
|
|
46
|
+
# Only patch Cursor and Windsurf
|
|
47
|
+
mcpspend init --client cursor --client windsurf
|
|
27
48
|
|
|
28
|
-
#
|
|
29
|
-
mcpspend
|
|
30
|
-
mcpspend wrap -- npx @modelcontextprotocol/server-github
|
|
49
|
+
# Manual single-server wrap (no client config touched)
|
|
50
|
+
mcpspend wrap --key mcps_live_xxx -- npx @modelcontextprotocol/server-filesystem /data
|
|
31
51
|
|
|
32
52
|
Environment variables: MCPSPEND_API_KEY, MCPSPEND_ENDPOINT, MCPSPEND_PROJECT_ID, MCPSPEND_AGENT_NAME, MCPSPEND_DISABLED=1
|
|
33
53
|
Config file: ~/.mcpspend/config.json
|
|
34
54
|
`;
|
|
35
55
|
function parseArgs(argv) {
|
|
36
|
-
const result = {
|
|
56
|
+
const result = {
|
|
57
|
+
command: 'help',
|
|
58
|
+
wrapOpts: {},
|
|
59
|
+
initOpts: { clients: [], dryRun: false, unwrap: false },
|
|
60
|
+
childArgs: [],
|
|
61
|
+
};
|
|
37
62
|
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
38
63
|
result.command = 'help';
|
|
39
64
|
return result;
|
|
@@ -43,6 +68,52 @@ function parseArgs(argv) {
|
|
|
43
68
|
return result;
|
|
44
69
|
}
|
|
45
70
|
const cmd = argv[0];
|
|
71
|
+
if (cmd === 'init') {
|
|
72
|
+
result.command = 'init';
|
|
73
|
+
let i = 1;
|
|
74
|
+
while (i < argv.length) {
|
|
75
|
+
const a = argv[i];
|
|
76
|
+
const next = argv[i + 1];
|
|
77
|
+
switch (a) {
|
|
78
|
+
case '--key':
|
|
79
|
+
result.initOpts.apiKey = next;
|
|
80
|
+
i += 2;
|
|
81
|
+
break;
|
|
82
|
+
case '--project':
|
|
83
|
+
result.initOpts.projectId = next;
|
|
84
|
+
i += 2;
|
|
85
|
+
break;
|
|
86
|
+
case '--endpoint':
|
|
87
|
+
result.initOpts.endpoint = next;
|
|
88
|
+
i += 2;
|
|
89
|
+
break;
|
|
90
|
+
case '--agent':
|
|
91
|
+
result.initOpts.agentName = next;
|
|
92
|
+
i += 2;
|
|
93
|
+
break;
|
|
94
|
+
case '--client':
|
|
95
|
+
result.initOpts.clients.push(next);
|
|
96
|
+
i += 2;
|
|
97
|
+
break;
|
|
98
|
+
case '--dry-run':
|
|
99
|
+
result.initOpts.dryRun = true;
|
|
100
|
+
i += 1;
|
|
101
|
+
break;
|
|
102
|
+
case '--unwrap':
|
|
103
|
+
result.initOpts.unwrap = true;
|
|
104
|
+
i += 1;
|
|
105
|
+
break;
|
|
106
|
+
default:
|
|
107
|
+
process.stderr.write(`mcpspend: unknown option ${a}\n`);
|
|
108
|
+
process.exit(2);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
if (cmd === 'doctor') {
|
|
114
|
+
result.command = 'doctor';
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
46
117
|
if (cmd === 'wrap') {
|
|
47
118
|
result.command = 'wrap';
|
|
48
119
|
let i = 1;
|
|
@@ -124,6 +195,28 @@ async function main() {
|
|
|
124
195
|
process.stdout.write(`${VERSION}\n`);
|
|
125
196
|
return;
|
|
126
197
|
}
|
|
198
|
+
if (parsed.command === 'init') {
|
|
199
|
+
const report = (0, init_js_1.runInit)({
|
|
200
|
+
apiKey: parsed.initOpts.apiKey,
|
|
201
|
+
projectId: parsed.initOpts.projectId,
|
|
202
|
+
endpoint: parsed.initOpts.endpoint,
|
|
203
|
+
agentName: parsed.initOpts.agentName,
|
|
204
|
+
clientFilter: parsed.initOpts.clients.length ? parsed.initOpts.clients : undefined,
|
|
205
|
+
dryRun: parsed.initOpts.dryRun,
|
|
206
|
+
unwrap: parsed.initOpts.unwrap,
|
|
207
|
+
});
|
|
208
|
+
process.stdout.write((0, init_js_1.formatReport)(report, parsed.initOpts.unwrap) + '\n');
|
|
209
|
+
if (report.clients.some((c) => c.status === 'error'))
|
|
210
|
+
process.exit(1);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (parsed.command === 'doctor') {
|
|
214
|
+
const report = await (0, init_js_1.runDoctor)(VERSION);
|
|
215
|
+
process.stdout.write((0, init_js_1.formatDoctor)(report) + '\n');
|
|
216
|
+
if (!report.apiKeyConfigured || !report.endpointReachable)
|
|
217
|
+
process.exit(1);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
127
220
|
if (parsed.command === 'config') {
|
|
128
221
|
if (parsed.configAction === 'show') {
|
|
129
222
|
const cfg = (0, config_js_1.loadConfig)();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface McpServerEntry {
|
|
2
|
+
command: string;
|
|
3
|
+
args?: string[];
|
|
4
|
+
env?: Record<string, string>;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface ClientConfig {
|
|
8
|
+
mcpServers?: Record<string, McpServerEntry>;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
export interface ClientDefinition {
|
|
12
|
+
id: 'claude-desktop' | 'cursor' | 'windsurf' | 'vscode' | 'vscode-workspace' | 'claude-code';
|
|
13
|
+
name: string;
|
|
14
|
+
configPaths: () => string[];
|
|
15
|
+
serversKey?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const CLIENTS: ClientDefinition[];
|
|
18
|
+
export interface DiscoveredClient {
|
|
19
|
+
client: ClientDefinition;
|
|
20
|
+
path: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function discoverClients(): DiscoveredClient[];
|
|
23
|
+
export declare function readClientConfig(path: string): ClientConfig;
|
|
24
|
+
export declare function writeClientConfig(path: string, config: ClientConfig, backupSuffix?: string): string;
|
|
25
|
+
export interface WrapResult {
|
|
26
|
+
serverName: string;
|
|
27
|
+
status: 'wrapped' | 'already-wrapped' | 'skipped';
|
|
28
|
+
reason?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function isAlreadyWrapped(entry: McpServerEntry): boolean;
|
|
31
|
+
export interface WrapOptions {
|
|
32
|
+
apiKey?: string;
|
|
33
|
+
projectId?: string;
|
|
34
|
+
endpoint?: string;
|
|
35
|
+
agentName?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Wrap style:
|
|
38
|
+
* - 'npx' (default): emits `npx -y @mcpspend/proxy wrap -- <orig>`. Works without a global install.
|
|
39
|
+
* - 'bin': emits `mcpspend wrap -- <orig>`. Requires `npm i -g @mcpspend/proxy`.
|
|
40
|
+
*/
|
|
41
|
+
style?: 'npx' | 'bin';
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Wrap a single MCP server entry. Returns a NEW entry; does not mutate the original.
|
|
45
|
+
*
|
|
46
|
+
* - Keeps `env` untouched.
|
|
47
|
+
* - API key is NOT baked into args (would leak into git-tracked dotfiles). The proxy
|
|
48
|
+
* reads it from ~/.mcpspend/config.json (mode 0600) or MCPSPEND_API_KEY env.
|
|
49
|
+
*/
|
|
50
|
+
export declare function wrapEntry(entry: McpServerEntry, opts?: WrapOptions): McpServerEntry;
|
|
51
|
+
export declare function unwrapEntry(entry: McpServerEntry): McpServerEntry | null;
|
|
52
|
+
/**
|
|
53
|
+
* Apply wrapping to all MCP servers in a config. Returns the modified config + per-server results.
|
|
54
|
+
*/
|
|
55
|
+
export declare function wrapAllServers(config: ClientConfig, serversKey: string, opts?: WrapOptions): {
|
|
56
|
+
config: ClientConfig;
|
|
57
|
+
results: WrapResult[];
|
|
58
|
+
};
|
|
59
|
+
export declare function unwrapAllServers(config: ClientConfig, serversKey: string): {
|
|
60
|
+
config: ClientConfig;
|
|
61
|
+
results: WrapResult[];
|
|
62
|
+
};
|
package/dist/clients.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CLIENTS = void 0;
|
|
4
|
+
exports.discoverClients = discoverClients;
|
|
5
|
+
exports.readClientConfig = readClientConfig;
|
|
6
|
+
exports.writeClientConfig = writeClientConfig;
|
|
7
|
+
exports.isAlreadyWrapped = isAlreadyWrapped;
|
|
8
|
+
exports.wrapEntry = wrapEntry;
|
|
9
|
+
exports.unwrapEntry = unwrapEntry;
|
|
10
|
+
exports.wrapAllServers = wrapAllServers;
|
|
11
|
+
exports.unwrapAllServers = unwrapAllServers;
|
|
12
|
+
const node_fs_1 = require("node:fs");
|
|
13
|
+
const node_os_1 = require("node:os");
|
|
14
|
+
const node_path_1 = require("node:path");
|
|
15
|
+
const home = (0, node_os_1.homedir)();
|
|
16
|
+
const isWin = (0, node_os_1.platform)() === 'win32';
|
|
17
|
+
const isMac = (0, node_os_1.platform)() === 'darwin';
|
|
18
|
+
function appData() {
|
|
19
|
+
if (isWin)
|
|
20
|
+
return process.env.APPDATA || (0, node_path_1.join)(home, 'AppData', 'Roaming');
|
|
21
|
+
if (isMac)
|
|
22
|
+
return (0, node_path_1.join)(home, 'Library', 'Application Support');
|
|
23
|
+
return process.env.XDG_CONFIG_HOME || (0, node_path_1.join)(home, '.config');
|
|
24
|
+
}
|
|
25
|
+
exports.CLIENTS = [
|
|
26
|
+
{
|
|
27
|
+
id: 'claude-desktop',
|
|
28
|
+
name: 'Claude Desktop',
|
|
29
|
+
configPaths: () => [(0, node_path_1.join)(appData(), 'Claude', 'claude_desktop_config.json')],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'cursor',
|
|
33
|
+
name: 'Cursor',
|
|
34
|
+
configPaths: () => [(0, node_path_1.join)(home, '.cursor', 'mcp.json')],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'windsurf',
|
|
38
|
+
name: 'Windsurf',
|
|
39
|
+
configPaths: () => [
|
|
40
|
+
(0, node_path_1.join)(home, '.codeium', 'windsurf', 'mcp_config.json'),
|
|
41
|
+
(0, node_path_1.join)(home, '.codeium', 'windsurf-next', 'mcp_config.json'),
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'vscode',
|
|
46
|
+
name: 'VS Code (user)',
|
|
47
|
+
configPaths: () => {
|
|
48
|
+
const candidates = [];
|
|
49
|
+
if (isWin) {
|
|
50
|
+
candidates.push((0, node_path_1.join)(appData(), 'Code', 'User', 'mcp.json'));
|
|
51
|
+
}
|
|
52
|
+
else if (isMac) {
|
|
53
|
+
candidates.push((0, node_path_1.join)(home, 'Library', 'Application Support', 'Code', 'User', 'mcp.json'));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
candidates.push((0, node_path_1.join)(home, '.config', 'Code', 'User', 'mcp.json'));
|
|
57
|
+
}
|
|
58
|
+
return candidates;
|
|
59
|
+
},
|
|
60
|
+
serversKey: 'servers',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'claude-code',
|
|
64
|
+
name: 'Claude Code',
|
|
65
|
+
configPaths: () => [(0, node_path_1.join)(home, '.claude.json')],
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
function discoverClients() {
|
|
69
|
+
const found = [];
|
|
70
|
+
for (const c of exports.CLIENTS) {
|
|
71
|
+
for (const p of c.configPaths()) {
|
|
72
|
+
if ((0, node_fs_1.existsSync)(p)) {
|
|
73
|
+
found.push({ client: c, path: p });
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return found;
|
|
79
|
+
}
|
|
80
|
+
function readClientConfig(path) {
|
|
81
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
82
|
+
return {};
|
|
83
|
+
try {
|
|
84
|
+
const raw = (0, node_fs_1.readFileSync)(path, 'utf-8');
|
|
85
|
+
if (!raw.trim())
|
|
86
|
+
return {};
|
|
87
|
+
return JSON.parse(raw);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
throw new Error(`Failed to parse ${path}: ${err.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function writeClientConfig(path, config, backupSuffix = '.mcpspend.bak') {
|
|
94
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
|
|
95
|
+
let backupPath;
|
|
96
|
+
if ((0, node_fs_1.existsSync)(path)) {
|
|
97
|
+
backupPath = path + backupSuffix;
|
|
98
|
+
if (!(0, node_fs_1.existsSync)(backupPath)) {
|
|
99
|
+
(0, node_fs_1.copyFileSync)(path, backupPath);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
(0, node_fs_1.writeFileSync)(path, JSON.stringify(config, null, 2) + '\n');
|
|
103
|
+
return backupPath ?? '';
|
|
104
|
+
}
|
|
105
|
+
// We support two wrap shapes:
|
|
106
|
+
// - npx-first (default, recommended): command='npx', args=['-y','@mcpspend/proxy','wrap',...]
|
|
107
|
+
// No global install required — works the moment Node is on PATH.
|
|
108
|
+
// - bin-direct (legacy / for users who installed -g @mcpspend/proxy):
|
|
109
|
+
// command='mcpspend', args=['wrap',...]
|
|
110
|
+
// `isAlreadyWrapped` accepts BOTH shapes so we don't re-wrap when migrating from
|
|
111
|
+
// the old layout, and so users who manually pinned the global binary aren't disturbed.
|
|
112
|
+
const MCPSPEND_BIN_NAMES = new Set(['mcpspend', 'mcpspend.cmd', 'mcpspend.exe']);
|
|
113
|
+
const NPX_BIN_NAMES = new Set(['npx', 'npx.cmd', 'npx.exe']);
|
|
114
|
+
const PROXY_PKG = '@mcpspend/proxy';
|
|
115
|
+
// True if `pkgArg` is some form of @mcpspend/proxy: bare, @version, or @tag.
|
|
116
|
+
// '@mcpspend/proxy', '@mcpspend/proxy@0.2.1', '@mcpspend/proxy@latest' → true
|
|
117
|
+
function isProxyPackageArg(pkgArg) {
|
|
118
|
+
if (!pkgArg)
|
|
119
|
+
return false;
|
|
120
|
+
if (pkgArg === PROXY_PKG)
|
|
121
|
+
return true;
|
|
122
|
+
return pkgArg.startsWith(PROXY_PKG + '@');
|
|
123
|
+
}
|
|
124
|
+
function isAlreadyWrapped(entry) {
|
|
125
|
+
const cmd = (entry.command || '').toLowerCase();
|
|
126
|
+
const base = cmd.split(/[\\/]/).pop() || cmd;
|
|
127
|
+
const args = entry.args || [];
|
|
128
|
+
// Shape A: `mcpspend wrap ...`
|
|
129
|
+
if (MCPSPEND_BIN_NAMES.has(base) && args[0] === 'wrap')
|
|
130
|
+
return true;
|
|
131
|
+
// Shape B: `npx [-y] @mcpspend/proxy[@version] wrap ...`
|
|
132
|
+
if (NPX_BIN_NAMES.has(base)) {
|
|
133
|
+
const skipFlag = args[0] === '-y' || args[0] === '--yes' ? 1 : 0;
|
|
134
|
+
if (isProxyPackageArg(args[skipFlag]) && args[skipFlag + 1] === 'wrap')
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Find the position of the original command/args after the wrap prefix. Returns
|
|
141
|
+
* the index of the `--` separator, or -1 if the entry isn't wrapped.
|
|
142
|
+
*/
|
|
143
|
+
function findOriginalCommandStart(entry) {
|
|
144
|
+
if (!isAlreadyWrapped(entry))
|
|
145
|
+
return -1;
|
|
146
|
+
const args = entry.args || [];
|
|
147
|
+
const sepIdx = args.indexOf('--');
|
|
148
|
+
return sepIdx === -1 ? -1 : sepIdx;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Wrap a single MCP server entry. Returns a NEW entry; does not mutate the original.
|
|
152
|
+
*
|
|
153
|
+
* - Keeps `env` untouched.
|
|
154
|
+
* - API key is NOT baked into args (would leak into git-tracked dotfiles). The proxy
|
|
155
|
+
* reads it from ~/.mcpspend/config.json (mode 0600) or MCPSPEND_API_KEY env.
|
|
156
|
+
*/
|
|
157
|
+
function wrapEntry(entry, opts = {}) {
|
|
158
|
+
const style = opts.style || 'npx';
|
|
159
|
+
const wrapArgs = ['wrap'];
|
|
160
|
+
if (opts.endpoint)
|
|
161
|
+
wrapArgs.push('--endpoint', opts.endpoint);
|
|
162
|
+
if (opts.projectId)
|
|
163
|
+
wrapArgs.push('--project', opts.projectId);
|
|
164
|
+
if (opts.agentName)
|
|
165
|
+
wrapArgs.push('--agent', opts.agentName);
|
|
166
|
+
wrapArgs.push('--', entry.command, ...(entry.args || []));
|
|
167
|
+
if (style === 'bin') {
|
|
168
|
+
return { ...entry, command: 'mcpspend', args: wrapArgs };
|
|
169
|
+
}
|
|
170
|
+
// npx style — prefix wrapArgs with `-y @mcpspend/proxy`.
|
|
171
|
+
return { ...entry, command: 'npx', args: ['-y', PROXY_PKG, ...wrapArgs] };
|
|
172
|
+
}
|
|
173
|
+
function unwrapEntry(entry) {
|
|
174
|
+
const sepIdx = findOriginalCommandStart(entry);
|
|
175
|
+
if (sepIdx === -1)
|
|
176
|
+
return null;
|
|
177
|
+
const args = entry.args || [];
|
|
178
|
+
if (sepIdx + 1 >= args.length)
|
|
179
|
+
return null;
|
|
180
|
+
const restored = {
|
|
181
|
+
...entry,
|
|
182
|
+
command: args[sepIdx + 1],
|
|
183
|
+
args: args.slice(sepIdx + 2),
|
|
184
|
+
};
|
|
185
|
+
return restored;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Apply wrapping to all MCP servers in a config. Returns the modified config + per-server results.
|
|
189
|
+
*/
|
|
190
|
+
function wrapAllServers(config, serversKey, opts = {}) {
|
|
191
|
+
const results = [];
|
|
192
|
+
const servers = config[serversKey] || {};
|
|
193
|
+
const next = {};
|
|
194
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
195
|
+
if (!entry || typeof entry !== 'object' || !('command' in entry)) {
|
|
196
|
+
next[name] = entry;
|
|
197
|
+
results.push({ serverName: name, status: 'skipped', reason: 'no command field' });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (isAlreadyWrapped(entry)) {
|
|
201
|
+
next[name] = entry;
|
|
202
|
+
results.push({ serverName: name, status: 'already-wrapped' });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
next[name] = wrapEntry(entry, opts);
|
|
206
|
+
results.push({ serverName: name, status: 'wrapped' });
|
|
207
|
+
}
|
|
208
|
+
return { config: { ...config, [serversKey]: next }, results };
|
|
209
|
+
}
|
|
210
|
+
function unwrapAllServers(config, serversKey) {
|
|
211
|
+
const results = [];
|
|
212
|
+
const servers = config[serversKey] || {};
|
|
213
|
+
const next = {};
|
|
214
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
215
|
+
const restored = unwrapEntry(entry);
|
|
216
|
+
if (restored) {
|
|
217
|
+
next[name] = restored;
|
|
218
|
+
results.push({ serverName: name, status: 'wrapped', reason: 'restored to original' });
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
next[name] = entry;
|
|
222
|
+
results.push({ serverName: name, status: 'skipped', reason: 'not wrapped by mcpspend' });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { config: { ...config, [serversKey]: next }, results };
|
|
226
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
endpoint: string;
|
|
4
|
+
projectId?: string;
|
|
5
|
+
agentName?: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function loadConfig(cliOverrides?: Partial<Config>): Config;
|
|
9
|
+
export declare function saveConfig(updates: Partial<Config>): string;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { loadConfig, saveConfig } from './config.js';
|
|
2
|
+
export { runProxy } from './proxy.js';
|
|
3
|
+
export type { Config } from './config.js';
|
|
4
|
+
export type { ToolCallEvent } from './ingest.js';
|
|
5
|
+
export { CLIENTS, discoverClients, readClientConfig, writeClientConfig, wrapAllServers, unwrapAllServers, wrapEntry, unwrapEntry, isAlreadyWrapped, } from './clients.js';
|
|
6
|
+
export type { ClientDefinition, ClientConfig, DiscoveredClient, McpServerEntry, WrapResult, WrapOptions, } from './clients.js';
|
|
7
|
+
export { runInit, runDoctor, formatReport, formatDoctor } from './init.js';
|
|
8
|
+
export type { InitOptions, InitReport, ClientReport, DoctorReport } from './init.js';
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.runProxy = exports.saveConfig = exports.loadConfig = void 0;
|
|
3
|
+
exports.formatDoctor = exports.formatReport = exports.runDoctor = exports.runInit = exports.isAlreadyWrapped = exports.unwrapEntry = exports.wrapEntry = exports.unwrapAllServers = exports.wrapAllServers = exports.writeClientConfig = exports.readClientConfig = exports.discoverClients = exports.CLIENTS = exports.runProxy = exports.saveConfig = exports.loadConfig = void 0;
|
|
4
4
|
var config_js_1 = require("./config.js");
|
|
5
5
|
Object.defineProperty(exports, "loadConfig", { enumerable: true, get: function () { return config_js_1.loadConfig; } });
|
|
6
6
|
Object.defineProperty(exports, "saveConfig", { enumerable: true, get: function () { return config_js_1.saveConfig; } });
|
|
7
7
|
var proxy_js_1 = require("./proxy.js");
|
|
8
8
|
Object.defineProperty(exports, "runProxy", { enumerable: true, get: function () { return proxy_js_1.runProxy; } });
|
|
9
|
+
// Programmatic API for embedders (VS Code extension, etc.)
|
|
10
|
+
var clients_js_1 = require("./clients.js");
|
|
11
|
+
Object.defineProperty(exports, "CLIENTS", { enumerable: true, get: function () { return clients_js_1.CLIENTS; } });
|
|
12
|
+
Object.defineProperty(exports, "discoverClients", { enumerable: true, get: function () { return clients_js_1.discoverClients; } });
|
|
13
|
+
Object.defineProperty(exports, "readClientConfig", { enumerable: true, get: function () { return clients_js_1.readClientConfig; } });
|
|
14
|
+
Object.defineProperty(exports, "writeClientConfig", { enumerable: true, get: function () { return clients_js_1.writeClientConfig; } });
|
|
15
|
+
Object.defineProperty(exports, "wrapAllServers", { enumerable: true, get: function () { return clients_js_1.wrapAllServers; } });
|
|
16
|
+
Object.defineProperty(exports, "unwrapAllServers", { enumerable: true, get: function () { return clients_js_1.unwrapAllServers; } });
|
|
17
|
+
Object.defineProperty(exports, "wrapEntry", { enumerable: true, get: function () { return clients_js_1.wrapEntry; } });
|
|
18
|
+
Object.defineProperty(exports, "unwrapEntry", { enumerable: true, get: function () { return clients_js_1.unwrapEntry; } });
|
|
19
|
+
Object.defineProperty(exports, "isAlreadyWrapped", { enumerable: true, get: function () { return clients_js_1.isAlreadyWrapped; } });
|
|
20
|
+
var init_js_1 = require("./init.js");
|
|
21
|
+
Object.defineProperty(exports, "runInit", { enumerable: true, get: function () { return init_js_1.runInit; } });
|
|
22
|
+
Object.defineProperty(exports, "runDoctor", { enumerable: true, get: function () { return init_js_1.runDoctor; } });
|
|
23
|
+
Object.defineProperty(exports, "formatReport", { enumerable: true, get: function () { return init_js_1.formatReport; } });
|
|
24
|
+
Object.defineProperty(exports, "formatDoctor", { enumerable: true, get: function () { return init_js_1.formatDoctor; } });
|
package/dist/ingest.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Config } from './config.js';
|
|
2
|
+
export interface ToolCallEvent {
|
|
3
|
+
serverName: string;
|
|
4
|
+
toolName: string;
|
|
5
|
+
model: string;
|
|
6
|
+
inputTokens: number;
|
|
7
|
+
outputTokens: number;
|
|
8
|
+
latencyMs: number;
|
|
9
|
+
success: boolean;
|
|
10
|
+
errorCode?: string;
|
|
11
|
+
calledAt: string;
|
|
12
|
+
projectId?: string;
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class Ingest {
|
|
16
|
+
private cfg;
|
|
17
|
+
private queue;
|
|
18
|
+
private timer;
|
|
19
|
+
private inflight;
|
|
20
|
+
private dropped;
|
|
21
|
+
constructor(cfg: Config);
|
|
22
|
+
enqueue(event: ToolCallEvent): void;
|
|
23
|
+
private scheduleFlush;
|
|
24
|
+
flush(): Promise<void>;
|
|
25
|
+
shutdown(): Promise<void>;
|
|
26
|
+
}
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ClientDefinition, WrapResult } from './clients.js';
|
|
2
|
+
export interface InitOptions {
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
projectId?: string;
|
|
5
|
+
endpoint?: string;
|
|
6
|
+
agentName?: string;
|
|
7
|
+
clientFilter?: string[];
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
unwrap?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface ClientReport {
|
|
12
|
+
client: ClientDefinition['id'];
|
|
13
|
+
name: string;
|
|
14
|
+
path: string;
|
|
15
|
+
status: 'patched' | 'no-changes' | 'error' | 'dry-run';
|
|
16
|
+
backupPath?: string;
|
|
17
|
+
servers: WrapResult[];
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface InitReport {
|
|
21
|
+
apiKeyConfigured: boolean;
|
|
22
|
+
apiKeySource: 'flag' | 'env' | 'config' | 'none';
|
|
23
|
+
clientsFound: number;
|
|
24
|
+
clientsPatched: number;
|
|
25
|
+
clients: ClientReport[];
|
|
26
|
+
}
|
|
27
|
+
export declare function runInit(opts?: InitOptions): InitReport;
|
|
28
|
+
export declare function formatReport(report: InitReport, unwrap?: boolean): string;
|
|
29
|
+
export interface DoctorReport {
|
|
30
|
+
cliVersion: string;
|
|
31
|
+
apiKeyConfigured: boolean;
|
|
32
|
+
apiKeySource: 'env' | 'config' | 'none';
|
|
33
|
+
endpointReachable?: boolean;
|
|
34
|
+
endpointError?: string;
|
|
35
|
+
clients: Array<{
|
|
36
|
+
client: ClientDefinition['id'];
|
|
37
|
+
name: string;
|
|
38
|
+
path?: string;
|
|
39
|
+
detected: boolean;
|
|
40
|
+
serversCount: number;
|
|
41
|
+
wrappedCount: number;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
export declare function runDoctor(cliVersion: string): Promise<DoctorReport>;
|
|
45
|
+
export declare function formatDoctor(report: DoctorReport): string;
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runInit = runInit;
|
|
4
|
+
exports.formatReport = formatReport;
|
|
5
|
+
exports.runDoctor = runDoctor;
|
|
6
|
+
exports.formatDoctor = formatDoctor;
|
|
7
|
+
const clients_js_1 = require("./clients.js");
|
|
8
|
+
const config_js_1 = require("./config.js");
|
|
9
|
+
function runInit(opts = {}) {
|
|
10
|
+
if (opts.apiKey) {
|
|
11
|
+
(0, config_js_1.saveConfig)({ apiKey: opts.apiKey });
|
|
12
|
+
}
|
|
13
|
+
if (opts.endpoint) {
|
|
14
|
+
(0, config_js_1.saveConfig)({ endpoint: opts.endpoint });
|
|
15
|
+
}
|
|
16
|
+
const cfg = (0, config_js_1.loadConfig)();
|
|
17
|
+
const apiKeySource = opts.apiKey
|
|
18
|
+
? 'flag'
|
|
19
|
+
: process.env.MCPSPEND_API_KEY
|
|
20
|
+
? 'env'
|
|
21
|
+
: cfg.apiKey
|
|
22
|
+
? 'config'
|
|
23
|
+
: 'none';
|
|
24
|
+
let discovered = (0, clients_js_1.discoverClients)();
|
|
25
|
+
if (opts.clientFilter && opts.clientFilter.length) {
|
|
26
|
+
const set = new Set(opts.clientFilter);
|
|
27
|
+
discovered = discovered.filter((d) => set.has(d.client.id));
|
|
28
|
+
}
|
|
29
|
+
const clientReports = [];
|
|
30
|
+
for (const { client, path } of discovered) {
|
|
31
|
+
const serversKey = client.serversKey || 'mcpServers';
|
|
32
|
+
try {
|
|
33
|
+
const current = (0, clients_js_1.readClientConfig)(path);
|
|
34
|
+
const { config: next, results } = opts.unwrap
|
|
35
|
+
? (0, clients_js_1.unwrapAllServers)(current, serversKey)
|
|
36
|
+
: (0, clients_js_1.wrapAllServers)(current, serversKey, {
|
|
37
|
+
projectId: opts.projectId,
|
|
38
|
+
endpoint: opts.endpoint,
|
|
39
|
+
agentName: opts.agentName,
|
|
40
|
+
});
|
|
41
|
+
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
42
|
+
if (opts.dryRun) {
|
|
43
|
+
clientReports.push({
|
|
44
|
+
client: client.id,
|
|
45
|
+
name: client.name,
|
|
46
|
+
path,
|
|
47
|
+
status: 'dry-run',
|
|
48
|
+
servers: results,
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!changed) {
|
|
53
|
+
clientReports.push({
|
|
54
|
+
client: client.id,
|
|
55
|
+
name: client.name,
|
|
56
|
+
path,
|
|
57
|
+
status: 'no-changes',
|
|
58
|
+
servers: results,
|
|
59
|
+
});
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const backupPath = (0, clients_js_1.writeClientConfig)(path, next);
|
|
63
|
+
clientReports.push({
|
|
64
|
+
client: client.id,
|
|
65
|
+
name: client.name,
|
|
66
|
+
path,
|
|
67
|
+
status: 'patched',
|
|
68
|
+
backupPath: backupPath || undefined,
|
|
69
|
+
servers: results,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
clientReports.push({
|
|
74
|
+
client: client.id,
|
|
75
|
+
name: client.name,
|
|
76
|
+
path,
|
|
77
|
+
status: 'error',
|
|
78
|
+
servers: [],
|
|
79
|
+
error: err.message,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
apiKeyConfigured: !!cfg.apiKey || apiKeySource === 'env',
|
|
85
|
+
apiKeySource,
|
|
86
|
+
clientsFound: discovered.length,
|
|
87
|
+
clientsPatched: clientReports.filter((r) => r.status === 'patched').length,
|
|
88
|
+
clients: clientReports,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function formatReport(report, unwrap = false) {
|
|
92
|
+
const lines = [];
|
|
93
|
+
const action = unwrap ? 'Unwrap' : 'Wrap';
|
|
94
|
+
if (report.clientsFound === 0) {
|
|
95
|
+
lines.push('No supported MCP clients found on this machine.');
|
|
96
|
+
lines.push('');
|
|
97
|
+
lines.push('Looked for:');
|
|
98
|
+
for (const c of clients_js_1.CLIENTS) {
|
|
99
|
+
lines.push(` • ${c.name} — ${c.configPaths().join(', ')}`);
|
|
100
|
+
}
|
|
101
|
+
return lines.join('\n');
|
|
102
|
+
}
|
|
103
|
+
lines.push(`Found ${report.clientsFound} MCP client(s):`);
|
|
104
|
+
for (const r of report.clients) {
|
|
105
|
+
const tag = r.status === 'patched' ? '✓ patched'
|
|
106
|
+
: r.status === 'no-changes' ? '· no changes'
|
|
107
|
+
: r.status === 'dry-run' ? '∼ dry-run'
|
|
108
|
+
: `✗ error: ${r.error}`;
|
|
109
|
+
lines.push(` [${r.name}] ${tag}`);
|
|
110
|
+
lines.push(` ${r.path}`);
|
|
111
|
+
if (r.backupPath) {
|
|
112
|
+
lines.push(` backup: ${r.backupPath}`);
|
|
113
|
+
}
|
|
114
|
+
for (const s of r.servers) {
|
|
115
|
+
const sym = s.status === 'wrapped' ? '+'
|
|
116
|
+
: s.status === 'already-wrapped' ? '='
|
|
117
|
+
: '·';
|
|
118
|
+
const reason = s.reason ? ` (${s.reason})` : '';
|
|
119
|
+
lines.push(` ${sym} ${s.serverName}${reason}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
lines.push('');
|
|
123
|
+
if (!report.apiKeyConfigured && !unwrap) {
|
|
124
|
+
lines.push('⚠ No API key configured. Tracking will be disabled until you set one:');
|
|
125
|
+
lines.push(' mcpspend config set apiKey mcps_live_xxx');
|
|
126
|
+
lines.push(' Get a key at https://mcpspend.com/dashboard/keys');
|
|
127
|
+
}
|
|
128
|
+
else if (!unwrap) {
|
|
129
|
+
lines.push(`API key source: ${report.apiKeySource}`);
|
|
130
|
+
}
|
|
131
|
+
if (action === 'Wrap' && report.clientsPatched > 0) {
|
|
132
|
+
lines.push('');
|
|
133
|
+
lines.push('Restart your MCP clients (Claude Desktop, Cursor, etc.) to pick up the new config.');
|
|
134
|
+
}
|
|
135
|
+
return lines.join('\n');
|
|
136
|
+
}
|
|
137
|
+
async function runDoctor(cliVersion) {
|
|
138
|
+
const cfg = (0, config_js_1.loadConfig)();
|
|
139
|
+
const apiKeySource = process.env.MCPSPEND_API_KEY
|
|
140
|
+
? 'env'
|
|
141
|
+
: cfg.apiKey
|
|
142
|
+
? 'config'
|
|
143
|
+
: 'none';
|
|
144
|
+
const discovered = (0, clients_js_1.discoverClients)();
|
|
145
|
+
const discoveredById = new Map(discovered.map((d) => [d.client.id, d]));
|
|
146
|
+
const clients = clients_js_1.CLIENTS.map((c) => {
|
|
147
|
+
const found = discoveredById.get(c.id);
|
|
148
|
+
if (!found) {
|
|
149
|
+
return { client: c.id, name: c.name, detected: false, serversCount: 0, wrappedCount: 0 };
|
|
150
|
+
}
|
|
151
|
+
let serversCount = 0;
|
|
152
|
+
let wrappedCount = 0;
|
|
153
|
+
try {
|
|
154
|
+
const cfg = (0, clients_js_1.readClientConfig)(found.path);
|
|
155
|
+
const serversKey = c.serversKey || 'mcpServers';
|
|
156
|
+
const servers = cfg[serversKey] || {};
|
|
157
|
+
serversCount = Object.keys(servers).length;
|
|
158
|
+
for (const s of Object.values(servers)) {
|
|
159
|
+
if (s && typeof s.command === 'string' && (0, clients_js_1.isAlreadyWrapped)(s))
|
|
160
|
+
wrappedCount++;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch { }
|
|
164
|
+
return { client: c.id, name: c.name, path: found.path, detected: true, serversCount, wrappedCount };
|
|
165
|
+
});
|
|
166
|
+
let endpointReachable;
|
|
167
|
+
let endpointError;
|
|
168
|
+
try {
|
|
169
|
+
const url = (cfg.endpoint || 'https://api.mcpspend.com') + '/health';
|
|
170
|
+
const ac = new AbortController();
|
|
171
|
+
const timer = setTimeout(() => ac.abort(), 5000);
|
|
172
|
+
const resp = await fetch(url, { signal: ac.signal });
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
endpointReachable = resp.ok;
|
|
175
|
+
if (!resp.ok)
|
|
176
|
+
endpointError = `HTTP ${resp.status}`;
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
endpointReachable = false;
|
|
180
|
+
endpointError = err.message;
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
cliVersion,
|
|
184
|
+
apiKeyConfigured: apiKeySource !== 'none',
|
|
185
|
+
apiKeySource,
|
|
186
|
+
endpointReachable,
|
|
187
|
+
endpointError,
|
|
188
|
+
clients,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function formatDoctor(report) {
|
|
192
|
+
const lines = [];
|
|
193
|
+
lines.push(`mcpspend v${report.cliVersion}`);
|
|
194
|
+
lines.push('');
|
|
195
|
+
lines.push(`API key: ${report.apiKeyConfigured ? '✓' : '✗'} (${report.apiKeySource})`);
|
|
196
|
+
lines.push(`Endpoint: ${report.endpointReachable ? '✓ reachable' : `✗ ${report.endpointError || 'unreachable'}`}`);
|
|
197
|
+
lines.push('');
|
|
198
|
+
lines.push('Clients:');
|
|
199
|
+
for (const c of report.clients) {
|
|
200
|
+
if (!c.detected) {
|
|
201
|
+
lines.push(` · ${c.name} — not installed`);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const wrap = c.serversCount === 0 ? 'no servers configured' : `${c.wrappedCount}/${c.serversCount} wrapped`;
|
|
205
|
+
lines.push(` ✓ ${c.name} — ${wrap}`);
|
|
206
|
+
if (c.path)
|
|
207
|
+
lines.push(` ${c.path}`);
|
|
208
|
+
}
|
|
209
|
+
if (!report.apiKeyConfigured) {
|
|
210
|
+
lines.push('');
|
|
211
|
+
lines.push('Next: mcpspend config set apiKey mcps_live_xxx (get one at https://mcpspend.com/dashboard/keys)');
|
|
212
|
+
}
|
|
213
|
+
return lines.join('\n');
|
|
214
|
+
}
|
package/dist/proxy.d.ts
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcpspend/proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Transparent proxy CLI for MCP servers — tracks tool calls, latency, and cost via MCPSpend.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://mcpspend.com",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"mcpspend": "dist/cli.js"
|
|
23
23
|
},
|
|
24
24
|
"main": "dist/index.js",
|
|
25
|
+
"types": "dist/index.d.ts",
|
|
25
26
|
"files": [
|
|
26
27
|
"dist",
|
|
27
28
|
"README.md"
|