Skip to main content

constellation MCP server

The constellation MCP (Model Context Protocol) server exposes the same operations as the pt CLI but as MCP tools — so MCP-native agent runtimes (Claude Desktop, Claude Code with .mcp.json, Cursor, Codex, ChatGPT custom connectors) can call them as part of a normal turn instead of as one-shot subprocesses.

Not working in the monorepo?

Use the hosted connector instead — no checkout, no token, no local server. Point your client at https://mcp.planetb2b.com/api/mcp and sign in; mcp.planetb2b.com has the per-client steps (INF-349).

A one-command installer (npx @planetb2b/mcp-connect, INF-350) is written but not yet published to npm, so that command does not resolve today — follow the per-client steps on the connect page.

The rest of this page covers the local stdio server, which is what you want when you are developing the MCP server or working against a local stack.

Renamed (PLT-107)

The server identity was constellation-pt before the wiki tools were merged in. Update the server key in your config to constellation (examples below) — a one-time per-developer change. Setting the optional WIKI_BASE_URL env var additionally registers the 24 wiki tools (list_spaces, search_pages, get_page, traverse_dependencies, rebuild_space_index, …). The hosted Streamable-HTTP endpoint keeps its existing URL until a separate coordinated infra rename.

The wire protocol is stdio JSON-RPC for desktop / CLI agents and Streamable HTTP for ChatGPT. Backend: same REST API as the CLI.

The full source-of-truth setup notes live in tools/mcp-server/README.md. The summaries below are extracted for the most common setups.

For interactive developer machines, skip static API keys entirely:

export PT_BASE_URL=https://constellation.planetb2b.com/projects # required: keys the profile + derives the Directory origin
npx pt login # opens the browser to the Directory consent screen (loopback + PKCE)

Credentials land in a user-level store (~/.config/constellation/credentials.json, 0600) and auto-refresh for ~30 days per consent — a server-side secret rotation no longer breaks your setup, and fresh git worktrees need no token copy. pt whoami shows the authenticated user; pt logout revokes the grant server-side and clears the store.

Register the MCP server user-scoped (once per machine), running the command from your main checkout, so it survives the worktree-per-task workflow:

claude mcp add constellation -s user \
-e PT_BASE_URL=https://constellation.planetb2b.com/projects \
-e WIKI_BASE_URL=https://constellation.planetb2b.com/wiki \
-- npx -y tsx "$(pwd)/tools/mcp-server/src/index.ts"

⚠ "$(pwd)/…" expands at registration time, pinning your main checkout's absolute path into the stored command. A relative path would resolve against each session's cwd — broken in a fresh worktree before npm install, and broken outside the repo entirely. Pinned, the main checkout's node_modules serves every session and a fresh worktree needs no per-worktree step for the MCP.

⚠ Pass both env vars in the registration itself. A user-scoped server is launched outside any worktree's .env.local, so without PT_BASE_URL the server exits at startup, and without WIKI_BASE_URL it starts but silently registers no wiki tools. (Inside the repo these are usually picked up from the root .env/.env.local, which is why a first run can look fine and a fresh worktree then fail.)

Legacy shadowing

Claude resolves MCP servers local → project → user. If your repo checkout still has a project-scoped constellation entry in .mcp.json, it shadows the user-scoped one and its static token wins — remove that entry after migrating. pt login warns when it detects this.

For Codex CLI, keep the ~/.codex/config.toml entry below but omit the PT_AUTH_TOKEN line — the credential store supplies auth. CI and headless CLI/stdio environments use a SERVICE-principal PT_AUTH_TOKEN issued through the operator path below; it always takes precedence over the store. Hosted Streamable HTTP clients use connector OAuth once audience enforcement is enabled.

CI/headless automation: get a SERVICE-principal API key​

The pt CLI, the stdio MCP server, and direct PT-zone API calls accept a static PT auth token when browser consent is impossible. New automation on those surfaces must use a dedicated SERVICE principal with its reviewed integration grant, not a key self-issued by an employee:

  1. Provision the registered integration profile if it does not already exist.
  2. As an operator holding api_key.issue_for_service, issue on behalf of the target SERVICE user through POST /api/auth/api-keys/service.
  3. Store the one-time response as PT_AUTH_TOKEN in the automation's secret store.

The complete provisioning, issuance, rotation, and revocation procedure is in CI credentials and expiry. SERVICE keys have a fixed 90-day TTL and are attributable to the integration. The employee Settings page and plain POST /api/auth/api-keys endpoint self-issue for the signed-in human; they do not create a SERVICE key and must not be used for new automation.

