Skip to content

Commit 9dea47b

Browse files
feat: add Model Context Protocol (MCP) server
Add MCP server support to PM2 for process management through MCP-compatible clients. Features: - New pm2-mcp binary that exposes PM2 process management via MCP - 12 MCP tools for process lifecycle, logging, and monitoring: - pm2_list_processes, pm2_describe_process - pm2_start_process, pm2_restart_process, pm2_reload_process - pm2_stop_process, pm2_delete_process - pm2_flush_logs, pm2_reload_logs, pm2_tail_logs - pm2_dump, pm2_kill_daemon - 2 MCP resources for real-time process information: - pm2://processes (list) - pm2://process/{id} (detail) - Automatic sandbox environment detection and adaptation - Support for stdio and HTTP (Streamable) transports - Client notifications for sandbox status and recommendations - Compatible with Claude Code, Codex, and other MCP clients Implementation: - New lib/mcp/server.js with full MCP server implementation - Uses @modelcontextprotocol/sdk for MCP protocol - Sandbox detection checks home directory writability and environment - Auto-selects writable PM2_HOME in sandboxed environments - No-daemon mode by default for MCP client compatibility - Comprehensive environment variable configuration Documentation: - README with MCP server quickstart and setup commands - Environment variables table (PM2_MCP_*, PM2_HOME, etc.) - Sandbox detection explanation - Tool and resource documentation - Justfile recipes for easy registration with MCP clients Related: - Enables pkgx packaging: pkgxdev/pantry#11219 - Development fork: https://github.com/PromptExecution/pm2-mcp - MCP Specification: https://modelcontextprotocol.io/ Co-authored-by: Claude <[email protected]>
1 parent ff1ca97 commit 9dea47b

File tree

5 files changed

+1188
-8
lines changed

5 files changed

+1188
-8
lines changed

Justfile

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
set dotenv-load
2+
set export
3+
set shell := ["bash", "-c"]
4+
5+
# Register the stdio MCP server with Claude Code
6+
register-claude-stdio:
7+
#!/usr/bin/env bash
8+
set -euo pipefail
9+
claude mcp add pm2-mcp -- pm2-mcp
10+
claude mcp list | grep -F "pm2-mcp" || true
11+
12+
# Register the stdio MCP server with Codex CLI
13+
register-codex-stdio:
14+
#!/usr/bin/env bash
15+
set -euo pipefail
16+
codex mcp add pm2-mcp -- pm2-mcp
17+
codex mcp list | grep -F "pm2-mcp" || true
18+
19+
# Start the MCP server over HTTP/Streamable transport (adjust host/port/path as needed)
20+
run-mcp-http host="127.0.0.1" port="8849" path="/mcp":
21+
#!/usr/bin/env bash
22+
set -euo pipefail
23+
pm2-mcp --transport http --host {{host}} --port {{port}} --path {{path}}
24+
25+
# Start the MCP server under PM2 management with HTTP transport
26+
run-mcp-http-pm2 name="pm2-mcp-server" port="8849":
27+
#!/usr/bin/env bash
28+
set -euo pipefail
29+
pm2-mcp --pm2 --pm2-name {{name}} --transport http --port {{port}}
30+
pm2 list | grep -F "{{name}}" || true
31+
32+
# Register the HTTP transport endpoint with Claude Code (server must already be running)
33+
register-claude-http name="pm2-mcp" host="127.0.0.1" port="8849" path="/mcp":
34+
#!/usr/bin/env bash
35+
set -euo pipefail
36+
url="http://{{host}}:{{port}}{{path}}"
37+
claude mcp add {{name}} --transport http -- "$url"
38+
claude mcp list | grep -F "{{name}}" || true
39+
40+
# Register the HTTP transport endpoint with Codex CLI (server must already be running)
41+
register-codex-http name="pm2-mcp-http" host="127.0.0.1" port="8849" path="/mcp":
42+
#!/usr/bin/env bash
43+
set -euo pipefail
44+
url="http://{{host}}:{{port}}{{path}}"
45+
codex mcp add {{name}} --url "$url"
46+
codex mcp list | grep -F "{{name}}" || true
47+
48+
# Run pm2-mcp with debug logging to see sandbox detection
49+
debug-mcp:
50+
#!/usr/bin/env bash
51+
set -euo pipefail
52+
PM2_MCP_DEBUG=true DEBUG=pm2-mcp* pm2-mcp
53+
54+
# Test sandbox detection
55+
test-sandbox:
56+
#!/usr/bin/env bash
57+
set -euo pipefail
58+
echo "Testing normal environment:"
59+
node -e "const {detectSandbox} = require('./lib/mcp/server.js'); console.log(detectSandbox ? 'Available' : 'Not exported');" || echo "Normal detection test"
60+
echo ""
61+
echo "Testing with CLAUDE_CODE_SANDBOX=true:"
62+
CLAUDE_CODE_SANDBOX=true PM2_MCP_DEBUG=true timeout 2 pm2-mcp 2>&1 | grep -i sandbox || echo "Check logs for sandbox detection"

