# EasyClaw MCP Server: Architecture & Implementation Whitepaper

**Version 3.0.0 — March 2026**

---

## 1. Executive Summary

The EasyClaw MCP Server is a production-grade Model Context Protocol (MCP) service that powers Agent Johnny 5, the AI administrator bundled with every EasyClaw deployment. It provides 13 specialized tools and 18 searchable knowledge guides to help users install, configure, secure, and troubleshoot OpenClaw — an open-source AI gateway platform.

The server is built on **FastMCP** and **Starlette**, backed by **SQLite with FTS5 full-text search**, and deployed as a Docker container behind Traefik reverse proxy. It exposes two interfaces through a single process: a standard REST API for admin operations (user management, subscriptions, OTP auth) and a Streamable HTTP MCP endpoint for tool invocation by AI agents.

The service runs at `mcp.easyclaw.help` on a Hostinger VPS alongside the EasyClaw marketing site, processing tool calls from paying subscribers at a flat rate of $25/month with no feature tiers.

---

## 2. Problem Statement

OpenClaw is a powerful but complex platform. Users face challenges across the full lifecycle:

- **Installation**: Multi-step process requiring Node.js, npm, systemd configuration, firewall rules, and gateway initialization — each step has failure modes specific to the user's environment.
- **Configuration**: The `openclaw.json` configuration file accepts dozens of keys spanning model selection, memory settings, channel integrations, security policies, and performance tuning. Invalid keys crash the gateway.
- **Model Selection**: OpenRouter provides access to hundreds of AI models with varying cost, speed, context window, and capability tradeoffs. Users need guidance matched to their use case.
- **Security**: Default installations are insecure. Hardening requires knowledge of rate limiting, API key rotation, input validation, and network policies.
- **Troubleshooting**: Error messages are often opaque. Common issues (FTS5 syntax errors, port conflicts, auth failures) have non-obvious solutions.

A traditional documentation site can't solve these problems — users need contextual, interactive guidance that understands their specific situation. The MCP protocol enables exactly this: an AI agent that can call specialized tools to search knowledge, validate configurations, diagnose errors, and generate solutions in real time.

---

## 3. Architecture

### 3.1 System Topology

```
Internet
  │
  ▼
Traefik (reverse proxy + automatic TLS)
  ├── easyclaw.help      → easyclaw-web  (nginx:alpine, static HTML)
  └── mcp.easyclaw.help  → easyclaw-mcp  (Python 3.12, port 8000)
                                │
                           ┌────┴────┐
                           │ FastMCP  │ ← MCP tools (SSE/Streamable HTTP)
                           │ + Admin  │ ← REST API (JSON)
                           │ + SQLite │ ← FTS5 knowledge search
                           └────┬────┘
                                │
                           SearXNG (private search instance)
```

Both containers run on Docker's `n8n_default` network. The MCP container also joins `searxng_searxng-network` for private web search capabilities.

### 3.2 The Dual-ASGI Dispatcher

A key architectural decision is the use of a **raw ASGI dispatcher** instead of Starlette's `Mount`. Starlette Mount causes redirect loops when FastMCP runs behind a reverse proxy — the path stripping interacts poorly with FastMCP's internal routing.

The solution is a hand-written ASGI application function:

```python
async def combined_app(scope, receive, send):
    path = scope.get("path", "")

    if path.startswith("/admin") or path == "/health":
        await admin_app(scope, receive, send)      # Starlette REST
    else:
        # Extract auth from raw ASGI headers
        auth_header = extract_auth_header(scope)
        auth_result = authenticate_request(auth_header)
        token = _auth_context.set(auth_result)     # Set context var
        try:
            await mcp_app(scope, receive, send)    # FastMCP SSE
        finally:
            _auth_context.reset(token)             # Clean up
```

This gives full control over routing, avoids the redirect loop, and enables auth context injection before tool execution begins.

### 3.3 Auth via Context Variables

Authentication state flows from the ASGI dispatcher to individual tool functions through Python's `contextvars.ContextVar`. This is both thread-safe and async-safe:

```python
_auth_context: contextvars.ContextVar[AuthResult | None] = contextvars.ContextVar(
    'auth_context', default=None
)
```

The dispatcher sets the context variable before forwarding to FastMCP. Each tool function reads it:

