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.
- package/README.md +228 -0
- package/dist/server.js +501 -0
- 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
|
+
}
|