xmcp supports deployment to Cloudflare Workers with a specialized build process that creates a fully-bundled, edge-ready worker.
Quick Start
1. Build for Cloudflare
This generates:
worker.js - Bundled Cloudflare Worker in your project root
wrangler.jsonc - Wrangler configuration (if not already present)
2. Test Locally
3. Deploy
How It Works
The --cf flag triggers specialized Cloudflare bundling:
// From: packages/xmcp/src/platforms/build-cloudflare-output.ts
async function buildCloudflareOutput(options = {}) {
const outputDir = rootDir;
const buildDir = path.join(rootDir, ".xmcp", "cloudflare");
// Check if Cloudflare worker build exists
const sourceFile = path.join(buildDir, "worker.js");
if (!fs.existsSync(sourceFile)) {
throw new Error("Cloudflare build output not found. Run: xmcp build --cf");
}
// Copy the bundled worker
const targetFile = path.join(outputDir, "worker.js");
fs.copyFileSync(sourceFile, targetFile);
// Generate wrangler config (only if the user doesn't have one already)
const wranglerTomlPath = path.join(outputDir, "wrangler.toml");
const wranglerJsoncPath = path.join(outputDir, "wrangler.jsonc");
if (!fs.existsSync(wranglerTomlPath) && !fs.existsSync(wranglerJsoncPath)) {
const projectName = getProjectName();
const wranglerConfig = generateWranglerConfig(projectName);
fs.writeFileSync(wranglerJsoncPath, wranglerConfig);
}
}
Generated wrangler.jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
// Wrangler config generated by: xmcp build --cf
// Docs: https://developers.cloudflare.com/workers/wrangler/configuration/
"name": "your-project-name",
"main": "worker.js",
"compatibility_date": "2026-02-28",
"compatibility_flags": ["nodejs_compat"],
// Observability (Workers Logs)
"observability": {
"enabled": true
}
// Uncomment to add environment variables:
// "vars": {
// "MY_VAR": "my-value"
// },
// Uncomment to add KV namespaces:
// "kv_namespaces": [
// { "binding": "MY_KV", "id": "your-kv-namespace-id" }
// ],
// Uncomment to add D1 databases:
// "d1_databases": [
// { "binding": "MY_DB", "database_name": "my-database", "database_id": "your-database-id" }
// ]
}
Development Workflow
For rapid development, use the watch mode:
This rebuilds worker.js whenever your source files change. Run this alongside wrangler dev:
# Terminal 1: Watch and rebuild
xmcp dev --cf
# Terminal 2: Run local worker
npx wrangler dev
Or use concurrently to run both commands together:
{
"scripts": {
"dev": "concurrently \"xmcp dev --cf\" \"npx wrangler dev\"",
"build": "xmcp build --cf",
"deploy": "npx wrangler deploy"
},
"devDependencies": {
"concurrently": "^9.2.0",
"wrangler": "^4.62.0"
}
}
Complete Example
Here’s a real working example from the xmcp repository:
import { XmcpConfig } from "xmcp";
const config: XmcpConfig = {
http: {
debug: true,
},
paths: {
tools: "./src/tools",
prompts: false,
resources: false,
},
};
export default config;
Testing Your Worker
Once wrangler dev is running (default port: 8787), test your endpoints:
Health Check
curl http://localhost:8787/health
curl -X POST http://localhost:8787/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
curl -X POST http://localhost:8787/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"hello","arguments":{"name":"World"}},"id":2}'
Cloudflare-Specific Features
xmcp exports Cloudflare-optimized middleware through the xmcp/cloudflare package.
API Key Authentication
import { XmcpConfig } from "xmcp";
import { apiKeyAuthMiddleware } from "xmcp/cloudflare";
const config: XmcpConfig = {
http: {
middlewares: [
apiKeyAuthMiddleware({
apiKey: "your-secret-key",
headerName: "x-api-key", // optional, defaults to 'x-api-key'
}),
],
},
};
export default config;
// From: packages/xmcp/src/runtime/platforms/cloudflare/middlewares/api-key.ts
export function cloudflareApiKeyAuthMiddleware(
config: { apiKey: string; headerName?: string }
): WebMiddleware {
const headerName = config.headerName ?? "x-api-key";
return async (request) => {
const apiKeyHeader = request.headers.get(headerName);
if (!apiKeyHeader || apiKeyHeader !== config.apiKey) {
return new Response(
JSON.stringify({ error: "Unauthorized: Missing or invalid API key" }),
{ status: 401, headers: { "Content-Type": "application/json" } }
);
}
return; // Allow request to continue
};
}
JWT Authentication
import { XmcpConfig } from "xmcp";
import { jwtAuthMiddleware } from "xmcp/cloudflare";
const config: XmcpConfig = {
http: {
middlewares: [
jwtAuthMiddleware({
secret: "your-jwt-secret",
algorithms: ["HS256"],
issuer: "your-issuer",
audience: "your-audience",
}),
],
},
};
export default config;
// From: packages/xmcp/src/runtime/platforms/cloudflare/middlewares/jwt.ts
import { jwtVerify, importJWK, importSPKI, importX509 } from "jose";
export function cloudflareJwtAuthMiddleware(
config: CloudflareJWTAuthMiddlewareConfig
): WebMiddleware {
return async (request) => {
const authHeader = request.headers.get("authorization");
const token = extractBearerToken(authHeader);
if (!token) {
return jsonUnauthorized(
"Unauthorized: Missing or malformed Authorization header"
);
}
try {
await verifyJwt(token, config);
return; // Valid token
} catch {
return jsonUnauthorized("Unauthorized: Invalid or expired token");
}
};
}
The JWT middleware supports multiple secret formats:
- Plain string: HMAC secrets (HS256, HS384, HS512)
- JWK: JSON Web Key format
- PEM: Public keys (RS256, ES256, etc.)
- X.509: Certificate format
Environment Variables
Configure secrets and environment variables in wrangler.jsonc:
{
"name": "my-xmcp-worker",
"main": "worker.js",
"compatibility_date": "2026-02-28",
"compatibility_flags": ["nodejs_compat"],
"vars": {
"ENVIRONMENT": "production"
},
// For secrets, use: wrangler secret put API_KEY
// Then access via: env.API_KEY in your middleware
}
Set secrets using Wrangler CLI:
wrangler secret put API_KEY
wrangler secret put JWT_SECRET
Never commit secrets to wrangler.jsonc. Use wrangler secret put or environment-specific configurations.
Cloudflare Bindings
Access Cloudflare platform features:
KV Storage
{
"kv_namespaces": [
{ "binding": "MY_KV", "id": "your-namespace-id" }
]
}
D1 Database
{
"d1_databases": [
{
"binding": "DB",
"database_name": "my-database",
"database_id": "your-database-id"
}
]
}
R2 Storage
{
"r2_buckets": [
{ "binding": "MY_BUCKET", "bucket_name": "my-bucket" }
]
}
Limitations
Cloudflare Workers have some constraints:
- CPU Time: 50ms on free plan, 30s on paid
- Memory: 128 MB
- Script Size: 1 MB compressed, 10 MB uncompressed
- No Filesystem: Use KV, R2, or D1 for persistence
The --cf build process handles these by:
- Bundling all dependencies into a single file
- Inlining React component bundles at compile time
- Using only Web APIs (no Node.js runtime dependencies)
- Polyfilling necessary Node.js APIs
Troubleshooting
”nodejs_compat” Required
If you see errors about Node.js APIs, ensure your wrangler.jsonc includes:
"compatibility_flags": ["nodejs_compat"]
Build Output Not Found
If you see:
Cloudflare build output not found. Run: xmcp build --cf
Ensure you’re running xmcp build --cf, not just xmcp build.
Worker Too Large
If your worker exceeds size limits:
- Review dependencies—remove unused packages
- Use Cloudflare bindings (KV, D1) instead of bundling data
- Enable code splitting if possible
Next Steps
Vercel Deployment
Deploy to Vercel instead
Self-Hosting
Run on your own infrastructure
Middleware
Add custom middleware
Configuration
Configure your xmcp server