Cybersecurity-Projects/PROJECTS/bug-bounty-platform/docs/research/CLAUDE-CODE-PROGRAMMATIC.md

36 KiB

Claude Code Programmatic Usage Research

Research Date: 2025-12-14 Purpose: Comprehensive investigation of using Claude Code programmatically as a backend API for FastAPI applications


Executive Summary

Claude Code can be used programmatically through three primary approaches:

  1. CLI with -p flag - Direct subprocess calls for simple automation
  2. Claude Agent SDK - Production-ready library (Python/TypeScript) that wraps the CLI
  3. FastAPI Integration - REST API wrapper around Claude Code execution

Key Finding: The Claude Agent SDK (formerly Claude Code SDK) is the recommended approach for FastAPI backend integration. It spawns the Claude CLI as a subprocess and handles authentication, tool execution, session management, and streaming automatically.

Context Access: Claude Code can access database/application context through:

  • Direct prompt strings (simple, works immediately)
  • MCP servers (advanced, for repeated database access and efficient token usage)
  • Combination approach (MCP for data sources, prompts for specific queries)

Authentication: Production deployments should use ANTHROPIC_API_KEY environment variable for pay-per-use API billing. Subscription-based authentication requires browser OAuth and is not suitable for headless server environments.


1. Calling Claude Code from Python Backend

Solution Summary

Use the Claude Agent SDK for Python rather than raw subprocess calls. The SDK provides a production-ready abstraction over the Claude Code CLI with built-in tool execution, session management, and streaming support.

Detailed Analysis

Understanding the Architecture

The Claude Agent SDK follows this architecture:

Your FastAPI App → Claude Agent SDK (Python) → Claude CLI (subprocess) → Anthropic API

"The SDK spawns the CLI as a subprocess; the CLI talks to the Anthropic API." - Claude Code Python SDK

The SDK abstracts subprocess management, JSON streaming, and message parsing while providing a rich type system for structured interactions.

Installation and Setup

Step 1: Install Claude Code CLI

# macOS/Linux/WSL
curl -fsSL https://claude.ai/install.sh | bash

# Or via npm
npm install -g @anthropic-ai/claude-code

# Or via Homebrew (macOS)
brew install --cask claude-code

Step 2: Install Claude Agent SDK

pip install claude-agent-sdk

Step 3: Set Authentication

export ANTHROPIC_API_KEY=your-api-key-here

Get your API key from the Anthropic Console.

Basic Usage Pattern

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions

async def main():
    async for message in query(
        prompt="Find and fix the bug in auth.py",
        options=ClaudeAgentOptions(allowed_tools=["Read", "Edit", "Bash"])
    ):
        print(message)

asyncio.run(main())

FastAPI Integration Example

from fastapi import FastAPI, BackgroundTasks
from pydantic import BaseModel
from claude_agent_sdk import query, ClaudeAgentOptions
import asyncio

app = FastAPI()

class PromptRequest(BaseModel):
    prompt: str
    context: str | None = None

class ClaudeResponse(BaseModel):
    content: str
    session_id: str | None = None

@app.post("/api/claude/generate", response_model=ClaudeResponse)
async def generate_content(request: PromptRequest):
    full_prompt = f"{request.context}\n\n{request.prompt}" if request.context else request.prompt

    result_content = ""
    session_id = None

    async for message in query(
        prompt=full_prompt,
        options=ClaudeAgentOptions(
            allowed_tools=["Read", "Write", "Edit", "Bash"],
            permission_mode="bypassPermissions"
        )
    ):
        if hasattr(message, 'content'):
            result_content += str(message.content)
        if hasattr(message, 'session_id'):
            session_id = message.session_id

    return ClaudeResponse(content=result_content, session_id=session_id)

Alternative Approach: Direct CLI Usage

For simpler use cases, you can call the CLI directly using the -p flag:

import subprocess
import json