```python
def _require_auth(tool_name: str, required_tier: str = "starter") -> AuthResult:
    auth = _auth_context.get()
    if not auth or not auth.authenticated:
        raise ValueError("Missing or invalid authorization")
    if not check_tier_access(auth, required_tier):
        raise ValueError(f"Requires {required_tier} tier")
    return auth
```

This pattern avoids passing auth objects through FastMCP's tool signature, which would pollute the tool's public API.

---

## 4. Database Design

### 4.1 SQLite with WAL Mode

The database is SQLite, chosen for zero-dependency deployment and sufficient performance for a single-tenant service. WAL (Write-Ahead Logging) mode enables concurrent reads alongside a single writer:

```python
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON")
```

### 4.2 Schema (7 Tables)

| Table | Purpose | Key Fields |
|-------|---------|------------|
| `users` | User accounts | `id` (UUID), `email` (unique) |
| `subscriptions` | Billing & API keys | `api_key_hash` (SHA-256), `tier`, `status`, `daily_limit` |
| `usage_logs` | Rate limiting & analytics | `api_key_hash`, `tool_name`, `timestamp`, `response_time_ms` |
| `knowledge_entries` | Dynamic guide overrides | `topic` (PK), `title`, `content`, `source` |
| `knowledge_fts` | FTS5 virtual table | `topic`, `title`, `content` (Porter stemming + Unicode) |
| `otp_codes` | Dashboard authentication | `email`, `code` (6-digit), `expires_at`, `used` |
| `guide_purchases` | Legacy guide downloads | `download_token`, `downloaded_at` |

### 4.3 Detailed Table Definitions

#### users

```sql
CREATE TABLE users (
    id TEXT PRIMARY KEY,          -- UUID
    email TEXT UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```

#### subscriptions

```sql
CREATE TABLE subscriptions (
    id TEXT PRIMARY KEY,                                -- sub_UUID
    user_id TEXT NOT NULL,
    stripe_customer_id TEXT,
    stripe_subscription_id TEXT,
    tier TEXT NOT NULL DEFAULT 'starter',              -- starter|pro|business|subscriber
    status TEXT NOT NULL DEFAULT 'active',             -- active|canceled|past_due
    api_key TEXT UNIQUE,                               -- ec_live_XXXX (48-char hex)
    api_key_hash TEXT UNIQUE,                          -- SHA-256 hash
    daily_limit INTEGER NOT NULL DEFAULT 100,          -- starter:100, pro:1000, business:999999, subscriber:5000
    current_period_end TIMESTAMP,                      -- Stripe billing cycle end
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);
```

#### usage_logs

```sql
CREATE TABLE usage_logs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    api_key_hash TEXT NOT NULL,
    tool_name TEXT NOT NULL,
    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    response_time_ms INTEGER,
    success BOOLEAN DEFAULT 1
);
CREATE INDEX idx_usage_hash_time ON usage_logs(api_key_hash, timestamp);
```

#### knowledge_entries (Dynamic Overrides)

```sql
CREATE TABLE knowledge_entries (
    topic TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    source TEXT DEFAULT 'manual',                      -- manual|webhook|n8n_sync
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```

#### knowledge_fts (Virtual FTS5 Table)

```sql
CREATE VIRTUAL TABLE knowledge_fts
USING fts5(topic, title, content, tokenize='porter unicode61');
```

#### otp_codes

```sql
CREATE TABLE otp_codes (
    email TEXT NOT NULL,
    code TEXT NOT NULL,                                -- 6-digit string
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    expires_at TIMESTAMP NOT NULL,                     -- TTL default 600s (10 min)
    used BOOLEAN DEFAULT 0
);
CREATE INDEX idx_otp_email ON otp_codes(email, used, expires_at);
```

### 4.4 API Key Security

API keys follow the format `ec_live_` followed by 48 random hex characters. They are **never stored in plaintext** — only SHA-256 hashes persist in the database:

```python
def hash_api_key(api_key: str) -> str:
    return hashlib.sha256(api_key.encode()).hexdigest()
```

The plaintext key is returned exactly once at subscription creation time. All subsequent operations (auth, rate limiting, usage logging) use the hash.

### 4.5 Rate Limiting

Daily rate limits reset automatically via SQLite's `date()` function — no background jobs or cron needed:

```sql
SELECT COUNT(*) FROM usage_logs
WHERE api_key_hash = ? AND date(timestamp) = date('now')
```

