worthless-mcp 0.3.8
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 +57 -0
- package/index.js +190 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# worthless-mcp
|
|
2
|
+
|
|
3
|
+
Zero-Python install wrapper for the [Worthless](https://github.com/shacharm2/worthless) MCP server. Thin Node.js shim so Claude Code, Cursor, and Windsurf can auto-install Worthless's MCP tools straight from `.mcp.json` without requiring Python on the host.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Add to your project's `.mcp.json`:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"worthless": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "worthless-mcp"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Restart your editor. On first launch the wrapper:
|
|
21
|
+
|
|
22
|
+
1. Finds or installs [`uv`](https://docs.astral.sh/uv/) (one-time, ~5 s).
|
|
23
|
+
2. Runs `uvx worthless[mcp]==<pinned-version> mcp` — the Python env is cached by `uvx`, so subsequent starts are instant.
|
|
24
|
+
3. Streams MCP protocol over stdio to the editor.
|
|
25
|
+
|
|
26
|
+
Cold start on a fresh Node-only machine: **< 30 s**.
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- Node.js ≥ 18
|
|
31
|
+
- Network access on first run (to fetch `uv` and the Python package)
|
|
32
|
+
- macOS, Linux, or Windows
|
|
33
|
+
|
|
34
|
+
## MCP tools exposed
|
|
35
|
+
|
|
36
|
+
| Tool | Description |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `worthless_status()` | Proxy state, protected keys, spend caps |
|
|
39
|
+
| `worthless_lock(env_path)` | Split the real API key, rewrite `.env` with a shard |
|
|
40
|
+
| `worthless_scan(paths, deep)` | Find accidental key exposures in the tree |
|
|
41
|
+
| `worthless_spend(alias)` | Per-provider spend readout |
|
|
42
|
+
|
|
43
|
+
## Flags (for manual invocation)
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx worthless-mcp --version # print wrapper version
|
|
47
|
+
npx worthless-mcp --help # show help
|
|
48
|
+
npx worthless-mcp # launch MCP server on stdio
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Versioning
|
|
52
|
+
|
|
53
|
+
This npm package version is pinned 1:1 to the `worthless` PyPI package. The wrapper execs `uvx worthless[mcp]==<this-version> mcp`, so bumping one always bumps the other.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
AGPL-3.0 — same as the upstream Python package.
|
package/index.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* worthless-mcp — thin Node.js wrapper around `uvx worthless[mcp] mcp`.
|
|
6
|
+
*
|
|
7
|
+
* Bootstraps uv on first run if it is not already installed, then execs the
|
|
8
|
+
* MCP server over stdio. Claude Code / Cursor / Windsurf pick this up via:
|
|
9
|
+
*
|
|
10
|
+
* .mcp.json:
|
|
11
|
+
* { "mcpServers": { "worthless": { "command": "npx", "args": ["-y", "worthless-mcp"] } } }
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { spawnSync, spawn, execFileSync } = require('child_process');
|
|
15
|
+
const { existsSync } = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
|
|
19
|
+
// Pin the Python package version to the same version as this npm package so
|
|
20
|
+
// the two halves always ship together. uvx reuses a cached environment when
|
|
21
|
+
// the spec is identical, so subsequent launches are instant.
|
|
22
|
+
const { version: PKG_VERSION } = require('./package.json');
|
|
23
|
+
const PYTHON_SPEC = `worthless[mcp]==${PKG_VERSION}`;
|
|
24
|
+
|
|
25
|
+
const HOME_BIN = path.join(os.homedir(), '.local', 'bin');
|
|
26
|
+
const CARGO_BIN = path.join(os.homedir(), '.cargo', 'bin');
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Locate uvx
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
function findUvx() {
|
|
33
|
+
// Fast-path: known install locations added by the uv installer.
|
|
34
|
+
for (const dir of [HOME_BIN, CARGO_BIN]) {
|
|
35
|
+
const bin = path.join(dir, process.platform === 'win32' ? 'uvx.exe' : 'uvx');
|
|
36
|
+
if (existsSync(bin)) return bin;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Fall back to PATH lookup.
|
|
40
|
+
const which = process.platform === 'win32' ? 'where' : 'which';
|
|
41
|
+
const r = spawnSync(which, ['uvx'], { encoding: 'utf8' });
|
|
42
|
+
if (r.status === 0) {
|
|
43
|
+
return r.stdout.trim().split('\n')[0].trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Bootstrap uv (first-run only)
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fetch a URL synchronously using a child Node process so we do not need
|
|
55
|
+
* curl/wget at all. Follows one level of redirects (astral.sh -> GitHub).
|
|
56
|
+
*/
|
|
57
|
+
function fetchSync(url) {
|
|
58
|
+
const script = `
|
|
59
|
+
const https = require('https');
|
|
60
|
+
function get(u) {
|
|
61
|
+
https.get(u, (r) => {
|
|
62
|
+
if (r.statusCode >= 300 && r.statusCode < 400 && r.headers.location) {
|
|
63
|
+
return get(r.headers.location);
|
|
64
|
+
}
|
|
65
|
+
let data = '';
|
|
66
|
+
r.on('data', (c) => { data += c; });
|
|
67
|
+
r.on('end', () => { process.stdout.write(data); });
|
|
68
|
+
}).on('error', (e) => { process.stderr.write(e.message); process.exit(1); });
|
|
69
|
+
}
|
|
70
|
+
get(${JSON.stringify(url)});
|
|
71
|
+
`;
|
|
72
|
+
return execFileSync(process.execPath, ['--eval', script], {
|
|
73
|
+
encoding: 'utf8',
|
|
74
|
+
timeout: 30_000,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function bootstrapUv() {
|
|
79
|
+
const platform = process.platform;
|
|
80
|
+
log('uv not found — installing (one-time setup)...');
|
|
81
|
+
|
|
82
|
+
if (platform === 'win32') {
|
|
83
|
+
const r = spawnSync(
|
|
84
|
+
'powershell',
|
|
85
|
+
['-NoProfile', '-Command', 'irm https://astral.sh/uv/install.ps1 | iex'],
|
|
86
|
+
{ stdio: ['ignore', 'ignore', 'inherit'] }
|
|
87
|
+
);
|
|
88
|
+
if (r.status !== 0) throw new Error('uv Windows install failed');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// macOS / Linux — prefer curl if available, otherwise use Node https.
|
|
93
|
+
const hasCurl = spawnSync('curl', ['--version'], { encoding: 'utf8' }).status === 0;
|
|
94
|
+
let installScript;
|
|
95
|
+
if (hasCurl) {
|
|
96
|
+
const r = spawnSync(
|
|
97
|
+
'sh',
|
|
98
|
+
['-c', 'curl -LsSf https://astral.sh/uv/install.sh'],
|
|
99
|
+
{ encoding: 'utf8', timeout: 30_000 }
|
|
100
|
+
);
|
|
101
|
+
if (r.status !== 0) throw new Error('Failed to download uv installer');
|
|
102
|
+
installScript = r.stdout;
|
|
103
|
+
} else {
|
|
104
|
+
installScript = fetchSync('https://astral.sh/uv/install.sh');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const r = spawnSync('sh', ['-s', '--', '--no-modify-path'], {
|
|
108
|
+
input: installScript,
|
|
109
|
+
stdio: ['pipe', 'ignore', 'inherit'],
|
|
110
|
+
});
|
|
111
|
+
if (r.status !== 0) throw new Error('uv installer exited with ' + r.status);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Helpers
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
function log(msg) {
|
|
119
|
+
process.stderr.write('[worthless-mcp] ' + msg + '\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Main
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
function main() {
|
|
127
|
+
const arg = process.argv[2];
|
|
128
|
+
if (arg === '--version' || arg === '-v') {
|
|
129
|
+
process.stdout.write(PKG_VERSION + '\n');
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
if (arg === '--help' || arg === '-h') {
|
|
133
|
+
process.stdout.write(
|
|
134
|
+
'worthless-mcp ' + PKG_VERSION + '\n' +
|
|
135
|
+
'\n' +
|
|
136
|
+
'Thin npm wrapper that launches the Worthless MCP server.\n' +
|
|
137
|
+
'Intended to be invoked by editors via .mcp.json:\n' +
|
|
138
|
+
'\n' +
|
|
139
|
+
' { "command": "npx", "args": ["-y", "worthless-mcp"] }\n' +
|
|
140
|
+
'\n' +
|
|
141
|
+
'Flags:\n' +
|
|
142
|
+
' -v, --version Print wrapper version (matches the pinned Python package)\n' +
|
|
143
|
+
' -h, --help Show this message\n'
|
|
144
|
+
);
|
|
145
|
+
process.exit(0);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let uvx = findUvx();
|
|
149
|
+
|
|
150
|
+
if (!uvx) {
|
|
151
|
+
try {
|
|
152
|
+
bootstrapUv();
|
|
153
|
+
} catch (err) {
|
|
154
|
+
log('Failed to install uv: ' + err.message);
|
|
155
|
+
log('Install uv manually: https://docs.astral.sh/uv/getting-started/installation/');
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
uvx = findUvx();
|
|
159
|
+
if (!uvx) {
|
|
160
|
+
log('uvx still not found after install — try opening a new terminal.');
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
log('uv installed at ' + uvx);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Pass stdio through — MCP protocol runs over stdin/stdout.
|
|
167
|
+
const proc = spawn(uvx, [PYTHON_SPEC, 'mcp'], {
|
|
168
|
+
stdio: 'inherit',
|
|
169
|
+
shell: false,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
proc.on('error', (err) => {
|
|
173
|
+
log('spawn error: ' + err.message);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
proc.on('close', (code, signal) => {
|
|
178
|
+
if (signal) {
|
|
179
|
+
// uvx exited via signal — re-raise so the parent shell sees the correct exit reason.
|
|
180
|
+
process.kill(process.pid, signal);
|
|
181
|
+
// Fallback: SIGKILL cannot be caught; if kill() doesn't terminate us
|
|
182
|
+
// synchronously, exit non-zero rather than hanging.
|
|
183
|
+
setTimeout(() => process.exit(1), 500).unref();
|
|
184
|
+
} else {
|
|
185
|
+
process.exit(code ?? 0);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "worthless-mcp",
|
|
3
|
+
"version": "0.3.8",
|
|
4
|
+
"description": "MCP server for Worthless split-key API proxy — wraps `uvx worthless mcp`",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"api-keys",
|
|
8
|
+
"security",
|
|
9
|
+
"proxy",
|
|
10
|
+
"worthless",
|
|
11
|
+
"claude-code",
|
|
12
|
+
"cursor",
|
|
13
|
+
"windsurf"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/shacharm2/worthless",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/shacharm2/worthless.git",
|
|
19
|
+
"directory": "packages/worthless-mcp"
|
|
20
|
+
},
|
|
21
|
+
"license": "AGPL-3.0",
|
|
22
|
+
"main": "index.js",
|
|
23
|
+
"bin": {
|
|
24
|
+
"worthless-mcp": "./index.js"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"index.js",
|
|
31
|
+
"package.json",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"registry": "https://registry.npmjs.org/"
|
|
37
|
+
}
|
|
38
|
+
}
|