def call_claude_cli(prompt: str) -> dict:
    """
    Call Claude Code CLI with -p flag for headless execution
    """
    proc = subprocess.run(
        ["claude", "-p", prompt, "--output-format", "json"],
        capture_output=True,
        text=True,
        timeout=300
    )

    if proc.returncode == 0:
        return json.loads(proc.stdout)
    else:
        raise RuntimeError(f"Claude CLI failed: {proc.stderr}")

result = call_claude_cli("What files are in this directory?")

CLI Flags for Programmatic Use:

Flag Purpose
-p or --print Non-interactive mode, prints response and exits
--output-format json Structured JSON output for parsing
--output-format stream-json Streaming JSON events
--max-turns N Limit agentic turns in non-interactive mode
--verbose Enable verbose logging for debugging
--system-prompt "text" Replace entire system prompt
--append-system-prompt "text" Add to default prompt

Production Best Practices

  1. Use the SDK over raw subprocess calls - Better error handling, type safety, session management
  2. Set timeouts - Prevent hanging requests
  3. Handle streaming - Process responses incrementally for better UX
  4. Use async/await - Leverage FastAPI's async capabilities
  5. Implement retry logic - Handle API rate limits and transient failures
  6. Monitor token usage - Track costs and optimize context

2. Database/Context Access for Programmatic Claude Code

Solution Summary

Claude Code has two primary methods for accessing database and application context:

  1. Direct Prompt Strings - Pass context directly in the prompt (simple, immediate)
  2. MCP Servers - Connect to databases via Model Context Protocol (efficient, reusable)

For production FastAPI backends, a hybrid approach works best: use MCP servers for persistent database connections and direct prompts for specific query parameters.

Detailed Analysis

Method 1: Passing Context via Prompt Strings

How It Works:

"Claude Code supports several methods for providing data: copy and paste directly into prompts (the most common approach), piping into Claude Code (e.g., cat foo.txt | claude) for logs, CSVs, and large data, or telling Claude to pull data via bash commands." - Claude Code Best Practices

Example: FastAPI Backend Loading Database Context

from fastapi import FastAPI
from sqlalchemy.ext.asyncio import AsyncSession
from claude_agent_sdk import query, ClaudeAgentOptions

@app.post("/api/generate-email")
async def generate_email(user_id: int, db: AsyncSession):
    user = await db.execute(
        select(User).where(User.id == user_id)
    )
    user_data = user.scalar_one()

    context = f"""
    User Information:
    - Name: {user_data.name}
    - Email: {user_data.email}
    - Purchase History: {user_data.orders}
    - Preferences: {user_data.preferences}
    """

    prompt = "Write a personalized marketing email for this user"

    async for message in query(
        prompt=f"{context}\n\n{prompt}",
        options=ClaudeAgentOptions(allowed_tools=[])
    ):
        # Process response
        pass

Advantages:

  • Simple and straightforward
  • Works immediately without setup
  • Full control over context

Disadvantages:

  • Repetitive data loading for similar queries
  • Can consume large amounts of tokens
  • No caching or optimization

"Loading entire directories into Claude for every request can be very expensive." - Claude Context Optimization

Method 2: MCP Server for Database Access

How It Works:

"Claude Code can connect to hundreds of external tools and data sources through the Model Context Protocol (MCP), an open source standard for AI-tool integrations, giving Claude Code access to tools, databases, and APIs." - Connect Claude Code to tools via MCP

MCP Architecture:

Claude Code (Client) → MCP Server → Your Database/API

The MCP server exposes database operations as "tools" that Claude Code can call programmatically.

Example: Database MCP Server

"DBHub by Bytebase is an open-source MCP server that can expose databases (Postgres, MySQL, etc.) to Claude, allowing Claude to directly query the database for schema info or even data." - Claude Code Best Practices

Setting Up MCP Server for PostgreSQL:

from claude_agent_sdk import query, ClaudeAgentOptions

