Skip to content

Add MCP server for programmatic session orchestration #289

@brendanlong

Description

@brendanlong

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.

  1. Human-readable default: get_messages returns what a user would see — text content + tool names — not raw JSON with full tool inputs/outputs
  2. Drill-down pattern: get_message for full details on specific messages, guided by IDs from summaries
  3. LLM summarization: summarize: true sends history to Sonnet for a concise overview, citing key message IDs
  4. System message filtering: System messages hidden by default (rarely useful for orchestration)
  5. 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

  1. Prisma schema + API key service + unit tests
  2. API key tRPC router + integration tests
  3. Message formatter (human-readable + drill-down) + unit tests
  4. MCP auth + server core with tool definitions + unit tests
  5. HTTP route wiring + integration tests
  6. LLM summarization for get_messages({ summarize: true })
  7. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions