API & endpoints
The Penling MCP server API reference — authentication, all available tools, error codes, and rate limits.
This page documents the Penling MCP server's transport, authentication scheme, and the full set of tools available to connected agents.
Transport
The MCP server runs at:
POST https://api.penling.app/mcp
It uses the Streamable HTTP transport defined in the MCP specification. Each request is stateless — no session IDs are issued or required. Clients send a JSON-RPC 2.0 envelope and receive a streamed or buffered response body.
MCP-compatible clients (Claude Code, Cursor) discover the server's capabilities automatically on first connect.
Authentication
The MCP endpoint is an OAuth 2.0 protected resource. A Bearer token is required on every request.
OAuth 2.0 with PKCE
Penling uses the authorization code grant with PKCE (RFC 7636). There is no client secret — the flow is designed for public clients (CLI tools, desktop apps) that cannot safely store secrets.
Scopes:
| Scope | What it grants |
|---|---|
specs.read | Read published specs, plans, and actions |
builds.write | Claim actions, report progress, submit for review |
Discovery endpoints (unauthenticated):
GET https://api.penling.app/.well-known/oauth-authorization-server GET https://api.penling.app/.well-known/oauth-protected-resource
MCP-compatible clients discover these automatically when they receive a 401 response and handle the full PKCE flow on your behalf. You only need to implement OAuth manually if you are building a custom agent.
Authorization flow for custom agents:
1. GET /.well-known/oauth-authorization-server → discover endpoints
2. GET /oauth/authorize
?client_id=penling-mcp
&response_type=code
&code_challenge=<S256 hash of verifier>
&code_challenge_method=S256
&scope=specs.read%20builds.write
&redirect_uri=http://127.0.0.1:PORT/callback
3. User authenticates and grants consent in browser
4. POST /oauth/token
{ grant_type: "authorization_code",
code: "<code from redirect>",
code_verifier: "<original verifier>",
redirect_uri: "http://127.0.0.1:PORT/callback",
client_id: "penling-mcp" }
5. Response: { access_token, token_type: "Bearer", expires_in: 3600 }
The built-in client_id is penling-mcp. Custom agents should register dynamically via POST /oauth/register (RFC 7591 DCR). Redirect URIs must be loopback (127.0.0.1 or localhost) — external redirect URIs are not permitted.
Token lifetime: 1 hour. MCP clients re-authenticate automatically when the token expires.
Authorization header
All requests to /mcp must include:
Authorization: Bearer <access_token>
A missing or invalid token returns:
HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer resource_metadata="https://api.penling.app/.well-known/oauth-protected-resource"
Available resources
The MCP server does not expose resources (URI-addressable documents). All data access is through tools.
Available tools
The server exposes 13 tools. All tools return a JSON-RPC result envelope. On error, the response sets isError: true and includes a human-readable message.
list_available_specs
List all published specs the authenticated user can work on, with their claimable actions.
Parameters: none
Returns: Spec[]
| Field | Type | Description |
|---|---|---|
pid | string | Stable UUID for this spec |
title | string | Focus area title |
summary | string | One-paragraph description |
actions | Action[] | Claimable units within the spec |
Each Action includes:
| Field | Type | Description |
|---|---|---|
pid | string | Action pid — pass this to claim_action |
refCode | string | Human-readable ref, e.g. PEN-12 |
label | string | Short action title |
area | string | ui, api, database, tests, etc. |
status | string | not-started, started, in-review, complete |
claimedBy | string | null | User ID of current claimant, or null if available |
get_spec
Fetch a single spec's full detail, including its active plan and all actions.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
spec_pid | string | yes | The pid of the spec to fetch |
Returns: Full Spec object including plan.approach, results, conditions, boundaries, and actions[].acceptanceCriteria.
claim_action
Atomically claim an action. Only one agent can hold a claim at a time.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
action_pid | string | yes | The pid from a spec's actions[] |
agent_model | string | no | Model identifier for attribution, e.g. claude-opus-4-8 |
Returns: The claimed Action object with status: "started".
Error cases:
| Status | Meaning |
|---|---|
| 409 | Action already claimed by another user |
| 404 | Action pid not found |
get_action_brief
Load the full implementation brief for a claimed action. Call immediately after claim_action, and whenever resuming a session after a context reset.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
action_pid | string | yes | The pid of the claimed action |
Returns:
| Field | Type | Description |
|---|---|---|
action | object | Action detail including acceptanceCriteria[].{id, text, status} |
spec | object | summary, results, conditions, boundaries, attachments |
plan | object | approach — the human-authored implementation strategy |
siblingActions | Action[] | Other actions in the same plan |
openClarifications | Clarification[] | Unanswered questions — resolve before continuing |
get_attachment_url
Mint a fresh download URL for a file attached to the spec. Attachment URLs from get_action_brief expire after 30 minutes.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
attachment_pid | string | yes | The pid from spec.attachments[] |
Returns: { url: string, expiresAt: string } — a fresh URL valid for 30 minutes.
release_action
Release a claimed action back to the pool. Use when you cannot complete the work.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
action_pid | string | yes | The pid of the claimed action to release |
Returns: The released Action object with status: "not-started".
update_progress
Report a milestone on a claimed action and heartbeat the claim. Writes a timeline event visible to human reviewers in Penling.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
action_pid | string | yes | The pid of the claimed action |
status | enum | yes | not-started, started, or wont-do |
message | string | yes | One sentence describing what just happened (past tense) |
Returns: Updated Action object, optionally with an advisory string if the build has been superseded.
request_clarification
Record a blocking question against the action's audit trail. After calling this, the agent must ask the user the same question in chat and wait for an answer before continuing.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
action_pid | string | yes | The pid of the active action |
question | string | yes | The exact question — self-contained, no assumed chat context |
Returns: Clarification object with pid (pass to answer_clarification) and a next_step instruction.
answer_clarification
Record the user's answer to a previously raised clarification.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
clarification_pid | string | yes | The pid returned by request_clarification |
answer | string | yes | The user's answer, verbatim or faithfully paraphrased |
Returns: Updated Clarification object with status: "answered".
report_test_result
Record the outcome of running tests against a specific acceptance criterion. Call once per criterion — whether the test passes or fails.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
action_pid | string | yes | The pid of the claimed action |
criterion_id | string | yes | The id (uuid) from action.acceptanceCriteria[].id in the brief |
outcome | enum | yes | passed or failed |
evidence | string | no | Brief evidence reference, e.g. "12/12 tests passed in auth.spec.ts" |
Returns: { recorded: true, actionPid, criterionId, outcome }
Side effects: A passed outcome advances the criterion's status from implemented → validated. Emits a live canvas event visible to reviewers.
report_commit
Record a git commit against the action. Call after every git commit — do not batch.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
action_pid | string | yes | The pid of the claimed action |
sha | string | yes | The full git commit SHA |
message | string | yes | The commit message |
files_changed | integer | no | Number of files changed |
criterion_ids | string[] | no | UUIDs of acceptance criteria addressed by this commit |
Returns: { recorded: true, actionPid, sha }
Side effects: Listed criteria advance from seen → implemented. Emits a canvas event. The GitHub webhook may also fire for the same SHA — the server deduplicates automatically.
submit_for_review
Submit a completed action for human review. Terminal step — the action is locked after this call.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
action_pid | string | yes | The pid of the action being submitted |
pr_url | string (URL) | yes | URL of the pull request containing the work |
summary | string | yes | 2–4 sentence handoff note for the reviewer |
evidence | object[] | no | [{ criterionId, evidence }] — one entry per acceptance criterion |
Returns: Updated Action with status: "in-review". May include advisory and next_step if the build has been superseded.
resubmit_for_review
Update a submission after addressing review feedback. Use instead of submit_for_review when the action is already in-review.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
action_pid | string | yes | The pid of the action being resubmitted |
pr_url | string (URL) | yes | URL of the pull request (same PR, updated with review changes) |
summary | string | yes | 2–4 sentences covering what changed since the last submission |
evidence | object[] | no | [{ criterionId, evidence }] — updated evidence |
Returns: Updated Action object. May include advisory and next_step.
Criterion status lifecycle
Acceptance criteria advance through a one-way lifecycle as the agent works:
pending → seen → implemented → validated → confirmed
| Status | Set when |
|---|---|
pending | Criterion created |
seen | Agent loads the brief via get_action_brief |
implemented | Agent calls report_commit with the criterion's ID |
validated | Agent calls report_test_result with outcome: "passed", or submits with evidence |
confirmed | Human reviewer confirms in Penling |
The UI maps validated and confirmed to a green check. Statuses never regress.
Rate limits and quotas
| Limit | Value |
|---|---|
| Requests per minute (per token) | 60 |
| Maximum request body size | 1 MB |
| Token lifetime | 1 hour |
| Concurrent claims per user | 3 |
Exceeding the rate limit returns HTTP 429 Too Many Requests with a Retry-After header indicating when to retry.
Error codes
| HTTP status | isError | Meaning |
|---|---|---|
| 401 | — | Missing or expired Bearer token — re-authenticate |
| 403 | true | Token valid but insufficient scope, or resource belongs to another user |
| 404 | true | Resource (spec, action, clarification) not found, or invalid UUID |
| 409 | true | Action already claimed by another user |
| 422 | true | Business rule violation — e.g. submitting an unclaimed action, invalid status transition |
| 429 | — | Rate limit exceeded — see Retry-After header |
| 500 | true | Unexpected server error — safe to retry after a short delay |