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 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
- stdio: "ignore",
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.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",