Skip to main content
Ithaca exposes its tools over the Model Context Protocol (MCP). Agents connect using one of two transports — stdio for local, trusted execution or streamable-HTTP for remote, hosted connections. Every remote connection is authenticated with a per-tenant bearer token and scoped to the caller’s capabilities.

Transports

The default transport for local development. The MCP server runs as a child process of the agent, communicating over stdin/stdout. No network, no auth — the trust boundary is the local machine.
# run the MCP server in stdio mode (the default)
export DOOMBERG_DB=./doomberg.db
uv run python -m doomberg_mcp
The agent launches this process and speaks MCP over its stdio pipes. This is the path used when you self-host or run the demo end-to-end script.
stdio mode has no authentication. It is intended for local, trusted use only. Never expose a stdio MCP server over the network.

Authentication

Remote connections authenticate with a per-tenant API key in the Authorization: Bearer header. The key format is frozen by the auth contract:
dmbg_<env>_<keyid>_<secret>
SegmentMeaning
dmbgFixed prefix (lets leaked keys be scanned and revoked)
<env>Deployment env tag (test by default, prod in production)
<keyid>Public, stable credential id (UUID4 hex). Used for lookup and audit attribution
<secret>High-entropy URL-safe token. Never stored — only sha256(salt + secret) is persisted

Verification flow

The TenantAuthMiddleware resolves every request:
  1. Extract the bearer token from the Authorization header.
  2. Split into (prefix, env, key_id, secret) — the secret may contain _, so only the first three separators are split.
  3. Look up the key_id row in the api_keys table.
  4. If the key is revoked (revoked = 1), return None → 401.
  5. Compute sha256(salt + secret) and compare with the stored hash using hmac.compare_digest (constant-time).
  6. On match, return {tenant_id, scopes, key_id} → the Principal.
The principal is stashed in a contextvars.ContextVar so the tool wrappers can pass it into the Toolset as _ctx. Every hosted call is then tenant-scoped and traced under that tenant.

Scope enforcement

Scopes are enforced per-request by inspecting the JSON-RPC body:
ScopeRequired for
data:readAny tools/call or resource read/list request
strategy:authorThe propose_strategy tool
runs:writeThe run_skill tool
runs:cancelThe cancel_run tool
approvals:readThe list_approvals tool
approvals:decideThe decide_approval tool (also requires a human actor)
artifacts:readThe get_artifact tool
Missing scopes return 403 missing scope: <scope>. Batch requests get the union of all required scopes.

The MCP initialize handshake

When the agent connects, the MCP client sends an initialize request. The Ithaca server responds with its capabilities and instructions:
// client → server
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": { ... },
    "clientInfo": {"name": "codex", "version": "1.0.0"}
  }
}

// server → client
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": {"listChanged": false},
      "resources": {"listChanged": false, "subscribe": false}
    },
    "serverInfo": {"name": "doomberg", "version": "0.0.0"},
    "instructions": "You are connected to Ithaca..."
  }
}
The instructions field carries the canonical workflow (see Agent Workflow). The agent is expected to read and follow it. After initialize, the client sends notifications/initialized and the session is ready for tools/list, tools/call, resources/list, and resources/read.

Tool discovery

Ithaca exposes 60+ tools. Loading all of them into the agent’s context at once is wasteful and can overflow the context window. Ithaca supports progressive discovery:

search_skills — progressive discovery

// tool call
search_skills(query="volatility", asset_class="market", intent="risk")

// expected output (abbreviated)
{
  "skills": [
    {
      "skill_id": "vol.realized",
      "name": "Realized Volatility",
      "description": "Annualized realized (close-to-close log-return) volatility...",
      "runtime_profile": "interactive",
      "asset_class": "vol",
      "source_classes": ["computed"],
      "trigger_compatible": false
    },
    {
      "skill_id": "vol.term_structure",
      "name": "Vol Term Structure",
      ...
    }
  ],
  "count": 6,
  "truncated": false
}
The agent searches for skills matching a query, asset class, or intent, then calls describe_skill for the ones it wants to use. This keeps the context window small.

