MCP API reference
SharedOS presents its permission-filtered tool catalogue as an MCP server, so a harness that already speaks MCP — Codex, Claude Code, DeepSeek Harness, Pi — runs natively against it without a shim. This page is the wire reference: transports, methods, status codes, and headers. Why this boundary exists, what crosses it, and what is deliberately never published are in MCP toolshare and ADR 0014.
This is the second network surface. The first is the
HTTP API, which serves your own code. They are not alternatives:
the kernel behind them is the same, and a tools/call here becomes exactly the
ToolCall that POST /v1/tools/invoke would have made.
import { createStreamableHttpMcpServer, McpToolServer } from "@aicoo/sharedos-mcp";
const bridge = await createStreamableHttpMcpServer({
server: new McpToolServer({ invoker, serverInfo: { name: "sharedos", version: "1" } }),
authorize: (token) => verifySessionToken(token),
});
// bridge.url -> http://127.0.0.1:53124/mcp
A bridge lives for one turn
The catalogue is resolved once per turn and cannot change underneath a running harness. Everything else about this surface follows from that:
initializeadvertisestools: { listChanged: false }— stated, not omitted. A client polling for catalogue changes would wait on a notification this server will never have cause to send.tools/listis served whole. There is no pagination, because a harness that acted on a prefix would be acting on a partially discovered catalogue — exactly the stale-discovery failurecatalogHashexists to catch.- There is no server-initiated stream.
GETis refused rather than left open. - A session id from an earlier bridge is answered with
404, not silently adopted, so a client re-initializes instead of calling into a turn that ended.
Transports
| Transport | Entry point | Lifetime |
|---|---|---|
| stdio | serveMcpOverStdio | The harness spawns SharedOS; the process ends with the invocation |
| Streamable HTTP | createStreamableHttpMcpServer | Loopback listener, closed with the turn |
Both carry the same McpToolServer. Choose stdio when SharedOS is the
subprocess — a turn-scoped bridge and a process lifetime that already match need
nothing to keep them in step. Choose HTTP when the harness is sandboxed or
remote and cannot be handed a stream pair at all; it is also the transport every
emitted harness configuration points at.
stdio
Newline-delimited JSON on a Readable/Writable pair. Messages are answered in
arrival order — MCP permits interleaving, but a bridge that answered out of order
would let a harness observe two calls resolving against one turn in an order the
audit trail does not show. Resolves when the input ends or the signal aborts.
Streamable HTTP
One route, on loopback by default.
| Option | Default | Meaning |
|---|---|---|
path | /mcp | The path every emitted harness configuration expects |
host | 127.0.0.1 | Loopback unless a host deliberately widens it |
port | 0 | Ask the OS for a free port, which is what a turn-scoped bridge wants |
authorize | absent | Validates the bearer token on every request |
The returned server carries url, port, sessionId, and close().
| Request | Status | Response |
|---|---|---|
POST with one message or a batch | 200 | The JSON-RPC answer, or an array for a batch |
POST whose answers are all notifications | 202 | Empty. Accepted, with nothing to say back |
POST with accept: text/event-stream | 200 | The same answers, framed as SSE |
DELETE | 200 | Empty. The client is done with the session |
| Any other method | 405 | {"error":"method_not_allowed"}, with allow: POST, DELETE |
| Any other path | 404 | Empty |
authorize returned false | 401 | {"error":"unauthorized"}, with www-authenticate: Bearer realm="sharedos" |
mcp-session-id from another bridge | 404 | {"error":"unknown_session"} |
| Body is not JSON | 400 | JSON-RPC -32700 |
| Empty JSON-RPC batch | 400 | JSON-RPC -32600 |
| Unhandled failure | 500 | {"error":"internal_error"} |
Every response carries mcp-session-id.
authorize receives the bearer token, or undefined when none was sent. Leave
it absent for a loopback bridge whose port is known only to the subprocess it was
opened for. Supply it for a sandboxed or remote harness, where the execution
token identifies the broker session — mintExecutionToken and
verifyExecutionToken are in the same package. The token identifies a session
and never carries authority. Authorization is still the kernel's, per call,
against a grant.
Methods
| Method | Params | Result |
|---|---|---|
initialize | protocolVersion? | protocolVersion, capabilities, serverInfo, instructions? |
ping | — | {} |
tools/list | — | tools[], _meta |
tools/call | name, arguments? | content[], structuredContent?, isError |
notifications/initialized | — | No response |
Anything else is -32601.
initialize
{
"protocolVersion": "2025-06-18",
"capabilities": { "tools": { "listChanged": false } },
"serverInfo": { "name": "sharedos", "version": "1" },
"instructions": "…"
}
Supported protocol versions are 2025-06-18 (latest), 2025-03-26, and
2024-11-05. A version outside that set — including none at all — negotiates to
the latest rather than failing, so an older client is never refused at the door
for a version string.
tools/list
{
"tools": [
{
"name": "files.search",
"description": "Semantically search files inside an explicitly granted path.",
"inputSchema": { "type": "object", "required": ["path", "query"], "properties": {} },
"outputSchema": { "type": "object", "properties": {} },
"annotations": { "readOnlyHint": true },
"_meta": { "sharedos/namespace": "files", "sharedos/source": "sharedos" }
}
],
"_meta": {
"sharedos/catalogHash": "…",
"sharedos/executionId": "execution-1"
}
}
This is the same effective catalogue GET /v1/tools returns for the same
context: registered and namespace enabled and allowed by some grant. A
tool the caller may not use does not appear, so a model driven by this list never
learns it exists.
name is the canonical SharedOS tool ID, unchanged. A harness may rewrite it for
its own display — Claude Code, Codex, and DeepSeek all use
mcp__<server>__<tool> — and that alias is recorded on the bridge for diagnosis,
never on the ToolCall, so it cannot reach an authorization decision even by
mistake.
Two fields change name on the way out. SharedOS annotates a tool readOnly and
destructive; what is published are MCP's own readOnlyHint and
destructiveHint. Namespace and source are not MCP fields at all, so they travel
under _meta as sharedos/namespace and sharedos/source. What is never
published — the required capability, the grants behind it, the issuing authority,
namespace settings, credentials, handler references — is listed in
MCP toolshare.
catalogHash is 64 hex characters over the tools and nothing else, so two
harnesses handed the same semantic tool set produce the same hash even though
their execution ids, transports, and aliases differ. A catalogue holds at most
512 tools.
tools/call
{
"name": "files.search",
"arguments": { "path": ["Work", "Projects", "atlas"], "query": "ship date" }
}
The exposed name is mapped back to the canonical ID, the call is given a fresh
SharedOS call id, and the invoker hands it to the kernel — where the arguments
are parsed, the exact resource is re-derived from them, and that resource and
action are authorized again. A name that was never published is passed through to
be refused tool_unavailable rather than rejected at the bridge, so an
unpublished tool and a misspelled one fail the same way.
{
"content": [{ "type": "text", "text": "{\"hits\":[…]}" }],
"structuredContent": { "hits": [] },
"isError": false
}
structuredContent is present only when the published tool declares an
outputSchema and the output is a JSON object.
A denied call is a successful JSON-RPC response. Refusals and failures come back as a tool result, not a JSON-RPC error:
{
"content": [
{
"type": "text",
"text": "{\"status\":\"denied\",\"code\":\"no_matching_grant\",\"message\":\"…\"}"
}
],
"isError": true,
"_meta": { "sharedos/status": "denied", "sharedos/code": "no_matching_grant" }
}
Read _meta["sharedos/status"] to separate denied from failed, and
sharedos/code for the reason code. A client that treats every
isError alike will report a permission decision as an outage.
JSON-RPC errors
Reserved for messages that never reached a tool at all.
| Code | Meaning |
|---|---|
-32700 | Body is not JSON |
-32600 | Not a JSON-RPC message, or an empty batch |
-32601 | Unknown method |
-32602 | tools/call without a tool name |
-32603 | Internal failure |
Harness configuration
@aicoo/sharedos-mcp emits the connection file each harness expects, so a
bridge URL becomes a working configuration without hand-editing.
McpHarnessId | File | Function |
|---|---|---|
codex | config.toml | codexMcpConfig |
claude-code | .mcp.json | claudeCodeMcpConfig |
deepseek | cordis.patch.yml | deepseekMcpConfig |
pi | JSON options | piMcpConfig |
harnessMcpConfigFile(harness, connection) returns the filename and contents for
any of them; claudeAgentSdkMcpOptions reuses the Claude Code server block for
the Agent SDK; harnessToolAlias reproduces the display name a harness will
show.
Escalation over MCP
A turn driven over MCP can end by asking a human to decide, the same as one
driven in-process. The affordance is published as an ordinary tool,
sharedos.escalate, and is permission-filtered like any other — an agent with no
grant over it does not see it in the catalogue at all.
It is never invoked. A driver that recognizes the name ends the turn with an escalate outcome instead of making a tool call, so nothing reaches the kernel. See ADR 0018 and ADR 0011.
Calling it with curl
BRIDGE=http://127.0.0.1:53124/mcp
AUTH="authorization: Bearer $EXECUTION_TOKEN"
JSON='content-type: application/json'
curl -s "$BRIDGE" -H "$AUTH" -H "$JSON" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}'
curl -s "$BRIDGE" -H "$AUTH" -H "$JSON" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
curl -s "$BRIDGE" -H "$AUTH" -H "$JSON" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
curl -s "$BRIDGE" -H "$AUTH" -H "$JSON" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"files.search","arguments":{"path":["Work"],"query":"ship date"}}}'
Related
- MCP toolshare: what crosses the boundary, and what never does.
- HTTP API reference: the same kernel for your own code.
- Tool catalog: the three availability gates behind every list.
- Reason and error codes: what a
deniedstatus means.