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.
Files changed (3) hide show
  1. package/README.md +57 -0
  2. package/index.js +190 -0
  3. 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
+ }