PullPulse exposes its review state over the Model Context Protocol so your coding agents can read what the humans see: which pull requests are waiting, why each one costs to review, who should review it, and how it will be routed into Slack.
It is a first-class product surface, not a debug endpoint. Everything the Slack app knows, an agent with a token can ask for.
Endpoint
POST https://<pullpulse-host>/mcp
DELETE https://<pullpulse-host>/mcp # session termination
GET https://<pullpulse-host>/mcp # 405; there is no server-initiated stream
<pullpulse-host> is the PullPulse service host. You do not have to look it up: the exact URL is
printed next to the token in the message /pullpulse mcp sends you.
Protocol versions
The server is dual-era. It implements the revision the specification currently names,
2026-07-28, and it also speaks the initialization-based revisions every shipping client still
uses:
| Revision | How a client selects it | Sessions |
|---|---|---|
2026-07-28 |
Send _meta["io.modelcontextprotocol/protocolVersion"] on every request, mirrored into the MCP-Protocol-Version, Mcp-Method and Mcp-Name headers |
None |
2025-11-25, 2025-06-18, 2025-03-26 |
Open with initialize |
Mcp-Session-Id, issued on initialize, required afterwards, released with DELETE |
You do not have to choose: point any MCP client at the URL and it will negotiate. Modern clients get
a stateless endpoint; initialize gets a session id back in the Mcp-Session-Id response header.
Sessions live in KV for 24 hours and are bound to the token that created them — a session id
presented with a different token is treated as unknown. An expired or unknown session returns 404
so the client re-initializes rather than retrying forever.
Responses
application/jsonfor a normal result.text/event-streamwhen the client accepts SSE and sent_meta.progressTokenand called a tool that goes to the network (find_similar_prs,post_pr_note). The stream carriesnotifications/progressframes followed by the response.202 Acceptedwith no body for a notification.- JSON-RPC batches are accepted in the legacy era and rejected in
2026-07-28, which removed them.
Authentication
Every request needs a workspace-scoped bearer token:
Authorization: Bearer pp_<64 hex characters>
Tokens are minted from Slack:
/pullpulse mcp → list tokens and mint a new one
/pullpulse mcp revoke <name> → revoke one
Only the SHA-256 of a token is stored, so it is shown exactly once, at mint time. Copy it then.
Scopes are read (everything except post_pr_note) and write (adds post_pr_note). Mint a
read-only token for anything you would not want writing to GitHub — which is most agents.
A missing or rejected token gets 401 with a WWW-Authenticate: Bearer realm="pullpulse", …
header; error="invalid_request" means you sent none, error="invalid_token" means yours is
unknown or revoked.
Tenancy. Every tool is scoped to the workspace that issued the token, and no argument can widen that scope. Asking for another workspace's repository returns "not tracked", never data.
Client configuration
Claude Code
claude mcp add --transport http pullpulse https://<pullpulse-host>/mcp \
--header "Authorization: Bearer pp_your_token_here"
Or in .mcp.json, checked into the repository so the whole team gets it (keep the token in the
environment, not in the file):
{
"mcpServers": {
"pullpulse": {
"type": "http",
"url": "https://<pullpulse-host>/mcp",
"headers": { "Authorization": "Bearer ${PULLPULSE_MCP_TOKEN}" }
}
}
}
Cursor
~/.cursor/mcp.json for every project, or .cursor/mcp.json for one:
{
"mcpServers": {
"pullpulse": {
"url": "https://<pullpulse-host>/mcp",
"headers": { "Authorization": "Bearer pp_your_token_here" }
}
}
}
Slackbot MCP client
In Slack, open Settings → Slackbot → Connectors → Add MCP server and supply:
- Server URL —
https://<pullpulse-host>/mcp - Authentication — Bearer token,
pp_… - Name — PullPulse
Mint a dedicated read-only token for this one: it is shared by everyone in the workspace who talks to Slackbot.
Anything else
Any Streamable HTTP client works. The endpoint is a single URL, the credential is a bearer token, and there is no OAuth dance to implement.
curl -sS https://<pullpulse-host>/mcp \
-H "Authorization: Bearer $PULLPULSE_MCP_TOKEN" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-11-25","capabilities":{},
"clientInfo":{"name":"curl","version":"8"}}}'
Tools
All eight return both a human-readable text summary and a complete structuredContent. Text is
hard-capped at 24 KB — narrow a query with repo or limit rather than paginating.
| Tool | Arguments | Answers |
|---|---|---|
list_review_queue |
githubLogin?, repo?, limit? |
What is waiting on review, ranked by review effort, with attention flags and estimated review minutes |
get_pr_context |
repo, number |
Everything about one PR: its complexity tier and attention flags, CODEOWNERS, reviewers, checks, cached AI brief, read-first file ordering, how it was routed |
find_similar_prs |
query, repo?, limit? |
Precedent, over the same embeddings the enrichment pipeline writes |
suggest_reviewers |
repo, number |
Who should review, with the evidence — CODEOWNERS plus real review history |
get_repo_policy |
repo |
Effective .pullpulse.yml, and which rule would match a PR here |
list_agent_prs |
repo?, agent?, limit? |
Pull requests opened by agent accounts, with CI state and whether a human has looked |
review_stats |
sinceHours?, repo? |
Median time to first human review, open count, agent share, stale count |
post_pr_note |
repo, number, markdown |
write scope. Posts a comment on the PR through the GitHub App |
post_pr_note appends an <!-- pullpulse:relay --> marker so PullPulse's own Slack↔GitHub sync
does not echo the comment back into Slack as if a human had written it.
The tool descriptions returned by tools/list are the real documentation — they state when to
reach for each tool and how to read its result. This table is a map, not a substitute.
Resources
| URI | Contents |
|---|---|
pullpulse://review-queue |
The workspace review queue as JSON, attachable without a tool call |
pullpulse://policy/{repo} |
Effective routing policy for a watched repository |
resources/list enumerates the queue plus one policy resource per watched repository;
resources/templates/list returns the {repo} template.
Prompts
| Name | Arguments | What it does |
|---|---|---|
review-this-pr |
repo, number |
Inlines the PR's live context, then walks a review procedure: read-first ordering, then what each attention flag obliges you to verify, then a verdict with citations |
triage-agent-queue |
repo?, agent? |
Inlines the live agent queue and sorts it into back-to-the-agent / merge candidate / needs a human / close, with the decision rule for each |
Both expand against live data at prompts/get time, so the model starts from facts rather than
from a plan to go and fetch them.
Errors
Standard JSON-RPC codes, plus the ones the MCP specification reserves:
| Code | Meaning |
|---|---|
-32700 |
Malformed JSON |
-32600 |
Invalid request envelope, or a batch sent to 2026-07-28 |
-32601 |
Unknown method (HTTP 404 in the modern era) |
-32602 |
Invalid params, unknown tool, unknown prompt, unknown resource |
-32603 |
Internal error |
-32020 |
A mirrored header disagrees with the request body |
-32021 |
A required client capability was not declared |
-32022 |
Unsupported protocol version; data.supported lists what to retry with |
Error messages are deliberately mapped, never raw: an internal failure returns "Internal error" rather than a stack, a query or anything else that would leak into an agent's context.
Business-logic failures — an untracked pull request, a repository that is not watched, a read-only
token calling post_pr_note — come back as a normal result with isError: true and an explanatory
structuredContent, because those are things a model can react to.
Observability
Every call writes an mcp.call point to Analytics Engine, indexed by workspace, with the tool name
(or method) as the detail dimension. That is how you find out which tools your agents actually use.