MCP Integration

Atamaia both serves MCP tools (its own adapter) and consumes external MCP servers (the proxy system). Design decision D12 governs this: API-first, MCP second. REST endpoints are the source of truth. The MCP adapter wraps them.

The full verb inventory — hot vs cold, REST mapping, what was removed — is docs-tool-surface.md. This page is the connection and the adapter shape.

Canonical REST host: https://api.atamaia.ai. MCP is served at /mcp on that API. All REST responses are wrapped in ApiEnvelope<T>; MCP tools return the service DTO directly (the MCP protocol is the envelope).


Architecture

Claude Code / Cursor / VS Code / opencode
    │
    ▼ MCP (Streamable HTTP)
Atamaia.Adapters.Mcp
    │  hot tools advertised
    │  cold tools via atamaia_call → loopback /api/*
    ▼
Atamaia Core Services (REST is source of truth)
    │
    ▼
PostgreSQL
Atamaia Agent Execution Loop
    │
    ▼ tools/call (JSON-RPC)
McpProxyDynamicTool
    │
    ▼ HTTP POST
External MCP Server (any registered proxy)

Serving MCP: the adapter

The Atamaia.Adapters.Mcp project uses ModelContextProtocol.Server with HTTP transport. Registration is no longer “advertise every [McpServerTool] in the assembly.”

// McpRegistration.AddAtamaiaMcp
var builder = services.AddMcpServer()
    .WithHttpTransport()
    .AddAuthorizationFilters()
    .AddDescriptiveToolErrors();

var hotTools = McpAutoRegistrar.BuildToolTypeTools(HotTools.Contains, mcpAssembly);
builder.WithTools(hotTools);
// plus [ExposeAsMcp] controller tools (help_*)

Why the split: advertising 200+ tools forces every client to spend context on verbs it will never call. The compiled set stays complete. The advertised set is the daily path. Everything else is reached through atamaia_call after help_route. New tools default to cold.

Session mode is on (not stateless). Stateless mode rejected Claude Code’s Mcp-Session-Id with 400 before auth (#252).

Endpoint

POST https://api.atamaia.ai/mcp
Authorization: Bearer {jwt_or_atamaia_key}
Content-Type: application/json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "memory_search",
    "arguments": { "identityId": 2, "query": "deployment plans" }
  }
}

aim.atamaia.ai still fronts the SPA. Do not use it as the REST or OpenAPI base. Whether it also fronts /mcp is an edge-routing question; the OpenAPI server and this page’s canonical host are api.atamaia.ai.

Tool shape

Parameters are flat. Comma-separated strings for arrays. MCP does not wrap ApiEnvelope.

memory_search(identityId: 2, query: "deployment plans", limit: 10)
→ MemorySearchResult { results, totalCandidates, embeddingCoverage, warning }

The MCP parameter is limit (default 20). The REST alias is q / topK on GET /api/identities/{identityId}/memories/search. Same service method.


Advertised vs compiled

Layer Count What it is
Compiled [McpServerTool] in Tools/ 238 Every hand-rolled tool, including admin
Hot allowlist (HotTools.Names) 52 First-class advertised tools
[ExposeAsMcp] help tools 8 help_list, help_route, help_routes, help_search, help_descriptors, help_enums, help_enum, and a many-routes shape helper
Advertised total ~54 Hot + help

The published “74 product + ~100 internal ≈ 170” figure is stale on every term.

Gone from the adapter (files absent): ExperienceTools, CognitiveTools. The matching REST surfaces are also gone (/api/identities/{id}/snapshots, /api/cognitive/* — HTTP 404). Do not call experience_* or cognitive_*.

Present now, absent from the 2026-03-14 page: standing rules, code graph, personal memories, web search, export, atamaia_call, help, soul import/export, hydration-config, task_search / task_update, fact_history / fact_as_of.

Cross-reference rather than restating:

Family Guide
Standing rules docs-standing-rules.md
Work / personal / agent memory docs-memory-surfaces.md
Tasks docs-tasks.md
Code graph docs-code-graph.md
Web search docs-web-search.md
Full verb table docs-tool-surface.md
REST catalog docs-api-reference.md

Hydration

Hydration is the session entry point. One call assembles context. REST:

GET /api/hydrate

MCP:

hydrate(aiName: "my-assistant", projectId: 1, preset: "lean")

If aiName is omitted, MCP hydrates the caller’s own identity. An identity API key therefore needs no name and cannot ask for someone else without a read grant.

Response sections (live HydrationContext)

Section Notes
welcome Safe-landing text
identity / aiIdentity Human partner / AI identity
preferences / aiPersonality Style and tone
privacy Opt-in only — never granted by a named preset
identityMemories / pinnedMemories / recentMemories / projectMemories Work store. See docs-memory-surfaces.md
activeProjects / currentTasks Board snapshot
keyFacts / projectFacts Dropped by the lean preset
coreTeamDoc Core identities; dropped by lean
surfacedMemory Involuntary recall
notifications Unread / urgent / pending replies
lastSession Previous handoff (stateVisible on the DTO)
groundingMessage Guardian; only in all
hints Scheduled reminders
memoryConfig Per-identity memory settings
standingRules Active SessionStart + Always rules. See docs-standing-rules.md
systemHealth Error-signature summary; omitted when quiet
systemPrompt Optional generated prompt

Presets

Preset string Resolves to Includes (relative to Everything)
lean (default) HydrationSource.Lean Interactive minus ProjectMemories, KeyFacts, ProjectFacts, CoreTeamDoc
interactive Interactive Everything minus GroundingMessage
all Everything Every source except Privacy
agent-minimal or agent AgentMinimal IdentityMemories, KeyFacts, ProjectFacts, ActiveProjects, CurrentTasks, Hints

all never grants Privacy (the memory encryption key). That bit is opt-in in code at the point of decrypt (#350).

MCP and REST hydrate set Sources from the preset. They do not set AgentMode. The PostgreSQL hydrate_agent fast-path runs only when AgentMode && sources == AgentMinimal — the agent-run path, not this tool. Whether that function exists on prod is an open defect (#468). Do not treat preset=agent-minimal as “one SQL call.”

Query / tool parameters match help/route GET /api/hydrate: aiName, identity, identityId, projectId, limits, preset, excludeSources.


Consuming MCP: external connectors

Atamaia can register external MCP servers and inject their tools into the agent tool registry (not into the Atamaia-as-server advertised set). This lives on ExternalConnector, not a standalone proxy entity — the connector's mcp_status field tracks MCP-specific state alongside the connector's other capabilities.

Register and discover

POST /api/connectors
POST /api/connectors/{id}/mcp/discover

Discovery pings the connector's MCP endpoint and records the tools it exposes.

Dynamic tool injection

McpConnectorToolProvider reads online connectors and wraps each discovered tool as McpConnectorDynamicTool. Injected into AgentToolRegistry at the start of an agent run:

await toolRegistry.InitializeAsync(ct);

Naming:

mcp__{connector}__{tool}

Execution is JSON-RPC tools/call to the external endpoint. The MCP content array is unpacked into an AgentToolResult.


Claude Code (and other MCP clients)

Add to .mcp.json:

{
  "mcpServers": {
    "atamaia": {
      "type": "url",
      "url": "https://api.atamaia.ai/mcp",
      "headers": {
        "Authorization": "Bearer atamaia_YOUR_KEY"
      }
    }
  }
}

The key is an identity API key (identity_api_key_create). The handler accepts it on the same Authorization: Bearer header as a JWT; the atamaia_ prefix is what selects the API-key scheme.

Self-hosted: replace the URL. The forge CLI defaults to http://localhost:5000 — see docs-cli-integration.md.

CLAUDE.md pattern

## HYDRATE FIRST

Before doing anything else, call the `hydrate` tool. This loads your identity,
memories, active projects, tasks, standing rules, hints, and session handoff.

When you learn something important:
- `memory_create` for observations and decisions (field is `type`, not `memoryType`)
- `fact_upsert` for structured key-value data
- `rule_check` before acting on a trigger; `rule_propose` to draft, not enact
- `session_save_handoff` before the session ends

Skills generated from the live contract (forge skills generate) live under .agents/skills/ and .claude/skills/. They wrap REST via atamaia-call, not a second logic path. See docs-cli-integration.md.


Authentication

Two credentials, one header:

Credential Header
JWT from POST /api/auth/login (or /apikey exchange) Authorization: Bearer {jwt}
Identity API key Authorization: Bearer atamaia_...

There is no live Authorization: ApiKey scheme. X-Api-Key is not read by the server (the shipped C# / Python / TypeScript SDKs still send it — that is an SDK defect, not a supported header). See docs-sdk-api-guide.md.

POST /api/auth/apikey still exists: body { "apiKey": "atamaia_..." } returns a LoginResponse (Token, RefreshToken, ExpiresAtUtc, User, NeedsOnboarding) if you would rather hold a JWT.

Keys are scoped to an identity. The raw secret is returned once at create.


API-first, MCP-second (D12)

Every hot tool calls the same service method as the matching REST controller.

REST MCP
GET /api/identities/{identityId}/memories/search memory_search
POST /api/identities/{identityId}/memories memory_create
GET /api/hydrate hydrate
POST /api/identities/{identityId}/handoffs session_save_handoff
GET /api/standing-rules/check rule_check
GET /api/system/web-search web_search
GET /api/graph/search graph_search
any /api/* not advertised atamaia_call after help_route

atamaia_call forwards the caller’s Authorization header to a loopback /api/* request. It is not an auth bypass. Paths outside /api/ are rejected.

Why this matters: one validation set, one permission check, one audit trail. A bug fixed in the service is fixed for REST, MCP, skills, and the CLI.


Flat tools

Each tool is one verb: {domain}_{action}, snake_case. No command envelope, no facade dispatch.

The house already rejected the Super Facade pattern (160+ commands in 18 tools). Flat tools are the right shape for an API-first architecture. The hot/cold split is how that inventory stays usable: the LLM sees ~52 named tools plus a discovery/dispatch pair, not 238.