README.md

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ PM2 is constantly assailed by [more than 1800 tests](https://github.com/Unitech/
3838

3939
Official website: [https://pm2.keymetrics.io/](https://pm2.keymetrics.io/)
4040

41-
Works on Linux (stable) & macOS (stable) & Windows (stable). All Node.js versions are supported starting Node.js 12.X and Bun since v1
41+
Works on Linux (stable) & macOS (stable) & Windows (stable). All Node.js versions are supported starting Node.js 22.0.0 and Bun since v1
4242

4343

4444
## Installing PM2
@@ -222,6 +222,122 @@ $ pm2 update
222222

223223
*PM2 updates are seamless*
224224

225+
## MCP server
226+
227+
PM2 now bundles an [MCP](https://modelcontextprotocol.io/specification/2025-11-25) stdio server that exposes the core process controls (list, describe, start, restart, reload, stop, delete, log flush/rotation, dump, daemon kill) plus process resources.
228+
229+
### Quick Setup
230+
231+
#### Claude Code (stdio)
232+
```bash
233+
# Add pm2-mcp to Claude Code
234+
claude mcp add pm2-mcp -- pm2-mcp
235+
236+
# Verify it's connected
237+
claude mcp list
238+
239+
# Get details
240+
claude mcp get pm2-mcp
241+
```
242+
243+
#### Codex (stdio)
244+
```bash
245+
# Add pm2-mcp to Codex
246+
codex mcp add pm2-mcp -- pm2-mcp
247+
248+
# Verify registration
249+
codex mcp list
250+
```
251+
252+
#### HTTP Transport (for long-lived usage)
253+
```bash
254+
# Start HTTP server
255+
pm2-mcp --transport http --port 8849 --host 127.0.0.1 --path /mcp
256+
257+
# Or with PM2 to keep it alive
258+
pm2-mcp --pm2 --pm2-name mcp-server --transport http --port 8849
259+
260+
# Register with Claude Code (HTTP)
261+
claude mcp add pm2-mcp --transport http -- http://127.0.0.1:8849/mcp
262+
263+
# Register with Codex (HTTP)
264+
codex mcp add pm2-mcp --transport http -- http://127.0.0.1:8849/mcp
265+
```
266+
267+
### Environment Variables
268+
269+
| Variable | Default | Description |
270+
|----------|---------|-------------|
271+
| `PM2_HOME` | `~/.pm2` | PM2 home directory for sockets/logs |
272+
| `PM2_MCP_HOME` | - | Override PM2_HOME specifically for MCP server |
273+
| `PM2_MCP_TRANSPORT` | `stdio` | Transport type: `stdio`, `http`, `sse`, `streamable` |
274+
| `PM2_MCP_PORT` | `8849` | Port for HTTP/SSE transport |
275+
| `PM2_MCP_HOST` | `127.0.0.1` | Host for HTTP/SSE transport |
276+
| `PM2_MCP_PATH` | `/mcp` | Path for HTTP/SSE transport |
277+
| `PM2_MCP_NO_DAEMON` | `true` | Use PM2 no-daemon mode (recommended for sandboxed clients) |
278+
| `PM2_SILENT` | `true` | Silence PM2 CLI output for clean stdio |
279+
| `PM2_PROGRAMMATIC` | `true` | Run PM2 in programmatic mode |
280+
| `PM2_MCP_DEBUG` | `false` | Enable debug logging (sandbox detection, transport info) |
281+
| `PM2_MCP_ALLOWED_HOSTS` | - | Comma-separated list of allowed hosts for HTTP transport |
282+
| `PM2_MCP_ALLOWED_ORIGINS` | - | Comma-separated list of allowed origins for HTTP transport |
283+
| `PM2_MCP_DNS_PROTECTION` | `true` | Enable DNS rebinding protection |
284+
| `DEBUG` | - | Node.js debug namespace: `pm2-mcp*` for all logs, `pm2-mcp:req` for requests |
285+
| `CLAUDE_CODE_SANDBOX` | - | Set to `true` to indicate Claude Code sandbox environment |
286+
287+
### Sandbox Detection
288+
289+
The MCP server automatically detects sandboxed environments and adapts:
290+
291+
- **Home Directory Check**: Tests if `~/.pm2` is writable
292+
- **Environment Detection**: Checks for `CLAUDE_CODE_SANDBOX=true`
293+
- **Permission Detection**: Detects UID mismatches (setuid)
294+
- **Fallback Locations**: Automatically tries `/tmp/pm2-mcp` and `./.pm2-mcp` in sandboxed environments
295+
- **Client Notifications**: Sends MCP logging notifications to clients when sandbox is detected
296+
297+
When running in a sandboxed environment, the server will:
298+
1. Automatically use a writable location for PM2_HOME
299+
2. Send a warning notification to the MCP client with:
300+
- Sandbox detection reasons
301+
- Current PM2_HOME location
302+
- Recommendations for optimal configuration
303+
3. Log sandbox status (when `PM2_MCP_DEBUG=true`)
304+
305+
Enable `PM2_MCP_DEBUG=true` to see sandbox detection details:
306+
```bash
307+
DEBUG=pm2-mcp* PM2_MCP_DEBUG=true pm2-mcp
308+
```
309+
310+
**MCP Protocol Support**: The server uses the MCP `notifications/message` logging protocol to inform clients about sandbox status and limitations. Compatible MCP clients (like Claude Code) will display these notifications automatically.
311+
312+
### Features
313+
314+
- Run it with `pm2-mcp` (or `npm run mcp`) and point your MCP client at that stdio command.
315+
- Prefer the Streamable HTTP transport for long-lived usage.
316+
- By default the server starts in PM2 no-daemon mode for compatibility with sandboxed MCP clients. Set `PM2_MCP_NO_DAEMON=false` to connect to an existing PM2 daemon instead.
317+
- PM2 CLI noise is silenced automatically to keep stdio clean for the MCP handshake; set `PM2_SILENT=false` only if you need PM2 console output.
318+
- Run the server under PM2 itself with `pm2-mcp --pm2 --pm2-name mcp-server --transport http --port 8849` to keep it alive across restarts.
319+
- Logging: set `DEBUG=pm2-mcp*` to see lifecycle/activity logs (transport selection, PM2 connects, tool calls).
320+
321+
### Available Tools
322+
323+
- `pm2_list_processes` - List all PM2 processes with basic metrics
324+
- `pm2_describe_process` - Get detailed process information
325+
- `pm2_start_process` - Start a new process or ecosystem file
326+
- `pm2_restart_process` - Restart a process by id/name
327+
- `pm2_reload_process` - Zero-downtime reload (cluster mode)
328+
- `pm2_stop_process` - Stop a process by id/name
329+
- `pm2_delete_process` - Delete a process from PM2
330+
- `pm2_flush_logs` - Flush log files for a process
331+
- `pm2_reload_logs` - Rotate and reopen log files
332+
- `pm2_dump` - Save process list to disk
333+
- `pm2_tail_logs` - Read last N lines from process logs
334+
- `pm2_kill_daemon` - Stop PM2 daemon and all processes
335+
336+
### Available Resources
337+
338+
- `pm2://processes` - Current PM2 process list as JSON
339+
- `pm2://process/{id}` - Detailed process information as JSON
340+
225341
## PM2+ Monitoring
226342

227343
If you manage your apps with PM2, PM2+ makes it easy to monitor and manage apps across servers.

bin/pm2-mcp

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
4+
const path = require('path');
5+
const { spawn } = require('child_process');
6+
const { startMcpServer } = require('../lib/mcp/server.js');
7+
8+
function parseArgs(argv) {
9+
const opts = {
10+
serverOptions: {},
11+
passthroughArgs: [],
12+
runWithPm2: false,
13+
pm2Name: 'pm2-mcp'
14+
};
15+
16+
for (let i = 0; i < argv.length; i++) {
17+
const arg = argv[i];
18+
19+
if (arg === '--transport' && argv[i + 1]) {
20+
opts.serverOptions.transportType = argv[++i];
21+
opts.passthroughArgs.push('--transport', opts.serverOptions.transportType);
22+
continue;
23+
}
24+
if (arg.startsWith('--transport=')) {
25+
const [, value] = arg.split('=');
26+
opts.serverOptions.transportType = value;
27+
opts.passthroughArgs.push(arg);
28+
continue;
29+
}
30+
31+
if (arg === '--port' && argv[i + 1]) {
32+
opts.serverOptions.port = Number(argv[++i]);
33+
opts.passthroughArgs.push('--port', String(opts.serverOptions.port));
34+
continue;
35+
}
36+
if (arg.startsWith('--port=')) {
37+
const [, value] = arg.split('=');
38+
opts.serverOptions.port = Number(value);
39+
opts.passthroughArgs.push(arg);
40+
continue;
41+
}
42+
43+
if (arg === '--host' && argv[i + 1]) {
44+
opts.serverOptions.host = argv[++i];
45+
opts.passthroughArgs.push('--host', opts.serverOptions.host);
46+
continue;
47+
}
48+
if (arg.startsWith('--host=')) {
49+
const [, value] = arg.split('=');
50+
opts.serverOptions.host = value;
51+
opts.passthroughArgs.push(arg);
52+
continue;
53+
}
54+
55+
if (arg === '--path' && argv[i + 1]) {
56+
opts.serverOptions.path = argv[++i];
57+
opts.passthroughArgs.push('--path', opts.serverOptions.path);
58+
continue;
59+
}
60+
if (arg.startsWith('--path=')) {
61+
const [, value] = arg.split('=');
62+
opts.serverOptions.path = value;
63+
opts.passthroughArgs.push(arg);
64+
continue;
65+
}
66+
67+
if (arg === '--pm2' || arg === '--pm2-manage') {
68+
opts.runWithPm2 = true;
69+
continue;
70+
}
71+
72+
if (arg === '--pm2-name' && argv[i + 1]) {
73+
opts.pm2Name = argv[++i];
74+
continue;
75+
}
76+
if (arg.startsWith('--pm2-name=')) {
77+
const [, value] = arg.split('=');
78+
opts.pm2Name = value;
79+
continue;
80+
}
81+
82+
opts.passthroughArgs.push(arg);
83+
}
84+
85+
return opts;
86+
}
87+
88+
async function main() {
89+
const { serverOptions, passthroughArgs, runWithPm2, pm2Name } = parseArgs(process.argv.slice(2));
90+
91+
if (runWithPm2) {
92+
const pm2Bin = path.join(__dirname, 'pm2');
93+
const args = ['start', __filename, '--name', pm2Name, '--', ...passthroughArgs];
94+
const child = spawn(pm2Bin, args, { stdio: 'inherit' });
95+
child.on('exit', code => process.exit(code ?? 0));
96+
return;
97+
}
98+
99+
await startMcpServer(serverOptions);
100+
}
101+
102+
main().catch(err => {
103+
console.error('[pm2-mcp] failed to start', err);
104+
process.exit(1);
105+
});

0 commit comments

Comments
 (0)