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:

http
Authorization: Bearer <your client key>
Key kindAccepted on the MCP endpointAccepted by the SDK (agent connection)
clientYesNo
fullYesYes
agentNo, 403Yes

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.

bash
claude mcp add --transport http orders-api 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 Authorization header, so a key goes through the mcp-remote bridge, which needs Node.js for npx. The key goes in env and the header refers to it as ${AUTH}, which avoids a space inside a command-line argument. Restart Claude Desktop afterwards.
  • Cursor. Use .cursor/mcp.json in your project, or ~/.cursor/mcp.json for every project.
  • VS Code. Use .vscode/mcp.json in your workspace. The top-level key is servers, not mcpServers.
  • 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.

bash
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.

js
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

RequestWhat comes back
initializeProtocol 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/listThe tools your agent registered: name, description (or the description you overrode in the dashboard), and the JSON Schema
tools/callThe 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/readResources by uri. A read returns contents with uri, mimeType (default application/json) and text
prompts/list, prompts/getPrompts 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:

SituationResult
The handler threwisError: true; the content is the thrown message
The agent did not answer in timeisError: true, Request for tool "<name>" timed out after <ms>ms.
The arguments do not match the schemaisError: true, for example Input validation error: Invalid arguments for tool get_order: data must have required property 'id'
The agent disconnected mid-callisError: true, Agent disconnected before returning a result.
No agent has that toolisError: true, No connected agent serves tool "<name>".
An unknown or hidden toolJSON-RPC error -32602, Tool <name> not found
An unknown resourceJSON-RPC error -32602, Resource not found: <uri>
A resource handler threwJSON-RPC error -32603 with the thrown message
A missing prompt argumentJSON-RPC error -32602, for example Invalid arguments for prompt summarise_order: data must have required property 'id'
An unknown methodJSON-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:read sees only the tools that read: those whose agent sets readOnlyHint: true, or that you marked Read. Unlabelled tools count as writes and are absent; calling one gives Tool <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/list answers 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:

SituationStatusJSON-RPC code and message
No agent is connected503-32001: No agent connected for server "<project id>".
Agents are connected but every one is disabled503-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.

bash
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:

bash
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.

CheckStatusBody
Credential present and valid401{"error":"Missing API key."} or {"error":"Invalid API key."}, with a WWW-Authenticate: Bearer realm="mcp" header
Too many wrong keys from one address429{"error":"Too many failed authentication attempts."} with Retry-After
Right key, wrong kind403{"error":"This is an agent key. MCP clients connect with a client key."}
Request body larger than 4 MB413
Project suspended403JSON-RPC -32003: This project is suspended: <reason>
Address not on the project's client allow-list403JSON-RPC -32004: This address is not on the project's client allow-list.
Project request rate exceeded429{"error":"Rate limit exceeded: <n> requests per minute."} with Retry-After (seconds)
The URL's project differs from the key's403{"error":"Forbidden - agent identity mismatch."}
Monthly allowance used up (plans that block)429JSON-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 disabled503JSON-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-After header, then retry. Both initialize and tools/list count; 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/read and prompts/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.