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.

EndpointPOST https://reblog.so/api/mcp
TransportStreamable HTTP -- stateless JSON-RPC 2.0. No session, no SSE stream to hold open.
Content typeapplication/json
Auth headerAuthorization: Bearer <oauth access token | project api key>
Protocol version2025-06-18, echoed on initialize
CORSOpen. OPTIONS preflight is answered with a 204.
GET /api/mcpReturns a plain JSON description of the server and its tool names. Handy for a smoke test; it is not part of the protocol.

Methods#

MethodPurpose
initializeHandshake. Returns the protocol version, the server capabilities, the server identity, and a set of instructions describing what this particular connection can do.
tools/listThe catalogue, filtered to what this connection may actually call.
tools/callRun one tool. Answered synchronously on the same request.
pingEmpty result. Keepalive.
notifications/initializedAccepted and ignored. No response body.
notifications/cancelledAccepted 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#

Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {},
    "clientInfo": { "name": "my-agent", "version": "1.0.0" }
  }
}
Response
{
  "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_*.

Response, abridged
{
  "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#

Request
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "list_articles",
    "arguments": {
      "project_id": "7906b84e",
      "status": "draft",
      "page_size": 5
    }
  }
}
Response
{
  "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_id is optional.
  • Several -- every project-scoped tool gains a project_id argument in the catalogue and it must be supplied. Get the ids from list_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.

Refused
{
  "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. 1

    Ask

    Put the questions to the user, proposing answers from project_context rather than asking cold.

  2. 2

    Save

    Store the answers with configure_agent.

  3. 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 with list_jobs, and cancel_job stops one.
  • The endpoint accepts 5 requests per second per grant or per key. Over it, the call is refused and recorded.