Skip to main content
This page explains exactly how authentication works when Crow calls your MCP server, and how your MCP server authenticates with your own backend API. Every header, every token, every hop.

The Auth Chain

There are three hops in the full chain. Here’s what happens at each one:
┌─────────┐         ┌──────────────┐         ┌────────────┐         ┌──────────────┐
│  Widget  │───(1)──→│ Crow Backend │───(2)──→│ MCP Server │───(3)──→│ Your Backend │
│ (browser)│         │              │         │  (yours)   │         │    API       │
└─────────┘         └──────────────┘         └────────────┘         └──────────────┘

Hop 1: Widget → Crow Backend

The widget sends the user’s identity token (a JWT your backend minted) with every chat message.
WhatValue
TokenIdentity JWT (signed with your Crow verification secret)
Sent asidentity_token field in the chat request body
Validated byCrow backend using your product’s verification secret
PurposeProves who the end user is
This is the standard Identity Verification flow. If you haven’t set it up, users are treated as anonymous and identity headers won’t be available.

Hop 2: Crow Backend → Your MCP Server

When Crow connects to your MCP server to execute tools, it sends three types of headers:
HeaderSourceExample Value
X-Service-KeyAuto-injected from your product’s service keyHCF8nVjR8Y...
Static headersWhatever you configured in the dashboardAuthorization: Bearer sk-...
Identity headersResolved from header mappings (see below)X-User-ID: user_456
Your MCP server automatically receives the service key — Crow reads it from your product config and injects it into every request. You don’t need to configure anything for this to happen.

Hop 3: Your MCP Server → Your Backend API

Your MCP server wraps your existing backend API. When a tool executes, it calls your API endpoints to actually do the work (query your database, create records, etc.). Your backend API already has auth — Clerk, Auth0, Firebase, API keys, whatever you use. The MCP server can’t use those because it’s not a user. It’s a service acting on behalf of users. The solution: your backend adds a second auth path that accepts the service key. When the service key is present and valid, your backend trusts the request and reads scoping context (user ID, tenant ID) from headers instead of from a user JWT.
WhatValue
HeaderX-Service-Key
ValueThe same service key from hop 2, forwarded by your MCP server
Validated byYour backend’s auth middleware
PurposeProves the request is from a trusted service (your MCP server, called by Crow)
Your existing user auth (Clerk, Auth0, Firebase, etc.) stays completely unchanged. The service key adds a second auth path alongside it — it doesn’t replace anything.

Service Keys

A service key is a shared secret that lets your MCP server authenticate with your backend API. Think of it like a Stripe secret key — Crow generates it, you copy it to your backend.

How It Works

Crow dashboard (Deploy → API Keys)


Crow generates and stores the service key

    ├──→ Crow auto-injects it as X-Service-Key header
    │    when calling your MCP server (Hop 2)

    └──→ You copy it from the dashboard and add it to
         your backend's .env file (for Hop 3 validation)
The key lives in two places:
WhereEnv VarPurpose
Your MCP serverReceived automatically via X-Service-Key header from CrowForwarded to your backend API
Your backend APIMCP_SERVICE_KEY in .envValidates incoming requests from the MCP server
Crow is the source of truth. You never generate the key yourself.

Where to Find Your Service Key

  1. Go to DeployAPI Keys in the Crow dashboard
  2. Copy the Service Key
  3. Add it to your backend’s .env:
    MCP_SERVICE_KEY=HCF8nVjR8YON5eBzlMHuTqO8IwD3c0DSynVnTKq5/80=
    

Adding Service Key Auth to Your Backend

Add a ~15-line middleware alongside your existing auth. This does not replace your current auth — it adds a second path.
import os, hmac
from fastapi import Request, HTTPException

SERVICE_KEY = os.environ.get("MCP_SERVICE_KEY", "")