describe_skill — one skill’s manifest

// tool call
describe_skill(skill_id="vol.realized")

// expected output (abbreviated)
{
  "skill_id": "vol.realized",
  "version": "1.0",
  "name": "Realized Volatility",
  "description": "Annualized realized (close-to-close log-return) volatility...",
  "input_schema": {...},
  "output_schema": {...},
  "runtime": {"profile": "interactive"},
  "source_classes": ["computed"]
}

tools/list — list all tools

The standard MCP tools/list method returns every registered tool. This is the full catalog — use it when you need the complete list, but prefer search_skills for progressive discovery in a research session.

Resource URIs

Ithaca exposes read-only resources under the doomberg:// scheme. These are tenant-scoped (HTTP mode requires data:read) and return credential-redacted JSON.
URIDescription
doomberg://skills/catalogCompact skill catalog for progressive discovery
doomberg://skills/{skill_id}/{version}One versioned skill manifest
doomberg://runs/{run_id}Durable run with jobs, artifacts, and replayable trace events
doomberg://policies/currentCurrent deterministic platform policy (trading disabled, execution limits, typed error codes)
doomberg://accountsTenant-scoped account and provider connection summary (credentials redacted)
doomberg://data/providersTenant-scoped data-provider and dataset snapshot summary

Example: read the policies resource

// resources/read
{"uri": "doomberg://policies/current"}

// expected output
{
  "kind": "policies.current",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "trading": {"enabled": false, "detail": "data and backtest only"},
  "paper_trading_default": false,
  "live_execution": {"enabled": false, "requires_connected_provider": false},
  "execution": {
    "max_notional_usd": 10000,
    "max_quantity": 100,
    "max_freshness_ms": 60000
  },
  "typed_errors": ["AUTH_REQUIRED", "BROKER_CONNECTION_REQUIRED", ...]
}

Credential redaction

Resource responses never expose credential material. Provider connections return credential_configured: true/false but never credential_ref. IBKR-sourced data is sanitized: source_account, account_ref, host, quote_path, and order_path are stripped; the provider name is replaced with the public data-source label.

Session lifecycle

A Ithaca MCP session maps to a persistent research run in the platform store. The lifecycle:
connect (initialize)
  → session_context (opens/reuses a research run, emits session_open)
  → tool calls (each traced under the run_id)
  → subscribe_run / get_run (poll progress)
  → disconnect (session ends; run persists for replay)

Session correlation

The Toolset keys research sessions off (tenant_id, agent_id):
  • In stdio mode, agent_id defaults to a per-process UUID. All calls in one process share a research run.
  • In HTTP mode, agent_id is the verified key_id. All calls from one credential share a research run.
  • If session_context is called with an explicit session_id (a research-run UUID), subsequent calls attach to that run. This lets the web UI start a session and the agent join it.

Trace events

Every tool call emits a tool_call event (with the tool name and arguments) and a tool_result event (with a provenance summary). These are appended to the trace_event table with a gap-free monotonic seq per run_id. The web UI replays them via SSE.
Event typeWhen
session_openFirst session_context call in a session
reasoningsession_context with a prompt, or agent-narrated reasoning
tool_callBefore a tool executes
tool_resultAfter a tool returns
strategy_proposedpropose_strategy succeeds
run_transitionRun state changes (draft→queued→running→succeeded)
denialA money-path action is refused (agent tried to promote)
errorA tool or transition fails

Disconnect and replay

When the MCP session ends, the research run persists. The human can open app.doomberg.meSessions and replay the entire trace from seq=0. SSE replay uses Last-Event-ID to resume from any point without gaps.

Install an Agent

The one-liner installer that registers the hosted MCP endpoint with your agent.

Agent Workflow

The canonical research workflow and server instructions.

Security Model

Tenant isolation, API key security, and the glass-box principle.