Files
gh-jezweb-claude-skills-ski…/references/http-transport-fundamentals.md
2025-11-30 08:24:23 +08:00

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 /sse base 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:

  1. Client connects to: https://my-mcp.workers.dev/sse
  2. Worker receives request with pathname = "/sse"
  3. Check: pathname.startsWith("/sse") → TRUE
  4. MCP server handles request
  5. 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:

  1. Client connects to: https://my-mcp.workers.dev
  2. Worker receives request with pathname = "/"
  3. MCP server handles request at root
  4. 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:// (not http://)

Step 4: Restart Claude Desktop

Config changes require restart:

  1. Quit Claude Desktop completely
  2. Reopen Claude Desktop
  3. 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:

  • /sse and /mcp don'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:

  1. Base path in serveSSE() determines client URL

    • serveSSE("/sse") → Client uses https://worker.dev/sse
    • serveSSE("/") → Client uses https://worker.dev
  2. Always use pathname.startsWith() for matching

    • Matches sub-paths like /sse/tools/list
  3. Test with curl after deployment

    • curl https://worker.dev/sse should return server info
  4. Update client config after every deployment

    • Development: http://localhost:8788/sse
    • Production: https://worker.workers.dev/sse
  5. 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!