webtool-log-cleaner-mcp 1.0.0 → 1.0.1
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 +86 -0
- package/bin/install-mcp-clients.cjs +648 -0
- package/bin/webtool-log-cleaner-mcp.cjs +22 -2
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -68,9 +68,95 @@ Notes:
|
|
|
68
68
|
- The npm wrapper still needs Python 3.11+ installed.
|
|
69
69
|
- On first run, it bootstraps the Python package with `pip --user` if missing.
|
|
70
70
|
|
|
71
|
+
## Automatic MCP client installation
|
|
72
|
+
|
|
73
|
+
Use the installer to configure one or many clients automatically.
|
|
74
|
+
|
|
75
|
+
Interactive:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx -y webtool-log-cleaner-mcp install
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Non-interactive user-scope install:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx -y webtool-log-cleaner-mcp install --clients codex,cursor,claude-desktop
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Project-scope install for shareable repo config files:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx -y webtool-log-cleaner-mcp install --scope project --clients cursor,claude-code,vscode
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
List supported clients:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx -y webtool-log-cleaner-mcp install --list-clients
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
You can also use the dedicated global command after `npm i -g webtool-log-cleaner-mcp`:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
webtool-log-cleaner-install --clients all
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Current supported clients:
|
|
106
|
+
- `codex`
|
|
107
|
+
- `cursor`
|
|
108
|
+
- `claude-desktop`
|
|
109
|
+
- `claude-code`
|
|
110
|
+
- `gemini-cli`
|
|
111
|
+
- `qwen-code`
|
|
112
|
+
- `vscode`
|
|
113
|
+
|
|
114
|
+
Scope support:
|
|
115
|
+
- `user`: all listed clients
|
|
116
|
+
- `project`: `cursor`, `claude-code`, `vscode`
|
|
117
|
+
|
|
118
|
+
Project-scope outputs:
|
|
119
|
+
- Cursor: `.cursor/mcp.json`
|
|
120
|
+
- Claude Code: `.mcp.json`
|
|
121
|
+
- VS Code: `.vscode/mcp.json`
|
|
122
|
+
|
|
123
|
+
Notes:
|
|
124
|
+
- VS Code user installs use `code --add-mcp` when the CLI is available and otherwise write the user-level `mcp.json` file directly.
|
|
125
|
+
- Windows launchers use `cmd /c npx -y webtool-log-cleaner-mcp` so stdio clients can start the npm wrapper reliably.
|
|
126
|
+
|
|
127
|
+
## Shareable MCP Bundle
|
|
128
|
+
|
|
129
|
+
This repo also includes an MCP Bundle manifest so you can package the server into a single `.mcpb` artifact for one-click distribution in bundle-aware clients.
|
|
130
|
+
|
|
131
|
+
Build the bundle locally:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
python3 scripts/build_mcpb.py
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Or:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npm run bundle:mcpb
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Output:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
dist/webtool-log-cleaner-1.0.1.mcpb
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Bundle contents:
|
|
150
|
+
- `manifest.json`
|
|
151
|
+
- `pyproject.toml`
|
|
152
|
+
- `uv.lock`
|
|
153
|
+
- `README.md`
|
|
154
|
+
- `src/webtool_log_cleaner/**`
|
|
155
|
+
|
|
71
156
|
## Local development
|
|
72
157
|
|
|
73
158
|
```bash
|
|
159
|
+
node --test tests/*.test.cjs
|
|
74
160
|
uv run pytest -q
|
|
75
161
|
uv run webtool-log-cleaner
|
|
76
162
|
```
|
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const { spawnSync } = require("node:child_process");
|
|
7
|
+
const readline = require("node:readline/promises");
|
|
8
|
+
const { stdin, stdout } = require("node:process");
|
|
9
|
+
|
|
10
|
+
const SERVER_KEY = "webtool-log-cleaner";
|
|
11
|
+
const SERVER_PACKAGE = "webtool-log-cleaner-mcp";
|
|
12
|
+
const DEFAULT_SCOPE = "user";
|
|
13
|
+
|
|
14
|
+
const CLIENTS = [
|
|
15
|
+
{
|
|
16
|
+
id: "codex",
|
|
17
|
+
name: "OpenAI Codex CLI",
|
|
18
|
+
scopes: ["user"],
|
|
19
|
+
install: installCodex,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "cursor",
|
|
23
|
+
name: "Cursor",
|
|
24
|
+
scopes: ["user", "project"],
|
|
25
|
+
install: installCursor,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "claude-desktop",
|
|
29
|
+
name: "Claude Desktop",
|
|
30
|
+
scopes: ["user"],
|
|
31
|
+
install: installClaudeDesktop,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "claude-code",
|
|
35
|
+
name: "Claude Code",
|
|
36
|
+
scopes: ["user", "project"],
|
|
37
|
+
install: installClaudeCode,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "gemini-cli",
|
|
41
|
+
name: "Gemini CLI",
|
|
42
|
+
scopes: ["user"],
|
|
43
|
+
install: installGemini,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "qwen-code",
|
|
47
|
+
name: "Qwen Code",
|
|
48
|
+
scopes: ["user"],
|
|
49
|
+
install: installQwen,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "vscode",
|
|
53
|
+
name: "VS Code",
|
|
54
|
+
scopes: ["user", "project"],
|
|
55
|
+
install: installVsCode,
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
function log(message) {
|
|
60
|
+
stdout.write(`${message}\n`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function error(message) {
|
|
64
|
+
stdout.write(`[error] ${message}\n`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function expandHome(filePath) {
|
|
68
|
+
if (!filePath) {
|
|
69
|
+
return filePath;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (filePath.startsWith("~")) {
|
|
73
|
+
return path.join(os.homedir(), filePath.slice(1));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return filePath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ensureParentDirectory(targetPath) {
|
|
80
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveProjectPath(projectPath) {
|
|
84
|
+
const resolved = path.resolve(projectPath || process.cwd());
|
|
85
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
|
|
86
|
+
throw new Error(`project path must be an existing directory: ${resolved}`);
|
|
87
|
+
}
|
|
88
|
+
return resolved;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getLauncher(platform = process.platform) {
|
|
92
|
+
if (platform === "win32") {
|
|
93
|
+
return {
|
|
94
|
+
command: "cmd",
|
|
95
|
+
args: ["/c", "npx", "-y", SERVER_PACKAGE],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
command: "npx",
|
|
101
|
+
args: ["-y", SERVER_PACKAGE],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getStdioServerConfig(options = {}) {
|
|
106
|
+
const launcher = getLauncher(options.platform);
|
|
107
|
+
if (options.includeType) {
|
|
108
|
+
return {
|
|
109
|
+
type: "stdio",
|
|
110
|
+
...launcher,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return launcher;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function writeJsonConfig(targetPath, updater) {
|
|
117
|
+
const resolved = expandHome(targetPath);
|
|
118
|
+
let data = {};
|
|
119
|
+
|
|
120
|
+
if (fs.existsSync(resolved)) {
|
|
121
|
+
const raw = fs.readFileSync(resolved, "utf-8").trim();
|
|
122
|
+
if (raw) {
|
|
123
|
+
data = JSON.parse(raw);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const next = updater(data);
|
|
128
|
+
ensureParentDirectory(resolved);
|
|
129
|
+
fs.writeFileSync(resolved, `${JSON.stringify(next, null, 2)}\n`, "utf-8");
|
|
130
|
+
return resolved;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function upsertTomlSection(content, header, block) {
|
|
134
|
+
const lines = content.length > 0 ? content.split(/\r?\n/) : [];
|
|
135
|
+
const startIndex = lines.findIndex((line) => line.trim() === header);
|
|
136
|
+
|
|
137
|
+
if (startIndex >= 0) {
|
|
138
|
+
let endIndex = lines.length;
|
|
139
|
+
for (let i = startIndex + 1; i < lines.length; i += 1) {
|
|
140
|
+
if (lines[i].trim().startsWith("[")) {
|
|
141
|
+
endIndex = i;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return [
|
|
147
|
+
...lines.slice(0, startIndex),
|
|
148
|
+
...block.split("\n"),
|
|
149
|
+
...lines.slice(endIndex),
|
|
150
|
+
].join("\n");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let next = content;
|
|
154
|
+
if (next.trim().length > 0 && !next.endsWith("\n")) {
|
|
155
|
+
next += "\n";
|
|
156
|
+
}
|
|
157
|
+
if (next.trim().length > 0) {
|
|
158
|
+
next += "\n";
|
|
159
|
+
}
|
|
160
|
+
next += `${block}\n`;
|
|
161
|
+
return next;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getVsCodeUserConfigPath(platform = process.platform, env = process.env) {
|
|
165
|
+
if (platform === "darwin") {
|
|
166
|
+
return "~/Library/Application Support/Code/User/mcp.json";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (platform === "win32") {
|
|
170
|
+
const appData = env.APPDATA;
|
|
171
|
+
if (!appData) {
|
|
172
|
+
throw new Error("APPDATA is not set; cannot resolve VS Code MCP config path.");
|
|
173
|
+
}
|
|
174
|
+
return path.win32.join(appData, "Code", "User", "mcp.json");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return "~/.config/Code/User/mcp.json";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function getProjectClientConfigPath(clientId, projectPath) {
|
|
181
|
+
if (clientId === "cursor") {
|
|
182
|
+
return path.join(projectPath, ".cursor", "mcp.json");
|
|
183
|
+
}
|
|
184
|
+
if (clientId === "claude-code") {
|
|
185
|
+
return path.join(projectPath, ".mcp.json");
|
|
186
|
+
}
|
|
187
|
+
if (clientId === "vscode") {
|
|
188
|
+
return path.join(projectPath, ".vscode", "mcp.json");
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function unsupportedScope(client, scope) {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
message: `${client.name} only supports ${client.scopes.join("/")} installs.`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function installCursor(options) {
|
|
201
|
+
const target = options.scope === "project"
|
|
202
|
+
? getProjectClientConfigPath("cursor", options.projectPath)
|
|
203
|
+
: "~/.cursor/mcp.json";
|
|
204
|
+
const serverConfig = getStdioServerConfig({ includeType: true });
|
|
205
|
+
|
|
206
|
+
const location = writeJsonConfig(target, (config) => {
|
|
207
|
+
const next = { ...(config || {}) };
|
|
208
|
+
next.mcpServers = { ...(next.mcpServers || {}) };
|
|
209
|
+
next.mcpServers[SERVER_KEY] = serverConfig;
|
|
210
|
+
return next;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return { ok: true, message: `Updated ${location}` };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function installClaudeDesktop(options) {
|
|
217
|
+
if (options.scope !== "user") {
|
|
218
|
+
return unsupportedScope(CLIENTS.find((client) => client.id === "claude-desktop"), options.scope);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let target;
|
|
222
|
+
if (process.platform === "darwin") {
|
|
223
|
+
target = "~/Library/Application Support/Claude/claude_desktop_config.json";
|
|
224
|
+
} else if (process.platform === "win32") {
|
|
225
|
+
const appData = process.env.APPDATA;
|
|
226
|
+
if (!appData) {
|
|
227
|
+
return { ok: false, message: "APPDATA is not set; cannot resolve Claude Desktop config path." };
|
|
228
|
+
}
|
|
229
|
+
target = path.join(appData, "Claude", "claude_desktop_config.json");
|
|
230
|
+
} else {
|
|
231
|
+
target = "~/.config/Claude/claude_desktop_config.json";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const serverConfig = getStdioServerConfig();
|
|
235
|
+
const location = writeJsonConfig(target, (config) => {
|
|
236
|
+
const next = { ...(config || {}) };
|
|
237
|
+
next.mcpServers = { ...(next.mcpServers || {}) };
|
|
238
|
+
next.mcpServers[SERVER_KEY] = serverConfig;
|
|
239
|
+
return next;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return { ok: true, message: `Updated ${location}` };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function installGemini(options) {
|
|
246
|
+
if (options.scope !== "user") {
|
|
247
|
+
return unsupportedScope(CLIENTS.find((client) => client.id === "gemini-cli"), options.scope);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const serverConfig = getStdioServerConfig();
|
|
251
|
+
const location = writeJsonConfig("~/.gemini/settings.json", (config) => {
|
|
252
|
+
const next = { ...(config || {}) };
|
|
253
|
+
next.mcpServers = { ...(next.mcpServers || {}) };
|
|
254
|
+
next.mcpServers[SERVER_KEY] = serverConfig;
|
|
255
|
+
return next;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return { ok: true, message: `Updated ${location}` };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function installQwen(options) {
|
|
262
|
+
if (options.scope !== "user") {
|
|
263
|
+
return unsupportedScope(CLIENTS.find((client) => client.id === "qwen-code"), options.scope);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const serverConfig = getStdioServerConfig();
|
|
267
|
+
const location = writeJsonConfig("~/.qwen/settings.json", (config) => {
|
|
268
|
+
const next = { ...(config || {}) };
|
|
269
|
+
next.mcpServers = { ...(next.mcpServers || {}) };
|
|
270
|
+
next.mcpServers[SERVER_KEY] = serverConfig;
|
|
271
|
+
return next;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return { ok: true, message: `Updated ${location}` };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function installVsCodeProject(options) {
|
|
278
|
+
const target = getProjectClientConfigPath("vscode", options.projectPath);
|
|
279
|
+
const serverConfig = getStdioServerConfig({ includeType: true });
|
|
280
|
+
|
|
281
|
+
const location = writeJsonConfig(target, (config) => {
|
|
282
|
+
const next = { ...(config || {}) };
|
|
283
|
+
next.servers = { ...(next.servers || {}) };
|
|
284
|
+
next.servers[SERVER_KEY] = serverConfig;
|
|
285
|
+
return next;
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
return { ok: true, message: `Updated ${location}` };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function installVsCodeUser() {
|
|
292
|
+
const serverConfig = {
|
|
293
|
+
name: SERVER_KEY,
|
|
294
|
+
...getStdioServerConfig({ includeType: true }),
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const cliResult = spawnSync("code", ["--add-mcp", JSON.stringify(serverConfig)], {
|
|
298
|
+
stdio: "inherit",
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (!cliResult.error && cliResult.status === 0) {
|
|
302
|
+
return { ok: true, message: "Added server via `code --add-mcp ...`" };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (cliResult.error && cliResult.error.code !== "ENOENT") {
|
|
306
|
+
return {
|
|
307
|
+
ok: false,
|
|
308
|
+
message: `VS Code CLI failed: ${cliResult.error.message}`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let target;
|
|
313
|
+
try {
|
|
314
|
+
target = getVsCodeUserConfigPath();
|
|
315
|
+
} catch (err) {
|
|
316
|
+
return { ok: false, message: err.message };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const location = writeJsonConfig(target, (config) => {
|
|
320
|
+
const next = { ...(config || {}) };
|
|
321
|
+
next.servers = { ...(next.servers || {}) };
|
|
322
|
+
next.servers[SERVER_KEY] = getStdioServerConfig({ includeType: true });
|
|
323
|
+
return next;
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return { ok: true, message: `Updated ${location}` };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function installVsCode(options) {
|
|
330
|
+
if (options.scope === "project") {
|
|
331
|
+
return installVsCodeProject(options);
|
|
332
|
+
}
|
|
333
|
+
return installVsCodeUser();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function installCodex(options) {
|
|
337
|
+
if (options.scope !== "user") {
|
|
338
|
+
return unsupportedScope(CLIENTS.find((client) => client.id === "codex"), options.scope);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const target = expandHome("~/.codex/config.toml");
|
|
342
|
+
ensureParentDirectory(target);
|
|
343
|
+
|
|
344
|
+
const launcher = getLauncher();
|
|
345
|
+
const header = `[mcp_servers.${SERVER_KEY}]`;
|
|
346
|
+
const block = [
|
|
347
|
+
header,
|
|
348
|
+
`command = "${launcher.command}"`,
|
|
349
|
+
`args = [${launcher.args.map((value) => JSON.stringify(value)).join(", ")}]`,
|
|
350
|
+
"startup_timeout_ms = 40_000",
|
|
351
|
+
].join("\n");
|
|
352
|
+
|
|
353
|
+
let content = "";
|
|
354
|
+
if (fs.existsSync(target)) {
|
|
355
|
+
content = fs.readFileSync(target, "utf-8");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let nextContent = upsertTomlSection(content, header, block);
|
|
359
|
+
if (!nextContent.endsWith("\n")) {
|
|
360
|
+
nextContent += "\n";
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
fs.writeFileSync(target, nextContent, "utf-8");
|
|
364
|
+
return { ok: true, message: `Updated ${target}` };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function installClaudeCodeUser() {
|
|
368
|
+
const launcher = getLauncher();
|
|
369
|
+
const command = [
|
|
370
|
+
"mcp",
|
|
371
|
+
"add",
|
|
372
|
+
"--scope",
|
|
373
|
+
"user",
|
|
374
|
+
SERVER_KEY,
|
|
375
|
+
"--",
|
|
376
|
+
launcher.command,
|
|
377
|
+
...launcher.args,
|
|
378
|
+
];
|
|
379
|
+
|
|
380
|
+
const result = spawnSync("claude", command, {
|
|
381
|
+
stdio: "inherit",
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
if (result.error) {
|
|
385
|
+
if (result.error.code === "ENOENT") {
|
|
386
|
+
return {
|
|
387
|
+
ok: false,
|
|
388
|
+
message: "`claude` CLI not found. Install Claude Code CLI or configure manually.",
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
ok: false,
|
|
394
|
+
message: `Claude Code CLI failed: ${result.error.message}`,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (result.status !== 0) {
|
|
399
|
+
return {
|
|
400
|
+
ok: false,
|
|
401
|
+
message: "`claude mcp add --scope user ...` failed. Run it manually to inspect CLI output.",
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return { ok: true, message: "Added server via `claude mcp add --scope user ...`" };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function installClaudeCodeProject(options) {
|
|
409
|
+
const target = getProjectClientConfigPath("claude-code", options.projectPath);
|
|
410
|
+
const serverConfig = getStdioServerConfig();
|
|
411
|
+
|
|
412
|
+
const location = writeJsonConfig(target, (config) => {
|
|
413
|
+
const next = { ...(config || {}) };
|
|
414
|
+
next.mcpServers = { ...(next.mcpServers || {}) };
|
|
415
|
+
next.mcpServers[SERVER_KEY] = serverConfig;
|
|
416
|
+
return next;
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
return { ok: true, message: `Updated ${location}` };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function installClaudeCode(options) {
|
|
423
|
+
if (options.scope === "project") {
|
|
424
|
+
return installClaudeCodeProject(options);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return installClaudeCodeUser();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function parseClientSelection(raw) {
|
|
431
|
+
const input = (raw || "").trim();
|
|
432
|
+
if (!input || input.toLowerCase() === "all") {
|
|
433
|
+
return CLIENTS.map((client) => client.id);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const tokens = input
|
|
437
|
+
.split(/[\s,]+/)
|
|
438
|
+
.map((token) => token.trim())
|
|
439
|
+
.filter(Boolean);
|
|
440
|
+
|
|
441
|
+
const ids = new Set();
|
|
442
|
+
for (const token of tokens) {
|
|
443
|
+
const maybeIndex = Number(token);
|
|
444
|
+
if (Number.isInteger(maybeIndex) && maybeIndex >= 1 && maybeIndex <= CLIENTS.length) {
|
|
445
|
+
ids.add(CLIENTS[maybeIndex - 1].id);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const byId = CLIENTS.find((client) => client.id === token);
|
|
450
|
+
if (byId) {
|
|
451
|
+
ids.add(byId.id);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
throw new Error(`Unknown client selector: ${token}`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return Array.from(ids);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function parseArgs(argv) {
|
|
462
|
+
const parsed = {
|
|
463
|
+
list: false,
|
|
464
|
+
clients: null,
|
|
465
|
+
projectPath: process.cwd(),
|
|
466
|
+
scope: DEFAULT_SCOPE,
|
|
467
|
+
yes: false,
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
471
|
+
const arg = argv[index];
|
|
472
|
+
|
|
473
|
+
if (arg === "--list-clients") {
|
|
474
|
+
parsed.list = true;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (arg === "--yes" || arg === "-y") {
|
|
479
|
+
parsed.yes = true;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (arg.startsWith("--clients=")) {
|
|
484
|
+
parsed.clients = arg.split("=", 2)[1] || "";
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (arg === "--clients") {
|
|
489
|
+
parsed.clients = argv[index + 1] || "";
|
|
490
|
+
index += 1;
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (arg.startsWith("--scope=")) {
|
|
495
|
+
parsed.scope = arg.split("=", 2)[1] || "";
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (arg === "--scope") {
|
|
500
|
+
parsed.scope = argv[index + 1] || "";
|
|
501
|
+
index += 1;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (arg.startsWith("--project-path=")) {
|
|
506
|
+
parsed.projectPath = arg.split("=", 2)[1] || "";
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (arg === "--project-path") {
|
|
511
|
+
parsed.projectPath = argv[index + 1] || "";
|
|
512
|
+
index += 1;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (!["user", "project"].includes(parsed.scope)) {
|
|
520
|
+
throw new Error(`Unknown scope: ${parsed.scope}`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return parsed;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function printClients() {
|
|
527
|
+
log("Supported clients:");
|
|
528
|
+
CLIENTS.forEach((client, idx) => {
|
|
529
|
+
log(`${idx + 1}. ${client.name} (${client.id}) [${client.scopes.join("/")}]`);
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function promptForClients() {
|
|
534
|
+
printClients();
|
|
535
|
+
log("");
|
|
536
|
+
log("Select one, many, or all clients.");
|
|
537
|
+
log("Examples: `1`, `1,3,5`, `codex,cursor`, or `all`");
|
|
538
|
+
|
|
539
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
540
|
+
try {
|
|
541
|
+
return await rl.question("Clients to configure: ");
|
|
542
|
+
} finally {
|
|
543
|
+
rl.close();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function runInstall(argv = []) {
|
|
548
|
+
let parsed;
|
|
549
|
+
try {
|
|
550
|
+
parsed = parseArgs(argv);
|
|
551
|
+
} catch (err) {
|
|
552
|
+
error(err.message);
|
|
553
|
+
return 1;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (parsed.list) {
|
|
557
|
+
printClients();
|
|
558
|
+
return 0;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
let selectedRaw = parsed.clients;
|
|
562
|
+
if (!selectedRaw && !parsed.yes) {
|
|
563
|
+
selectedRaw = await promptForClients();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (!selectedRaw && parsed.yes) {
|
|
567
|
+
selectedRaw = "all";
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
let selectedIds;
|
|
571
|
+
try {
|
|
572
|
+
selectedIds = parseClientSelection(selectedRaw || "all");
|
|
573
|
+
} catch (err) {
|
|
574
|
+
error(err.message);
|
|
575
|
+
return 1;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const selectedClients = CLIENTS.filter((client) => selectedIds.includes(client.id));
|
|
579
|
+
if (selectedClients.length === 0) {
|
|
580
|
+
error("No clients selected.");
|
|
581
|
+
return 1;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
let projectPath;
|
|
585
|
+
try {
|
|
586
|
+
projectPath = resolveProjectPath(parsed.projectPath);
|
|
587
|
+
} catch (err) {
|
|
588
|
+
error(err.message);
|
|
589
|
+
return 1;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
log(
|
|
593
|
+
`Installing ${SERVER_KEY} MCP config into ${selectedClients.length} client(s) ` +
|
|
594
|
+
`with scope=${parsed.scope}...`
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
let failures = 0;
|
|
598
|
+
for (const client of selectedClients) {
|
|
599
|
+
try {
|
|
600
|
+
if (!client.scopes.includes(parsed.scope)) {
|
|
601
|
+
failures += 1;
|
|
602
|
+
log(`- ${client.name}: FAILED (${client.name} does not support scope=${parsed.scope})`);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const result = client.install({
|
|
607
|
+
projectPath,
|
|
608
|
+
scope: parsed.scope,
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
if (result.ok) {
|
|
612
|
+
log(`- ${client.name}: OK (${result.message})`);
|
|
613
|
+
} else {
|
|
614
|
+
failures += 1;
|
|
615
|
+
log(`- ${client.name}: FAILED (${result.message})`);
|
|
616
|
+
}
|
|
617
|
+
} catch (err) {
|
|
618
|
+
failures += 1;
|
|
619
|
+
log(`- ${client.name}: FAILED (${err.message})`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (failures > 0) {
|
|
624
|
+
error(`${failures} client install(s) failed.`);
|
|
625
|
+
return 1;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
log("All selected clients updated.");
|
|
629
|
+
return 0;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (require.main === module) {
|
|
633
|
+
runInstall(process.argv.slice(2)).then((code) => {
|
|
634
|
+
process.exit(code);
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
module.exports = {
|
|
639
|
+
CLIENTS,
|
|
640
|
+
getLauncher,
|
|
641
|
+
getProjectClientConfigPath,
|
|
642
|
+
getStdioServerConfig,
|
|
643
|
+
getVsCodeUserConfigPath,
|
|
644
|
+
parseArgs,
|
|
645
|
+
parseClientSelection,
|
|
646
|
+
runInstall,
|
|
647
|
+
upsertTomlSection,
|
|
648
|
+
};
|
|
@@ -59,9 +59,9 @@ function selectPython() {
|
|
|
59
59
|
|
|
60
60
|
for (const candidate of candidates) {
|
|
61
61
|
const probe = spawnSync(candidate.command, [...candidate.baseArgs, "--version"], {
|
|
62
|
-
|
|
62
|
+
encoding: "utf-8",
|
|
63
63
|
});
|
|
64
|
-
if (probe.status === 0) {
|
|
64
|
+
if (probe.status === 0 && isSupportedPythonVersion(probe.stdout, probe.stderr)) {
|
|
65
65
|
return candidate;
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -69,6 +69,18 @@ function selectPython() {
|
|
|
69
69
|
return null;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
function isSupportedPythonVersion(stdout = "", stderr = "") {
|
|
73
|
+
const text = `${stdout || ""} ${stderr || ""}`.trim();
|
|
74
|
+
const match = text.match(/Python\s+(\d+)\.(\d+)(?:\.(\d+))?/i);
|
|
75
|
+
if (!match) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const major = Number(match[1]);
|
|
80
|
+
const minor = Number(match[2]);
|
|
81
|
+
return major > 3 || (major === 3 && minor >= 11);
|
|
82
|
+
}
|
|
83
|
+
|
|
72
84
|
function ensurePythonPackageInstalled(python) {
|
|
73
85
|
const importProbe = spawnSync(
|
|
74
86
|
python.command,
|
|
@@ -153,6 +165,14 @@ function launchServer(python, forwardedArgs, envOverrides) {
|
|
|
153
165
|
}
|
|
154
166
|
|
|
155
167
|
function main() {
|
|
168
|
+
if (args[0] === "install") {
|
|
169
|
+
const installer = require("./install-mcp-clients.cjs");
|
|
170
|
+
installer.runInstall(args.slice(1)).then((code) => {
|
|
171
|
+
process.exit(code);
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
156
176
|
const python = selectPython();
|
|
157
177
|
if (!python) {
|
|
158
178
|
fail("Python 3.11+ is required but was not found in PATH.");
|
package/package.json
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webtool-log-cleaner-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "npx launcher for the Python webtool-log-cleaner MCP server",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"bundle:mcpb": "python3 scripts/build_mcpb.py",
|
|
7
|
+
"test:node": "node --test tests/*.test.cjs"
|
|
8
|
+
},
|
|
5
9
|
"bin": {
|
|
6
|
-
"webtool-log-cleaner-mcp": "bin/webtool-log-cleaner-mcp.cjs"
|
|
10
|
+
"webtool-log-cleaner-mcp": "bin/webtool-log-cleaner-mcp.cjs",
|
|
11
|
+
"webtool-log-cleaner-install": "bin/install-mcp-clients.cjs"
|
|
7
12
|
},
|
|
8
13
|
"files": [
|
|
9
14
|
"bin/webtool-log-cleaner-mcp.cjs",
|
|
15
|
+
"bin/install-mcp-clients.cjs",
|
|
10
16
|
"README.md"
|
|
11
17
|
],
|
|
12
18
|
"license": "MIT",
|