async def verify_auth(request: Request):
    """Dual-path auth: service key (MCP server) OR user JWT (browsers).

    Path 1: Service key present → trusted service, read user from headers
    Path 2: No service key → normal user auth (Clerk, Auth0, etc.)
    """
    # Path 1: Service key (from MCP server via Crow)
    service_key = request.headers.get("x-service-key")
    if service_key and SERVICE_KEY:
        if hmac.compare_digest(service_key, SERVICE_KEY):
            # Trusted — read user context from headers
            request.state.user_id = request.headers.get("x-user-id")
            request.state.tenant_id = request.headers.get("x-tenant-id")
            request.state.auth_type = "service_key"
            return

    # Path 2: Normal user auth (your existing flow, unchanged)
    authorization = request.headers.get("authorization", "")
    if authorization.startswith("Bearer "):
        token = authorization.removeprefix("Bearer ")
        claims = await verify_user_jwt(token)  # your existing function
        request.state.user_id = claims.get("user_id")
        request.state.tenant_id = claims.get("tenant_id")
        request.state.auth_type = "user_jwt"
        return

    raise HTTPException(status_code=401, detail="Unauthorized")
Key detail: When the service key is present, your backend trusts the scoping headers (X-User-ID, X-Tenant-ID, etc.) because the caller has proven it’s a trusted service. Without the service key, those headers are ignored and normal user auth applies.
Always use constant-time comparison (hmac.compare_digest in Python, crypto.timingSafeEqual in Node) to prevent timing attacks. Never use == to compare secrets.

Your MCP Server — Forwarding the Key

Your MCP server receives X-Service-Key from Crow automatically. It just needs to include it when calling your backend:
import os
import httpx
from fastmcp import FastMCP
from fastmcp.dependencies import Depends, CurrentHeaders

mcp = FastMCP("My Backend")

# Your backend API (not Crow's)
API_BASE = os.environ.get("API_BASE_URL", "http://localhost:8000")

async def get_api_client(headers: dict = CurrentHeaders()) -> httpx.AsyncClient:
    """HTTP client that forwards the service key to your backend."""
    key = headers.get("x-service-key", "")
    return httpx.AsyncClient(
        base_url=API_BASE,
        headers={
            "Content-Type": "application/json",
            "X-Service-Key": key,          # Forward to your backend
            "X-User-ID": headers.get("x-user-id", ""),    # Forward user context
            "X-Tenant-ID": headers.get("x-tenant-id", ""),
        },
        timeout=30.0,
    )

@mcp.tool()
async def get_order(
    order_id: str,
    client: httpx.AsyncClient = Depends(get_api_client),
) -> dict:
    """Look up an order."""
    resp = await client.get(f"/api/orders/{order_id}")
    resp.raise_for_status()
    return resp.json()
For local development, you can also set SERVICE_KEY as an env var on the MCP server as a fallback:
key = os.environ.get("SERVICE_KEY", "") or headers.get("x-service-key", "")
In production, the header from Crow is the primary source.

Identity Headers

Identity headers let your MCP server (and your backend) know who the end user is. Crow extracts claims from the user’s identity token and sends them as HTTP headers.

How Identity Flows Through the Chain

1. Your backend mints an identity JWT for the widget:
   { "user_id": "user_456", "email": "alice@example.com", "tenant_id": "t_789" }

2. Widget sends it to Crow with every chat message

3. Crow decodes the JWT and has the claims available

4. For each header mapping you configured, Crow resolves the value:
   identity.user_id    → "user_456"
   identity.email      → "alice@example.com"
   identity.tenant_id  → "t_789"

5. Crow sends these as HTTP headers to your MCP server:
   X-User-ID: user_456
   X-Email: alice@example.com
   X-Tenant-ID: t_789

6. Your MCP server forwards them to your backend API

Configuring Header Mappings

In the dashboard, under IntegrationServer-Side MCPHeader Mappings, add mappings:
Header NameSourceWhat It Sends
X-User-IDidentity.user_idThe logged-in user’s ID
X-Emailidentity.emailThe user’s email
X-Tenant-IDidentity.tenant_idCustom claim from your JWT
X-Product-IDproduct.idThe Crow product ID

Available Sources

identity.* — Any claim from the user’s identity JWT:
SourceValue
identity.user_idUser ID (from user_id claim)
identity.emailEmail (from email claim)
identity.nameName (from name claim)
identity.<custom>Any custom claim you include when minting the JWT
Identity claims are only available for authenticated users. If the user hasn’t been identified via Identity Verification, identity headers won’t be sent.
product.* — Fields from the Crow product configuration:
SourceValue
product.idThe product ID
product.nameThe product display name
product.organization_idThe org that owns this product

