Skip to content

v0.5.0

Latest

Choose a tag to compare

@TadMSTR TadMSTR released this 26 Oct 02:06
· 1 commit to main since this release
07b1f0c

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. HostOriginGuard applies mcp's TransportSecurityMiddleware: a Host other than 127.0.0.1, localhost, [::1] or the bound address (any port) gets 421, a request carrying a non-empty Origin gets 403, and a websocket is closed with 1008. PM2_MCP_ALLOWED_HOSTS adds exact Host values, 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-audit over the locked runtime and dev sets reports no known vulnerabilities.
  • get_logs accepts only a registered service name, like the write verbs. pm2 logs takes an id, a namespace, all or an option in the same argument position.
  • An unrecognised PM2_MCP_ENV_MODE refuses to start. Any value other than shadow or enforce used to run shadow silently.

Added

  • uv.lock. CI installs every job from it (uv sync --frozen), checks uv 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_MODE still defaults to shadow.
  • get_status reports env_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 for uv.lock and the action pins.

Changed

  • src/ layout: server.py is now src/pm2_mcp/server.py, with a pm2-mcp console entry point (pm2_mcp.server:main). ecosystem.config.js runs it.
  • requirements.txt and requirements-dev.txt are removed; dev tools are the dev extra.
  • requires-python is >=3.11, the versions CI tests.
  • CI's build job installs the wheel with the locked runtime set, imports pm2_mcp from 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.yml no longer persists the checkout token.

Fixed

  • get_status reported MCP_HOST/MCP_PORT from the environment rather than the bind in use, so with --host/--port passed (as ecosystem.config.js does) it could report a port the server was not listening on.
  • The comment on _ENV_MODE said 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 allowed Host.
  • Mutation gate: every trust-boundary mutant must be killed. It failed only on survived, so no tests, not checked (an aborted run) and timeout passed, and so did the positive control and the floors. tests/test_mutation_gate.py drives 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 %25 in Host. A % in the bind host is now refused.
  • .gitignore: core.[0-9]* instead of core.*, which would also have ignored a core.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_env 25, _allowlist_env 1, _denylist_env 4, _run_pm2 22, _is_loopback 6). _is_loopback is now 9, with its floor raised to match, after the scope-ID refusal above.