Repository navigation
Deploy note: the launch path changes. The PM2 app now runs the pm2-mcp console entry point from the venv, not python3 server.py from the checkout. Reinstall from uv.lock (uv sync --frozen --no-dev --no-editable), then pm2 delete pm2-mcp and the env -i clean start from docs/operations.md ("Starting it cleanly"), its check, and only then pm2 save. A pm2 restart after the pull crash-loops. See docs/operations.md, "Upgrading".
Security
- Host/Origin check on the HTTP app.
HostOriginGuardapplies mcp'sTransportSecurityMiddleware: aHostother than127.0.0.1,localhost,[::1]or the bound address (any port) gets 421, a request carrying a non-emptyOrigingets 403, and a websocket is closed with 1008.PM2_MCP_ALLOWED_HOSTSadds exactHostvalues, refuses to start on an entry containing*, and is unset by default. Refusals are logged by status, never by header value. - Dependency minimums raised, each with the advisory it closes in
pyproject.toml:fastmcp>=3.4.10,<4,mcp>=1.30,<2, and floors on anyio, certifi, cryptography, joserfc, opentelemetry-api, PyJWT, pydantic-settings, python-multipart, starlette and urllib3.pip-auditover the locked runtime and dev sets reports no known vulnerabilities. get_logsaccepts only a registered service name, like the write verbs.pm2 logstakes an id, a namespace,allor an option in the same argument position.- An unrecognised
PM2_MCP_ENV_MODErefuses to start. Any value other thanshadoworenforceused to run shadow silently.
Added
uv.lock. CI installs every job from it (uv sync --frozen), checksuv lock --check, audits the locked runtime and dev sets, and runs a gating job against the newest versions the ranges allow (also weekly).- Write verbs tested against a real pm2 (vikunja#771).
tests/test_integration_pm2.py(pytest -m integration) drives restart, stop, start, reload, flush and save through the server's functions against a private pm2 daemon, in shadow and enforce mode, and checks each one's effect. A negative control proves a broken child environment is detected. A CI job runs it with pm2 6.0.14.PM2_MCP_ENV_MODEstill defaults to shadow. get_statusreportsenv_mode.- Secret scanning:
secret-scan.yml(gitleaks 8.30.1 pinned by version and digest, PR/push gate, weekly full-history scan) and a planted-secret self-test. - CodeQL and OSSF Scorecard workflows,
CODEOWNERS, and Dependabot foruv.lockand the action pins.
Changed
src/layout:server.pyis nowsrc/pm2_mcp/server.py, with apm2-mcpconsole entry point (pm2_mcp.server:main).ecosystem.config.jsruns it.requirements.txtandrequirements-dev.txtare removed; dev tools are thedevextra.requires-pythonis>=3.11, the versions CI tests.- CI's build job installs the wheel with the locked runtime set, imports
pm2_mcpfrom it outside the tree, runs the entry point, and requires it to refuse a non-loopback bind. - Workflows carry top-level
permissions: contents: read; actions moved to current pins;release.ymlno longer persists the checkout token.
Fixed
get_statusreportedMCP_HOST/MCP_PORTfrom the environment rather than the bind in use, so with--host/--portpassed (asecosystem.config.jsdoes) it could report a port the server was not listening on.- The comment on
_ENV_MODEsaid the mode was not read from the environment; it is. --host 127.0.0.2(any 127.0.0.0/8 address, which the bind check accepts) started and then answered every request 421. The bound address is now an allowedHost.- Mutation gate: every trust-boundary mutant must be
killed. It failed only onsurvived, sono tests,not checked(an aborted run) andtimeoutpassed, and so did the positive control and the floors.tests/test_mutation_gate.pydrives the script through a stub mutmut, one case per status. - A scoped IPv6 bind (
--host ::1%lo) passed the loopback check, then answered every request 421; clients write the zone as%25inHost. A%in the bind host is now refused. .gitignore:core.[0-9]*instead ofcore.*, which would also have ignored acore.py.- Mutation gate: the survivor check is no longer anchored on the module path, which the
src/move changed. An anchored pattern left behind by such a move matches nothing and passes with survivors present. The src move left the counts unchanged (_clean_env25,_allowlist_env1,_denylist_env4,_run_pm222,_is_loopback6)._is_loopbackis now 9, with its floor raised to match, after the scope-ID refusal above.