Default limits: 100/day for starter tier, 5000/day for subscriber tier.

### 4.6 Subscription Creation (Idempotent)

```python
def create_subscription(user_id, tier, api_key, ...):
    # Return existing active subscription if one exists
    existing = conn.execute("""
        SELECT ... FROM subscriptions
        WHERE user_id = ? AND status = 'active'
        ORDER BY created_at DESC LIMIT 1
    """, (user_id,)).fetchone()
    if existing:
        return dict(existing)

    # Create new
    sub_id = f"sub_{uuid.uuid4().hex[:12]}"
    key_hash = hash_api_key(api_key)
    daily_limit = daily_limits.get(tier, 100)
    conn.execute("INSERT INTO subscriptions ...")
```

### 4.7 OTP Generation & Verification

```python
# Generate: 6-digit random, 10-min TTL, invalidates previous unused codes
def generate_otp(email: str, ttl_seconds: int = 600) -> str:
    conn.execute("UPDATE otp_codes SET used = 1 WHERE email = ? AND used = 0", (email,))
    code = f"{random.randint(0, 999999):06d}"
    expires = datetime.now(timezone.utc).timestamp() + ttl_seconds
    conn.execute("INSERT INTO otp_codes (...)", (email, code, now, expires))
    return code

# Verify: check timestamp, mark used
def verify_otp(email: str, code: str) -> bool:
    now = datetime.now(timezone.utc).isoformat()
    row = conn.execute("""
        SELECT rowid FROM otp_codes
        WHERE email = ? AND code = ? AND used = 0 AND expires_at > ?
    """, (email, code, now)).fetchone()
    if row:
        conn.execute("UPDATE otp_codes SET used = 1 WHERE rowid = ?", (row["rowid"],))
        return True
    return False
```

---

## 5. The Knowledge Base

### 5.1 Two-Level Architecture

The knowledge system has two layers:

1. **Static Base**: A `KNOWLEDGE_BASE` Python dictionary in `database.py` containing 18 built-in guides. These ship with the container image.
2. **Dynamic Overrides**: The `knowledge_entries` table, populated via the admin API or n8n workflows. Dynamic entries override static ones by topic key.

On startup and on-demand via `/admin/knowledge/reload`, the system merges both layers and rebuilds the FTS5 index.

### 5.2 The 18 Guides

| Guide | Content |
|-------|---------|
| `quick_start` | 5-minute install walkthrough |
| `openrouter_models` | Model comparison across 20+ models with pricing |
| `persistent_memory` | Memory configuration (vector, file, hybrid) |
| `channels` | Discord, Slack, Telegram, WhatsApp, Matrix integration |
| `troubleshooting` | 20+ known error patterns with solutions |
| `skills` | ClawHub skill system and custom skill creation |
| `security_hardening` | Rate limiting, API key rotation, input validation |
| `automation_crons` | Scheduled tasks and automated workflows |
| `sub_agents` | Multi-agent orchestration patterns |
| `browser_automation` | Headless browser control |
| `voice_tts` | Text-to-speech and voice interface setup |
| `docker_deployment` | Container-based deployment guide |
| `cost_optimization` | Model selection and caching strategies for cost |
| `clawhub_skills_detailed` | Deep dive into skill pack ecosystem |
| `mcp_integration` | MCP server setup and tool development |
| `configuration_reference` | Complete `openclaw.json` field reference |
| `onboarding_wizard` | First-run configuration walkthrough |
| `easyclaw_code_guide` | EasyClaw-specific development patterns |

### 5.3 Full-Text Search with FTS5

Search uses SQLite's FTS5 extension with Porter stemming and BM25 ranking:

```sql
CREATE VIRTUAL TABLE knowledge_fts
USING fts5(topic, title, content, tokenize='porter unicode61');
```

The `search_knowledge` tool queries FTS5 first, with a LIKE-based fallback for queries containing FTS5 operator characters that would cause syntax errors:

```python
try:
    rows = conn.execute("""
        SELECT topic, title, content, rank
        FROM knowledge_fts WHERE knowledge_fts MATCH ?
        ORDER BY rank LIMIT 5
    """, (query,)).fetchall()
except sqlite3.OperationalError:
    # FTS5 syntax error — fall back to substring search
    rows = conn.execute("""
        SELECT topic, title, content
        FROM knowledge_fts
        WHERE content LIKE ? OR title LIKE ? OR topic LIKE ?
        LIMIT 5
    """, (f"%{query}%", f"%{query}%", f"%{query}%")).fetchall()
```

