Initial commit
This commit is contained in:
506
references/http-transport-fundamentals.md
Normal file
506
references/http-transport-fundamentals.md
Normal file
@@ -0,0 +1,506 @@
|
||||
# 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:**
|
||||
```typescript
|
||||
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:**
|
||||
```json
|
||||
{
|
||||
"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:**
|
||||
```typescript
|
||||
export default {
|
||||
fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
||||
return MyMCP.serveSSE("/").fetch(request, env, ctx);
|
||||
// ↑ Base path is "/" (root)
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Client configuration:**
|
||||
```json
|
||||
{
|
||||
"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:**
|
||||
```typescript
|
||||
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:**
|
||||
```json
|
||||
{
|
||||
"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**
|
||||
```typescript
|
||||
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()`**
|
||||
```typescript
|
||||
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:
|
||||
```typescript
|
||||
const { pathname } = new URL(request.url);
|
||||
console.log(pathname); // "/sse"
|
||||
```
|
||||
|
||||
### Step 3: Path Matching
|
||||
|
||||
Worker checks if path matches:
|
||||
```typescript
|
||||
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:
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
MyMCP.serveSSE("/sse").fetch(...)
|
||||
```
|
||||
|
||||
**Client:**
|
||||
```json
|
||||
"url": "https://worker.dev" // ❌ Missing /sse
|
||||
```
|
||||
|
||||
**Result:** 404 Not Found
|
||||
|
||||
**Fix:**
|
||||
```json
|
||||
"url": "https://worker.dev/sse" // ✅ Include /sse
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Mistake 2: Wrong Base Path
|
||||
|
||||
**Server:**
|
||||
```typescript
|
||||
if (pathname.startsWith("/api")) {
|
||||
return MyMCP.serveSSE("/api").fetch(...);
|
||||
}
|
||||
```
|
||||
|
||||
**Client:**
|
||||
```json
|
||||
"url": "https://worker.dev/sse" // ❌ Server expects /api
|
||||
```
|
||||
|
||||
**Result:** 404 Not Found
|
||||
|
||||
**Fix:**
|
||||
```json
|
||||
"url": "https://worker.dev/api" // ✅ Match server path
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Mistake 3: Localhost After Deployment
|
||||
|
||||
**Development:**
|
||||
```json
|
||||
"url": "http://localhost:8788/sse" // ✅ Works in dev
|
||||
```
|
||||
|
||||
**After deployment** (forgot to update):
|
||||
```json
|
||||
"url": "http://localhost:8788/sse" // ❌ Worker is deployed!
|
||||
```
|
||||
|
||||
**Result:** Connection refused / timeout
|
||||
|
||||
**Fix:**
|
||||
```json
|
||||
"url": "https://my-mcp.YOUR_ACCOUNT.workers.dev/sse" // ✅ Deployed URL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Mistake 4: Using Exact Match Instead of `startsWith()`
|
||||
|
||||
**Server:**
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
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`:
|
||||
```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:**
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT
|
||||
if (pathname.startsWith("/sse")) { ... }
|
||||
|
||||
// ❌ WRONG
|
||||
if (pathname === "/sse") { ... }
|
||||
```
|
||||
|
||||
### 2. Add Health Check Endpoint
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```typescript
|
||||
// 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!
|
||||
Reference in New Issue
Block a user