These Directory keys are not MCP OAuth access tokens: they carry aud=constellation-platform, not aud=MCP_RESOURCE_ID. A hosted HTTP connector accepts them only during the temporary OAUTH_ENFORCE_AUD=false rollout. Once the documented strict state is enabled, /api/mcp rejects every raw Directory employee or SERVICE key; hosted automation must use connector OAuth or a future MCP-audience service-credential flow.

Employee self-issuance remains temporarily supported only for ADR-037's two enumerated compatibility clients: the existing hosted-Claude PT_AUTH_TOKEN hook and an existing manual-Bearer client that cannot complete OAuth. Those HTTP paths work only while OAUTH_ENFORCE_AUD=false and must migrate to OAuth before strict enforcement; PLT-1125 owns that migration and the deletion condition. Their setup is documented in the canonical MCP server README; this is not an onboarding path for another integration.

Ending an npx pt login session​

The same page carries an Active CLI sessions card, one entry per active npx pt login — that is, per refresh-token family. No machine identifier is stored, so two logins from the same machine are two entries. Each shows when it signed in, when it last refreshed, and when it expires (30 days from consent — refreshing does not extend it).

End session revokes that session's whole refresh-token family, not just the short-lived bridge token it happens to be holding. That distinction is the point: revoking only the access token left the family alive, so the CLI minted a replacement at its next refresh. Ending the session also revokes the outstanding access token, so that machine normally stops working within the same ~30 seconds rather than keeping access for the rest of that token's lifetime — one hour by default, up to 24 hours where MCP_PT_TOKEN_TTL has been raised to its ceiling.

One exception, and the UI says so when it applies: a session whose access-token rows predate migration 035 carries no cli_family_id, so there is nothing to link them and only the family is revoked. The session cannot refresh again, but a token it was already issued keeps working until it expires on its own. That population drains within one token lifetime of the deploy.

Other sessions are unaffected — each npx pt login is its own family. The machine whose session you ended must run npx pt login again; there is no way to resume a revoked family.

The machine-minted pt-cli-oauth rows in the key list above are those sessions' bridge tokens. Revoking one there does not end the session — use this card.

Claude Code (.mcp.json)​

The user-scoped claude mcp add above is the preferred path. If you instead use a project-scoped .mcp.json at the monorepo root (gitignored — per-developer), omit any auth field — after npx pt login the stdio server reads the credential store:

{
"mcpServers": {
"constellation": {
"command": "npx",
"args": ["tsx", "tools/mcp-server/src/index.ts"],
"env": {
"PT_BASE_URL": "https://constellation.planetb2b.com/projects",
"WIKI_BASE_URL": "https://constellation.planetb2b.com/wiki"
}
}
}
}
Omitting the auth field is not enough on its own

Run from the repo, the server loads root .env/.env.local before resolving auth, and a PT_AUTH_TOKEN/PT_AUTH_COOKIE from there takes precedence over the credential store. So a leftover value in root .env/.env.local — or a project-scoped .mcp.json still carrying a static token (Claude resolves local → project → user) — silently keeps using the stale credential even with a token-free config. To rely on the store: clear PT_AUTH_TOKEN and PT_AUTH_COOKIE from root .env/.env.local (and any .mcp.json auth field), then confirm with npx pt whoami — it must report "via the pt login credential store". New CI/headless automation uses a SERVICE-principal token from the operator issuance path; PT_AUTH_COOKIE is local/mock-development only.

For local dev against localhost:3002 against a logged-in browser / mock-auth session, add a PT_AUTH_COOKIE you grab from your browser dev tools:

{
"env": {
"PT_BASE_URL": "http://localhost:3002",
"PT_AUTH_COOKIE": "constellation-auth=<paste value here>"
}
}

Claude Code picks up the change next session.

Claude Desktop​

Claude Desktop is not configured by claude mcp add (that writes Claude Code's config) — edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or the equivalent on your platform. Run npx pt login once, then use the same store-backed config, with no auth field (the stdio server reads the credential store):

{
"mcpServers": {
"constellation": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/constellation/tools/mcp-server/src/index.ts"],
"env": {
"PT_BASE_URL": "https://constellation.planetb2b.com/projects",
"WIKI_BASE_URL": "https://constellation.planetb2b.com/wiki"
}
}
}
}

Note the absolute path — Claude Desktop runs from $HOME, not the repo root. Restart Claude Desktop after editing. A CI/headless-style deployment may add only a SERVICE-principal PT_AUTH_TOKEN from the operator issuance path.

OpenAI Codex CLI​

Codex CLI reads MCP servers from ~/.codex/config.toml (TOML, not JSON):

[mcp_servers.constellation]
command = "npx"
args = ["tsx", "/absolute/path/to/constellation/tools/mcp-server/src/index.ts"]
env = { PT_BASE_URL = "https://constellation.planetb2b.com/projects", WIKI_BASE_URL = "https://constellation.planetb2b.com/wiki" }