Results return 600-character previews with a prompt to use `get_full_guide` for the complete content.

### 5.4 FTS Index Rebuild

```python
def _rebuild_knowledge_fts():
    # Merge: base dict + dynamic table overrides
    combined = {}
    for topic, data in KNOWLEDGE_BASE.items():
        combined[topic] = (topic, data["title"], data["content"])

    # Dynamic overrides (latest wins)
    rows = conn.execute("SELECT topic, title, content FROM knowledge_entries").fetchall()
    for row in rows:
        combined[row["topic"]] = (row["topic"], row["title"], row["content"])

    # Rebuild if count changed
    if existing != len(combined):
        conn.execute("DELETE FROM knowledge_fts")
        for entry in combined.values():
            conn.execute("INSERT INTO knowledge_fts VALUES (?, ?, ?)", entry)
```

### 5.5 Dynamic Updates via n8n

A Knowledge Scanner workflow (ID `KZHRXa39AplFMTm6`) runs daily at 3am UTC. It pushes updated guide content to the admin API:

```bash
POST /admin/knowledge
{
    "topic": "openrouter_models",
    "title": "OpenRouter Model Guide",
    "content": "Updated content...",
    "source": "n8n_sync"
}
```

After updates, a reload request rebuilds the FTS5 index:

```bash
POST /admin/knowledge/reload
```

Upsert logic in the database:

```sql
INSERT INTO knowledge_entries (topic, title, content, source, updated_at)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(topic) DO UPDATE SET
    title = excluded.title,
    content = excluded.content,
    source = excluded.source,
    updated_at = CURRENT_TIMESTAMP
```

This allows knowledge to stay current without redeploying the container.

---

## 6. The 13 MCP Tools

### 6.1 Tool Catalog

| # | Tool | Auth Tier | Description |
|---|------|-----------|-------------|
| 1 | `search_knowledge` | starter | FTS5 search across all 18 guides, returns top 5 matches with relevance ranking |
| 2 | `get_full_guide` | starter | Retrieve complete guide content by topic key |
| 3 | `get_model_recommendation` | starter | Use-case-based model recommendations (coding, general, reasoning, fast, cheap, creative, etc.) |
| 4 | `validate_config` | starter | Configuration checklist covering model, memory, security, channels, skills, performance, deployment |
| 5 | `search_web` | starter | Private SearXNG web search (Google + DuckDuckGo engines, 10 results) |
| 6 | `recommend_skills` | starter | ClawHub skill pack recommendations by category |
| 7 | `generate_config` | starter | Complete `openclaw.json` templates (developer, business, creator, production, budget, minimal, security) |
| 8 | `security_audit` | starter | 11-item security checklist with severity levels and verification commands |
| 9 | `estimate_cost` | starter | Monthly cost calculator across major models (Claude, GPT-4.1, DeepSeek, Gemini, etc.) |
| 10 | `diagnose_issue` | subscriber | Error pattern matching against 20+ known issues with step-by-step fixes |
| 11 | `validate_deployment` | subscriber | 9-step deployment health check with bash verification commands |
| 12 | `get_foreman_config` | subscriber | Complete validated config generation including systemd service file |
| 13 | `troubleshoot_gateway` | subscriber | Guided diagnostic flowcharts for 7 gateway failure categories |

### 6.2 Tier Hierarchy

```python
tier_levels = {"starter": 1, "pro": 2, "business": 3, "subscriber": 3}
# Access check: user_level >= required_level
```

All paying customers ($25/mo) receive `subscriber` tier, which grants access to all 13 tools. The `starter` tier exists for potential future free-tier offerings.

### 6.3 Tool Implementation Pattern

Every tool follows a consistent pattern:

```python
@mcp.tool()
def tool_name(param: str, optional_param: str = "default") -> str:
    start = time.time()
    auth = _require_auth("tool_name", "starter")  # Auth + tier check

    # ... tool logic ...

    log_tool_usage(auth, "tool_name", int((time.time() - start) * 1000))
    return formatted_result
```

1. **Auth check** via `_require_auth()` — validates Bearer token and tier
2. **Business logic** — database queries, template rendering, external HTTP calls
3. **Usage logging** — records tool name, response time, success/failure
4. **Return string** — all tools return formatted text, not JSON

