10 KiB
HTTP Transport Fundamentals
Deep dive on URL paths and routing for Cloudflare MCP servers
This document explains how URL path configuration works in MCP servers and why mismatches are the #1 cause of connection failures.
The Problem
Most common MCP server connection error:
❌ 404 Not Found
❌ Connection failed
❌ MCP Inspector shows "Failed to connect"
Root cause: Client URL doesn't match server base path configuration
How Base Paths Work
The Core Concept
When you call MyMCP.serveSSE("/sse"), you're telling the MCP server:
"All MCP endpoints are available under the
/ssebase path"
This means:
- Initial connection:
https://worker.dev/sse - List tools:
https://worker.dev/sse/tools/list - Call tool:
https://worker.dev/sse/tools/call - List resources:
https://worker.dev/sse/resources/list
The base path is prepended to ALL MCP-specific endpoints automatically.
Example 1: Serving at /sse
Server code:
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
const { pathname } = new URL(request.url);
if (pathname.startsWith("/sse")) {
return MyMCP.serveSSE("/sse").fetch(request, env, ctx);
// ↑ Base path is "/sse"
}
return new Response("Not Found", { status: 404 });
}
};
Client configuration:
{
"mcpServers": {
"my-mcp": {
"url": "https://my-mcp.workers.dev/sse"
// ↑ Must include /sse
}
}
}
What happens:
- Client connects to:
https://my-mcp.workers.dev/sse - Worker receives request with
pathname = "/sse" - Check:
pathname.startsWith("/sse")→ TRUE ✅ - MCP server handles request
- Tools available at:
/sse/tools/list/sse/tools/call- etc.
Example 2: Serving at / (root)
Server code:
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
return MyMCP.serveSSE("/").fetch(request, env, ctx);
// ↑ Base path is "/" (root)
}
};
Client configuration:
{
"mcpServers": {
"my-mcp": {
"url": "https://my-mcp.workers.dev"
// ↑ No /sse!
}
}
}
What happens:
- Client connects to:
https://my-mcp.workers.dev - Worker receives request with
pathname = "/" - MCP server handles request at root
- Tools available at:
/tools/list/tools/call- etc. (no /sse prefix)
Example 3: Custom base path
Server code:
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
const { pathname } = new URL(request.url);
if (pathname.startsWith("/api/mcp")) {
return MyMCP.serveSSE("/api/mcp").fetch(request, env, ctx);
// ↑ Base path is "/api/mcp"
}
return new Response("Not Found", { status: 404 });
}
};
Client configuration:
{
"mcpServers": {
"my-mcp": {
"url": "https://my-mcp.workers.dev/api/mcp"
// ↑ Must match base path exactly
}
}
}
Why pathname.startsWith() is Critical
❌ WRONG: Using exact match
if (pathname === "/sse") {
return MyMCP.serveSSE("/sse").fetch(request, env, ctx);
}
Problem: This ONLY matches /sse exactly
/sse→ ✅ Matches/sse/tools/list→ ❌ Doesn't match! 404!/sse/tools/call→ ❌ Doesn't match! 404!
✅ CORRECT: Using startsWith()
if (pathname.startsWith("/sse")) {
return MyMCP.serveSSE("/sse").fetch(request, env, ctx);
}
Result: This matches ALL paths under /sse
/sse→ ✅ Matches/sse/tools/list→ ✅ Matches/sse/tools/call→ ✅ Matches/sse/resources/list→ ✅ Matches
Request/Response Lifecycle
Let's trace a complete MCP request from start to finish.
Step 1: Client Connection
Client initiates connection:
POST https://my-mcp.workers.dev/sse
Step 2: Worker Receives Request
Worker fetch() handler is called:
const { pathname } = new URL(request.url);
console.log(pathname); // "/sse"
Step 3: Path Matching
Worker checks if path matches:
if (pathname.startsWith("/sse")) { // TRUE!
return MyMCP.serveSSE("/sse").fetch(request, env, ctx);
}
Step 4: MCP Server Handles Request
MCP Agent processes the request and returns available endpoints.
Step 5: Client Lists Tools
Client makes follow-up request:
POST https://my-mcp.workers.dev/sse/tools/list
Worker receives:
const { pathname } = new URL(request.url);
console.log(pathname); // "/sse/tools/list"
if (pathname.startsWith("/sse")) { // TRUE! Matches because of startsWith()
return MyMCP.serveSSE("/sse").fetch(request, env, ctx);
}
MCP server sees /tools/list (after stripping /sse base path) and returns tool list.
Common Mistakes and Fixes
Mistake 1: Missing Base Path in Client URL
Server:
MyMCP.serveSSE("/sse").fetch(...)
Client:
"url": "https://worker.dev" // ❌ Missing /sse
Result: 404 Not Found
Fix:
"url": "https://worker.dev/sse" // ✅ Include /sse
Mistake 2: Wrong Base Path
Server:
if (pathname.startsWith("/api")) {
return MyMCP.serveSSE("/api").fetch(...);
}
Client:
"url": "https://worker.dev/sse" // ❌ Server expects /api
Result: 404 Not Found
Fix:
"url": "https://worker.dev/api" // ✅ Match server path
Mistake 3: Localhost After Deployment
Development:
"url": "http://localhost:8788/sse" // ✅ Works in dev
After deployment (forgot to update):
"url": "http://localhost:8788/sse" // ❌ Worker is deployed!
Result: Connection refused / timeout
Fix:
"url": "https://my-mcp.YOUR_ACCOUNT.workers.dev/sse" // ✅ Deployed URL
Mistake 4: Using Exact Match Instead of startsWith()
Server:
if (pathname === "/sse") { // ❌ Only matches /sse exactly
return MyMCP.serveSSE("/sse").fetch(...);
}
Result:
/sse→ ✅ Works (initial connection)/sse/tools/list→ ❌ 404 (tool listing fails)
Fix:
if (pathname.startsWith("/sse")) { // ✅ Matches all sub-paths
return MyMCP.serveSSE("/sse").fetch(...);
}
Debugging Workflow
When MCP connection fails:
Step 1: Check Worker is Running
curl https://YOUR-WORKER.workers.dev/
Expected: Some response (even 404 is OK - means Worker is running) Problem: Timeout or connection refused → Worker not deployed
Step 2: Test MCP Endpoint
curl https://YOUR-WORKER.workers.dev/sse
Expected: JSON response with MCP server info Problem: 404 → Client URL doesn't match server base path
Step 3: Verify Client Config
Check ~/.config/claude/claude_desktop_config.json:
{
"mcpServers": {
"my-mcp": {
"url": "https://YOUR-WORKER.workers.dev/sse"
}
}
}
Verify:
- URL matches deployed Worker URL
- URL includes base path (e.g.,
/sse) - No typos in domain or path
- Using
https://(nothttp://)
Step 4: Restart Claude Desktop
Config changes require restart:
- Quit Claude Desktop completely
- Reopen Claude Desktop
- Check for MCP server in tools list
Multiple Transports
You can serve multiple transports at different paths:
Server:
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
const { pathname } = new URL(request.url);
// SSE at /sse
if (pathname.startsWith("/sse")) {
return MyMCP.serveSSE("/sse").fetch(request, env, ctx);
}
// HTTP at /mcp
if (pathname.startsWith("/mcp")) {
return MyMCP.serve("/mcp").fetch(request, env, ctx);
}
// Health check
if (pathname === "/" || pathname === "/health") {
return new Response(JSON.stringify({
transports: {
sse: "/sse",
http: "/mcp"
}
}));
}
return new Response("Not Found", { status: 404 });
}
};
Clients can choose:
- SSE:
"url": "https://worker.dev/sse" - HTTP:
"url": "https://worker.dev/mcp"
Why this works:
/sseand/mcpdon't conflict- Each transport has isolated namespace
- Health check available at root
/
Best Practices
1. Always Use startsWith() for Path Matching
// ✅ CORRECT
if (pathname.startsWith("/sse")) { ... }
// ❌ WRONG
if (pathname === "/sse") { ... }
2. Add Health Check Endpoint
if (pathname === "/" || pathname === "/health") {
return new Response(JSON.stringify({
name: "My MCP Server",
version: "1.0.0",
transports: {
sse: "/sse",
http: "/mcp"
},
status: "ok"
}));
}
Benefits:
- Quickly verify Worker is running
- Discover available transports
- Debug connection issues
3. Use Descriptive 404 Messages
return new Response(JSON.stringify({
error: "Not Found",
requestedPath: pathname,
availablePaths: ["/sse", "/mcp", "/health"],
hint: "Client URL must include base path (e.g., /sse)"
}), { status: 404 });
4. Test After Every Deployment
# Deploy
npx wrangler deploy
# Test immediately
curl https://YOUR-WORKER.workers.dev/sse
# Update client config
# Restart Claude Desktop
5. Document Base Path in Comments
// SSE transport at /sse
// Client URL MUST be: https://worker.dev/sse
if (pathname.startsWith("/sse")) {
return MyMCP.serveSSE("/sse").fetch(request, env, ctx);
}
Summary
Key Takeaways:
-
Base path in
serveSSE()determines client URLserveSSE("/sse")→ Client useshttps://worker.dev/sseserveSSE("/")→ Client useshttps://worker.dev
-
Always use
pathname.startsWith()for matching- Matches sub-paths like
/sse/tools/list
- Matches sub-paths like
-
Test with curl after deployment
curl https://worker.dev/sseshould return server info
-
Update client config after every deployment
- Development:
http://localhost:8788/sse - Production:
https://worker.workers.dev/sse
- Development:
-
Restart Claude Desktop after config changes
- Config only loaded at startup
Remember: The #1 MCP connection failure is URL path mismatch. Always verify your base paths match!