async def main():
    async for message in query(
        prompt="Find emails of 10 random users who used feature ENG-4521",
        options=ClaudeAgentOptions(
            mcp_servers={
                "postgres": {
                    "command": "npx",
                    "args": ["-y", "@modelcontextprotocol/server-postgres"],
                    "env": {
                        "POSTGRES_URL": "postgresql://user:pass@localhost:5432/db"
                    }
                }
            }
        )
    ):
        print(message)

Token Efficiency:

"Instead of loading entire directories into Claude for every request, Claude Context efficiently stores your codebase in a vector database and only uses related code in context to keep costs manageable." - Claude Context

Context Usage Comparison:

  • With all MCP tools enabled: 143k/200k tokens (72%) - MCP tools consuming 82.0k tokens (41.0%)
  • With selective MCP tools: 67k/200k tokens (34%) - MCP tools taking only 5.7k tokens (2.8%)

Best Practice Pattern:

  1. Configure MCP server for database connection (one-time setup)
  2. Pass specific query parameters via prompt
  3. Let Claude use MCP tools to fetch relevant data
  4. Claude processes and returns results

Example Implementation:

from fastapi import FastAPI
from claude_agent_sdk import query, ClaudeAgentOptions

app = FastAPI()

@app.post("/api/claude/analyze-users")
async def analyze_users(feature_id: str):
    async for message in query(
        prompt=f"Analyze user engagement for feature {feature_id} using the database",
        options=ClaudeAgentOptions(
            mcp_servers={
                "postgres": {
                    "command": "npx",
                    "args": ["-y", "@modelcontextprotocol/server-postgres"],
                    "env": {
                        "POSTGRES_URL": os.getenv("DATABASE_URL")
                    }
                }
            },
            allowed_tools=["Read", "Bash"]
        )
    ):
        # Process streaming response
        pass

When to Use Which Approach

Use Case Recommended Method Reason
One-off queries with small data Direct prompt Simple, no setup needed
Repeated database queries MCP server Token efficient, reusable
Large datasets MCP server Avoids context window limits
Mixed sources (DB + files + APIs) Hybrid Best of both worlds
Production FastAPI backend Hybrid Flexible and efficient

3. MCP Server Architecture for Programmatic Usage

Solution Summary

When using Claude Code programmatically via subprocess/SDK, you do NOT need to mount MCP servers on your FastAPI app (no app.mount("/mcp", mcp)). Instead, MCP servers are configured within the Claude Agent SDK options and run as separate processes that Claude Code connects to directly.

Detailed Analysis

Understanding "Who Calls Who"

Critical Distinction:

  1. fastapi-mcp - Exposes your FastAPI endpoints AS MCP tools FOR Claude to call
  2. Claude Agent SDK mcp_servers option - Connects Claude TO external MCP servers

These are two different patterns:

Pattern A: Claude calls your FastAPI tools (via fastapi-mcp)

User → Claude Desktop → MCP Proxy → Your FastAPI App (with fastapi-mcp)

Pattern B: Your FastAPI app calls Claude (which uses MCP servers)

User → Your FastAPI App → Claude Agent SDK → Claude CLI → External MCP Server → Database

For your use case (FastAPI backend calling Claude Code), you want Pattern B.

MCP Server Architecture Explanation

"The Model Context Protocol defines a client-server architecture for tool use. An MCP server wraps an external service or data source behind a common protocol (with defined actions, or 'tools'), while an MCP client (like Claude Code or other AI agent frameworks) connects to the server to invoke those tools." - Claude Code as an MCP Server

Call Flow:

1. User clicks button in React
2. React → FastAPI endpoint
3. FastAPI → Claude Agent SDK (query function)
4. SDK spawns Claude CLI subprocess
5. Claude CLI connects to MCP server (separate process)
6. MCP server queries PostgreSQL
7. Results flow back through the chain

"Client sends user message to the model. Model analyzes context and decides to call a tool exposed by MCP (or multiple tools). Client forwards the tool call to the MCP server over the chosen transport. Server executes the tool and returns results. Model receives tool output and composes the final answer to the user." - Create a MCP Server for Claude Code

