pm2-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@pm2-mcplist my PM2 services"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
pm2-mcp
An MCP server that gives agents structured read and limited write access to PM2 services. Built with FastMCP, transport is streamable-http bound to localhost.
I built this because every other approach to PM2 inspection from an agent involves either raw shell access (homelab-ops run_command) or scraping human-readable pm2 status output. This server speaks directly to pm2 jlist, returns typed fields, and validates service names before issuing any write operations.
flowchart LR
subgraph shell["Shell access"]
direction LR
s1["agent"] -->|"run_command\npm2 jlist"| s2["unparsed\nJSON string"]
end
subgraph mcp["pm2-mcp"]
direction LR
m1["agent"] -->|"list_services"| m2["typed fields\nstatus · uptime_ms\ncpu_pct · memory_mb"]
endTools
Read
Tool | Description |
| List all PM2 services. Optional |
| Full detail for one service by name — script path, cwd, args, log file paths, created_at, plus all summary fields. |
| Tail recent log output. |
| Server metadata and PM2 health summary — configured host/port, PM2 version, service counts by status. |
Write
Tool | Description |
| Restart a service. Validates the name first — returns |
| Stop a service. Does not remove it from the PM2 process list. |
| Resume a stopped service already registered in PM2. Does not register new processes. |
| Gracefully reload a service (zero-downtime). Preferred over |
| Persist the current PM2 process list to disk. Call after write operations to survive reboots. |
| Clear log files for a service. |
Response shape — list_services
[
{
"name": "my-service",
"pm_id": 12,
"status": "online",
"pid": 18432,
"uptime_ms": 3720000,
"restarts": 0,
"cpu_pct": 0.2,
"memory_mb": 48.5,
"exec_mode": "fork_mode"
}
]get_service extends this with script, cwd, args, log_file, error_file, and created_at.
Related MCP server: RE-call MCP Memory Server
Non-goals
Things this server deliberately does not do. Each is a decision, not a gap:
It does not register new PM2 processes.
start_serviceresumes a process PM2 already knows about; it will not create one. Registering a process means deciding its script, interpreter, working directory, log paths and environment — that belongs in a reviewedecosystem.config.js, not in an agent tool call.There is no
deleteverb, and there will not be one.pm2 deletediscards the process definition, and re-creating it re-captures the calling shell's environment — the exact mechanism behind #767. The destructive verb with the worst failure mode is the one least worth automating.It has no authentication, by design. It binds loopback only and is expected to stay that way. Adding auth would imply the port could safely be exposed, which is not the posture this server is built for. See docs/threat-model.md.
It does not publish an installable artefact. No PyPI package, no version badge. It is deployed from a checkout as a PM2 process. This is why Showcase is its terminal tier rather than a step toward Flagship.
It does not manage the PM2 daemon itself — no
pm2 kill, no daemon resurrection, no startup-script installation. Those are host administration, not process inspection.
Setup
Requirements
Python 3.11+
PM2 installed and in PATH for the user running the server
fastmcp>=3.2.4(seerequirements.txt)
pip install -r requirements.txtRun as a PM2 process (recommended)
This repository ships the ecosystem.config.js it actually deploys with — use that rather
than the sketch this section used to contain:
pm2 start ecosystem.config.js --only pm2-mcp
pm2 saveThree things in it are host-specific: script (the venv interpreter path), cwd, and the
log paths. Leave the rest alone — in particular env: {}, which is empty deliberately and
carries a comment explaining why. See examples/ and
docs/operations.md.
Start it from PM2 at boot, not from an interactive shell. pm2 start hands the calling
shell's entire environment to the daemon, and pm2 save writes it to disk. That is how a
live credential ended up in a world-readable dump.pm2 for nine days.
Or inline with env vars:
MCP_HOST=127.0.0.1 MCP_PORT=8486 pm2 start server.py --interpreter python3 --name pm2-mcp
pm2 saveThe server manages itself like any other PM2 service — it will appear in its own list_services output.
Run directly
python server.py # 127.0.0.1:8486
python server.py --port 9000 # override the port only
python server.py --host 127.0.0.2 --port 9000 # both
python server.py --help # full usageNon-loopback binds are refused; see Bind address and port.
Bind address and port
The bind is resolved from three sources, in this order of precedence:
Precedence | Source | Example |
1 (highest) | Command-line flags |
|
2 | Environment variables |
|
3 (lowest) | Built-in defaults |
|
Precedence is per-field, not all-or-nothing: passing only --host leaves the port to
MCP_PORT or the default.
Variable | Default | Description |
|
| Bind address. Must be loopback. |
|
| Port for the MCP server |
Copy .env.example to .env and fill in the values you need. Blank environment variables
are treated as unset and use the defaults shown above.
The bind is loopback-only, and that is enforced
--host and MCP_HOST accept 127.0.0.0/8, ::1 and localhost. Anything else exits
non-zero instead of starting — including 0.0.0.0, a LAN address, and any hostname.
This is deliberate and there is no override flag. pm2-mcp has no authentication of any
kind, and its write verbs can stop or restart any PM2 process on the host. A --host that
accepted 0.0.0.0 would turn a documented-safe posture into a one-word footgun, so the
refusal is in code rather than in a comment. See docs/threat-model.md.
A hostname is refused rather than resolved: a name can point anywhere, and can be repointed after the check passes without the process restarting, so resolution is not a security boundary.
Note: these flags were accepted but silently ignored before v0.4.0 —
server.pyhad no argument parsing and read onlyMCP_HOST/MCP_PORT. They are live as of #770.
Wiring to Claude Code
Add to ~/.claude/settings.json under mcpServers:
{
"mcpServers": {
"pm2": {
"type": "streamable-http",
"url": "http://127.0.0.1:8486/mcp"
}
}
}Security
The server binds loopback only, and that is enforced in code rather than defaulted — a non-loopback --host or MCP_HOST exits non-zero instead of starting, with no override flag. See Bind address and port. Any client that can reach port 8486 can restart or stop services, because there is no authentication. That is intentional for local agent use: keep it loopback-only and don't proxy it externally.
The write tools (restart_service, stop_service, start_service, reload_service, flush_logs) validate service names against the live PM2 process list before acting. An unrecognized name returns {ok: false, error: "service '...' not found"} without touching PM2.
Before invoking the pm2 CLI the server scrubs its own environment. Today that means a denylist: PM2's IPC variables, and anything matching the CLAUDE prefix. pm2 start and pm2 restart --update-env copy the caller's whole environment into the target app and pm2 save persists it, so this is the difference between a credential staying in memory and being written to disk.
A named allowlist (vikunja#610) is also implemented and will replace that prefix-match denylist once enabled — but it is not live yet. It runs in shadow mode by default: it computes and logs which variables it would withhold, per pm2 invocation, and changes nothing. Enforcing it is an opt-in source change (PM2_MCP_ENV_MODE=enforce), gated on vikunja#771. Which environment reaches the pm2 child, and why, is the most consequential thing about running this server — see docs/threat-model.md §2 for the full reasoning rather than re-deriving it here.
Full posture, including the gaps that are accepted rather than fixed: docs/threat-model.md.
Testing
pip install -r requirements.txt -r requirements-dev.txt
pytest -vAll tests mock _run_pm2 — no PM2 installation required.
Coverage and its floor are configured in pyproject.toml, so a bare pytest enforces them; there is no separate CI-only flag. Changes to _clean_env or _run_pm2 should also pass the mutation gate:
MUTMUT=.venv/bin/mutmut ./scripts/mutation-gate.shSee CONTRIBUTING.md for what that gate does and, more importantly, what it does not cover.
Documentation
Request flow, module lifecycle, and the environment boundary | |
Deploying, health checks, recovery | |
What's protected, what isn't, and why | |
Real client config and a worked crash-loop diagnosis | |
Dev setup, test loop, mutation gate | |
Reporting a vulnerability |
License
MIT — see LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
MCP server for agentverse documentation, generated by doc2mcp.
MCP Server for an Agent Task Marketplace
An MCP server that provides read access to your cloud storage providers, bank accounts and more.
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
Related MCP Servers
- AlicenseAqualityBmaintenanceAn MCP server implementation that enables interaction with the Letta API for managing agents, memory blocks, and tools in the Letta system.36113 npm79MIT
- AlicenseAqualityAmaintenanceMCP server exposing memory search, index, and stats tools for agents, with honesty guards to prevent re-litigation of settled decisions.5192 PyPI6PolyForm Noncommercial 1.0.0
- AlicenseNot gradedqualityAmaintenanceMCP server for permissioned, structured agent-to-agent communications, enabling agents to coordinate and negotiate through scoped, typed messages with authentication and audit logging.453 PyPIMIT
- FlicenseAqualityCmaintenanceAn MCP server that gives an agent terminal and file-management capabilities, including running shell/Python commands, searching and reading files, and creating/editing/deleting files and folders.21-