Reading Identity Headers in Your MCP Server

from fastmcp.dependencies import Depends, CurrentHeaders

async def get_user_id(headers: dict = CurrentHeaders()) -> str:
    """Read user ID forwarded by Crow."""
    return headers.get("x-user-id", "")

@mcp.tool()
async def get_my_orders(
    user_id: str = Depends(get_user_id),
) -> dict:
    """Get orders for the current user."""
    if not user_id:
        return {"error": "User not authenticated"}
    return await db.orders.find_by_user(user_id)

Putting It All Together

Here’s the complete picture: Crow calls your MCP server, which calls your backend API, which serves user-specific data. Dashboard configuration:
Server-Side MCP:
  Name: my-backend
  URL: https://mcp.yourapp.com/mcp
  Transport: Streamable HTTP

Header Mappings:
  X-User-ID    ← identity.user_id
  X-Tenant-ID  ← identity.tenant_id
Your backend .env:
MCP_SERVICE_KEY=HCF8nVjR8YON5eBzlMHuTqO8IwD3c0DSynVnTKq5/80=
Your MCP server:
import os
import httpx
from fastmcp import FastMCP
from fastmcp.dependencies import Depends, CurrentHeaders

mcp = FastMCP("My Backend")
API_BASE = os.environ.get("API_BASE_URL", "https://api.yourapp.com")

async def get_api_client(headers: dict = CurrentHeaders()) -> httpx.AsyncClient:
    return httpx.AsyncClient(
        base_url=API_BASE,
        headers={
            "Content-Type": "application/json",
            "X-Service-Key": headers.get("x-service-key", ""),
            "X-User-ID": headers.get("x-user-id", ""),
            "X-Tenant-ID": headers.get("x-tenant-id", ""),
        },
        timeout=30.0,
    )

@mcp.tool()
async def get_my_orders(
    client: httpx.AsyncClient = Depends(get_api_client),
) -> dict:
    """Get the current user's recent orders."""
    resp = await client.get("/api/orders/mine")
    resp.raise_for_status()
    return resp.json()
Your backend middleware (see Adding Service Key Auth above). What happens when a user asks “show me my orders”:
1. Widget → Crow:       identity_token (JWT with user_id: "user_456")
2. Crow validates JWT, extracts user_id
3. Crow → MCP server:   X-Service-Key: HCF8n..., X-User-ID: user_456
4. MCP server → Your backend: X-Service-Key: HCF8n..., X-User-ID: user_456
5. Your backend validates service key ✓, reads X-User-ID
6. Your backend queries orders for user_456
7. Returns orders → MCP server → Crow → Widget → User sees their order list

Edge Cases

ScenarioWhat Happens
Anonymous user (no identity token)Identity headers are not sent. Your MCP server receives the service key but no user context. Tools still work for non-user-specific actions.
Service key not set on productX-Service-Key header is not sent. Your MCP server won’t be able to authenticate with your backend.
Identity claim doesn’t existIf a header mapping references a claim not in the JWT (e.g., identity.tenant_id but you didn’t include tenant_id when minting), that header is simply not sent. No error.
MCP server is downCrow surfaces an error to the user: “Could not connect to the MCP server.” The agent continues without those tools.

Security Checklist

  • Never expose your service key client-side. It’s a server-to-server secret between your MCP server and your backend.
  • Always validate the service key in your backend middleware.
  • Use constant-time comparison for secret validation — never ==.
  • Set up Identity Verification if your tools need to know who the user is.
  • Use HTTPS in production for your MCP server URL.

Troubleshooting

IssueCauseFix
Your backend returns 401Service key mismatch or not setCopy the service key from Crow dashboard → Deploy → API Keys and add to your backend’s .env
Identity headers are emptyUser not authenticated in the widgetSet up Identity Verification, ensure frontend calls identify()
X-Service-Key header missingNo service key on the productCheck DeployAPI Keys in dashboard
MCP server not receiving headersWrong transportUse Streamable HTTP (not SSE) for header support
Custom claims not forwardedClaim not in JWTEnsure your backend includes the claim when minting the identity token