Configuration in FastAPI Backend

You configure MCP servers in the SDK options, not on the FastAPI app:

from fastapi import FastAPI
from claude_agent_sdk import query, ClaudeAgentOptions
import os

app = FastAPI()

@app.post("/api/generate")
async def generate_content(prompt: str):
    async for message in query(
        prompt=prompt,
        options=ClaudeAgentOptions(
            mcp_servers={
                "postgres": {
                    "command": "npx",
                    "args": ["-y", "@modelcontextprotocol/server-postgres"],
                    "env": {
                        "POSTGRES_URL": os.getenv("DATABASE_URL")
                    }
                },
                "filesystem": {
                    "command": "npx",
                    "args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"]
                }
            }
        )
    ):
        yield message

No app.mount("/mcp", mcp) needed because:

  • MCP servers are spawned as child processes by the Claude CLI
  • They communicate via stdio/SSE/HTTP transports
  • They're not part of your FastAPI application's HTTP routes

When Would You Use fastapi-mcp?

You would use fastapi-mcp if you wanted to expose your FastAPI endpoints to Claude Desktop/CLI as tools:

from fastapi import FastAPI
from fastapi_mcp import FastApiMCP

app = FastAPI()

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    return {"user": user_id}

mcp = FastApiMCP(app=app)
mcp.mount()

Then configure Claude Desktop to use your API:

{
  "mcpServers": {
    "my-api": {
      "command": "mcp-proxy",
      "args": ["http://localhost:8000/mcp"]
    }
  }
}

"The mcp-proxy proxies requests to a remote MCP server over SSE transport, wrapping your real server and translating Claude's SSE-based requests into regular HTTP requests that your FastAPI app understands." - FastAPI-MCP Quick Start

This is NOT what you want for your use case. You want Claude Code to call external MCP servers (like database), not to call your FastAPI app.

Claude Code as Both MCP Client and Server

Interestingly, Claude Code can act as both:

"You can run claude mcp serve to expose Claude Code while Claude Code itself connects to GitHub and Postgres MCP servers. This means Claude Code can act as both an MCP client and an MCP server simultaneously, allowing for layered agent architectures." - Claude Code as an MCP Server

But for your FastAPI backend use case, Claude Code is purely an MCP client connecting to database/tool servers.


4. Production Deployment Considerations

Solution Summary

For production deployment of Claude Code in a FastAPI backend:

  1. Container Strategy: Same container as FastAPI (simpler) or separate container (better isolation)
  2. Authentication: Use ANTHROPIC_API_KEY environment variable (not subscription-based OAuth)
  3. Billing: API pay-per-use is recommended for production over subscription plans
  4. Headless Mode: Fully supported via environment variable authentication

Detailed Analysis

Container Deployment Options

Option 1: Same Container (Recommended for MVP)

FROM python:3.12-slim

RUN apt-get update && apt-get install -y curl && \
    curl -fsSL https://claude.ai/install.sh | bash

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

ENV ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
ENV DATABASE_URL=${DATABASE_URL}

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Advantages:

  • Simpler deployment
  • Fewer network hops
  • Lower latency

Disadvantages:

  • Larger container image
  • Tight coupling

Option 2: Separate Container (Recommended for Production)


services:
  backend:
    build: ./backend
    environment:
      - CLAUDE_SERVICE_URL=http://claude-service:8001
    depends_on:
      - claude-service
      - postgres

  claude-service:
    build: ./claude-service
    environment:
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
    volumes:
      - ./workspace:/workspace

  postgres:
    image: postgres:16
    environment:
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}

Claude Service Implementation:


from fastapi import FastAPI
from claude_agent_sdk import query, ClaudeAgentOptions

app = FastAPI()

