port-kill-mcp 0.1.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.
Files changed (3) hide show
  1. package/README.md +228 -0
  2. package/dist/server.js +501 -0
  3. package/package.json +32 -0
package/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # Port Kill MCP Server
2
+
3
+ Expose Port Kill as MCP tools for Cursor.
4
+
5
+ ## Tools
6
+
7
+ ### Port Management
8
+ - `list(ports?, docker?, verbose?, remote?)`
9
+ - `kill(ports, remote?)`
10
+ - `reset(remote?)`
11
+ - `audit(suspiciousOnly?, remote?)`
12
+ - `guardStatus(baseUrl?)` (uses dashboard API)
13
+
14
+ ### Cache Management
15
+ - `cacheList(lang?, includeNpx?, includeJsPm?, includeHf?, includeTorch?, includeVercel?, includeCloudflare?, staleDays?)`
16
+ - `cacheClean(lang?, includeNpx?, includeJsPm?, includeHf?, includeTorch?, includeVercel?, includeCloudflare?, safeDelete?, force?, staleDays?)`
17
+ - `cacheRestore()`
18
+ - `cacheDoctor()`
19
+
20
+
21
+ ## Quick Install
22
+
23
+ Add the following to your MCP config e.g. `.cursor/mcp.json`
24
+
25
+ ```json
26
+ {
27
+ "mcpServers": {
28
+ "port-kill-mcp": {
29
+ "command": "npx",
30
+ "args": ["-y", "https://gitpkg.vercel.app/treadiehq/port-kill/mcp?main"]
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ ## Installation From Source
37
+
38
+ Alternatively, if you don't want to use `npx 'https://gitpkg.vercel.app/treadiehq/port-kill/mcp?main'`.
39
+
40
+ Checkout the repo, build the server & install `port-kill-mcp` globally:
41
+ ```bash
42
+ npm install
43
+ npm run build
44
+ npm install -g .
45
+ ```
46
+
47
+ Then add to your config e.g. `.cursor/mcp.json`
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "port-kill-mcp": {
53
+ "command": "port-kill-mcp"
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ ## Usage
60
+
61
+ Ask your AI to use the tools.
62
+
63
+ ```text
64
+ What process is running on port 3000?
65
+ ```
66
+
67
+ ```text
68
+ Kill the process running on port 3000
69
+ ```
70
+
71
+ ```text
72
+ Kill all processes using dev ports
73
+ ```
74
+
75
+ ```text
76
+ List all my development caches
77
+ ```
78
+
79
+ ```text
80
+ Clean up my NPX cache
81
+ ```
82
+
83
+ ```text
84
+ Show me system diagnostics for cache health
85
+ ```
86
+
87
+
88
+ ## Tools Extended
89
+
90
+ ### 1. **`list`** - List Processes on Ports
91
+ **Purpose**: List all processes running on specified ports with detailed information
92
+
93
+ **Example Usage**:
94
+ ```text
95
+ "What processes are running on port 3000?"
96
+ "List all processes on development ports"
97
+ "Show me what's running on ports 3000, 8000, and 8080"
98
+ ```
99
+
100
+ ### 2. **`kill`** - Kill Processes on Ports
101
+ **Purpose**: Kill all processes running on specified ports
102
+
103
+ **Example Usage**:
104
+ ```text
105
+ "Kill the process running on port 3000"
106
+ "Kill all processes on ports 3000, 8000, and 8080"
107
+ "Free up port 5432"
108
+ ```
109
+
110
+ ### 3. **`reset`** - Reset Common Dev Ports
111
+ **Purpose**: Kill processes on common development ports (3000, 5000, 8000, 5432, 3306, 6379, 27017, 8080, 9000)
112
+
113
+ **Example Usage**:
114
+ ```text
115
+ "Reset all development ports"
116
+ "Kill all processes using common dev ports"
117
+ "Clean up my development environment"
118
+ ```
119
+
120
+ ### 4. **`audit`** - Security Audit
121
+ **Purpose**: Run security audit on processes and ports to identify suspicious or unauthorized processes
122
+
123
+ **Example Usage**:
124
+ ```text
125
+ "Run a security audit on all ports"
126
+ "Check for suspicious processes on development ports"
127
+ "Audit port 3000 for security issues"
128
+ ```
129
+
130
+ ### 5. **`guardStatus`** - Port Guard Status
131
+ **Purpose**: Check the status of Port Guard if running via dashboard API
132
+
133
+ **Example Usage**:
134
+ ```text
135
+ "Check the port guard status"
136
+ "What's the current guard configuration?"
137
+ "Show me the dashboard guard status"
138
+ ```
139
+
140
+ ### 6. **`cacheList`** - List Development Caches
141
+ **Purpose**: List all detected development caches with size and metadata
142
+
143
+ **Example Usage**:
144
+ ```text
145
+ "List all my development caches"
146
+ "Show me NPX cache usage"
147
+ "List Python caches in my project"
148
+ "Show me JavaScript package manager caches"
149
+ ```
150
+
151
+ ### 7. **`cacheClean`** - Clean Development Caches
152
+ **Purpose**: Clean detected caches with safe backup functionality
153
+
154
+ **Example Usage**:
155
+ ```text
156
+ "Clean up my NPX cache"
157
+ "Remove all Rust build caches"
158
+ "Clean Python cache files"
159
+ "Clean JavaScript package manager caches"
160
+ ```
161
+
162
+ ### 8. **`cacheRestore`** - Restore Cache Backup
163
+ **Purpose**: Restore the most recent cache backup
164
+
165
+ **Example Usage**:
166
+ ```text
167
+ "Restore my last cache backup"
168
+ "Undo the last cache cleanup"
169
+ ```
170
+
171
+ ### 9. **`cacheDoctor`** - System Diagnostics
172
+ **Purpose**: Run system diagnostics and health checks for cache management
173
+
174
+ **Example Usage**:
175
+ ```text
176
+ "Show me system diagnostics for cache health"
177
+ "Check my cache storage usage"
178
+ "Run cache health diagnostics"
179
+ ```
180
+
181
+
182
+ ## Dev
183
+
184
+ ```bash
185
+ cd mcp
186
+ npm install
187
+ npm run dev
188
+ ```
189
+
190
+ Cursor config: `.cursor/mcp.json` is included. Restart Cursor to detect the server.
191
+
192
+ Environment variables:
193
+
194
+ ```bash
195
+ # Override binary path (defaults to ./target/release/port-kill-console)
196
+ export PORT_KILL_BIN=/abs/path/to/port-kill-console
197
+
198
+ # Override working directory for commands (defaults to repo root)
199
+ export PORT_KILL_CWD=/abs/path/for/commands
200
+
201
+ # Override per-tool timeout in seconds (default 300 = 5 minutes)
202
+ export PORT_KILL_MCP_TOOL_TIMEOUT_SECONDS=60
203
+ ```
204
+
205
+
206
+ ### Optional HTTP wrapper (use outside MCP/Cursor)
207
+
208
+ ```bash
209
+ # Start the MCP server with an HTTP wrapper for tools (POST /tool)
210
+ HTTP_PORT=8787 npm run dev
211
+
212
+ # Call a tool over HTTP
213
+ curl -s -X POST \
214
+ -H 'content-type: application/json' \
215
+ --data '{
216
+ "name": "list",
217
+ "args": { "ports": "3000,8000" }
218
+ }' \
219
+ http://localhost:8787/tool
220
+
221
+ # Override binary and working dir if needed
222
+ PORT_KILL_BIN=/abs/path/to/port-kill-console \
223
+ PORT_KILL_CWD=/abs/path/to/project \
224
+ PORT_KILL_MCP_TOOL_TIMEOUT_SECONDS=60 \
225
+ HTTP_PORT=8787 npm run dev
226
+ ```
227
+
228
+
package/dist/server.js ADDED
@@ -0,0 +1,501 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // Import MCP SDK using direct paths (package exports not working properly)
4
+ const path = require("node:path");
5
+ // have to check potentially up one level for node_modules when installed by npx
6
+ const sdkPkgPath = require.resolve("@modelcontextprotocol/sdk/package.json");
7
+ let sdkDir = path.dirname(sdkPkgPath).replace(path.join("dist", "cjs"), "");
8
+ const fs = require("node:fs");
9
+ // check if exists
10
+ if (!fs.existsSync(path.join(sdkDir, "dist", "cjs", "server", "mcp.js"))) {
11
+ const altSdkDir = path.join(__dirname, "../node_modules/@modelcontextprotocol/sdk");
12
+ console.log("SDK not found in", sdkDir, "using", altSdkDir);
13
+ sdkDir = altSdkDir;
14
+ }
15
+ const { McpServer } = require(path.join(sdkDir, "dist", "cjs", "server", "mcp.js"));
16
+ const { StdioServerTransport } = require(path.join(sdkDir, "dist", "cjs", "server", "stdio.js"));
17
+ const { z } = require("zod");
18
+ const { execFile } = require("node:child_process");
19
+ const activeChildProcesses = new Set();
20
+ const isWindows = process.platform === "win32";
21
+ let _binPath = null;
22
+ function binPath() {
23
+ if (_binPath)
24
+ return _binPath;
25
+ // Assume workspace root is project root; allow override via env
26
+ const bin = process.env.PORT_KILL_BIN || "./target/release/port-kill-console";
27
+ if (!fs.existsSync(path.join(process.cwd(), bin))) {
28
+ console.error("Binary not found, falling back to port-kill-console", bin);
29
+ _binPath = "port-kill-console";
30
+ return "port-kill-console";
31
+ }
32
+ else {
33
+ _binPath = bin;
34
+ }
35
+ return _binPath;
36
+ }
37
+ /**
38
+ * Validate and sanitize input to prevent command injection.
39
+ * Only allows alphanumeric characters, commas, hyphens, dots, colons, and @ symbols.
40
+ */
41
+ function sanitizeInput(input) {
42
+ if (!input)
43
+ return "";
44
+ // Whitelist: alphanumeric, comma, hyphen, dot, colon, @, underscore, forward slash
45
+ // This covers: ports (3000,8080), ranges (3000-4000), remote (user@host), paths
46
+ const sanitized = String(input).replace(/[^a-zA-Z0-9,\-.:@_\/]/g, "");
47
+ if (sanitized !== String(input)) {
48
+ console.error(`[security] Input sanitized: "${input}" -> "${sanitized}"`);
49
+ }
50
+ return sanitized;
51
+ }
52
+ /**
53
+ * Validate port input specifically - must be digits, commas, or hyphens only
54
+ */
55
+ function validatePorts(ports) {
56
+ if (!ports)
57
+ return "";
58
+ const sanitized = String(ports).replace(/[^0-9,\-]/g, "");
59
+ if (sanitized !== String(ports)) {
60
+ throw new Error(`Invalid port specification: "${ports}". Only digits, commas, and hyphens allowed.`);
61
+ }
62
+ return sanitized;
63
+ }
64
+ /**
65
+ * Validate remote host input - must match user@host pattern
66
+ */
67
+ function validateRemote(remote) {
68
+ if (!remote)
69
+ return "";
70
+ const trimmed = String(remote).trim();
71
+ if (trimmed === "")
72
+ return "";
73
+ // Pattern: optional user@ followed by hostname (alphanumeric, dots, hyphens)
74
+ const remotePattern = /^([a-zA-Z0-9_][a-zA-Z0-9_.-]*@)?[a-zA-Z0-9][a-zA-Z0-9.-]*$/;
75
+ if (!remotePattern.test(trimmed)) {
76
+ throw new Error(`Invalid remote specification: "${remote}". Expected format: user@host or hostname.`);
77
+ }
78
+ return trimmed;
79
+ }
80
+ /**
81
+ * Execute command safely using execFile (no shell interpretation).
82
+ * Arguments are passed as an array to prevent injection.
83
+ */
84
+ function run(executable, args, ctx) {
85
+ // Filter out empty arguments
86
+ const cleanArgs = args.filter(arg => arg !== "" && arg !== undefined && arg !== null);
87
+ return new Promise((resolve, reject) => {
88
+ const child = execFile(executable, cleanArgs, {
89
+ cwd: process.env.PORT_KILL_CWD || process.cwd(),
90
+ maxBuffer: 10 * 1024 * 1024
91
+ });
92
+ activeChildProcesses.add(child);
93
+ if (ctx)
94
+ ctx.ownedChildren.add(child);
95
+ let stdoutBuf = "";
96
+ let stderrBuf = "";
97
+ if (child.stdout)
98
+ child.stdout.on("data", (d) => { stdoutBuf += String(d); });
99
+ if (child.stderr)
100
+ child.stderr.on("data", (d) => { stderrBuf += String(d); });
101
+ child.on("error", (err) => {
102
+ activeChildProcesses.delete(child);
103
+ reject(err);
104
+ });
105
+ child.on("close", (code, signal) => {
106
+ activeChildProcesses.delete(child);
107
+ if (code === 0) {
108
+ resolve(stdoutBuf.trim());
109
+ return;
110
+ }
111
+ const reason = signal ? `terminated by signal ${signal}` : `exit code ${code}`;
112
+ const error = new Error(`Command failed (${reason}): ${stderrBuf || stdoutBuf}`);
113
+ error.code = code ?? reason;
114
+ reject(error);
115
+ });
116
+ });
117
+ }
118
+ // Forward termination signals to any active child processes.
119
+ // Note: SIGKILL cannot be intercepted or forwarded by a Node.js process.
120
+ function forwardSignal(signal) {
121
+ for (const child of activeChildProcesses) {
122
+ try {
123
+ if (!isWindows && child.pid) {
124
+ try {
125
+ process.kill(-child.pid, signal);
126
+ }
127
+ catch { /* best-effort */ }
128
+ }
129
+ child.kill(signal);
130
+ }
131
+ catch {
132
+ // best-effort; ignore
133
+ }
134
+ }
135
+ }
136
+ process.on("SIGTERM", () => {
137
+ forwardSignal("SIGTERM");
138
+ setTimeout(() => process.exit(143), 50);
139
+ });
140
+ process.on("SIGINT", () => {
141
+ forwardSignal("SIGINT");
142
+ setTimeout(() => process.exit(130), 50);
143
+ });
144
+ // Tool handler function
145
+ function getToolTimeoutMs() {
146
+ const fromEnv = parseInt(process.env.PORT_KILL_MCP_TOOL_TIMEOUT_SECONDS || "", 10);
147
+ if (Number.isFinite(fromEnv) && fromEnv > 0)
148
+ return fromEnv * 1000;
149
+ return 5 * 60 * 1000; // default 5 minutes
150
+ }
151
+ function killInvocationChildren(ctx, signal = "SIGTERM") {
152
+ for (const child of ctx.ownedChildren) {
153
+ try {
154
+ if (!isWindows && child.pid) {
155
+ try {
156
+ process.kill(-child.pid, signal);
157
+ }
158
+ catch { /* best-effort */ }
159
+ }
160
+ child.kill(signal);
161
+ }
162
+ catch {
163
+ // ignore
164
+ }
165
+ }
166
+ }
167
+ async function invokeWithTimeout(name, args) {
168
+ const ctx = { ownedChildren: new Set() };
169
+ const timeoutMs = getToolTimeoutMs();
170
+ let settled = false;
171
+ return await new Promise(async (resolve, reject) => {
172
+ const timer = setTimeout(() => {
173
+ if (settled)
174
+ return;
175
+ killInvocationChildren(ctx, "SIGTERM");
176
+ settled = true;
177
+ const err = new Error(`Tool \"${name}\" timed out after ${Math.floor(timeoutMs / 1000)} seconds`);
178
+ err.code = "ETIMEDOUT";
179
+ reject(err);
180
+ }, timeoutMs);
181
+ try {
182
+ const result = await handler(name, args, ctx);
183
+ if (!settled) {
184
+ settled = true;
185
+ clearTimeout(timer);
186
+ resolve(result);
187
+ }
188
+ }
189
+ catch (e) {
190
+ if (!settled) {
191
+ settled = true;
192
+ clearTimeout(timer);
193
+ reject(e);
194
+ }
195
+ }
196
+ });
197
+ }
198
+ const handler = async (name, args, ctx) => {
199
+ const bin = binPath();
200
+ switch (name) {
201
+ case "list": {
202
+ const cmdArgs = ["--console"];
203
+ if (args?.ports) {
204
+ const ports = validatePorts(args.ports);
205
+ if (ports)
206
+ cmdArgs.push("--ports", ports);
207
+ }
208
+ if (args?.docker)
209
+ cmdArgs.push("--docker");
210
+ if (args?.verbose)
211
+ cmdArgs.push("--verbose");
212
+ if (args?.remote) {
213
+ const remote = validateRemote(args.remote);
214
+ if (remote)
215
+ cmdArgs.push("--remote", remote);
216
+ }
217
+ cmdArgs.push("--json");
218
+ const out = await run(bin, cmdArgs, ctx);
219
+ return { content: out };
220
+ }
221
+ case "kill": {
222
+ const ports = validatePorts(args?.ports);
223
+ if (!ports)
224
+ throw new Error("ports argument is required for kill");
225
+ const cmdArgs = ["--kill-all", "--ports", ports];
226
+ if (args?.remote) {
227
+ const remote = validateRemote(args.remote);
228
+ if (remote)
229
+ cmdArgs.push("--remote", remote);
230
+ }
231
+ const out = await run(bin, cmdArgs, ctx);
232
+ return { content: out };
233
+ }
234
+ case "reset": {
235
+ const cmdArgs = ["--reset"];
236
+ if (args?.remote) {
237
+ const remote = validateRemote(args.remote);
238
+ if (remote)
239
+ cmdArgs.push("--remote", remote);
240
+ }
241
+ const out = await run(bin, cmdArgs, ctx);
242
+ return { content: out };
243
+ }
244
+ case "audit": {
245
+ const cmdArgs = ["--audit"];
246
+ if (args?.suspiciousOnly)
247
+ cmdArgs.push("--suspicious-only");
248
+ if (args?.remote) {
249
+ const remote = validateRemote(args.remote);
250
+ if (remote)
251
+ cmdArgs.push("--remote", remote);
252
+ }
253
+ if (args?.ports) {
254
+ const ports = validatePorts(args.ports);
255
+ if (ports)
256
+ cmdArgs.push("--ports", ports);
257
+ }
258
+ cmdArgs.push("--json");
259
+ const out = await run(bin, cmdArgs, ctx);
260
+ return { content: out };
261
+ }
262
+ case "guardStatus": {
263
+ const baseUrl = sanitizeInput(args?.baseUrl) || "http://localhost:3000";
264
+ // Validate URL format
265
+ try {
266
+ new URL(baseUrl);
267
+ }
268
+ catch {
269
+ throw new Error(`Invalid baseUrl: "${args?.baseUrl}"`);
270
+ }
271
+ const resp = await fetch(`${baseUrl}/api/guard/status`);
272
+ const json = await resp.json();
273
+ return { content: JSON.stringify(json) };
274
+ }
275
+ case "cacheList": {
276
+ const cmdArgs = ["cache", "--list"];
277
+ if (args?.includeNpx)
278
+ cmdArgs.push("--npx");
279
+ if (args?.includeJsPm)
280
+ cmdArgs.push("--js-pm");
281
+ if (args?.includeHf)
282
+ cmdArgs.push("--hf");
283
+ if (args?.includeTorch)
284
+ cmdArgs.push("--torch");
285
+ if (args?.includeVercel)
286
+ cmdArgs.push("--vercel");
287
+ if (args?.includeCloudflare)
288
+ cmdArgs.push("--cloudflare");
289
+ if (args?.lang && args.lang !== "auto") {
290
+ const lang = sanitizeInput(args.lang);
291
+ if (lang)
292
+ cmdArgs.push("--lang", lang);
293
+ }
294
+ if (args?.staleDays) {
295
+ const days = parseInt(String(args.staleDays), 10);
296
+ if (Number.isFinite(days) && days > 0) {
297
+ cmdArgs.push("--stale-days", String(days));
298
+ }
299
+ }
300
+ cmdArgs.push("--json");
301
+ const out = await run(bin, cmdArgs, ctx);
302
+ return { content: out };
303
+ }
304
+ case "cacheClean": {
305
+ const cmdArgs = ["cache", "--clean"];
306
+ if (args?.includeNpx)
307
+ cmdArgs.push("--npx");
308
+ if (args?.includeJsPm)
309
+ cmdArgs.push("--js-pm");
310
+ if (args?.includeHf)
311
+ cmdArgs.push("--hf");
312
+ if (args?.includeTorch)
313
+ cmdArgs.push("--torch");
314
+ if (args?.includeVercel)
315
+ cmdArgs.push("--vercel");
316
+ if (args?.includeCloudflare)
317
+ cmdArgs.push("--cloudflare");
318
+ if (args?.lang && args.lang !== "auto") {
319
+ const lang = sanitizeInput(args.lang);
320
+ if (lang)
321
+ cmdArgs.push("--lang", lang);
322
+ }
323
+ if (args?.safeDelete !== false)
324
+ cmdArgs.push("--safe-delete");
325
+ if (args?.force)
326
+ cmdArgs.push("--force");
327
+ if (args?.staleDays) {
328
+ const days = parseInt(String(args.staleDays), 10);
329
+ if (Number.isFinite(days) && days > 0) {
330
+ cmdArgs.push("--stale-days", String(days));
331
+ }
332
+ }
333
+ cmdArgs.push("--json");
334
+ const out = await run(bin, cmdArgs, ctx);
335
+ return { content: out };
336
+ }
337
+ case "cacheRestore": {
338
+ const out = await run(bin, ["cache", "--restore-last", "--json"], ctx);
339
+ return { content: out };
340
+ }
341
+ case "cacheDoctor": {
342
+ const out = await run(bin, ["cache", "--doctor", "--json"], ctx);
343
+ return { content: out };
344
+ }
345
+ default:
346
+ throw new Error(`Unknown tool: ${name}`);
347
+ }
348
+ };
349
+ // Create MCP server with proper tool registration
350
+ const server = new McpServer({
351
+ name: "port-kill-mcp",
352
+ version: "0.1.1"
353
+ });
354
+ // Register each tool individually with proper Zod schemas
355
+ server.registerTool("list", {
356
+ description: "List processes on ports",
357
+ inputSchema: {
358
+ ports: z.string().describe("Port range in the format '{from}-{to}' (e.g. '3000-3300') or comma-separated list of ports to check (e.g. 3000,8000,8080). Set to an empty string to check all ports in the 2000-9000 range (recommended value is 2000-9999 to check all dev ports)"),
359
+ docker: z.boolean().describe("Enable docker support (recommended value is true) when enabled processes using ports will be cross referenced with docker to determine if they are running in a container"),
360
+ verbose: z.boolean().describe("Enable verbose output (recommended value is false)"),
361
+ remote: z.string().describe("Set to an ssh 'user@host' string to check ports on a remote machine over SSH. Leave as an empty string to run locally (recommended value is an empty string)")
362
+ }
363
+ }, async (args) => {
364
+ const result = await invokeWithTimeout("list", args || {});
365
+ return { content: [{ type: "text", text: result.content }] };
366
+ });
367
+ server.registerTool("kill", {
368
+ description: "Kill processes on given ports. Args: ports (comma)",
369
+ inputSchema: {
370
+ ports: z.string().describe("Port range in the format '{from}-{to}' (e.g. '3000-3300') or comma-separated list of ports whose processes will be killed (e.g. 3000,8000,8080)"),
371
+ remote: z.string().describe("Set to an ssh 'user@host' string to kill processes on a remote machine over SSH. Leave as an empty string to run locally (recommended value is an empty string)"),
372
+ }
373
+ }, async (args) => {
374
+ const result = await invokeWithTimeout("kill", args || {});
375
+ return { content: [{ type: "text", text: result.content }] };
376
+ });
377
+ server.registerTool("reset", {
378
+ description: "Kill common dev ports (3000,5000,8000,5432,3306,6379,27017,8080,9000)",
379
+ inputSchema: {
380
+ remote: z.string().describe("Set to an ssh 'user@host' string to reset ports on a remote machine over SSH. Leave as an empty string to run locally (recommended value is an empty string)"),
381
+ }
382
+ }, async (args) => {
383
+ const result = await invokeWithTimeout("reset", args || {});
384
+ return { content: [{ type: "text", text: result.content }] };
385
+ });
386
+ server.registerTool("audit", {
387
+ description: "Run security audit. Returns detailed audit results for all processes on all ports.",
388
+ inputSchema: {
389
+ ports: z.string().describe("Port range in the format '{from}-{to}' (e.g. '3000-3300') or comma-separated list of ports to check (e.g. 3000,8000,8080). Set to an empty string to check all ports in the 2000-9000 range (recommended value is 2000-9999 to check all dev ports)"),
390
+ suspiciousOnly: z.boolean().describe("Set to true to only show suspicious/unauthorized processes, set to false to show all processes listening on scanned ports (recommended value is false)"),
391
+ remote: z.string().describe("Set to an ssh 'user@host' string to run the audit on a remote machine over SSH. Leave as an empty string to run locally (recommended value is an empty string)")
392
+ }
393
+ }, async (args) => {
394
+ const result = await invokeWithTimeout("audit", args || {});
395
+ return { content: [{ type: "text", text: `Ports scanned: ${args?.ports ?? 'default ports'}` }, { type: "text", text: result.content }] };
396
+ });
397
+ server.registerTool("guardStatus", {
398
+ description: "Return Port Guard status if running via dashboard API.",
399
+ inputSchema: {
400
+ baseUrl: z.string().describe("Dashboard base URL (default is http://localhost:3000)")
401
+ }
402
+ }, async (args) => {
403
+ const result = await invokeWithTimeout("guardStatus", args || {});
404
+ return { content: [{ type: "text", text: result.content }] };
405
+ });
406
+ server.registerTool("cacheList", {
407
+ description: "List all detected caches with size and metadata",
408
+ inputSchema: {
409
+ lang: z.string().describe("Language filter (auto, rust, js, py, java)").optional(),
410
+ includeNpx: z.boolean().describe("Include NPX cache analysis").optional(),
411
+ includeJsPm: z.boolean().describe("Include JS package manager caches").optional(),
412
+ includeHf: z.boolean().describe("Include Hugging Face caches").optional(),
413
+ includeTorch: z.boolean().describe("Include PyTorch caches").optional(),
414
+ includeVercel: z.boolean().describe("Include Vercel caches").optional(),
415
+ includeCloudflare: z.boolean().describe("Include Cloudflare caches").optional(),
416
+ staleDays: z.number().describe("Days to consider NPX packages stale").optional()
417
+ }
418
+ }, async (args) => {
419
+ const result = await invokeWithTimeout("cacheList", args || {});
420
+ return { content: [{ type: "text", text: result.content }] };
421
+ });
422
+ server.registerTool("cacheClean", {
423
+ description: "Clean detected caches with safe backup",
424
+ inputSchema: {
425
+ lang: z.string().describe("Language filter (auto, rust, js, py, java)").optional(),
426
+ includeNpx: z.boolean().describe("Include NPX cache analysis").optional(),
427
+ includeJsPm: z.boolean().describe("Include JS package manager caches").optional(),
428
+ includeHf: z.boolean().describe("Include Hugging Face caches").optional(),
429
+ includeTorch: z.boolean().describe("Include PyTorch caches").optional(),
430
+ includeVercel: z.boolean().describe("Include Vercel caches").optional(),
431
+ includeCloudflare: z.boolean().describe("Include Cloudflare caches").optional(),
432
+ safeDelete: z.boolean().describe("Use safe delete with backup").optional(),
433
+ force: z.boolean().describe("Force cleanup without confirmation").optional(),
434
+ staleDays: z.number().describe("Days to consider NPX packages stale").optional()
435
+ }
436
+ }, async (args) => {
437
+ const result = await invokeWithTimeout("cacheClean", args || {});
438
+ return { content: [{ type: "text", text: result.content }] };
439
+ });
440
+ server.registerTool("cacheRestore", {
441
+ description: "Restore the most recent cache backup",
442
+ inputSchema: {}
443
+ }, async (args) => {
444
+ const result = await invokeWithTimeout("cacheRestore", args || {});
445
+ return { content: [{ type: "text", text: result.content }] };
446
+ });
447
+ server.registerTool("cacheDoctor", {
448
+ description: "Run system diagnostics and health checks",
449
+ inputSchema: {}
450
+ }, async (args) => {
451
+ const result = await invokeWithTimeout("cacheDoctor", args || {});
452
+ return { content: [{ type: "text", text: result.content }] };
453
+ });
454
+ // Start server with stdio transport
455
+ async function startServer() {
456
+ const transport = new StdioServerTransport();
457
+ await server.connect(transport);
458
+ console.error("Port Kill MCP server running on stdio", binPath());
459
+ }
460
+ startServer().catch(console.error);
461
+ // Optional HTTP wrapper so non-MCP clients can call the same tools
462
+ if (process.env.HTTP_PORT) {
463
+ const httpPort = parseInt(process.env.HTTP_PORT, 10) || 8787;
464
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
465
+ const http = require("node:http");
466
+ const srv = http.createServer(async (req, res) => {
467
+ try {
468
+ if (!req.url) {
469
+ res.statusCode = 400;
470
+ return res.end("Bad Request");
471
+ }
472
+ const url = new URL(req.url, `http://localhost:${httpPort}`);
473
+ if (req.method === "POST" && url.pathname === "/tool") {
474
+ let body = "";
475
+ req.on("data", (chunk) => body += chunk);
476
+ req.on("end", async () => {
477
+ try {
478
+ const { name, args } = JSON.parse(body || "{}");
479
+ const result = await invokeWithTimeout(name, args || {});
480
+ res.setHeader("content-type", "application/json");
481
+ res.end(JSON.stringify({ ok: true, result }));
482
+ }
483
+ catch (e) {
484
+ res.statusCode = 500;
485
+ res.end(JSON.stringify({ ok: false, error: e?.message || String(e) }));
486
+ }
487
+ });
488
+ return;
489
+ }
490
+ res.statusCode = 404;
491
+ res.end("Not Found");
492
+ }
493
+ catch (e) {
494
+ res.statusCode = 500;
495
+ res.end(JSON.stringify({ ok: false, error: e?.message || String(e) }));
496
+ }
497
+ });
498
+ srv.listen(httpPort, () => {
499
+ console.log(`[port-kill-mcp] HTTP wrapper listening on :${httpPort}`);
500
+ });
501
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "port-kill-mcp",
3
+ "version": "0.1.1",
4
+ "type": "commonjs",
5
+ "main": "dist/server.js",
6
+ "bin": {
7
+ "port-kill-mcp": "dist/server.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "package.json"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc -p .",
15
+ "start": "node dist/server.js",
16
+ "dev": "tsx src/server.ts"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^24.5.2",
20
+ "tsx": "^4.19.2",
21
+ "typescript": "^5.6.3"
22
+ },
23
+ "dependencies": {
24
+ "@modelcontextprotocol/sdk": "^1.18.0",
25
+ "zod": "^3.23.8"
26
+ },
27
+ "description": "MCP server for port management and cache cleanup",
28
+ "license": "MIT",
29
+ "engines": {
30
+ "node": ">=18"
31
+ }
32
+ }