MCP Server
Serve a workspace's governed tools to Claude Code, Cursor, and other MCP clients at /mcp.
Since v1.3.0, SOIT is itself an MCP server. It serves each workspace's tools to MCP clients — Claude Code, Cursor, the official SDKs, and any other client of the streamable HTTP transport — at /mcp. A client connects with a SOIT API key; the tools it is offered are the ones that key may invoke, and every call it makes is a governed run, the same as a direct tool call.
This page is about MCP clients calling SOIT. For SOIT calling external MCP servers as plugin tools, see MCP and Skills.
Connecting
| Setting | Value |
|---|---|
| URL | The API address plus /mcp, for example https://soit.example.com/mcp |
| Transport | Streamable HTTP |
| Credential | Authorization: Bearer sk_…, a SOIT API key |
The key names the workspace. A signed-in session works too, with the workspace in X-Workspace-Id. Listing and calling tools take a credential that may write in the workspace (a Dev, Admin, or Owner, and a key with the write scope); a read-only credential is offered no tools. Settings › API lists where clients connect, with the Claude Code command.
Claude Code
claude mcp add --transport http soit http://localhost:9200/mcp \
--header "Authorization: Bearer $SOIT_API_KEY"Or share it with a project in .mcp.json, which Claude Code reads from the project root and expands ${SOIT_API_KEY} in:
{
"mcpServers": {
"soit": {
"type": "http",
"url": "http://localhost:9200/mcp",
"headers": {
"Authorization": "Bearer ${SOIT_API_KEY}"
}
}
}
}Cursor
Put this in .cursor/mcp.json (one project) or ~/.cursor/mcp.json (every project). Cursor expands ${env:SOIT_API_KEY}:
{
"mcpServers": {
"soit": {
"url": "http://localhost:9200/mcp",
"headers": {
"Authorization": "Bearer ${env:SOIT_API_KEY}"
}
}
}
}Other clients
examples/mcp/ in the SOIT repository includes a Python client built on the official MCP Python SDK (pip install mcp) that lists the tools and calls one. Keep the key out of files you commit.
Tools
tools/list offers every tool of the workspace the credential may invoke: built-ins, installed plugin tools, and the tools of enabled MCP servers. A key with Allowed tools set sees only those.
MCP clients and model APIs accept only letters, digits, _, and - in a tool name, so each SOIT ref is given a name:
| SOIT ref | MCP tool name |
|---|---|
tool:function:time_now | function_time_now |
mcp_tool:github:create_issue | github_create_issue |
A name that would collide with another, or run past 64 characters, carries a short digest of its ref. Each tool's _meta holds its ref as ai.soit/tool_ref.
tools/call runs the tool through the tool gateway SOIT's agents use: secrets, egress policy, rate limits, budgets, audit, and cost apply, and the call is a run with mode=tool whose id comes back in the result's _meta as ai.soit/run_id. A result that is a JSON object is also returned as structuredContent.
Refusals the model can act on — arguments that do not match the tool's schema, a blocked address, a spent budget — come back as a tool result with isError: true and the reason, so the model can correct itself. An unknown tool name is the protocol error -32602.
Approval
A tool whose policy requires approval does not run on the first call. The result is an error that says so, with the approval id (ai.soit/approval_id) and run id, and the request appears in Govern › Approvals. Once a reviewer decides, the same call with the same arguments runs the tool or reports the rejection. Calling again before the decision returns the same pending approval; arguments that differ make a new call, with a new approval. Who decides, deadlines, and delegation are described in Approvals.
Protocol revisions
The endpoint is stateless whichever revision a client speaks:
- Every
POST /mcpcarries one JSON-RPC message and is answered on its own, withapplication/json. No session is kept, noMcp-Session-Idis issued, and one a client sends is ignored, so any API instance can answer any request. GET /mcp(a server-to-client stream) andDELETE /mcp(ending a session) answer405: the server sends no notifications and makes no requests of the client.- Notifications are accepted with
202; batches are refused. - A request naming a version SOIT does not support, in its
MCP-Protocol-Versionheader or its_meta, is refused with400andUnsupportedProtocolVersion(-32022), whosedatalists the supported versions.
| Revision | Behavior |
|---|---|
2026-07-28 (stateless) | No handshake. Each request declares io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities in params._meta (without them: 400, -32602) and repeats itself in the MCP-Protocol-Version and Mcp-Method headers, plus Mcp-Name for tools/call. A missing or disagreeing header is refused with 400 and HeaderMismatch (-32020). server/discover returns the supported versions, capabilities, and instructions. |
2024-11-05 to 2025-11-25 | Clients open with initialize, which answers with the client's version when supported and otherwise the latest of these; ping is answered. Results keep those revisions' shapes. |
In the 2026-07-28 revision every result carries resultType: "complete" and the server's identity in _meta (io.modelcontextprotocol/serverInfo). server/discover and tools/list say how long they may be reused (ttlMs) and that only the caller may cache them (cacheScope: "private"), because the tool list is the credential's own.
Authorization
A request without a working credential is answered 401 with a challenge that names the endpoint's OAuth 2.1 protected resource metadata (RFC 9728):
WWW-Authenticate: Bearer resource_metadata="https://soit.example.com/.well-known/oauth-protected-resource/mcp"The metadata, also served at /.well-known/oauth-protected-resource, names the resource and says that bearer tokens go in the Authorization header. SOIT authenticates MCP clients with its API keys, which clients send as a configured header, and does not issue OAuth tokens itself. A deployment that puts an OAuth authorization server in front of SOIT lists it in MCP_AUTHORIZATION_SERVERS, and the metadata publishes it for clients that discover their authorization that way.
Deployment settings
| Setting | Purpose |
|---|---|
MCP_RESOURCE_URL | The endpoint's public URL, named in the metadata and the challenge. Empty derives it from the request; behind a proxy that rewrites the host, set it. The production compose file sets it from SOIT_PUBLIC_HOSTNAME. |
MCP_AUTHORIZATION_SERVERS | OAuth authorization servers to publish, as a JSON list. Defaults to none. |
MCP_ALLOWED_ORIGINS | Browser origins, besides the endpoint's own, that may call it. A request from any other Origin is refused with 403, which keeps a web page from reaching the endpoint through a user's browser. Clients that are not browsers send no Origin. Defaults to none. |
A proxy in front of the API must route /mcp and /.well-known/oauth-protected-resource* to the API as well as /api/* and /v1/*; the production reference's Caddyfile does. /mcp and /.well-known/* answer their protocols' own shapes, not SOIT's response envelope.
From v1.4.0, a process started with SOIT_ROLE=gateway serves /mcp and its discovery metadata too, along with /v1 and /api/v1/tools, so MCP traffic can run on processes of its own; see Running the gateway on its own.
Known limitations
As of v1.4.0:
- Only tools are served: no resources, prompts, sampling, elicitation, subscriptions, or progress notifications. Tool results are returned whole.
- The tool list is read when asked; the server sends no
list_changednotification when tools are installed or removed. - A call that needs approval runs only when the client calls again; nothing runs it on the client's behalf.
In v1.3.0 a key's calls-per-minute and daily limits counted model calls only. From v1.4.0 they count MCP tools/call too; a refusal is a tool result whose _meta carries ai.soit/retry_after and ai.soit/quota and whose text says when to retry. See Per-key limits.
Related pages
- OpenAI-Compatible Gateway — keys, limits, and direct tool calls.
- MCP and Skills — MCP servers SOIT consumes as plugins.
- Gateway API reference — the
/mcpendpoints.