@app.post("/execute")
async def execute_claude(prompt: str, context: dict = None):
    async for message in query(
        prompt=prompt,
        options=ClaudeAgentOptions(
            mcp_servers={
                "postgres": {
                    "command": "npx",
                    "args": ["-y", "@modelcontextprotocol/server-postgres"],
                    "env": {"POSTGRES_URL": os.getenv("DATABASE_URL")}
                }
            }
        )
    ):
        yield message

Advantages:

  • Better isolation
  • Independent scaling
  • Cleaner architecture
  • Can reuse claude-service across multiple apps

Authentication in Containerized Environments

Critical Finding: Subscription-based authentication (Claude Pro/Max) is NOT suitable for production containers.

"The SDK also supports authentication via third-party API providers like Amazon Bedrock (set CLAUDE_CODE_USE_BEDROCK=1 environment variable) and Google Vertex AI (set CLAUDE_CODE_USE_VERTEX=1 environment variable)." - Claude Code SDK Python Authentication

Authentication Hierarchy:

  1. ANTHROPIC_API_KEY (Recommended for Production)

    export ANTHROPIC_API_KEY=sk-ant-api03-...
    
    • Pay-per-use billing
    • Headless compatible
    • No browser OAuth required
    • Works in containers
  2. Subscription (Claude Pro/Max) - NOT for containers

    • Requires browser login (claude auth login)
    • OAuth flow with callback
    • Not suitable for headless servers
    • Container deployment issues
  3. Long-lived Access Tokens (Alternative)

    claude setup-token
    export CLAUDE_CODE_OAUTH_TOKEN=<token>
    
    • Works in containers
    • Linked to subscription
    • Can be mounted as secret

Docker Volume Mounting (Alternative to Environment Variables):

"For Docker containers, you can mount the credential file as a read-only volume using docker run -v ~/.config/claude-code/auth.json:/root/.config/claude-code/auth.json:ro -it my-dev-image." - Claude Code Docker Complete Guide

Recommended Production Pattern:

FROM python:3.12-slim

RUN curl -fsSL https://claude.ai/install.sh | bash
RUN pip install claude-agent-sdk

ENV ANTHROPIC_API_KEY=""

CMD ["python", "app.py"]
docker run -e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY myapp

Headless Authentication Issues (2025 Status)

Known Issues:

"There are known issues with headless mode authentication: Any time I attempt to run claude via claude -p 'test' it returns Invalid API key · Fix external API key. In a non-interactive environment, the CLI still requires executing /login even though an API key is provided." - GitHub Issue #5666

Workaround:

The Claude Agent SDK handles authentication better than raw CLI calls. Use the SDK instead of direct claude -p commands:


from claude_agent_sdk import query, ClaudeAgentOptions
import os

os.environ["ANTHROPIC_API_KEY"] = "your-key"

async for message in query(
    prompt="test",
    options=ClaudeAgentOptions(setting_sources=None)
):
    print(message)

Verification:

"Run /status in Claude Code periodically to verify your current authentication method. To verify if an API key is set as an environment variable, run /status in Claude Code." - Managing API Key Environment Variables

Cost Comparison: Subscription vs API

Claude Max Subscription:

  • $100/month → 5x usage of Pro
  • $200/month → 20x usage of Pro (Opus 4.5 access)

Anthropic API:

  • Claude Opus 4.5: $5.00/M input tokens, $25.00/M output tokens
  • Claude Sonnet 4.5: $3.00/M input, $15.00/M output (≤200K context)
  • Claude Sonnet 4.5: $6.00/M input, $22.50/M output (>200K context)

Value Analysis:

"If you max out the $100 Max plan, that's approximately $7,400 worth of API usage equivalent, 74x the amount you pay. You can get $150 worth of API usage for the price of $20 a month with a Claude Pro subscription." - LLM Subscriptions vs APIs Value

Recommendation:

"API is best for production deployments, automated pipelines, backends, or when you want to control exact spend. The API removes per-seat fees and allows unlimited developers behind a single deployment, but you pay per token." - Claude Code Usage Limits and Subscription Plans

