Skip to content

MCP server

This is the operator’s reference for trueppm-mcp, the read-only Model Context Protocol server. For the user-facing feature overview — what it answers and how it wires into an AI client — see MCP server (read-only). This page covers the operational side: how to configure it, which transport to run, how to deploy it in a container, and what its security posture is.

trueppm-mcp is a thin protocol adapter. It is not part of the API deployment — it runs as a separate process that talks to TruePPM only over HTTP via the public REST API, carrying a project API token as a bearer credential. It never imports Django, never touches the database or the ORM, and holds no privileged path: your role-based permissions are enforced exactly once, at the API layer, identically for this server and the web client.

There are two placement models, and the transport you choose follows from the placement:

  • Next to the AI client (stdio). The client — Claude Desktop, Cursor, Zed — spawns trueppm-mcp as a local subprocess and speaks MCP over the pipe. This is the primary model and the default transport. Nothing listens on a network port.
  • As a network service (HTTP or SSE). For a web-based or shared assistant that cannot spawn a local subprocess, run trueppm-mcp as a long-lived process (or container) that listens on a port. The AI client connects over Streamable HTTP or SSE.

The server authenticates with a project API token (tppm_<64-hex>), the same token type used for inbound task sync. Mint one from a project’s or program’s settings; the raw token is shown once, so copy it immediately. Only the SHA-256 digest is stored server-side.

Give the token the mcp:read scope when you mint it. That scope grants safe-method (GET) access to the viewsets the MCP wraps and is rejected at every write path — so even though the API also accepts an older unrestricted legacy:full token, an mcp:read token is the correct least-privilege credential for this server. A token is bound to a single project or a single program; it can read only what its role on that scope permits.

The server is configured entirely from the environment — there is no config file on disk, which keeps it spawnable as a subprocess with no state to manage.

VariableRequiredDescription
TRUEPPM_API_URLyesBase URL of your instance, e.g. https://ppm.example.com. The /api/v1 suffix is appended automatically if you omit it (and is idempotent if you include it).
TRUEPPM_API_TOKENyesA project API token (tppm_<64-hex>) with the mcp:read scope.

If either variable is missing or blank, the process exits immediately with exit code 2 and an actionable message on stderr — it names the absent variable and never echoes the token. On startup the server calls GET /api/v1/auth/me/ once to confirm the token authenticates, so a bad token fails the boot with a clear error rather than letting every query 401 later.

Select the transport with --transport. --host and --port apply only to the network transports and are ignored for stdio.

FlagTransportDefault host / portUse it when
--transport stdiostdio (default)The AI client spawns the server as a local subprocess.
--transport httpStreamable HTTP127.0.0.1:8000A web/shared assistant connects over HTTP.
--transport sseServer-Sent Events127.0.0.1:8000A client that only speaks the older SSE transport.
Terminal window
pip install trueppm-mcp
TRUEPPM_API_URL=https://ppm.example.com \
TRUEPPM_API_TOKEN=tppm_your_token_here \
trueppm-mcp # stdio; Ctrl-C to stop

For wiring stdio into claude_desktop_config.json, see the feature guide.

Terminal window
TRUEPPM_API_URL=https://ppm.example.com \
TRUEPPM_API_TOKEN=tppm_your_token_here \
trueppm-mcp --transport http --host 127.0.0.1 --port 8000

The default bind host is 127.0.0.1 — loopback only — so an accidental launch never exposes the server on a public interface. Bind to 0.0.0.0 only behind a reverse proxy that terminates TLS and controls access; the server itself speaks plain HTTP and holds a live credential, so it must never face the public internet directly.

The package ships a Dockerfile (packages/mcp/Dockerfile). It is a two-stage, non-root image with the Python build tools removed from the runtime layer, so it scans clean and runs as an unprivileged user (UID 1000). Build it from the package directory:

Terminal window
docker build -t trueppm-mcp packages/mcp/

Run it in stdio mode — pass -i so the client can drive it over the pipe:

Terminal window
docker run --rm -i \
-e TRUEPPM_API_URL=https://ppm.example.com \
-e TRUEPPM_API_TOKEN=tppm_your_token_here \
trueppm-mcp

Run it as a network service — publish the port and bind to 0.0.0.0 inside the container (the container boundary is the loopback equivalent here; still front it with a TLS-terminating proxy for anything beyond a private network):

Terminal window
docker run --rm -p 8000:8000 \
-e TRUEPPM_API_URL=https://ppm.example.com \
-e TRUEPPM_API_TOKEN=tppm_your_token_here \
trueppm-mcp --transport http --host 0.0.0.0 --port 8000

Pass the token via a Docker/Kubernetes secret or an env-file, never on the command line where it would land in shell history and the process table.

  • One enforcement point. Authorization is enforced by the API, identically for this server and the web client. The MCP process holds no privileged path and is not a second copy of the permission model. It can see nothing the token could not already read in the web client.
  • Read-only by scope, not just convention. The server defines only read tools and issues only GET requests, and an mcp:read token is rejected at every write path at the API layer. The two guarantees are independent — even a bug that added a write call would be refused by the token scope.
  • Least-privilege tokens. Scope each token to the single project or program the assistant needs, with mcp:read. Revoke it from the same settings screen the moment it is no longer needed; revocation takes effect immediately because every request re-checks the token.
  • No secret in logs. The token is never logged, never echoed in an error, and never included in a stack trace or a repr. Configuration errors name the missing variable, not its value.
  • Fail-closed boot. A token that does not authenticate fails startup with a clear message, so a misconfigured client never silently runs against the wrong instance.
  • Self-hosted. All traffic stays between your AI client, the server, and your own API. No third-party service is involved, and no plan or inference leaves your box.
  • Network exposure is opt-in. The default bind is loopback. Put any network transport behind a reverse proxy that terminates TLS; the server speaks plain HTTP and must never face the public internet directly.
SymptomLikely cause
Exit code 2, “Configuration error” on stderrTRUEPPM_API_URL or TRUEPPM_API_TOKEN is unset or blank.
Boot fails with “the TruePPM API rejected the configured token (HTTP 401)“The token is missing, malformed, or revoked. Mint a fresh mcp:read token.
A tool returns a 404 for a resource you expect to seeThe token’s role on its project/program does not permit reading that resource — 404 is the deliberate existence oracle, identical to the web client.
A tool that used to work now errors on a writeAn mcp:read token is refused at write paths by design; this server issues no writes, so this indicates a misrouted call, not a permission gap to widen.