MCP
Protocol
The wire format: JSON-RPC methods, the handshake, how a tool call is dispatched and routed to a project, the argument guard, and the setup handshake a writing tool can answer with.
You only need this page if you are writing the client yourself, or debugging one. Claude, Claude Code and Cursor handle all of it for you.
| Endpoint | POST https://reblog.so/api/mcp |
| Transport | Streamable HTTP -- stateless JSON-RPC 2.0. No session, no SSE stream to hold open. |
| Content type | application/json |
| Auth header | Authorization: Bearer <oauth access token | project api key> |
| Protocol version | 2025-06-18, echoed on initialize |
| CORS | Open. OPTIONS preflight is answered with a 204. |
GET /api/mcp | Returns a plain JSON description of the server and its tool names. Handy for a smoke test; it is not part of the protocol. |
Methods#
| Method | Purpose |
|---|---|
initialize | Handshake. Returns the protocol version, the server capabilities, the server identity, and a set of instructions describing what this particular connection can do. |
tools/list | The catalogue, filtered to what this connection may actually call. |
tools/call | Run one tool. Answered synchronously on the same request. |
ping | Empty result. Keepalive. |
notifications/initialized | Accepted and ignored. No response body. |
notifications/cancelled | Accepted and ignored. |
Anything else answers -32601 Method not found. Batches are supported: send an array of messages and get an array of responses back.
The handshake#
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": { "name": "my-agent", "version": "1.0.0" }
}
}{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": { "tools": { "listChanged": false } },
"serverInfo": { "name": "reblog-mcp", "version": "1.0.0" },
"instructions": "Reblog MCP server. Tools act on your articles and projects. ..."
}
}The instructions are per connection
They are assembled from the tools this connection is actually offered, so the server never tells an agent to call something it cannot see. When a capability shipped after the connection was authorized, the instructions say so and name the fix, instead of leaving the agent to report a broken API.
clientInfo is worth filling in honestly: it is recorded in the account owner's activity log, and it is the first thing anyone looks at when a connection misbehaves.
Listing tools#
The catalogue is filtered twice -- by the grant, and by whether at least one reachable project lets you run each tool -- so it is already the truth for this connection. Tools carry MCP annotations where the verb makes them unambiguous: readOnlyHint on the reading verbs, destructiveHint on delete_*.
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "list_articles",
"description": "List articles in a project. Supports pagination, search by title/handle, ...",
"inputSchema": {
"type": "object",
"properties": {
"page": { "type": "integer", "minimum": 1, "default": 1 },
"page_size": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
},
"additionalProperties": false
},
"annotations": { "readOnlyHint": true }
}
]
}
}Calling a tool#
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "list_articles",
"arguments": {
"project_id": "7906b84e",
"status": "draft",
"page_size": 5
}
}
}{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{ "type": "text", "text": "{\n \"items\": [ ... ],\n \"pagination\": { \"total\": 12, \"page\": 1 }\n}" }
]
}
}Results are returned as a single text content block holding pretty-printed JSON. A tool-level failure keeps the same shape and adds isError: true, with the message written for the model to act on. Envelope-level failures return a JSON-RPC error instead: see errors.
Choosing the project#
- One reachable project -- it is inferred;
project_idis optional. - Several -- every project-scoped tool gains a
project_idargument in the catalogue and it must be supplied. Get the ids fromlist_projects. - An id outside the grant -- refused with
-32602, naming the id. The project a tool acts on is always resolved from the connection, never trusted from the caller beyond membership.
project_id is stripped from the arguments before the handler runs, which is why it appears in no tool's published schema.
The argument guard#
A declared scalar argument that arrives as an object or an array is rejected before any handler builds a database query out of it -- that shape is how a NoSQL injection is smuggled in. An undeclared object argument is refused too, unless the tool explicitly accepts a rich body.
{
"jsonrpc": "2.0",
"id": 4,
"error": {
"code": -32602,
"message": "Invalid value for \"handle\": expected a string."
}
}The setup handshake#
A writing tool can answer with status: "setup_required" instead of doing the work. That is not a failure: it means the project has never configured the agent the job needs, and anything written now would be generic. The payload carries the questions to put to the user, plus a project_context to propose answers from.
- 1
Ask
Put the questions to the user, proposing answers from
project_contextrather than asking cold. - 2
Save
Store the answers with
configure_agent. - 3
Retry
Call the original tool again. It now runs.
Do not invent the answers
They are what makes a blog sound like its owner. When the user is not available, pass skip_setup: true and the work proceeds with a generic default -- and the project is not asked again.
Timing and limits#
- Tool calls are synchronous. The connection stays open for up to 300 seconds, because image generation and article writing routinely take 90 to 120.
- Long jobs that outlive a request return a job id instead: poll
get_job, or list them withlist_jobs, andcancel_jobstops one. - The endpoint accepts 5 requests per second per grant or per key. Over it, the call is refused and recorded.