When to Use Each:

Scenario Recommendation Reason
Development Claude Max Subscription Predictable costs, generous limits
CI/CD Automation Anthropic API Headless compatible
Production Backend Anthropic API Pay-per-use, scales with traffic
Multiple Developers Anthropic API No per-seat fees
High-volume Anthropic API Better cost control
Prototype/MVP Claude Max Subscription Faster setup

5. Complete Architecture Pattern

End-to-End Flow

┌─────────────┐
│   React     │
│   Frontend  │
└──────┬──────┘
       │ 1. User clicks button
       │
       ▼
┌─────────────────────────────────────┐
│         FastAPI Backend             │
│  ┌──────────────────────────────┐   │
│  │ 2. Load context from         │   │
│  │    PostgreSQL                │   │
│  │                              │   │
│  │ user_data = await db.query() │   │
│  └──────────────────────────────┘   │
│                                     │
│  ┌──────────────────────────────┐   │
│  │ 3. Call Claude Agent SDK     │   │
│  │                              │   │
│  │ async for msg in query(      │   │
│  │   prompt=prompt,             │   │
│  │   options={                  │   │
│  │     mcp_servers={            │   │
│  │       "postgres": {...}      │   │
│  │     }                        │   │
│  │   }                          │   │
│  │ )                            │   │
│  └──────────────────────────────┘   │
└──────────┬──────────────────────────┘
           │
           ▼
┌─────────────────────────┐
│  Claude Agent SDK       │
│  (Python Library)       │
└──────────┬──────────────┘
           │ 4. Spawns subprocess
           │
           ▼
┌─────────────────────────┐
│  Claude Code CLI        │
│  (claude binary)        │
└──────────┬──────────────┘
           │ 5. API request
           │
           ▼
┌─────────────────────────┐
│  Anthropic API          │
│  (Claude Opus/Sonnet)   │
└──────────┬──────────────┘
           │
           │ 6. Needs data
           │
           ▼
┌─────────────────────────┐
│  MCP Server             │
│  (Database Proxy)       │
└──────────┬──────────────┘
           │ 7. SQL query
           │
           ▼
┌─────────────────────────┐
│  PostgreSQL Database    │
└─────────────────────────┘

Complete Implementation Example


from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from claude_agent_sdk import query, ClaudeAgentOptions
import os
import json

app = FastAPI()

DATABASE_URL = os.getenv("DATABASE_URL")
engine = create_async_engine(DATABASE_URL)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

class GenerateRequest(BaseModel):
    user_id: int
    prompt: str

async def get_db():
    async with AsyncSessionLocal() as session:
        yield session

@app.post("/api/generate-email")
async def generate_email(request: GenerateRequest):
    async with AsyncSessionLocal() as db:
        result = await db.execute(
            select(User).where(User.id == request.user_id)
        )
        user = result.scalar_one_or_none()

        if not user:
            raise HTTPException(status_code=404, detail="User not found")

        context = f"""
        User Profile:
        - Name: {user.name}
        - Email: {user.email}
        - Join Date: {user.created_at}
        - Total Orders: {len(user.orders)}
        - Preferences: {user.preferences}
        """

        full_prompt = f"{context}\n\n{request.prompt}"

        async def stream_response():
            async for message in query(
                prompt=full_prompt,
                options=ClaudeAgentOptions(
                    allowed_tools=[],
                    permission_mode="bypassPermissions",
                    mcp_servers={
                        "postgres": {
                            "command": "npx",
                            "args": ["-y", "@modelcontextprotocol/server-postgres"],
                            "env": {
                                "POSTGRES_URL": DATABASE_URL
                            }
                        }
                    }
                )
            ):
                if hasattr(message, 'content'):
                    yield json.dumps({"content": message.content}) + "\n"

        return StreamingResponse(stream_response(), media_type="application/x-ndjson")

