The Stackbilt MCP Gateway is a Cloudflare Worker that acts as an OAuth-authenticated MCP endpoint, routing tool calls to backend product workers via Cloudflare service bindings.
MCP Client (Claude, Cursor, etc.)
│
▼
┌──────────────────────────────────────┐
│ OAuthProvider (index.ts) │
│ ├─ /health → bypass, return status │
│ ├─ /authorize, /login, /signup, │
│ │ /oauth/*, /register, /token │
│ │ → oauthHandler (oauth-handler) │
│ └─ /mcp → validateToken → gateway │
└──────────────────────────────────────┘
│ │
▼ ▼
┌─────────────┐ ┌──────────────┐
│ AUTH_SERVICE │ │ Gateway │
│ (stackbilt- │ │ (gateway.ts) │
│ auth) │ └──────┬───────┘
└─────────────┘ │
┌────────┴────────┐
▼ ▼
┌────────────┐ ┌────────────┐
│ STACKBILDER│ │ IMG_FORGE │
│ (edge-stack │ │ (img-forge- │
│ -architect) │ │ mcp) │
└────────────┘ └────────────┘
The Worker's fetch handler wraps everything in OAuthProvider from @cloudflare/workers-oauth-provider. This middleware:
- Handles
/register,/token,/.well-known/oauth-authorization-serverautomatically - Injects
OAUTH_PROVIDERhelpers into the environment - Passes authenticated requests to the inner handler with
ctx.propscontaininguserId,email,name
A /health check bypasses OAuth entirely for uptime monitoring. CORS headers are applied to all responses.
Handles all authentication UI and flows:
| Route | Method | Purpose |
|---|---|---|
/authorize |
GET | Show login form or process identity token |
/login |
POST | Email/password authentication |
/signup |
GET/POST | Account creation |
/oauth/github |
POST | Initiate GitHub SSO |
/oauth/google |
POST | Initiate Google SSO |
/oauth/callback |
GET | Social OAuth callback |
- Client redirects user to
/authorizewith standard OAuth 2.1 parameters - User authenticates via email/password or social SSO
- On success, the handler creates an HMAC-SHA256 signed identity token containing
{ userId, email, name, exp } - Redirects to
/authorize?identity_token=<signed_token> - The authorize handler verifies the signature and 5-minute expiry
- Auto-approves consent and calls
completeAuthorization()to issue an authorization code - Client exchanges the code for tokens at
/token
For GitHub/Google SSO, the gateway delegates to auth.stackbilt.dev/social-bridge:
- Gateway stores OAuth parameters (
client_id,redirect_uri,scope, etc.) inOAUTH_KVundersocial_state:{uuid}with 5-min TTL - Redirects to
auth.stackbilt.dev/social-bridge?provider=github&state={uuid}&return_url=... - Auth service handles the provider-specific OAuth dance
- Callback at
/oauth/callbackretrieves stored state, exchanges the code viaAUTH_SERVICE.exchangeSocialCode(), and creates the identity token
const PUBLIC_SIGNUPS_ENABLED = true;When false, all auth routes return a "Coming Soon" HTML page. This gates public access without code deployment — just flip the flag.
Core MCP JSON-RPC transport layer. Handles:
| HTTP Method | Behavior |
|---|---|
GET /health |
Return service status |
GET (SSE) |
Open event stream with keep-alive |
POST |
Process JSON-RPC request |
DELETE |
Close session |
- Sessions are created on
initializeand stored in-memory - Session ID: 32-char random hex (16 bytes via
crypto.getRandomValues) - TTL: 30 minutes (
SESSION_TTL_MS) - Garbage collection runs on every
tools/listcall, removing expired sessions - Session ID is passed via
Mcp-Session-Idheader
The gateway resolves auth from two sources:
- OAuth context props —
ctx.props.userId/email/nameset by OAuthProvider (primary path) - Bearer token fallback —
Authorization: Bearer <token>validated viaAUTH_SERVICE
On success, provisionTenant() is called via AUTH_SERVICE to ensure the user has an active tenant.
- Validate tool name exists in the aggregated catalog
- Look up route and risk level from the route table
- Generate a trace ID (
trc_{timestamp}_{random}) - Forward the request to the backend service binding as
POST /mcpwith JSON-RPC body - Parse response: JSON or SSE (extracts content from
messageevents) - Emit structured audit event
- Return result to client
Aggregates tool catalogs from all backend service bindings into a unified catalog.
buildAggregatedCatalog()callstools/liston each backend (STACKBILDER, IMG_FORGE)- Tools are returned with their original names (already namespaced:
image.generate,flow.create) - Each tool must have a corresponding entry in the route table with an explicit risk level
- Tools without declared risk levels are rejected at registration time
- The registry maps tool names to their backend for routing
Every tool's inputSchema is preserved from the backend and served to clients via tools/list. The gateway validates that tool arguments are non-null objects before dispatch.
Static mapping of tool names to backends and risk levels:
// Risk levels (Security Constitution)
enum RiskLevel {
READ_ONLY = 'READ_ONLY',
LOCAL_MUTATION = 'LOCAL_MUTATION',
EXTERNAL_MUTATION = 'EXTERNAL_MUTATION',
}| Tool | Backend | Risk Level |
|---|---|---|
flow.create |
STACKBILDER | LOCAL_MUTATION |
flow.status |
STACKBILDER | READ_ONLY |
flow.summary |
STACKBILDER | READ_ONLY |
flow.quality |
STACKBILDER | READ_ONLY |
flow.governance |
STACKBILDER | READ_ONLY |
flow.advance |
STACKBILDER | LOCAL_MUTATION |
flow.recover |
STACKBILDER | LOCAL_MUTATION |
image.generate |
IMG_FORGE | EXTERNAL_MUTATION |
image.list_models |
IMG_FORGE | READ_ONLY |
image.check_job |
IMG_FORGE | READ_ONLY |
Risk levels are used for audit classification, not for authorization enforcement — all authenticated users can call all tools within their quota.
Structured audit logging for every tool call, compliant with the Security Constitution.
Each audit event includes:
- Trace ID:
trc_{timestamp}_{random}for correlating request/response pairs - Tool name and risk level
- User ID and tenant ID
- Input summary: first 200 chars of arguments (redacted)
- Outcome: success/error, duration, response size
- Timestamp: ISO 8601
Before any data hits logs, the following patterns are redacted:
| Pattern | Example | Replacement |
|---|---|---|
| API keys | sb_live_abc123... |
[REDACTED:api_key] |
| Bearer tokens | Bearer eyJ... |
[REDACTED:bearer] |
| Long hex strings | 32+ hex chars | [REDACTED:hash] |
| Sensitive fields | password, secret, api_key |
Values replaced with [REDACTED] |
Audit events are sent to:
console.log— captured by Cloudflare Workers loggingPLATFORM_EVENTS_QUEUE—stackbilt-user-eventsqueue for the BizOps pipeline
Bearer token extraction and validation for non-OAuth paths:
- Extracts token from
Authorization: Bearer <token>header - Validates via
AUTH_SERVICE(delegated to the auth worker) - Maps auth errors to HTTP status codes (401 for invalid/expired, 403 for rate-limited/delinquent)
- OAuth 2.1 + PKCE: No client secrets stored on devices; code verifier prevents interception
- HMAC-signed identity tokens: Tamper-proof, 5-minute TTL, replaces cookies in stateless flow
- Open redirect prevention: OAuth deny flow validates
redirect_uriagainst registered client before redirecting - Secret redaction: No sensitive data in logs — API keys, tokens, passwords all scrubbed
- Null safety: Tool arguments validated as objects before dispatch; session IDs checked before operations
- Service isolation: Each backend runs in its own Worker; the gateway proxies without cross-tenant data leakage
Enforced by AUTH_SERVICE (delegated to the auth worker). The gateway receives:
insufficient_scope(403) — rate limited or payment delinquentinvalid_token(401) — expired or invalid token
| Package | Purpose |
|---|---|
@modelcontextprotocol/sdk |
MCP protocol types and utilities |
@cloudflare/workers-oauth-provider |
OAuth 2.1 + PKCE provider middleware |
agents |
Cloudflare Agents SDK |
zod |
Schema validation |