### 6.4 Notable Tool Implementations

#### search_web

Connects to a private SearXNG instance for privacy-respecting web search:

```python
async with httpx.AsyncClient() as client:
    resp = await client.get(f"{SEARXNG_URL}/search", params={
        "q": query, "format": "json", "engines": "google,duckduckgo"
    })
```

#### diagnose_issue

Maintains a pattern database of 20+ known errors with regex matching:

```python
patterns = [
    {"pattern": r"EACCES.*permission denied", "diagnosis": "...", "fix": "..."},
    {"pattern": r"port.*already in use", "diagnosis": "...", "fix": "..."},
    # ... 18+ more patterns
]
for p in patterns:
    if re.search(p["pattern"], error_text, re.IGNORECASE):
        return format_diagnosis(p)
```

#### security_audit

Returns actionable bash commands organized by severity:

```
CRITICAL:
  ✗ API key exposed in config file
    Fix: openclaw config set apiKey $(openssl rand -hex 32)

HIGH:
  ⚠ No rate limiting configured
    Fix: openclaw config set rateLimit.enabled true

RECOMMENDED:
  ○ Enable request logging
    Fix: openclaw config set logging.requests true
```

#### troubleshoot_gateway

Provides guided diagnostics for 7 failure categories:

- `not_starting` — process/port/config checks
- `crashing` — log analysis, memory, dependency checks
- `no_response` — network, firewall, bind address checks
- `auth_failed` — API key, OpenRouter connectivity checks
- `channel_disconnected` — channel-specific token/webhook checks
- `mcp_timeout` — MCP server connectivity checks
- `config_rejected` — JSON validation, known bad key checks

---

## 7. Admin API

The admin REST API handles operations that don't fit the MCP tool model — user management, subscription lifecycle, OTP authentication, and knowledge management. It's protected by an `X-Admin-Secret` header validated with HMAC-SHA256 timing-safe comparison.

### 7.1 Endpoints

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/admin/users` | POST | Create user account |
| `/admin/subscriptions` | POST | Create subscription with generated API key |
| `/admin/subscriptions/status` | PUT | Update subscription status (active/canceled/past_due) |
| `/admin/usage` | GET | Query usage statistics by api_key_hash |
| `/admin/lookup` | GET | Look up user/subscription/guide by email |
| `/admin/knowledge` | POST | Upsert knowledge entry |
| `/admin/knowledge` | GET | List all dynamic entries |
| `/admin/knowledge/reload` | POST | Rebuild FTS5 index |
| `/admin/knowledge/{topic}` | DELETE | Remove dynamic entry |
| `/admin/otp/generate` | POST | Generate 6-digit OTP (10-min TTL) |
| `/admin/otp/verify` | POST | Verify and consume OTP |
| `/admin/guide-purchases` | POST | Record guide purchase with download token |
| `/admin/validate-download` | GET | Validate download token, mark as downloaded |
| `/health` | GET | Health check (no auth required) |

### 7.2 Admin Authentication

```python
import hmac

def verify_admin(request_secret: str, stored_secret: str) -> bool:
    return hmac.compare_digest(request_secret, stored_secret)
```

Uses HMAC timing-safe comparison to prevent timing attacks on the admin secret.

### 7.3 CORS Policy

```python
CORSMiddleware(
    allow_origins=["https://easyclaw.help", "https://n8n.srv1071455.hstgr.cloud"],
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=["*"],
)
```

### 7.4 n8n Workflow Integration

The admin API is the glue between n8n workflows and the MCP server:

| Workflow | ID | Integration |
|----------|----|-------------|
| **Stripe Webhook Handler** | `Qpj2oEfORkFcJLjS` | On payment → `POST /admin/users` then `POST /admin/subscriptions` |
| **Dashboard Auth (OTP)** | `zQTcGGpXFSwz4z7f` | `POST /admin/otp/generate` → email code → `POST /admin/otp/verify` |
| **Knowledge Scanner** | `KZHRXa39AplFMTm6` | Daily 3am UTC → `POST /admin/knowledge` → `POST /admin/knowledge/reload` |
| **Checkout Creator** | `QrpTLtkO9bhAN62u` | Creates Stripe checkout sessions |
| **Guide Download** | `z1wSMkTONLZZOBey` | Legacy guide download validation |

### 7.5 Health Check Response

```json
{
    "status": "ok",
    "service": "easyclaw-mcp",
    "version": "3.0.0",
    "tools": 13,
    "guides": 18,
    "timestamp": "2026-03-08T12:00:00+00:00"
}
```

---

## 8. Authentication Flow

### 8.1 Bearer Token Validation Steps

```
Client                          MCP Server
  │                                 │
  ├── POST /mcp ──────────────────► │
  │   Authorization: Bearer ec_...  │
  │   Accept: text/event-stream     │
  │                                 │
  │                    ┌────────────┤
  │                    │ 1. Extract Authorization header from ASGI scope
  │                    │ 2. Strip "Bearer " prefix
  │                    │ 3. SHA-256 hash the API key
  │                    │ 4. Query: SELECT ... FROM subscriptions
  │                    │           WHERE api_key_hash = ? AND status = 'active'
  │                    │ 5. Check current_period_end > now()
  │                    │ 6. Check daily usage < daily_limit
  │                    │ 7. Set _auth_context ContextVar
  │                    └────────────┤
  │                                 │
  │ ◄── SSE: tool results ─────────┤