No PT_AUTH_TOKEN line — after npx pt login the credential store supplies auth. A CI/headless process that cannot complete login uses a SERVICE-principal token from the operator issuance path. Restart any running Codex sessions after editing.

Smoke test. Ask Codex something like "use the constellation MCP to list my IN_PROGRESS tasks across all five projects" — it should call get_workspace_context and the per-project list_tasks tools without further prompting.

Cursor / Aider / other stdio MCP hosts​

The same pattern applies — command + args + env, with whatever syntax that runtime's config uses (JSON for most, TOML for Codex). The repo-relative path works only for hosts that run from the repo root; otherwise use an absolute path. See tools/mcp-server/README.md for the canonical reference.

ChatGPT custom connector (manual-Bearer compatibility)​

The MCP server speaks Streamable HTTP in addition to stdio, so ChatGPT's Developer Mode custom connectors can talk to it directly — no local bridge required.

Prefer the hosted OAuth connector described at the top of this page. The manual-Bearer flow below is retained only for an existing client that cannot complete OAuth and only while the deployment still has OAUTH_ENFORCE_AUD=false; it uses ADR-037's enumerated employee-key compatibility path until PLT-1125 migrates it. It stops working when strict audience enforcement is enabled. Do not use it for new automation.

  1. Obtain the compatibility token by following the employee self-issuance subsection in the canonical MCP server README.
  2. Add the connector: in ChatGPT, open Settings → Connectors → Advanced → Developer mode → Add custom MCP.
  3. Fill in the URL (https://mcp.planetb2b.com/api/mcp — the hosted MCP service is its own Vercel deployment, separate from PT_BASE_URL), authentication (Bearer token, paste the JWT), and a name.
  4. Save and test in a new conversation.

Full ChatGPT setup walkthrough including the URL path and authentication mode is in tools/mcp-server/README.md.

OAuth connector scopes (read / write)​

Connector sessions authenticated via the one-click OAuth flow (Claude.ai custom connectors, ChatGPT OAuth mode) are scoped by the OAuth grant, not by the API key:

Coarse OAuth grants are a bounded compatibility path

The hosted omitted-scope flow and local pt login currently both grant coarse read write; neither is a least-privilege personal-agent credential. PLT-1125 owns capability-specific selection, enforcement, migration, and removal of this exception. Do not copy the coarse default into a new client contract.

  • Default grant is read write (INF-279): when the client omits the OAuth scope parameter — Claude's connector client does in practice — the connector grants full read + write, so mutation tools (create_task, update_task, resolve_issue, update_page, …) work out of the box. The 401 challenge also advertises scope="read write" so spec-following clients request it explicitly.
  • Write means every mutation tool, including destructive ones (delete_project, delete_space, …). The Directory consent screen discloses this before you click Allow.
  • A client that explicitly requests scope=read stays read-only: the per-token scope gate (INF-215) hides mutation tools from tools/list and rejects them on tools/call.
  • Writes act as you: the connector forwards a Directory-issued token carrying your own PT roles, so every mutation is authorised by your real permissions and attributed to your user in the PT audit trail.

While OAUTH_ENFORCE_AUD=false, a manual-Bearer API key carries no OAuth scope and exposes all tools, gated by the holder's PT roles. With OAUTH_ENFORCE_AUD=true, the hosted endpoint rejects that Directory key because it is not bound to MCP_RESOURCE_ID; use connector OAuth instead.

OAuth connector sessions and re-login​

Connector sessions renew themselves: an access token lasts 60 minutes on the hosted production connector, and every refresh rotates the refresh token. Presenting a refresh token that was already rotated is treated as theft and ends the session, so the client has to log in again. The exception is a re-presentation within OAUTH_REFRESH_REUSE_GRACE_SECONDS (default 60 s) of the rotation, which the connector answers as a concurrent refresh by the same client; before INF-655, a client that refreshed twice at once was logged out. Two client processes that each keep their own copy of one session still collide: whichever refreshes with its stale copy after the window ends the session. Revoking an already-rotated refresh token also ends the whole session. Details and the security trade-off are in the connector README's refresh-token rotation section.

What's exposed as MCP tools​

The MCP server surfaces the same operations as the CLI subcommands, with MCP-style names (snake_case instead of kebab-case). Most rows map directly; exceptions are noted in the table (get_initiative_summary has two CLI spellings, pt release-kb-curate has no MCP equivalent), and MCP-only tools with no CLI subcommand at all are listed below the table.

CLI subcommandMCP tool name
pt workspaceget_workspace_context
pt find-projectfind_project
pt list-projectslist_projects
pt get-project-overviewget_project_overview
pt create-projectcreate_project
pt update-projectupdate_project
pt delete-projectdelete_project
pt initiative / pt get-initiative-summaryget_initiative_summary
pt list-initiativeslist_initiatives
pt create-initiativecreate_initiative
pt update-initiativeupdate_initiative
pt delete-initiativedelete_initiative
pt list-taskslist_tasks
pt list-epic-childrenlist_epic_children
pt get-taskget_task
pt create-taskcreate_task
pt update-taskupdate_task
pt mark-awaiting-decisionmark_awaiting_decision
pt answer-decisionanswer_decision_request
pt cancel-decisioncancel_decision_request
pt claim-next-readyclaim_next_ready_task
pt create-task-linkcreate_task_link
pt delete-task-linkdelete_task_link
pt list-issueslist_issues
pt get-issueget_issue
pt create-issuecreate_issue
pt update-issueupdate_issue
pt assign-issue(subsumed into update_issue)
pt escalate-issue(subsumed into update_issue)
pt transition-issuetransition_issue
pt resolve-issueresolve_issue
pt list-stageslist_stages
pt consultconsult_coordinator
pt query-kbquery_knowledge_base
pt release-kb-curate(no MCP equivalent — CLI-only convenience wrapper; see release-and-migrations)

The MCP side additionally exposes create_stage, reorder_stages, find_user, and resolve_key with no CLI equivalent.

pt list-tasks's flow filters map to boolean list_tasks arguments of the same name: --ready → ready, --review → review, and --awaiting-decision (PT-905, the Decisions queue) → awaiting_decision. All three are mutually exclusive on both surfaces.

When WIKI_BASE_URL is set, 29 additional wiki tools are registered (no CLI equivalent). They talk to the wiki module over its REST API only — the same Directory-issued token works across zones:

  • Spaces — list_spaces, get_space, create_space, update_space, delete_space, decommission_space
  • Pages — search_pages, get_page, list_child_pages, create_page, update_page
  • Links & dependencies — list_page_links, link_pages, traverse_dependencies
  • Attachments — get_attachment
  • Maintenance — rebuild_space_index, append_space_log
  • Lint (honesty loop) — lint_space, list_lint_findings, record_lint_finding, resolve_lint_finding
  • Source ingestion — ingest_source, retract_source, revert_ingest_batch
  • Declassification approvals — list_declassification_approvals, get_finding_declassification_context, grant_declassification_approval, amend_declassification_approval, revoke_declassification_approval

Addressing a task by key​

Every tool and CLI command that takes a task accepts either its UUID or its key (INF-583, PLT-12). A UUID is used as given; a key is resolved first, and two properties of that resolution are worth knowing.

Resolution is scoped to the project you name. A key belonging to a different project is reported as not found in that project, not silently resolved elsewhere — task keys are unique per organisation, so the project you pass is what disambiguates which one you meant. resolve_key is the tool for the opposite question: given a bare key and no project, which entity is it?

A key that has been superseded still resolves (INF-583). Moving a task between projects re-keys it and leaves a redirect behind, and key resolution follows that redirect. So an old key whose task now lives in the project you asked about resolves to that task, rather than failing as it did before — the same behaviour resolve_key has always had. If you need to be certain which task you are acting on, pass the UUID: it is never redirected.

The project scope is not relaxed by the redirect. A superseded key whose task moved to a different project than the one you named still does not resolve there.

The redirect does not reach across organisations. Key resolution runs in your active organisation, so when you reach a project through a collaborator grant from another organisation, a superseded key for it still fails — only its current key works. That is unchanged from before, not a new restriction; pass the UUID if you have it.

Tool arguments are strict — a misnamed parameter is an error, not a no-op​

Every tool rejects arguments it does not declare. Passing an undeclared key fails the call, names the offending key, and the handler does not run.

The failure arrives as an ordinary tool result carrying isError: true — not as a JSON-RPC error response — so a client sees it the same way it sees any other tool failure, in the result text:

MCP error -32602: Input validation error: Invalid arguments for tool create_page: Unrecognized key(s) in object: 'content'

The MCP error -32602 prefix is part of that text, not a transport-level error code: the server catches the InvalidParams exception and reports it as tool output.

This matches what each tool has always advertised in tools/list (additionalProperties: false) — the server previously stripped undeclared keys and returned success instead of enforcing it. The practical consequence of that leniency: create_page given content: where the schema declares bodyMd: returned a real page id for a page published with an empty body, and nothing in the response indicated anything was wrong.

So when a call fails this way, read the key name in the error rather than retrying — the parameter exists under a different name. The common ones on create_page / update_page are bodyMd (not content or body), parent (not parentId), and pageType (not type); frontmatter is an object, not a JSON string.

See also​