Skip to main content
For clients that want the read tools without adopting an MCP client library: POST JSON-RPC to /mcp with a bearer token and parse the reply. This is supported, not a workaround.
Verified 2026-09-08 against the deployed service on @modelcontextprotocol/server 2.0.0, and re-measured 2026-10-13 on 2.2.0, the version the service now runs. Two shapes this guide never uses are refused with HTTP 400:
  • A request that claims protocol revision 2026-07-28 without the MCP-Protocol-Version header is refused with -32020 (since @modelcontextprotocol/server 2.2.0).
  • A JSON-RPC batch is refused at any size, a batch of one included. Send one request per POST.
A bare call as described below is unaffected.

It works, and you do not need initialize

/mcp answers a request that carries no protocol-revision claim statelessly, one request at a time. A bare tools/call returns a result with no handshake, no session ID, and nothing to keep alive between calls.
Every call is independent. There is no session to establish, resume or tear down.

Two things that will break your first request

Send both media types in Accept, or you get a 406. Accept: application/json on its own, and a missing Accept header, are both rejected before your request is read:
406 response
A 200 comes back SSE-framed, not as a JSON body. The response is content-type: text/event-stream and looks like this, so JSON.parse on the raw body fails:
200 response body
To parse the response:
  1. Take the line beginning data:.
  2. Strip that prefix.
  3. Parse what is left.
Despite the framing this is a single-shot request/response: one frame, then the connection closes. GET /mcp is a 404: there is no stream to subscribe to.

A working call

curl
tools/list works over the same bare POST and returns each tool’s input schema. Read argument names from there rather than guessing them, and read the required array as well as the properties, because on the write surface it is load-bearing.
dispute_create requires aliases even from a caller who names no parties:Nothing forces a hand-written payload through the schema, so this is the one place a client that reads only property names quietly diverges from what the service accepts.

Read this before you trust a result

Skipping initialize means you never receive the server’s instructions string, which is where the arbitration semantics are written down. An MCP client is handed it on connect; you are not. Fetch it yourself:

Skill document

Unauthenticated. Required reading, not background.
Treat that document as the source of truth. Four traps matter enough to name here, because all of them fail silently rather than erroring:
dispute_get, dispute_list and evidence_list all fan out when you omit it: across ethereum, then gnosis, then arbitrum-one, drained in that order. Page one is entirely ethereum, so an absent result means “not on ethereum yet”, not “does not exist”.
chain: "arbitrum-one" is Kleros v2 Beta, and Beta is the live production deployment: real disputes, real arbitration fees, real money. The name describes the release, not the network. The public testing deployment with test data is chain: "arbitrum-sepolia-testnet". A client that read “Beta” as a testnet has already answered a question about live disputes with test data.
arbitrum-sepolia is retired and is now refused. It used to name the Devnet, which is now arbitrum-sepolia-devnet, so a find-and-replace onto the advertised arbitrum-sepolia-testnet succeeds and silently reads a different deployment. Relatedly, any unresolvable chain is now an error: it previously fell through to a live three-chain production fan-out reported as success.
The result is isError: true plus rendered text: no code field, no structuredContent. AgentKit raises codes such as CHAIN_REQUIRED and CHAIN_NOT_SUPPORTED internally and they are real on its command line, but the MCP layer emits only the message, so they never reach you. Branch on isError; treat the text as prose that moves between releases.
The skill document also covers what ruled means on each Kleros generation, why dispute_get and dispute_list can disagree about it, and which tools refuse to fan out at all.

One request per POST

Send each JSON-RPC message in a POST of its own. A JSON-RPC array (a batch, of any length, 1 included) is refused with HTTP 400 before any tool runs:
400 batch refusal
The refusal is decided after your key is checked and metered, so a refused batch still spends one request of your limit.
The MCP protocol itself dropped JSON-RPC batching as of revision 2025-06-18, so a current MCP client never sends one.

One write at a time per key

dispute_create, evidence_pin and evidence_submit run one at a time per key (strictly, per signer your key’s Preset names), so your Beta and Testnet keys do not block each other.
A write that arrives while another write on the same key is still running is not queued. It comes back at once as isError: true with text beginning WRITE_IN_PROGRESS: . Nothing was sent, so call it again once your other write has returned.
If your client makes tool calls in parallel, serialise the writes. Reads are never held back by this, and writes on other keys do not block yours.

Limits and health

Your key is metered at 30 requests per minute, per key. One request is one POST, and since a POST carries exactly one call, one request is one tool call.
None of the three is JSON-RPC framed: all are plain JSON, and all are decided before your request reaches the JSON-RPC layer, so check the HTTP status first.
/health is unauthenticated so the platform can probe it. A green /health means the HTTP process is up; it does not mean the arbitration backend is reachable, which is only exercised by a real tool call.