```

### 8.2 AuthResult Data Class

```python
@dataclass
class AuthResult:
    authenticated: bool
    user_email: str | None = None
    tier: str | None = None
    daily_limit: int = 0
    daily_used: int = 0
    api_key_hash: str | None = None
    error: str | None = None
    error_code: str | None = None
```

### 8.3 Per-Tool Tier Enforcement

```python
@mcp.tool()
def diagnose_issue(error_text: str, context: str = "general") -> str:
    auth = _require_auth("diagnose_issue", "subscriber")  # Requires subscriber tier
    # ... tool logic ...
```

### 8.4 Usage Logging

```python
def log_tool_usage(auth_result: AuthResult, tool_name: str,
                   response_time_ms: int = 0, success: bool = True):
    if auth_result.api_key_hash:
        log_usage(auth_result.api_key_hash, tool_name, response_time_ms, success)
```

Every tool call is logged with the tool name, response time in milliseconds, and success/failure status. This data feeds both rate limiting and analytics.

---

## 9. How Clients Consume the MCP Service

### 9.1 Agent Johnny 5 (Primary Client)

Agent Johnny 5, the AI admin bundled with every EasyClaw installation, is the primary MCP client. It connects via the `MCPClient` class in `easyclaw-tui`:

```python
class MCPClient:
    """JSON-RPC 2.0 client with SSE transport"""

    async def call_tool(self, tool_name: str, arguments: dict) -> str:
        # POST to https://mcp.easyclaw.help/mcp
        # Headers: Authorization: Bearer ec_live_XXXX
        #          Accept: application/json, text/event-stream
        # Body: JSON-RPC 2.0 request
        # Response: SSE stream, parse data: lines
```

The agent has 22 tools total: 7 local tools (shell execution, file I/O, user confirmation) plus the 13 MCP tools. During installation, it can search knowledge for solutions, diagnose errors against known patterns, validate the deployment, and generate configuration files — all without human intervention.

### 9.2 Tool Discovery

MCP tools are registered with FastMCP using the `@mcp.tool()` decorator. FastMCP automatically generates the tool schema (name, description, parameters, types) and exposes them via the MCP protocol's `tools/list` method. Clients discover available tools dynamically — no hardcoded tool lists needed.

### 9.3 FastMCP Configuration

```python
mcp = FastMCP(
    "EasyClaw Knowledge Hub",
    stateless_http=True,
    host="0.0.0.0",          # CRITICAL: Required behind Traefik (otherwise 421)
    port=8000,
)
mcp_app = mcp.streamable_http_app()  # Returns ASGI app with /mcp endpoint
```

The `stateless_http=True` flag tells FastMCP to use Streamable HTTP transport instead of maintaining persistent SSE connections. The `host="0.0.0.0"` is mandatory when running behind a reverse proxy — without it, FastMCP rejects requests with HTTP 421 (Misdirected Request) because the Host header doesn't match localhost.

---

## 10. Deployment

### 10.1 Container Image

The Dockerfile uses Python 3.12-slim with layer caching for fast rebuilds:

```dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
HEALTHCHECK CMD python -c "import httpx; httpx.get('http://localhost:8000/health')"
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
```

Single worker is intentional — SQLite's WAL mode handles concurrent reads, and the write lock means multiple workers would contend on writes.

### 10.2 Dependencies

```
fastmcp>=2.0.0
uvicorn>=0.30.0
httpx>=0.27.0
python-dotenv>=1.0.0
starlette>=0.40.0
```

### 10.3 Docker Compose

```yaml
easyclaw-mcp:
  build: ./mcp
  container_name: easyclaw-mcp
  volumes:
    - ./data:/data          # SQLite persistence
  env_file: .env
  networks:
    - n8n_default           # Accessible by n8n workflows
    - searxng_searxng-network  # SearXNG web search
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.easyclaw-mcp.rule=Host(`mcp.easyclaw.help`)"
    - "traefik.http.routers.easyclaw-mcp.tls.certresolver=letsencrypt"
