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.
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 queriessophia.search_documents— hybrid retrieval (BM25 + dense + cross-encoder rerank)sophia.query_codebase— tree-sitter symbol graph walkssophia.execute_code— multi-call composition in a sandboxed V8 (see below)sophia.write_wiki_page— durable markdown notessophia.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
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 asAND e.id IN (?)injected into every read query at the SQL layer)profile—full(read + write),read-only, or a custom shapedisplay_nameandconnection_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.