Skip to content

MCP server (read-only)

TruePPM ships a read-only Model Context Protocol server, trueppm-mcp, that lets any MCP client — Claude Desktop, Cursor, Zed, and the like — ask real questions of your self-hosted instance: the critical path, a Monte Carlo slip forecast, sprint status, the risk register, My Work. Answers are computed server-side by the same CPM/Monte Carlo engine the web UI uses, never guessed by a model, and nothing leaves your box.

The MCP server is the first place you feel TruePPM’s AI principle — computed, not guessed. Every incumbent bolts an LLM onto a project database and lets the model guess dates; here the model only translates your question into an engine call, and the CPM/Monte Carlo engine supplies the number. The answer is a computation with a derivation, not a language model’s opinion. See Computed, not guessed for the full architectural picture.

trueppm-mcp is a thin protocol adapter. It runs next to your AI client — typically as a local subprocess the client spawns — and talks to TruePPM only over HTTP via the public REST API, carrying your API token as a bearer credential. It never touches the database or the ORM, so your role-based permissions are enforced exactly once, at the API layer. The server can see nothing you could not already read in the web client with the same token.

stdio (MCP)

HTTPS + BearerGET /api/v1/…

AI client(Claude etc.)

trueppm-mcpsubprocess

TruePPM API(self-hosted)

The server is configured entirely from the environment — no config file on disk. It authenticates with a personal access token (tppm_<64-hex>) carrying the mcp:read scope. The read surface accepts only owner-scoped (personal) tokens — a project- or program-scoped token is rejected here — so the credential reads only what your role permits and nothing beyond it.

The fastest way to connect: Personal Settings → API tokens → Create token, then choose the “Read-only for AI assistants” scope (mcp:read) and set an expiry (required for mcp:read). The reveal dialog shows the raw token once, plus a ready-to-paste claude_desktop_config.json snippet built from it — copy that straight into your client and skip the manual assembly below.

VariableRequiredDescription
TRUEPPM_API_URLyesBase URL of your instance, e.g. https://ppm.example.com (the /api/v1 suffix is added automatically if omitted)
TRUEPPM_API_TOKENyesA personal access token (tppm_<64-hex>) with the mcp:read scope and an expiry

Install from PyPI and run it as a local subprocess (the primary, stdio transport):

Terminal window
pip install trueppm-mcp
TRUEPPM_API_URL=https://ppm.example.com \
TRUEPPM_API_TOKEN=tppm_your_token_here \
trueppm-mcp # stdio (default); Ctrl-C to stop

On startup the server calls GET /api/v1/auth/me/ once to confirm the token authenticates, so a bad token fails the boot immediately with a clear message rather than letting every query fail later.

stdio is the primary transport: the AI client launches the server as a subprocess and speaks MCP over the pipe. If you minted an mcp:read token from the Personal Settings → API tokens page above, paste its generated snippet and you’re done. Otherwise, add an entry to claude_desktop_config.json by hand:

{
"mcpServers": {
"trueppm": {
"command": "trueppm-mcp",
"env": {
"TRUEPPM_API_URL": "https://ppm.example.com",
"TRUEPPM_API_TOKEN": "tppm_your_token_here"
}
}
}
}

Restart the client; it will spawn trueppm-mcp on demand. For the HTTP/SSE transports, Docker deployment, token scopes, and the full security posture, see the MCP server administration guide.

The server registers 18 read-only tools, each mapping to one existing REST endpoint and returning only what your role permits. Results are compacted for an LLM context budget: empty and null fields are omitted, long free-text fields are truncated (with a "truncated": true marker), and project/program results carry a caller_role field — your own authoritative role, passed straight through from the API.

Every list_* tool returns an { "items": [...], "total_count": N } envelope rather than a bare list. The API paginates at 50 rows per page, so the server follows the pages for you up to a 1,000-row cap; if more rows still exist than were returned, the envelope adds "truncated": true so the assistant knows it is looking at a partial set and should narrow the query (a filter, a smaller scope) rather than reason over an incomplete list.

