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.
Where it runs
Section titled “Where it runs”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-mcpas 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-mcpas a long-lived process (or container) that listens on a port. The AI client connects over Streamable HTTP or SSE.
Prerequisites: mint a scoped token
Section titled “Prerequisites: mint a scoped token”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.
Environment configuration
Section titled “Environment configuration”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.
| Variable | Required | Description |
|---|---|---|
TRUEPPM_API_URL | yes | Base 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_TOKEN | yes | A 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.
Transports
Section titled “Transports”Select the transport with --transport. --host and --port apply only to the
network transports and are ignored for stdio.
| Flag | Transport | Default host / port | Use it when |
|---|---|---|---|
--transport stdio | stdio (default) | — | The AI client spawns the server as a local subprocess. |
--transport http | Streamable HTTP | 127.0.0.1:8000 | A web/shared assistant connects over HTTP. |
--transport sse | Server-Sent Events | 127.0.0.1:8000 | A client that only speaks the older SSE transport. |
stdio (default)
Section titled “stdio (default)”pip install trueppm-mcpTRUEPPM_API_URL=https://ppm.example.com \TRUEPPM_API_TOKEN=tppm_your_token_here \ trueppm-mcp # stdio; Ctrl-C to stopFor wiring stdio into claude_desktop_config.json, see the
feature guide.
HTTP / SSE
Section titled “HTTP / SSE”TRUEPPM_API_URL=https://ppm.example.com \TRUEPPM_API_TOKEN=tppm_your_token_here \ trueppm-mcp --transport http --host 127.0.0.1 --port 8000The 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.
Docker
Section titled “Docker”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:
docker build -t trueppm-mcp packages/mcp/Run it in stdio mode — pass -i so the client can drive it over the pipe:
docker run --rm -i \ -e TRUEPPM_API_URL=https://ppm.example.com \ -e TRUEPPM_API_TOKEN=tppm_your_token_here \ trueppm-mcpRun 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):
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 8000Pass 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.
Security posture
Section titled “Security posture”- 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
GETrequests, and anmcp:readtoken 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.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause |
|---|---|
Exit code 2, “Configuration error” on stderr | TRUEPPM_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 see | The 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 write | An 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. |