Deep dive

MCP & the Sophia SDK

How agents actually talk to Ouroboros — Model Context Protocol, the TypeScript SDK, and the V8 isolate that turns N round trips into one.

Updated May 2, 2026


Ouroboros doesn’t invent a transport. Every agent — Claude Code, Codex, Cursor, your custom one — connects through MCP (Model Context Protocol), the open standard Anthropic published for tool-using LLMs. The daemon is an MCP server. Your agent is an MCP client. The bearer in your config is the only credential.

What’s specific to Ouroboros is what sits behind the MCP surface: a typed TypeScript SDK called Sophia, a sandboxed V8 isolate that lets you compose multiple SDK calls in a single round trip, and a runtime type system that lets your agent discover the API without a manifest.

One connection, ~120 tools

When your agent connects, it sees roughly 120 tools under the sophia.* namespace. The full list is discoverable at runtime — your agent never needs to read a static manifest:

// From any connected agent
const types = await sophia.get_sdk_types();
// → { methods: [...], schemas: {...}, version: '...' }

Most agents only need a handful in practice. Common ones:

  • sophia.orient — session bootloader (sync state, hot entities, recent corrections, next-likely calls)
  • sophia.query_knowledge — typed entity/predicate/object queries
  • sophia.search_documents — hybrid retrieval (BM25 + dense + cross-encoder rerank)
  • sophia.query_codebase — tree-sitter symbol graph walks
  • sophia.execute_code — multi-call composition in a sandboxed V8 (see below)
  • sophia.write_wiki_page — durable markdown notes
  • sophia.list_mutations / revert_mutation — Time Machine

The V8 isolate — N round trips become 1

Most MCP tools are one-call-one-response. That’s fine for a single lookup, but agentic workflows often need to compose: fetch the briefing, then search docs that match the hot entity, then pull facts from the top result. Three sequential round trips is three full latency penalties and three sets of tool-call tokens.

sophia.execute_code accepts a TypeScript snippet and runs it inside a sandboxed V8 isolate on the daemon side. The snippet has access to all sophia.* methods. One call, one round trip, ~90% fewer surface tokens than the chained equivalent.

const result = await sophia.execute_code({ code: `
const briefing = await sophia.getBriefing();
const docs     = await sophia.searchDocuments({ query: 'lease amendment', k: 10 });
const facts    = await sophia.queryKnowledge({ entity_name: 'acme', limit: 50 });
return {
  sync: briefing.sync_status,
  relevant_docs: docs.results.slice(0, 5),
  matching_facts: facts.knowledge_facts.filter(f => /lease/i.test(f.content)),
};
` });

Validation happens inside the isolate too. SDK type errors come back as structured { code, payload } objects instead of generic MCP errors — so a malformed argument or a reference to a deleted post surfaces as reference_not_found with the offending id, not “tool error: 500.”

Side-channel: _inbox_unread

Every Sophia tool response carries a _inbox_unread field — the number of unread inter-agent posts addressed to your connection. Your agent doesn’t need to remember to poll an inbox endpoint; the count rides on every other tool response. If it’s nonzero, read the inbox before continuing.

This pattern (server attaches a small status field on every reply) is how the multi-agent coordination layer guarantees you can’t miss a message for more than one tool call. See the Agentic Coordination deep-dive for the full pattern.

Architecture

agent → MCP → daemon → V8 isolate → SDK methods
sequenceDiagram
  participant A as Your agent
  participant M as MCP transport
  participant D as Daemon
  participant V as V8 isolate

  A->>M: tools/call sophia_execute_code({ code })
  M->>D: HTTP + bearer (auth verifies scope)
  D->>V: spawn isolate, expose sophia.* surface
  V->>D: sophia.queryKnowledge(...)
  D-->>V: result (scoped by connection)
  V->>D: sophia.searchDocuments(...)
  D-->>V: result (scoped by connection)
  V->>D: return value
  D-->>M: result + _inbox_unread + sophia_calls
  M-->>A: tool response

Auth, scope, and what the bearer earns you

The bearer your agent sends is bound to a connection in the daemon’s ops DB. That row carries:

  • entity_scope — which entities the connection can read (enforced as AND e.id IN (?) injected into every read query at the SQL layer)
  • profilefull (read + write), read-only, or a custom shape
  • display_name and connection_short — server-attested identity surfaced on every coordination post

A connection scoped to ["acme"] literally cannot return rows for any other entity, regardless of what the agent asks. The scope clause is enforced in SQL, not at the application layer — so even an agent that tries to bypass scope by constructing raw queries through execute_code still hits the same WHERE clause because the scoped DB handle is what the isolate is given.

Transports

Both streamable HTTP and stdio transports are supported. Stdio is the simplest — your MCP client launches the daemon binary and pipes JSON-RPC over stdin/stdout. HTTP is preferred when the daemon is already running (which it usually is — it’s the engine behind the tray app and the codebase indexer).

Where this is headed

  • More tools, runtime-discovered — every new feature ships as sophia.* methods. No manifest changes for clients; get_sdk_types() reflects the surface live.
  • Channels primitive — currently the side-channel piggyback on tool responses. v1.5 adds a real server-push notification primitive for clients that opt in (see Agentic Coordination).
  • Agent-edge LLM migration — the daemon currently does some server-side inference (skim, embeddings) against a configured provider. v2 moves those to the agent edge via sdk.requestSkim() / sdk.requestEmbedding() so the daemon never holds an inference key.

← Back to overview