ToolArgumentsReturns
list_projectsEvery project you can read, each with your caller_role.
get_projectproject_idFull project metadata and a health overview, with caller_role.
list_programsEvery program you can read, each with your caller_role.
get_program_healthprogram_idRollup health for one program (single-program; cross-program rollups are Enterprise).
list_program_backlogprogram_idA program’s backlog intake pool — items ranked by priority, with type, status, story points, and whether each has been pulled into a task. Single-program only.
ToolArgumentsReturns
list_tasksproject_id, and optional status, assignee, sprint, is_critical, type, updated_after (alias since)A project’s tasks, filtered and compacted.
get_tasktask_idFull detail for one task (dates, assignee, acceptance criteria, sprint), plus a compact why — whether the task is on the critical path and how much total float it has.
get_board_stateproject_idThe board’s columns and their task cards for one project.
list_my_workYour assigned tasks across every project you belong to.
ToolArgumentsReturns
get_schedule_summaryproject_idCPM finish, Monte Carlo P50/P80/P95, SPI, and the critical-task count, plus a compact why citing the CPM finish and how many critical-path tasks drive it.
get_monte_carlo_forecastproject_idThe latest persisted Monte Carlo run (P50/P80/P95, cpm_finish, delta). Read-only — never triggers a new simulation. Carries a compact why — the P80 risk premium over the CPM finish and the single largest duration-sensitivity driver.
get_release_forecastproject_idBacklog delivery forecast from the team’s velocity Monte Carlo: P50/P80 sprint counts and calendar dates to clear the committed backlog (plus P95 date). Always a range, never a single date; returns a warming_up shape when velocity history is thin.
whatifproject_id, task_id, one of duration_delta / new_duration, optional n_simulationsWhat breaks if this task’s duration changes. Perturbs one task and recomputes CPM + Monte Carlo in memory, persisting nothing. Returns current vs. whatif P50/P80/P95, the deterministic CPM finish for each, critical_path_changed, and delta_vs_current (signed calendar-day shifts, positive = later/worse), plus a compact why.
get_schedule_derivationproject_id, task_id, quantityThe server-computed why behind a value in full: the driving predecessor/successor, the binding constraint, lag and calendar contributions, and which pass set it. quantity is a CPM value (early_start, early_finish, late_start, late_finish, total_float, free_float) or a Monte Carlo percentile (p50, p80, p95). The primary answer tools above carry a one-line why; call this for the complete contribution chain.
list_risksproject_idThe project’s risk register (impact, probability, status).
ToolArgumentsReturns
list_sprintsproject_idThe project’s sprints (health bands and aggregates only — no per-person velocity).
get_sprintsprint_idOne sprint with its project’s health band (aggregates only).
ToolArgumentsReturns
whoamiThe identity behind your configured token — a quick connection check.

Once connected, ask your assistant natural-language questions and it will pick the right tool:

  • “Which of my projects are behind their P80 forecast?”
  • “Show me the critical path for the Apollo project and how much slack the near-critical tasks have.”
  • “What’s on my plate this sprint?”
  • “List the open high-impact risks for the Mercury program.”
  • “What breaks if I slip the integration task 5 days?"

"What breaks if I slip this task 5 days?”

Section titled “"What breaks if I slip this task 5 days?””

This is the tool no metered-connector agent can answer — it needs a scheduling engine, not a database read. Ask it in plain language and your assistant chains the tools for you:

  1. list_tasks (or get_task) to resolve the task you named to its task_id.
  2. whatif with that task_id and duration_delta: 5.

The engine runs a baseline and a perturbed pass, then answers with computed numbers — not a guess:

Slipping “Integration testing” by 5 days pushes the P80 finish from 2026-09-15 to 2026-09-22 (+7 calendar days) and the deterministic CPM finish by the same. critical_path_changed: true — the slip pulls the “Data migration” task onto the critical path, so it is now the one to watch.

whatif persists nothing: it writes no rows, caches nothing, and enqueues no recompute. Run it as many times as you like to compare options — pass duration_delta: -2 to see the effect of pulling a task in, or new_duration to set an absolute duration. It is reachable by any mcp:read token because it is a pure read/compute modeled as a GET.

  • 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.
  • No secret in logs. The token is never logged, never echoed in an error, and never included in a stack trace.
  • A refusal says why. When the API refuses a call, the 401/403 carries a structured refusal — a verdict, a coarse reason (identity if the credential was rejected, policy if a guard denied the call), and, when it is safe to name, the specific constraint that fired. The server surfaces it on the error it raises, so the reason reaches your AI client instead of a bare HTTP status. Two constraints are disclosed today, token_identity and capability_scope; both describe your own token and reveal nothing about the plan. Constraints that would name a resource you are not allowed to read are deliberately withheld — you still get the reason, just not the detail that would leak.
  • Read-only. The server defines only read tools and issues only GET requests. The write surface is held to a later release.
  • Self-hosted. All traffic stays between your AI client, the server, and your own API — no third-party service is involved.
  • Tool output is framed as untrusted data. Free-text fields (task descriptions, notes, risk mitigations, and the like) are wrapped in <untrusted-content> markers, and the server tells the connecting model explicitly that everything a tool returns is project data — never an instruction to follow, however it’s phrased. This is a framing mitigation for indirect prompt injection from user-authored content, not a content filter; it doesn’t sanitize text, and a well-behaved client and model are still required to honor it.