```

### 10.4 Environment Variables

```bash
EASYCLAW_DB_PATH=/data/easyclaw.db      # SQLite location
ADMIN_SECRET=<admin-secret>             # Admin API auth
SEARXNG_URL=http://searxng:8080         # Private search instance
```

### 10.5 Deployment Workflow

```bash
# MCP backend update
scp -r mcp/* root@31.220.49.171:/opt/easyclaw/mcp/
ssh root@31.220.49.171 "cd /opt/easyclaw && docker compose up -d --build easyclaw-mcp"

# Verify
curl https://mcp.easyclaw.help/health
# → {"status":"ok","version":"3.0.0","tools":13,"guides":18}
```

### 10.6 Startup Sequence

1. `create_app()` called
2. `init_db()` called — creates schema, populates FTS5
3. Starlette admin app registered with CORS middleware
4. ASGI dispatcher wired (admin + MCP routing)
5. FastMCP app created with `stateless_http=True`
6. uvicorn server starts on `0.0.0.0:8000`

---

## 11. Security Model

### 11.1 Defense in Depth

| Layer | Mechanism |
|-------|-----------|
| **Network** | Traefik TLS termination, automatic Let's Encrypt certificates |
| **CORS** | Allowlist: `easyclaw.help` and `n8n.srv1071455.hstgr.cloud` only |
| **Authentication** | Bearer token → SHA-256 hash → subscription lookup |
| **Authorization** | Per-tool tier checks via `_require_auth()` |
| **Rate Limiting** | Daily usage counting per API key hash, automatic daily reset |
| **Key Storage** | SHA-256 hashes only, plaintext never persisted after creation |
| **Admin API** | HMAC-SHA256 timing-safe secret comparison |
| **OTP** | 10-minute TTL, single-use, invalidates previous codes on new generation |
| **Database** | WAL mode (atomic writes), foreign keys enforced |
| **Subscription Expiry** | Checked at auth time (`current_period_end` validation) |

### 11.2 Key Design Decisions

- **No salt on API key hashes**: API keys are 48 random hex characters (192 bits of entropy), making rainbow table attacks infeasible.
- **Timing-safe admin comparison**: Prevents attackers from guessing the admin secret character-by-character via response time analysis.
- **Subscription expiry at auth time**: If a Stripe subscription lapses, the user's API key becomes invalid immediately — no grace period, no manual intervention.
- **OTP invalidation**: Generating a new OTP automatically invalidates all previous unused codes for that email, preventing code accumulation attacks.

---

## 12. Performance Characteristics

| Operation | Latency | Notes |
|-----------|---------|-------|
| Health check | <10ms | No auth, no database |
| Bearer token validation | ~1ms | SHA-256 hash + indexed query |
| Knowledge search (FTS5) | ~5ms | BM25 ranking, indexed |
| Full guide retrieval | ~2ms | Direct topic lookup |
| Rate limit check | ~1ms | Indexed by (api_key_hash, timestamp) |
| Config generation | ~1ms | Template substitution |
| Error diagnosis | ~2ms | Regex pattern matching |
| Web search (SearXNG) | 500-2000ms | External HTTP call |

The service handles the expected load (hundreds of subscribers, each making <100 tool calls/day) comfortably on a single SQLite database with a single uvicorn worker.

**Database**: SQLite WAL mode allows concurrent reads + single writer. No connection pool needed — each request opens a fresh connection (SQLite connections are lightweight).

---

## 13. Error Handling

### 13.1 FTS5 Syntax Errors

User queries containing FTS5 operators (`*`, `-`, `OR`, `AND`, `NOT`, `NEAR`) cause `sqlite3.OperationalError`. The search tool silently catches this and falls back to LIKE-based substring search.

### 13.2 JSON Parsing

Admin API endpoints catch malformed JSON bodies:

```python
try:
    body = await request.json()
except Exception:
    return JSONResponse({"error": "invalid JSON body"}, status_code=400)
```

### 13.3 Container Health

The Dockerfile includes a health check that the Docker daemon monitors:

```dockerfile
HEALTHCHECK CMD python -c "import httpx; httpx.get('http://localhost:8000/health')"
```

Note: `curl` is not available in the slim container image — health checks use Python's `httpx` directly.

---

## 14. File Structure

```
/opt/easyclaw/mcp/
├── server.py          # FastMCP, 13 tools, admin endpoints, ASGI dispatcher (2,172 lines)
├── database.py        # SQLite schema, FTS5, 18 knowledge guides (3,057 lines)
├── auth.py            # Bearer token validation, tier access control (110 lines)
├── requirements.txt   # fastmcp, uvicorn, httpx, starlette, python-dotenv
├── Dockerfile         # Python 3.12-slim, health check, single worker
└── /data/
    └── easyclaw.db    # SQLite database (created on startup)
```

---

## 15. Lessons Learned

1. **Starlette Mount + FastMCP + Reverse Proxy = redirect loops.** The raw ASGI dispatcher pattern is more code but eliminates an entire class of proxy-related bugs.

2. **FTS5 operators in user queries cause crashes.** Always wrap FTS5 MATCH queries in try/except with a LIKE fallback. Users will inevitably search for strings containing `*`, `-`, `OR`, or other FTS5 syntax characters.

3. **contextvars are the right abstraction for request-scoped state in ASGI.** Thread-local storage doesn't work with async, and passing auth through function signatures pollutes the tool API.

4. **SQLite's `date('now')` is a surprisingly elegant rate limiter.** No cron jobs, no Redis, no background workers — the reset happens naturally at the query level.

5. **Dynamic knowledge overrides are essential.** OpenClaw ships new features regularly. Being able to update guides via API without rebuilding the container keeps the knowledge base current.

6. **`host="0.0.0.0"` is required for FastMCP behind a reverse proxy.** Without it, FastMCP responds with HTTP 421 (Misdirected Request) because the Host header doesn't match localhost.

7. **Single uvicorn worker is correct for SQLite.** Multiple workers would contend on SQLite's write lock, causing `database is locked` errors under load.

8. **No curl in slim containers.** Health checks and internal HTTP calls must use Python's `httpx` instead.

---

## 16. Future Directions

- **Streaming tool responses**: Long-running tools (web search, deployment validation) could stream partial results via SSE instead of blocking until completion.
- **Tool composition**: Allow tools to call other tools internally — e.g., `diagnose_issue` could automatically `search_knowledge` for related guides.
- **Usage analytics dashboard**: Surface tool usage patterns, popular guides, and common error categories to inform knowledge base improvements.
- **Multi-tenant isolation**: If demand warrants, migrate from SQLite to PostgreSQL for proper connection pooling and concurrent writes.
- **Webhook-triggered knowledge updates**: Instead of daily cron, push updates to the knowledge base in real time when OpenClaw releases new features.

---

## 17. Key Architectural Decisions Summary

| Decision | Rationale |
|----------|-----------|
| **No Starlette Mount** | Avoids redirect loops with FastMCP behind Traefik reverse proxy |
| **Raw ASGI Dispatcher** | Full control over routing, auth injection, and error handling |
| **contextvars for Auth** | Thread-safe, async-safe auth context passing to tool functions |
| **SQLite WAL + FTS5** | Production-grade full-text search with zero external dependencies |
| **SHA-256 Key Hashing** | Industry-standard, no salt needed (keys are 192-bit random) |
| **Daily Rate Limits via SQL** | Self-resetting, no background workers or external services |
| **Dynamic Knowledge Override** | Update guides without container rebuild via admin API |
| **Admin Secret via HMAC** | Timing-safe comparison prevents side-channel attacks |
| **Stateless HTTP Transport** | No persistent connections, scales simply behind load balancers |
| **Single Uvicorn Worker** | Matches SQLite's single-writer constraint |

---

*EasyClaw MCP Server v3.0.0 — 13 tools, 18 guides, zero dependencies beyond Python and SQLite.*