@app.post("/api/analyze-database")
async def analyze_database(query_prompt: str):
    async def stream_response():
        async for message in query(
            prompt=f"Using the postgres MCP server, {query_prompt}",
            options=ClaudeAgentOptions(
                mcp_servers={
                    "postgres": {
                        "command": "npx",
                        "args": ["-y", "@modelcontextprotocol/server-postgres"],
                        "env": {
                            "POSTGRES_URL": os.getenv("DATABASE_URL")
                        }
                    }
                }
            )
        ):
            if hasattr(message, 'content'):
                yield json.dumps({"data": message.content}) + "\n"

    return StreamingResponse(stream_response(), media_type="application/x-ndjson")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

React Frontend Integration


import { useState } from 'react';

export function EmailGenerator() {
  const [loading, setLoading] = useState(false);
  const [content, setContent] = useState('');

  const handleGenerate = async () => {
    setLoading(true);
    setContent('');

    const response = await fetch('/api/generate-email', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        user_id: 123,
        prompt: 'Write a personalized welcome email'
      })
    });

    const reader = response.body?.getReader();
    const decoder = new TextDecoder();

    while (true) {
      const { done, value } = await reader!.read();
      if (done) break;

      const chunk = decoder.decode(value);
      const lines = chunk.split('\n').filter(Boolean);

      for (const line of lines) {
        const data = JSON.parse(line);
        setContent(prev => prev + data.content);
      }
    }

    setLoading(false);
  };

  return (
    <div>
      <button onClick={handleGenerate} disabled={loading}>
        {loading ? 'Generating...' : 'Generate Email'}
      </button>
      <div>{content}</div>
    </div>
  );
}

Docker Compose Setup


services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  backend:
    build: ./backend
    environment:
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
      - DATABASE_URL=postgresql+asyncpg://postgres:${POSTGRES_PASSWORD}@postgres:5432/myapp
    depends_on:
      - postgres
    ports:
      - "8000:8000"
    volumes:
      - ./backend:/app

  frontend:
    build: ./frontend
    environment:
      - VITE_API_URL=http://localhost:8000
    ports:
      - "5173:5173"
    volumes:
      - ./frontend:/app

volumes:
  postgres_data:

Backend Dockerfile


FROM python:3.12-slim

WORKDIR /app

RUN apt-get update && \
    apt-get install -y curl nodejs npm git && \
    curl -fsSL https://claude.ai/install.sh | bash && \
    apt-get clean

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

ENV PYTHONUNBUFFERED=1

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

Key Takeaways

1. Use the Claude Agent SDK (Not Raw CLI)

The SDK provides production-ready abstractions over subprocess management, authentication, streaming, and error handling. It's the recommended approach for FastAPI backends.

2. MCP Servers Are Optional But Powerful

For simple prompts with small context, pass data directly in the prompt. For repeated database access or large datasets, configure MCP servers within the SDK options.

3. MCP Servers Are NOT Mounted on FastAPI

You don't use app.mount("/mcp", mcp) for this use case. MCP servers are configured in the ClaudeAgentOptions and run as separate processes that Claude Code connects to.

4. Use API Key Authentication for Production

Set ANTHROPIC_API_KEY environment variable. Subscription-based authentication (Claude Pro/Max) requires browser OAuth and doesn't work well in containerized headless environments.

5. API Billing Is Better for Production

Pay-per-use API billing scales better than subscriptions for production backends. It removes per-seat fees and provides precise cost control.

6. Streaming Is Important for UX

Implement streaming responses to show progress to users. The SDK supports async streaming out of the box.

7. Session Management Enables Context

Use session IDs to maintain conversation context across multiple requests. This enables multi-turn interactions.


Sources and References

Official Documentation

Technical Guides

Integration Examples

MCP Resources

Cost and Deployment

Community Resources

GitHub Issues


Research Completed: 2025-12-14 Last Updated: 2025-12-14 Status: Comprehensive - Ready for implementation