Reference
MCP endpoint
The URL an MCP client calls, how it authenticates, the methods it can use, request and response examples, and every status code and error it can get back.
Each project has one MCP endpoint. Any client that speaks MCP over Streamable HTTP (Claude Code, Claude Desktop, Cursor, VS Code, the MCP Inspector, or your own code) calls it to list and run the tools, resources and prompts your agents registered. This page is the contract for that endpoint. To set up a specific client, see Connect a client.
The endpoint
POST https://api.spacefix.io/mcp/<project-id>
<project-id> is the project's id, shown on the project page next to MCP endpoint. The id is not a secret, and the endpoint grants nothing without a key.
The endpoint is stateless. There are no sessions, so any request can be sent on its own, and a client does not need to call initialize first. initialize returns an Mcp-Session-Id, but it only carries your client's name and version so the Logs page can show which client made each call; the gateway never requires or checks it. Only POST works. GET and DELETE are refused with 405.
Authentication
Send a client key (or a full key) in the Authorization header:
POST /mcp/8b43e8b5-1c7c-411f-a2d8-bbac5199432d HTTP/1.1
Authorization: Bearer sf_live_...
Content-Type: application/json
Accept: application/json, text/event-stream
| Situation | Response |
|---|---|
No Authorization header | 401 {"error":"Missing API key."} with WWW-Authenticate: Bearer realm="mcp" |
| Key unknown, revoked or expired | 401 {"error":"Invalid API key."} with WWW-Authenticate: Bearer realm="mcp", error="invalid_token", error_description="Invalid API key." |
| An agent key | 403 {"error":"This is an agent key. MCP clients connect with a client key."} |
| A key that belongs to another project | 403 {"error":"Forbidden - agent identity mismatch."} |
| Too many rejected keys from one address | 429 {"error":"Too many failed authentication attempts."} with Retry-After |
A key can carry a tool allow-list and can be read-only. Tools outside the allow-list, and for a read-only key every tool that does not read (no readOnlyHint: true and not marked Read by you), are absent from tools/list and answer as if they did not exist. Resources and prompts are always listed. A project's Client access set to Read only applies the same limit to every key and token. Revoking a key takes effect on the next request.
If OAuth is turned on for your deployment, the endpoint accepts OAuth access tokens instead of keys: by default mcp:write (or mcp:invoke) reads and writes, and mcp:read alone is read-only. See Security.
Headers
| Header | Required | Meaning |
|---|---|---|
Authorization: Bearer <key> | Yes | Your client or full key |
Content-Type: application/json | Yes, on POST | Otherwise 415 |
Accept: application/json, text/event-stream | Yes | Both types must be listed, otherwise 406 |
traceparent, tracestate | No | W3C trace context. If valid, your agent's span joins your trace. See Trace headers |
MCP-Protocol-Version | No | A protocol revision. Unsupported ones are refused with 400 |
Protocol revisions accepted: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05 and 2024-10-07. With no initialize the revision defaults to 2025-03-26.
The endpoint sets no CORS headers and answers no preflight, so a web page in a browser cannot call it. Server-side and desktop MCP clients are unaffected.
Methods
| Method | Behaviour |
|---|---|
initialize | Returns the negotiated protocolVersion, capabilities (one entry for each of tools, resources and prompts that your agents registered, each with listChanged) and serverInfo (name is the project id, version is 1.0.0) |
notifications/initialized and any other notification | 202 Accepted with an empty body |
ping | {"result":{}} |
tools/list | Every tool visible to this key. There is no pagination: the whole list comes back. Tools outside the key's allow-list, and tools that do not read when the caller is read-only, are left out. With nothing to show the list is empty |
tools/call | Runs a tool on one of your agents |
resources/list | Every registered resource: uri, name, description, mimeType. An empty list if none are registered |
resources/read | Reads a resource from an agent |
resources/templates/list | {"resourceTemplates":[]}. Resource templates are not supported |
prompts/list | Every registered prompt: name, title, description, arguments. An empty list if none are registered |
prompts/get | Renders a prompt on an agent |
Anything else (logging/setLevel, completion/complete, resources/subscribe ...) | JSON-RPC error -32601 Method not found |
A batch (a JSON array of requests) is accepted. The answers arrive as one event: message per request in the same response.
listChanged is advertised but never sent: there is no server-initiated stream, so a client that wants a fresh list calls tools/list again.
Without a connected agent there is nothing to list, so every method, including initialize and ping, fails with 503 and -32001 (see Errors).
Examples
Set these once:
U=https://api.spacefix.io/mcp/<project-id>
m() { curl -s -X POST "$U" \
-H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' "$@"; }
Initialize
m -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
event: message
data: {"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{"listChanged":true},"resources":{"listChanged":true},"prompts":{"listChanged":true}},"serverInfo":{"name":"8b43e8b5-1c7c-411f-a2d8-bbac5199432d","version":"1.0.0"}},"jsonrpc":"2.0","id":1}
List tools
m -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
event: message
data: {"result":{"tools":[{"name":"get_order","description":"Look up a single order by its id.","inputSchema":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]}}]},"jsonrpc":"2.0","id":2}
tools/list returns the schema your agent registered, unchanged.
Call a tool
m -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_order","arguments":{"id":"1"}}}'
event: message
data: {"result":{"content":[{"type":"text","text":"{\"id\":\"1\",\"item\":\"Widget\",\"total\":4200}"}]},"jsonrpc":"2.0","id":3}
Read a resource and render a prompt
m -d '{"jsonrpc":"2.0","id":4,"method":"resources/read","params":{"uri":"orders://all"}}'
m -d '{"jsonrpc":"2.0","id":5,"method":"prompts/get","params":{"name":"summarise_order","arguments":{"id":"1"}}}'
event: message
data: {"result":{"contents":[{"uri":"orders://all","mimeType":"application/json","text":"[{\"id\":\"1\",\"item\":\"Widget\",\"total\":4200}]"}]},"jsonrpc":"2.0","id":4}
event: message
data: {"result":{"description":"Summarise an order.","messages":[{"role":"user","content":{"type":"text","text":"Summarise this order politely: Widget"}}]},"jsonrpc":"2.0","id":5}
A batch
m -d '[{"jsonrpc":"2.0","id":20,"method":"ping"},{"jsonrpc":"2.0","id":21,"method":"resources/templates/list"}]'
event: message
data: {"result":{},"jsonrpc":"2.0","id":20}
event: message
data: {"result":{"resourceTemplates":[]},"jsonrpc":"2.0","id":21}
Responses
| Case | Response |
|---|---|
| A request that has an answer | 200, Content-Type: text/event-stream. The body is one event: message / data: <json-rpc response> frame per request, then the stream ends. While a slow call is running the connection sends a : keepalive comment every 15 seconds |
| A notification | 202 Accepted, empty body |
| A refusal by the gateway's own checks | Content-Type: application/json, with the status listed under Errors |
The answer to a tool call depends on what your handler returned:
| Your handler returned | The client receives |
|---|---|
| A string | {"content":[{"type":"text","text":"<the string>"}]} |
An object with a content array (already in MCP shape) | Passed through untouched, including structuredContent and isError |
| Anything else, including nothing | {"content":[{"type":"text","text":"<the value as JSON>"}]} (null for nothing) |
| An error | {"content":[{"type":"text","text":"<your error message>"}],"isError":true} |
A tool that fails is not a protocol error: it is a successful response with isError: true, so the model can read the message and recover. Resource and prompt failures are JSON-RPC errors, because MCP has no isError form for them.
A resource answer is {"contents":[{"uri","mimeType","text"}]}. A string result is the text, and anything else is JSON-encoded. The mimeType is the one the resource registered, or application/json if it has none. Only text is supported. A prompt answer passes through if it already has a messages array. A string becomes one user message, and anything else is JSON-encoded into one.
Arguments are validated before your agent runs
The gateway checks a tool call's arguments against the schema your agent registered, and a call that fails never reaches your handler and is not counted:
event: message
data: {"result":{"content":[{"type":"text","text":"Input validation error: Invalid arguments for tool get_order: data/id must be string"}],"isError":true},"jsonrpc":"2.0","id":6}
The keywords pattern, patternProperties and format are listed to clients as hints but are never enforced, so enforce those in your handler. A tools/call whose arguments is not an object is a JSON-RPC error: -32602 Invalid tools/call request: ....
Trace headers
If you send a valid traceparent (00-<32 hex>-<16 hex>-<2 hex>, with neither id all zeros), it is passed to your agent unchanged, with tracestate (first 512 characters). Your agent's span then becomes a child of yours, in the same trace. If you send none, or something malformed, the gateway starts a trace so every call can still be followed. The response does not carry a traceparent.
Every call that reaches your agent has a Request id and a Trace id, shown on the Logs page. See OpenTelemetry.
Rate limits
Requests are limited per minute per project, not per key. On the Free plan the limit is 600 a minute. Every request counts, including failed ones and initialize and tools/list. When you exceed it:
HTTP/1.1 429 Too Many Requests
Retry-After: 1
{"error":"Rate limit exceeded: 600 requests per minute."}
Retry-After is in whole seconds. This body is {"error": ...}, not JSON-RPC. See Plans and limits.
Errors
These are the responses the endpoint itself can produce, in the order the checks run. Messages are exact. <id> is the project id. The full list, including errors from agents and the dashboard, is in Errors.
| Status | JSON-RPC code | Message | Cause |
|---|---|---|---|
401 | none | Missing API key. / Invalid API key. | See Authentication |
403 | none | This is an agent key. MCP clients connect with a client key. | Wrong key kind |
429 | none | Too many failed authentication attempts. | Too many rejected keys from one address |
400 | none | The JSON parser's message, for example Unexpected token 'n', "not json" is not valid JSON | The body is not valid JSON |
413 | none | request entity too large | The body is over 4 MB |
403 | -32003 | This project is suspended: <reason> | The project or account is suspended |
403 | -32004 | This address is not on the project's client allow-list. | The caller's address is not in Allowed client addresses |
429 | none | Rate limit exceeded: <n> requests per minute. | Over the project's rate limit |
403 | none | Forbidden - agent identity mismatch. | The key (or token) is for a different project than the URL |
429 | -32002 | Monthly allowance of <n> calls on the <plan> plan is used up. It resets on the 1st (UTC), or upgrade the plan. | Free plan, allowance spent. Only tools/call, resources/read and prompts/get are refused |
503 | -32001 | No agent connected for server "<id>". | No agent is connected |
503 | -32001 | Every agent for server "<id>" is disabled. | Agents are connected but all disabled |
406 | -32000 | Not Acceptable: Client must accept both application/json and text/event-stream | Accept is missing one of the two types |
415 | -32000 | Unsupported Media Type: Content-Type must be application/json | Wrong Content-Type |
405 | -32000 | Method not allowed. | Not a POST |
400 | -32000 | Bad Request: Unsupported protocol version: <v> (supported versions: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07) | MCP-Protocol-Version names an unknown revision |
400 | -32600 | Bad Request: the request body is not a valid JSON-RPC message | Valid JSON that is not a JSON-RPC request |
200 | -32601 | Method not found | A method the endpoint does not implement. Listing tools, resources or prompts when there are none answers with an empty list, not this error |
200 | -32602 | Tool <name> not found | The tool is not registered, is disabled, is outside the key's allow-list, or does not read and the caller is read-only |
200 | -32602 | Resource not found: <uri>, with data: {"uri":"<uri>"} | No registered resource has that uri |
200 | -32602 | Invalid arguments for prompt <name>: <details> | A required prompt argument is missing |
200 | -32602 | Invalid tools/call request: <details> | params or arguments has the wrong type |
200 | -32603 | Your agent's error text, or Request for resource "<uri>" timed out after <ms>ms. / Request for prompt "<name>" timed out after <ms>ms. | A resource read or prompt render failed or timed out |
JSON-RPC errors raised by the gateway's own checks (-32001 to -32004) look like this:
{"jsonrpc":"2.0","id":8,"error":{"code":-32001,"message":"No agent connected for server \"8b43e8b5-1c7c-411f-a2d8-bbac5199432d\"."}}
A tool that fails after it was accepted is a 200 with isError: true, and one of these messages:
| Message | Cause |
|---|---|
Request for tool "<name>" timed out after <ms>ms. | Your agent did not answer within the project's call timeout (30 seconds by default). The call is counted |
Agent disconnected before returning a result. | The agent's connection dropped while the call was running |
Key revoked. / Key rotated. / Project deleted. / Account deleted. | The key or project was removed while the call was running |
Project suspended. / Account suspended. | The project or account was suspended while the call was running |
Service is shutting down. | The gateway was restarting |
No connected agent serves tool "<name>". | Agents are connected but none registered that tool at the moment the call was sent |
Every connected agent for this project is disabled. | Every agent was disabled between the checks and dispatch |
Tool failed. | The agent reported a failure without a message |
| Your handler's own error message | Your tool threw or returned an error |
Things worth knowing
- Calls are not retried. A call goes to one agent, once. If the connection drops after your handler started, the handler may still finish and its result is dropped, so make state-changing tools safe to repeat.
- Calls are shared across replicas in turn, per tool, across the project's enabled agents that registered it.
- Lists come from one agent.
tools/list,resources/listandprompts/listare built from the longest-connected enabled agent, so give every replica the same registration. - A cache hit still counts for rate limiting and, on Free, for the allowance check, but is not logged and not counted as a call.
- Argument names, not values, are logged. See Observability.
Next steps
- Connect a client: Claude Code, Claude Desktop, Cursor, VS Code and the MCP Inspector.
- Errors: every error a customer can see.
- Agent protocol: the other side, for connecting an agent without an SDK.