Summary
Add an MCP server (Streamable HTTP transport) at /api/mcp that allows external Claude instances to programmatically list, create, monitor, and interact with sessions. This enables orchestration workflows where one Claude coordinates multiple session agents.
Authentication: API Keys
New ApiKey database model with two scopes:
Full-control keys (ca_full_<32 hex>)
- Can do everything: list/create/delete sessions, read any session's messages, send prompts
- Managed through the web UI via a new tRPC router (create, list, revoke)
- Revocable, with
lastUsedAt tracking
Session-internal keys (ca_sess_<32 hex>)
- Intended to be passed to session runner containers as env vars
- Very limited scope: can only set the session name for their bound session
- Automatically cascade-deleted when the session is deleted
Storage
- SHA-256 hash stored (not Argon2 — full-entropy random tokens don't need slow hashing)
prefix field stores first 8 chars for UI identification
- Raw key shown once at creation time, never stored in plaintext
model ApiKey {
id String @id @default(uuid())
name String
prefix String @unique
keyHash String @unique
scope String @default("full") // "full" or "session_internal"
sessionId String?
session Session? @relation(fields: [sessionId], references: [id], onDelete: Cascade)
revokedAt DateTime?
lastUsedAt DateTime?
createdAt DateTime @default(now())
@@index([keyHash])
@@index([sessionId])
}
MCP Tools
Full-control tools
list_sessions
- Input:
{ status?, includeArchived? }
- Output: Array of
{ id, name, repoUrl, branch, status, currentBranch, createdAt, updatedAt, isClaudeRunning }
get_session
- Input:
{ sessionId }
- Output: Session details +
isClaudeRunning + lastMessageSequence
get_messages
- Input:
{ sessionId, afterMessageId?, summarize?, includeSystem? }
- Default (no
afterMessageId): Returns the most recent assistant message in human-readable form (text content + tool call names, skipping past system/tool-result messages to find the last thing Claude said)
afterMessageId: Returns all messages after that ID in human-readable form (for catching up after sending a prompt)
summarize: true: Sends conversation history to Sonnet, returns a concise summary (citing important message IDs — plans, questions, PRs, errors) + the latest assistant message in full. Falls back gracefully if no Anthropic API key is configured.
includeSystem: false (default): Filters out system messages (init, compact boundary, hooks). Pass true for debugging Clawed Abode itself.
- Human-readable format: assistant text content, tool names with brief descriptions, message IDs for drill-down
get_message (singular)
- Input:
{ messageId }
- Output: Full raw message content
- For drilling into specific messages (exact tool inputs/outputs, plan contents, etc.)
- The summary from
get_messages({ summarize: true }) guides agents here by citing message IDs
create_session
- Input:
{ name, repoFullName, branch, initialPrompt? }
- Output:
{ sessionId, status: 'creating' } — caller polls get_session
- Reuses existing
setupSession() background function
send_prompt
- Input:
{ sessionId, prompt, waitForCompletion? (default false), timeoutMs? (default 120000, max 300000) }
- Fire-and-forget (default): Returns
{ started: true, startSequence } immediately. Caller uses get_messages({ afterMessageId }) to see results.
- Wait mode: Blocks until result message or timeout. If auto-interrupted (AskUserQuestion, ExitPlanMode), returns partial results including the tool call that triggered the interrupt so the caller can decide what to do.
manage_session
- Input:
{ sessionId, action: 'start' | 'stop' | 'delete' }
- Output:
{ success: true, session: {...} }
interrupt_session
- Input:
{ sessionId }
- Output:
{ interrupted: boolean }
Session-internal tools
set_session_name
- Input:
{ name }
- Session ID derived from the key's bound session
- Allows agents to rename their own session (e.g., based on what they're working on)
Context Bloat Mitigation
This is a critical design consideration since MCP tool responses go into the calling Claude's context window.
- Human-readable default:
get_messages returns what a user would see — text content + tool names — not raw JSON with full tool inputs/outputs
- Drill-down pattern:
get_message for full details on specific messages, guided by IDs from summaries
- LLM summarization:
summarize: true sends history to Sonnet for a concise overview, citing key message IDs
- System message filtering: System messages hidden by default (rarely useful for orchestration)
- Latest assistant message focus: Default returns just the most recent assistant message, which is usually what the orchestrator needs
Summary Prompt Design
When summarize: true is used, the full conversation history is sent to Sonnet with instructions to:
- Give a concise overview of what the session has been doing
- Call out the current state (waiting for input, working, errored, idle)
- Cite message IDs for key moments: plans proposed, questions asked, PRs opened, errors encountered
- Keep it brief (a few paragraphs max)
- Must be configurable/optional since it has latency and cost implications
HTTP Transport
- Endpoint:
/api/mcp (Next.js App Router API route)
- Transport:
WebStandardStreamableHTTPServerTransport from @modelcontextprotocol/sdk (already installed)
- Mode: Stateless (no MCP session tracking — each request authenticates independently via API key)
- Auth:
Authorization: Bearer <api-key> header → validated, scope passed to tool handlers via authInfo.extra
File Structure
New files:
src/server/services/api-keys.ts + api-keys.test.ts
src/server/services/mcp-server.ts + mcp-server.test.ts
src/server/services/mcp-auth.ts + mcp-auth.test.ts
src/server/services/mcp-message-formatter.ts + mcp-message-formatter.test.ts
src/server/routers/apiKeys.ts + apiKeys.integration.test.ts
src/app/api/mcp/route.ts
Modified files:
prisma/schema.prisma — Add ApiKey model + Session relation
src/server/routers/index.ts — Register apiKeys router
doc/DESIGN.md — Document MCP server section
Implementation Order
- Prisma schema + API key service + unit tests
- API key tRPC router + integration tests
- Message formatter (human-readable + drill-down) + unit tests
- MCP auth + server core with tool definitions + unit tests
- HTTP route wiring + integration tests
- LLM summarization for
get_messages({ summarize: true })
- Documentation updates
Future Extensions (not in this issue)
- Auto-generated session-internal keys: Pass key to containers on startup so agents can set their own name
- MCP Resources: Expose session lists as MCP Resources
- Settings management tools: Tools for managing global/repo settings via MCP
- Streaming notifications: Stateful mode + SSE for real-time updates
- Additional scopes:
read-only scope for monitoring without mutation
- MCP-based key management: Let full-control keys create/revoke other keys via MCP tools
This issue was designed collaboratively with Claude. — Claude
Summary
Add an MCP server (Streamable HTTP transport) at
/api/mcpthat allows external Claude instances to programmatically list, create, monitor, and interact with sessions. This enables orchestration workflows where one Claude coordinates multiple session agents.Authentication: API Keys
New
ApiKeydatabase model with two scopes:Full-control keys (
ca_full_<32 hex>)lastUsedAttrackingSession-internal keys (
ca_sess_<32 hex>)Storage
prefixfield stores first 8 chars for UI identificationMCP Tools
Full-control tools
list_sessions{ status?, includeArchived? }{ id, name, repoUrl, branch, status, currentBranch, createdAt, updatedAt, isClaudeRunning }get_session{ sessionId }isClaudeRunning+lastMessageSequenceget_messages{ sessionId, afterMessageId?, summarize?, includeSystem? }afterMessageId): Returns the most recent assistant message in human-readable form (text content + tool call names, skipping past system/tool-result messages to find the last thing Claude said)afterMessageId: Returns all messages after that ID in human-readable form (for catching up after sending a prompt)summarize: true: Sends conversation history to Sonnet, returns a concise summary (citing important message IDs — plans, questions, PRs, errors) + the latest assistant message in full. Falls back gracefully if no Anthropic API key is configured.includeSystem: false(default): Filters out system messages (init, compact boundary, hooks). Passtruefor debugging Clawed Abode itself.get_message(singular){ messageId }get_messages({ summarize: true })guides agents here by citing message IDscreate_session{ name, repoFullName, branch, initialPrompt? }{ sessionId, status: 'creating' }— caller pollsget_sessionsetupSession()background functionsend_prompt{ sessionId, prompt, waitForCompletion? (default false), timeoutMs? (default 120000, max 300000) }{ started: true, startSequence }immediately. Caller usesget_messages({ afterMessageId })to see results.manage_session{ sessionId, action: 'start' | 'stop' | 'delete' }{ success: true, session: {...} }interrupt_session{ sessionId }{ interrupted: boolean }Session-internal tools
set_session_name{ name }Context Bloat Mitigation
This is a critical design consideration since MCP tool responses go into the calling Claude's context window.
get_messagesreturns what a user would see — text content + tool names — not raw JSON with full tool inputs/outputsget_messagefor full details on specific messages, guided by IDs from summariessummarize: truesends history to Sonnet for a concise overview, citing key message IDsSummary Prompt Design
When
summarize: trueis used, the full conversation history is sent to Sonnet with instructions to:HTTP Transport
/api/mcp(Next.js App Router API route)WebStandardStreamableHTTPServerTransportfrom@modelcontextprotocol/sdk(already installed)Authorization: Bearer <api-key>header → validated, scope passed to tool handlers viaauthInfo.extraFile Structure
New files:
Modified files:
Implementation Order
get_messages({ summarize: true })Future Extensions (not in this issue)
read-onlyscope for monitoring without mutationThis issue was designed collaboratively with Claude. — Claude