Guides
Connecting clients
Connect Claude Code, Claude Desktop, Cursor, VS Code, the MCP Inspector or your own code to your project's MCP endpoint.
An MCP client reaches your tools through one URL per project. This page gives that URL and the auth header, a configuration for each popular client, how to call the endpoint with plain HTTP, what a client sees when things go well and when they do not, and which checks can refuse a call.
The endpoint
Every project has one MCP endpoint:
https://api.spacefix.io/mcp/<project id>
The project id is the UUID shown on the project page. The API keys panel has copy buttons for it and for the full MCP endpoint, and the Connect a client panel shows ready-made configurations with your endpoint filled in.
The endpoint speaks MCP over Streamable HTTP: the client sends JSON-RPC messages with POST. It is stateless: there is no session, and GET and DELETE on the endpoint answer 405 with Method not allowed.
An endpoint answers only for its own project. A key from project A used on project B's URL is refused with 403 and Forbidden - agent identity mismatch.
Authentication
Send a client key (or a full key) as a bearer token on every request:
Authorization: Bearer <your client key>
| Key kind | Accepted on the MCP endpoint | Accepted by the SDK (agent connection) |
|---|---|---|
client | Yes | No |
full | Yes | Yes |
agent | No, 403 | Yes |
Create one client key per person or tool, with a label you will recognise (claude-desktop) and, if you like, an expiry, a tool allow-list and Read only access. See Projects and keys.
Client configuration
Below, replace <project id> and <your client key>. The name after mcp add (or the key in a JSON file) is a label you choose; the dashboard uses your project's name in lower case with dashes, for example orders-api.
claude mcp add --transport http orders-api https://api.spacefix.io/mcp/<project id> \
--header "Authorization: Bearer <your client key>"
{
"mcpServers": {
"orders-api": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://api.spacefix.io/mcp/<project id>", "--header", "Authorization:${AUTH}"],
"env": { "AUTH": "Bearer <your client key>" }
}
}
}
{
"mcpServers": {
"orders-api": {
"url": "https://api.spacefix.io/mcp/<project id>",
"headers": { "Authorization": "Bearer <your client key>" }
}
}
}
{
"servers": {
"orders-api": {
"type": "http",
"url": "https://api.spacefix.io/mcp/<project id>",
"headers": { "Authorization": "Bearer <your client key>" }
}
}
}
npx @modelcontextprotocol/inspector
# In the Inspector, choose Streamable HTTP, then enter:
# URL: https://api.spacefix.io/mcp/<project id>
# Header: Authorization: Bearer <your client key>
Notes for each client:
- Claude Code. Run the command once in a terminal. The server then appears in the client's list and its tools can be approved when used.
- Claude Desktop. Its configuration file has no field for an
Authorizationheader, so a key goes through themcp-remotebridge, which needs Node.js fornpx. The key goes inenvand the header refers to it as${AUTH}, which avoids a space inside a command-line argument. Restart Claude Desktop afterwards. - Cursor. Use
.cursor/mcp.jsonin your project, or~/.cursor/mcp.jsonfor every project. - VS Code. Use
.vscode/mcp.jsonin your workspace. The top-level key isservers, notmcpServers. - MCP Inspector. The quickest way to see exactly what the gateway returns for a list, a call and an error.
Calling the endpoint directly
Any HTTP client works. Two headers are required: Content-Type: application/json, and an Accept header that lists both application/json and text/event-stream.
ENDPOINT=https://api.spacefix.io/mcp/<project id>
KEY=<your client key>
# List tools
curl -s -X POST "$ENDPOINT" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# Call a tool
curl -s -X POST "$ENDPOINT" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_order","arguments":{"id":"1"}}}'
The reply is a one-event Server-Sent Events stream, even for a single answer:
event: message
data: {"result":{"content":[{"type":"text","text":"{\"id\":\"1\",\"item\":\"Widget\",\"total\":4200}"}]},"jsonrpc":"2.0","id":2}
The other methods work the same way: initialize, resources/list, resources/read with {"uri": "..."}, prompts/list, prompts/get with {"name": "...", "arguments": {...}}, and ping. You may send a JSON array to batch several messages in one request. Calling tools/list without a prior initialize works, because nothing is remembered between requests.
A TypeScript client
The official client package handles the event-stream framing for you.
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
const transport = new StreamableHTTPClientTransport(
new URL(`${process.env.SPACEFIX_SERVICE_URL}/mcp/${process.env.PROJECT_ID}`),
{ requestInit: { headers: { Authorization: `Bearer ${process.env.SPACEFIX_CLIENT_KEY}` } } }
);
const client = new Client({ name: 'my-app', version: '1.0.0' });
await client.connect(transport);
const { tools } = await client.listTools();
console.log(tools.map(t => t.name));
const result = await client.callTool({ name: 'get_order', arguments: { id: '1' } });
if (result.isError) {
// The handler threw, the call timed out, or the arguments were invalid. The text says why.
console.error(result.content);
} else {
console.log(result.content);
}
await client.close();
Two kinds of failure reach your code differently. A tool failure (the handler threw, it timed out, the arguments were wrong) is a normal result with isError: true. A protocol failure (an unknown tool or resource) is thrown as an error with a JSON-RPC code. A refusal at the HTTP layer (no agent, rate limit, bad key) makes connect or the call itself throw, with the HTTP status and the JSON body in the error.
What a client sees
| Request | What comes back |
|---|---|
initialize | Protocol version negotiation. serverInfo is { "name": "<project id>", "version": "1.0.0" }. The capabilities list tools, and resources and prompts when your agent registers any |
tools/list | The tools your agent registered: name, description (or the description you overrode in the dashboard), and the JSON Schema |
tools/call | The handler's return value as content. A string is sent as text; any other value as JSON text; a value already shaped like an MCP result (it has a content array) is passed through |
resources/list, resources/read | Resources by uri. A read returns contents with uri, mimeType (default application/json) and text |
prompts/list, prompts/get | Prompts and their string arguments. A string result becomes one user message; an object with messages is passed through |
ping | {} |
How failures are reported once an agent is connected:
| Situation | Result |
|---|---|
| The handler threw | isError: true; the content is the thrown message |
| The agent did not answer in time | isError: true, Request for tool "<name>" timed out after <ms>ms. |
| The arguments do not match the schema | isError: true, for example Input validation error: Invalid arguments for tool get_order: data must have required property 'id' |
| The agent disconnected mid-call | isError: true, Agent disconnected before returning a result. |
| No agent has that tool | isError: true, No connected agent serves tool "<name>". |
| An unknown or hidden tool | JSON-RPC error -32602, Tool <name> not found |
| An unknown resource | JSON-RPC error -32602, Resource not found: <uri> |
| A resource handler threw | JSON-RPC error -32603 with the thrown message |
| A missing prompt argument | JSON-RPC error -32602, for example Invalid arguments for prompt summarise_order: data must have required property 'id' |
| An unknown method | JSON-RPC error -32601, Method not found |
What tools/list shows
The list is built for each request, from your agent's registration and the controls you set:
- Allow-lists. If the key has a tool allow-list, tools outside it are absent from the list and cannot be called (
Tool <name> not found). Allow-lists apply to tools only; the key still sees every resource and prompt. - Read-only access. A read-only key, a project whose Client access is Read only, or an OAuth token with only
mcp:readsees only the tools that read: those whose agent setsreadOnlyHint: true, or that you marked Read. Unlabelled tools count as writes and are absent; calling one givesTool <name> not found. Resources and prompts are always listed. See Read-only keys. - Disabled tools. A tool you disabled in the dashboard is absent for every client.
- Overridden descriptions. A description you edited in the dashboard replaces the one your code registered.
- Nothing visible. If a key's allow-list matches no tool, a read-only client finds no tool that reads, or every tool is disabled,
tools/listanswers with an empty list. - Replicas. If you run several copies, the list comes from one of them (the longest-connected enabled one), on the assumption that all register the same tools. See Running replicas.
A client learns that tools changed by listing them again; the gateway never pushes change notifications.
When no agent is connected
The gateway answers every request, including initialize, with 503 when there is nothing to serve it, so a client sees a clear error instead of an empty server:
| Situation | Status | JSON-RPC code and message |
|---|---|---|
| No agent is connected | 503 | -32001: No agent connected for server "<project id>". |
| Agents are connected but every one is disabled | 503 | -32001: Every agent for server "<project id>" is disabled. |
The first means nothing is running or connected; the second means agents are connected but switched off on the Agents page.
Tracing headers
If your client already traces, send a W3C traceparent header (and optionally tracestate). The gateway passes it to your agent so the agent's span joins your trace, and stores the trace id on the call so you can find it in Logs and Traces. Without a valid traceparent the gateway starts a new trace, and a malformed header is replaced, never passed on.
curl -s -X POST "$ENDPOINT" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_order","arguments":{"id":"1"}}}'
That call appears under the trace id 4bf92f3577b34da6a3ce929d0e0e4736, marked as a client trace.
OAuth instead of a key
Some gateways are set up to authenticate MCP clients with OAuth access tokens from an identity provider instead of static keys. A person then approves the connection in a browser; Claude Desktop needs no bridge (you add the URL as a custom connector) and Claude Code needs no --header:
claude mcp add --transport http orders-api https://api.spacefix.io/mcp/<project id>
Two consequences to know. When OAuth is on, the MCP endpoint accepts access tokens only: a static client key is not accepted there, and key allow-lists do not apply to token holders. A token with mcp:write reads and writes; one with only mcp:read sees only the tools that read (the project's Client access can still limit both). Agents are unaffected and still connect with an agent key. If you are not sure which mode your service uses, call the endpoint without a key: a key-based endpoint answers 401 with Missing API key.
What can refuse a call
Checks run in this order and the first that fails ends the request.
| Check | Status | Body |
|---|---|---|
| Credential present and valid | 401 | {"error":"Missing API key."} or {"error":"Invalid API key."}, with a WWW-Authenticate: Bearer realm="mcp" header |
| Too many wrong keys from one address | 429 | {"error":"Too many failed authentication attempts."} with Retry-After |
| Right key, wrong kind | 403 | {"error":"This is an agent key. MCP clients connect with a client key."} |
| Request body larger than 4 MB | 413 | |
| Project suspended | 403 | JSON-RPC -32003: This project is suspended: <reason> |
| Address not on the project's client allow-list | 403 | JSON-RPC -32004: This address is not on the project's client allow-list. |
| Project request rate exceeded | 429 | {"error":"Rate limit exceeded: <n> requests per minute."} with Retry-After (seconds) |
| The URL's project differs from the key's | 403 | {"error":"Forbidden - agent identity mismatch."} |
| Monthly allowance used up (plans that block) | 429 | JSON-RPC -32002: Monthly allowance of <n> calls on the <plan> plan is used up. It resets on the 1st (UTC), or upgrade the plan. |
| No agent connected, or every one disabled | 503 | JSON-RPC -32001 (above) |
Rate limits and the allowance
- Rate limit. Each project has a requests-per-minute budget set by its plan (600 a minute on the Free plan by default). When it is spent, wait the number of seconds in the
Retry-Afterheader, then retry. Bothinitializeandtools/listcount; the results your agent posts back do not. - Allowance. Only calls that would run your code are refused when the allowance is used up:
tools/call,resources/readandprompts/get. Listing stays free, so a client can still connect and show the error. Plans that do not block let calls through. - Client address allow-list. Set in Project settings: up to 20 IP addresses or CIDR ranges, one per line. It applies to MCP clients only; your agent's outbound connection is never checked against it. A client behind a corporate proxy presents the proxy's address.
Next steps
- Projects and keys: create client keys, allow-lists, expiry, rotation.
- Building tools: what makes a tool easy for a model to use.
- Troubleshooting: a symptom-to-fix table for refused or failing calls.