Penling penguin markPenling
MCP & agents8 min read · Updated Jun 2026

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:

ScopeWhat it grants
specs.readRead published specs, plans, and actions
builds.writeClaim 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
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[]

FieldTypeDescription
pidstringStable UUID for this spec
titlestringFocus area title
summarystringOne-paragraph description
actionsAction[]Claimable units within the spec

Each Action includes:

FieldTypeDescription
pidstringAction pid — pass this to claim_action
refCodestringHuman-readable ref, e.g. PEN-12
labelstringShort action title
areastringui, api, database, tests, etc.
statusstringnot-started, started, in-review, complete
claimedBystring | nullUser 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:

ParameterTypeRequiredDescription
spec_pidstringyesThe 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:

ParameterTypeRequiredDescription
action_pidstringyesThe pid from a spec's actions[]
agent_modelstringnoModel identifier for attribution, e.g. claude-opus-4-8

Returns: The claimed Action object with status: "started".

Error cases:

StatusMeaning
409Action already claimed by another user
404Action 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:

ParameterTypeRequiredDescription
action_pidstringyesThe pid of the claimed action

Returns:

FieldTypeDescription
actionobjectAction detail including acceptanceCriteria[].{id, text, status}
specobjectsummary, results, conditions, boundaries, attachments
planobjectapproach — the human-authored implementation strategy
siblingActionsAction[]Other actions in the same plan
openClarificationsClarification[]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:

ParameterTypeRequiredDescription
attachment_pidstringyesThe 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:

ParameterTypeRequiredDescription
action_pidstringyesThe 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:

ParameterTypeRequiredDescription
action_pidstringyesThe pid of the claimed action
statusenumyesnot-started, started, or wont-do
messagestringyesOne 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:

ParameterTypeRequiredDescription
action_pidstringyesThe pid of the active action
questionstringyesThe 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:

ParameterTypeRequiredDescription
clarification_pidstringyesThe pid returned by request_clarification
answerstringyesThe 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:

ParameterTypeRequiredDescription
action_pidstringyesThe pid of the claimed action
criterion_idstringyesThe id (uuid) from action.acceptanceCriteria[].id in the brief
outcomeenumyespassed or failed
evidencestringnoBrief 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 implementedvalidated. 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:

ParameterTypeRequiredDescription
action_pidstringyesThe pid of the claimed action
shastringyesThe full git commit SHA
messagestringyesThe commit message
files_changedintegernoNumber of files changed
criterion_idsstring[]noUUIDs of acceptance criteria addressed by this commit

Returns: { recorded: true, actionPid, sha }

Side effects: Listed criteria advance from seenimplemented. 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:

ParameterTypeRequiredDescription
action_pidstringyesThe pid of the action being submitted
pr_urlstring (URL)yesURL of the pull request containing the work
summarystringyes2–4 sentence handoff note for the reviewer
evidenceobject[]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:

ParameterTypeRequiredDescription
action_pidstringyesThe pid of the action being resubmitted
pr_urlstring (URL)yesURL of the pull request (same PR, updated with review changes)
summarystringyes2–4 sentences covering what changed since the last submission
evidenceobject[]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
StatusSet when
pendingCriterion created
seenAgent loads the brief via get_action_brief
implementedAgent calls report_commit with the criterion's ID
validatedAgent calls report_test_result with outcome: "passed", or submits with evidence
confirmedHuman reviewer confirms in Penling

The UI maps validated and confirmed to a green check. Statuses never regress.

Rate limits and quotas

LimitValue
Requests per minute (per token)60
Maximum request body size1 MB
Token lifetime1 hour
Concurrent claims per user3

Exceeding the rate limit returns HTTP 429 Too Many Requests with a Retry-After header indicating when to retry.

Error codes

HTTP statusisErrorMeaning
401Missing or expired Bearer token — re-authenticate
403trueToken valid but insufficient scope, or resource belongs to another user
404trueResource (spec, action, clarification) not found, or invalid UUID
409trueAction already claimed by another user
422trueBusiness rule violation — e.g. submitting an unclaimed action, invalid status transition
429Rate limit exceeded — see Retry-After header
500trueUnexpected server error — safe to retry after a short delay