Executive Summary
Remote MCP is rapidly maturing for enterprise deployment as of November 2025. The protocol launched in late 2024 and underwent significant evolution through 2025, with major updates in March (Streamable HTTP) and June (OAuth 2.1 with resource indicators). Key players like Atlassian and Cloudflare have deployed production remote MCP servers, and the MCP Registry is transitioning to general availability.
Key Recommendation: Use Streamable HTTP transport + OAuth 2.1 with resource indicators. HTTP+SSE is deprecated.
Transport Protocols: Understanding the Evolution
Simple Explanation: HTTP+SSE vs Streamable HTTP
The Problem That Needed Solving:
MCP needs bidirectional communication—both the client (like Claude) and the server need to send messages to each other. The client sends requests like "list available tools" and the server needs to send back responses, but the server may also need to proactively send notifications or requests to the client.
HTTP+SSE Approach (Deprecated):
The original solution used two separate endpoints:
- Client → Server: HTTP POST endpoint (e.g.,
/messages) for client requests - Server → Client: Server-Sent Events (SSE) endpoint (e.g.,
/sse) for server messages
Client Server
| |
|--POST /messages (client req)----->|
|<------HTTP 200 (response)---------|
| |
|--GET /sse (open stream)---------->|
|<===SSE stream (server msgs)=======|
|<===SSE stream (more msgs)=========|
Why This Was Problematic:
- Complexity: Managing two separate connections with different lifecycles
- Session Management: Hard to keep state consistent across two endpoints
- No Resumability: If SSE connection drops, you lose all messages—server had to maintain long-lived connections
- Infrastructure Issues: Firewalls and proxies often block or timeout SSE streams
- Limited Flexibility: Basic servers forced to implement streaming even if not needed
Streamable HTTP Approach (Current Standard):
The new solution uses a single endpoint that intelligently handles both directions:
- Single Endpoint: All communication through one URL (e.g.,
/mcp) - Client → Server: Still uses HTTP POST, but server can choose response type:
- Simple JSON response for quick replies
- SSE stream if it needs to send multiple messages
- Server → Client (optional): If server needs to push messages, client opens GET request for SSE
- Session Management:
Mcp-Session-Idheader tracks state across all requests - Resumability:
Last-Event-IDheader allows recovering missed messages after disconnection
Client Server
| |
|--POST /mcp (initialize)---------->|
|<--HTTP 200 + Mcp-Session-Id------|
| |
|--POST /mcp (tool list)----------->|
|<--HTTP 200 JSON (simple resp)----|
| |
|--POST /mcp (complex query)------->|
|<==SSE stream (multi-part resp)====|
| |
[optional server-initiated messages]
|--GET /mcp (open notif stream)--->|
|<==SSE stream (server msgs)========|
| |
[if disconnected]
|--GET /mcp + Last-Event-ID-------->|
|<==SSE stream (resume from ID)=====|
Key Advantages:
- Simplified Architecture: One endpoint, one session, easier to reason about
- Flexible Response Model: Server picks JSON or SSE based on what's needed
- Better Infrastructure Compatibility: Standard HTTP patterns work better with proxies/load balancers
- Resumable Streams: Can recover from disconnections without losing messages
- Optional Streaming: Basic servers can skip SSE entirely and just return JSON
- Better Scaling: Stateless design allows horizontal scaling
In Practice:
Most MCP servers can be stateless and just return JSON responses. SSE streaming is only needed for:
- Long-running operations that send progress updates
- Servers that need to proactively notify clients
- Scenarios requiring bidirectional real-time communication
Bottom Line: Streamable HTTP is simpler, more flexible, and better suited for production infrastructure. If you're building a new MCP server, use Streamable HTTP exclusively—HTTP+SSE is deprecated and will likely be removed from client support in the near future.
Transport Protocols
Streamable HTTP (Current Standard - March 2025)
Status: ✅ Recommended for all new implementations
Architecture:
- Single HTTP endpoint supporting both POST and GET methods
- Client-to-server: HTTP POST with JSON-RPC messages
- Server-to-client: Optional SSE streams via HTTP GET
- Supports stateless operations with optional upgrade to SSE
Key Features:
- Session management via
Mcp-Session-Idheader - Resumable streams using SSE event IDs and
Last-Event-IDheader - Multiple simultaneous connections supported
- Message batching for efficiency
- Bidirectional communication via WebSockets (advanced scenarios)
Technical Details:
- Messages are UTF-8 encoded JSON-RPC
- POST requests must include Accept header:
application/json, text/event-stream - Server responds with 202 (notifications/responses) or SSE stream (requests)
- Session IDs should be globally unique and cryptographically secure (visible ASCII 0x21-0x7E)
- Single endpoint simplifies client architecture vs. HTTP+SSE
Message Flow:
Client → Server (POST):
- Single JSON-RPC message or batch
- Notifications/responses only → 202 Accepted
- Contains requests → SSE stream or JSON response
Server → Client (GET):
- Client opens SSE stream
- Server sends requests/notifications
- Optional: Resumability via Last-Event-ID header
Session Lifecycle:
- Server assigns
Mcp-Session-Idin InitializeResult response - Client includes session ID on all subsequent requests
- Server may terminate session (responds 404)
- Client initiates new session on 404
- Client can explicitly terminate via HTTP DELETE
Security Requirements:
- Validate Origin header (DNS rebinding prevention)
- Bind to localhost (127.0.0.1) for local deployment
- Implement proper authentication
- Use HTTPS exclusively for remote servers
HTTP+SSE (Deprecated)
Status: ❌ Avoid for new implementations
The original remote transport method from 2024-11-05 specification. Still supported but being phased out. Claude may drop support in coming months.
stdio (Local Only)
Status: Local deployments only
Subprocess-based transport where client launches MCP server and communicates via stdin/stdout. Not applicable for remote deployments but foundational to understanding the protocol.
OAuth Authentication Flow Deep Dive
Complete Authentication Sequence
Understanding the full OAuth 2.1 flow for MCP is critical for implementation. Here's the step-by-step sequence with all parties involved:
1. Initial Request & Discovery Phase
User/AI Agent MCP Client MCP Server Auth Server
| | | |
|--wants to use MCP--->| | |
| |--GET /mcp-------->| |
| |<-401 Unauthorized--| |
| | WWW-Authenticate: | |
| | resource_metadata | |
| | | |
| |--GET /.well-known/oauth-resource------>|
| |<-Protected Resource Metadata-----------|
| | (contains authz server issuer URL) |
| | | |
| |--GET /.well-known/oauth-authz-server-->|
| |<-Authorization Server Metadata---------|
| | (endpoints, supported features) |
What's Happening:
- Client tries to connect to MCP server without authentication
- Server rejects with 401 and points to its OAuth metadata
- Client discovers where the authorization server is
- Client learns what OAuth features are supported (PKCE, DCR, etc.)
2. Client Registration Phase
MCP Client Auth Server
| |
|--POST /register (Dynamic Client Reg)------->|
| { |
| "client_name": "Claude", |
| "redirect_uris": [ |
| "https://claude.ai/api/mcp/auth_callback" |
| ], |
| "grant_types": ["authorization_code"], |
| "token_endpoint_auth_method": "none" |
| } |
|<-201 Created + client credentials-----------|
| { |
| "client_id": "abc123...", |
| "client_secret": "xyz789..." (if confidential) |
| } |
What's Happening:
- Client registers itself with the authorization server
- Uses Dynamic Client Registration (DCR) if supported
- Gets back client_id (and optionally client_secret)
- If DCR not supported, user manually enters credentials
Registration Priority Order:
- Use pre-registered credentials (if available)
- Use Client ID Metadata Document URL (URL-based client ID)
- Use Dynamic Client Registration protocol
- Prompt user for manual credential entry
3. Authorization Request Phase (PKCE)
User Browser MCP Client Auth Server
| | |
| |--Generate PKCE-----|
| | code_verifier |
| | code_challenge |
| | |
|<-Open browser--------| |
| Authorization URL: |
| /authorize? |
| response_type=code |
| client_id=abc123 |
| redirect_uri=https://claude.ai/... |
| code_challenge=SHA256(verifier) |
| code_challenge_method=S256 |
| resource=https://mcp.example.com |
| scope=mcp:tool:* |
| state=random_state |
| |
|---------GET /authorize------------------->|
|<--------Login page------------------------|
| |
[User authenticates and consents] |
| |
|<--------Redirect to callback--------------|
| https://claude.ai/...? |
| code=auth_code_123 |
| state=random_state |
| |
|--Redirect to client---------------------->|
What's Happening:
- Client generates PKCE code_verifier (random string)
- Client creates code_challenge = BASE64URL(SHA256(code_verifier))
- Client opens browser to authorization server
- CRITICAL:
resourceparameter specifies the target MCP server - User logs in and approves permissions
- Auth server redirects back with authorization code
- State parameter prevents CSRF attacks
Key Security Points:
- PKCE is mandatory (S256 method required)
- Resource parameter must be included
- State parameter prevents CSRF
- Authorization code is single-use
4. Token Exchange Phase
MCP Client Auth Server
| |
|--POST /token---------------------------------|
| { |
| "grant_type": "authorization_code", |
| "code": "auth_code_123", |
| "redirect_uri": "https://claude.ai/...", |
| "client_id": "abc123", |
| "code_verifier": "original_verifier", |
| "resource": "https://mcp.example.com" |
| } |
| |
|<-200 OK + tokens-----------------------------|
| { |
| "access_token": "eyJhbGc...", |
| "token_type": "Bearer", |
| "expires_in": 3600, |
| "refresh_token": "refresh_xyz...", |
| "scope": "mcp:tool:read mcp:tool:execute" |
| } |
What's Happening:
- Client exchanges authorization code for access token
- Includes code_verifier to prove it initiated the flow (PKCE)
- CRITICAL:
resourceparameter included again - Auth server verifies code_challenge matches code_verifier
- Server issues access token bound to specific MCP server
Key Security Points:
- Code verifier proves PKCE challenge
- Resource parameter ensures token is scoped to correct server
- Tokens should be short-lived (refresh for long sessions)
5. Authenticated MCP Communication
MCP Client MCP Server Auth Server
| | |
|--POST /mcp-------->| |
| Authorization: | |
| Bearer eyJhbGc... | |
| Mcp-Session-Id | |
| (JSON-RPC msg) | |
| |--Verify token----->|
| | (JWT signature |
| | or introspect) |
| |<-Token valid-------|
| | + scopes |
|<-200 OK + data-----| |
| Mcp-Session-Id | |
| (response) | |
| | |
[subsequent requests use same token] |
| | |
|--POST /mcp-------->| |
| Authorization: |--Cached valid------|
| Bearer ... | (or re-verify) |
|<-200 OK------------| |
What's Happening:
- Client includes access token in Authorization header
- Server verifies token (JWT signature or introspection)
- Server checks token audience matches its resource URI
- Server validates scopes for requested operation
- Session ID tracks state across requests
6. Token Refresh Flow (When Token Expires)
MCP Client MCP Server Auth Server
| | |
|--POST /mcp-------->| |
| Bearer (expired) | |
|<-401 Unauthorized--| |
| WWW-Authenticate: | |
| error="invalid_ | |
| token" | |
| | |
|-------POST /token-------------------->|
| { |
| "grant_type": "refresh_token", |
| "refresh_token": "refresh_xyz...", |
| "resource": "https://mcp.example.com" |
| } |
|<------200 OK + new tokens-------------|
| { |
| "access_token": "new_eyJhbGc...", |
| "refresh_token": "new_refresh...", |
| "expires_in": 3600 |
| } |
| | |
|--POST /mcp-------->| |
| Bearer new_token | |
|<-200 OK------------| |
What's Happening:
- Access token expires (typically after 1 hour)
- Client uses refresh token to get new access token
- No user interaction required
- Resource parameter ensures new token for same MCP server
7. Insufficient Scope Handling (Step-Up Authorization)
MCP Client MCP Server Auth Server
| | |
|--POST /mcp-------->| |
| (tool requiring | |
| 'admin' scope) | |
|<-403 Forbidden-----| |
| WWW-Authenticate: | |
| error="insufficient_scope" |
| scope="mcp:admin" |
| | |
[Client initiates new authorization |
flow requesting additional scopes] |
| |
|-------Authorization flow-------------->|
| (same as Phase 3-4, but with |
| scope="mcp:admin mcp:tool:*") |
|<------New token with more scopes-------|
| | |
|--POST /mcp-------->| |
| Bearer new_token | |
| (with admin scope)| |
|<-200 OK------------| |
What's Happening:
- Client tries to call tool without required scope
- Server rejects with 403 and indicates needed scope
- Client re-authorizes requesting additional permissions
- User may see new consent screen for elevated permissions
- Client retries with new token
Two-Token Architecture (Cloudflare Pattern)
For enhanced security, many production MCP servers use a two-token pattern:
User/AI MCP Client MCP Server Upstream API
| | | |
|---Wants data----->| | |
| |--OAuth flow--->| |
| |<-MCP token-----| |
| | | |
| |--MCP request-->| |
| | Bearer MCP_ |--OAuth flow------->|
| | token |<-Upstream token----|
| | | (stored encrypted)|
| | | |
| | |--API call--------->|
| | | Bearer UPSTREAM_ |
| | | token |
| | |<-Data--------------|
| |<-Response------| |
|<--Data------------| | |
How It Works:
- MCP server acts as its own OAuth authorization server
- Issues "MCP tokens" to clients (Claude)
- Stores encrypted upstream provider tokens in secure storage (e.g., Workers KV, Redis)
- On each request: validates MCP token, retrieves upstream token, calls upstream API
Security Benefits:
- Compromised MCP token only grants MCP tool permissions
- Upstream credentials never exposed to client
- Enables fine-grained access control per user
- Implements allowlists for which tools each user can access
- Addresses OWASP "Excessive Agency" concern
Authentication Patterns
OAuth 2.1 with Resource Indicators (June 2025 Spec)
Status: ✅ Mandatory for enterprise deployments
The June 18, 2025 specification introduced mandatory security enhancements addressing OWASP concerns.
Resource Indicators (RFC 8707) - CRITICAL:
- Clients MUST include resource indicators in both authorization and token requests
- Must specify the canonical URI of the target MCP server
- Prevents token reuse across unrelated servers
- Addresses "Excessive Agency" security concern
- Makes token scoping explicit and predictable
- Enables fine-grained delegation and multi-tenant behavior
OAuth 2.1 Requirements:
- Authorization servers MUST implement OAuth 2.1
- MCP servers act as OAuth Resource Servers
- Must implement OAuth 2.0 Protected Resource Metadata (RFC 9728)
- Authorization servers must provide Authorization Server Metadata (RFC 8414)
- Support for both confidential and public clients
Specification Versions:
- 3/26 authorization specification
- 6/18 authorization specification (current)
Dynamic Client Registration (DCR)
Claude supports automated OAuth client registration:
- Auto re-registration: OAuth servers signal deleted clients via HTTP 401
invalid_clienterror - Custom credentials: Users can provide client ID/secret for servers without DCR support
- Token management: Full support for token expiry and refresh
- Simplifies client onboarding process
Two-Token Architecture (Cloudflare Pattern)
Status: 🟢 Best practice for production deployments
A security pattern that provides defense in depth:
How It Works:
- MCP server acts as OAuth provider
- Issues its own tokens to clients
- Stores encrypted upstream provider tokens in secure storage (e.g., Workers KV)
- Provides granular permission control per user
- Implements allowlists determining which tools are visible to authenticated users
Security Benefits:
- Compromised client tokens grant only MCP tool permissions, not full upstream access
- Addresses OWASP "Excessive Agency" concern
- Enables fine-grained access controls
- Token isolation layer
Claude-Specific Authentication Details
OAuth Configuration:
- Callback URL:
https://claude.ai/api/mcp/auth_callback(may migrate toclaude.com) - Client name: Claude
- Supports both 3/26 and 6/18 authorization specifications
- Token management: Full support for token expiry and refresh
IP-Based Access Control:
- Claude provides specific IP ranges for MCP connections
- Servers can implement IP allowlists for additional security
- See Anthropic documentation for current IP ranges
Building MCP Servers: Libraries, Tools & Developer Experience
SDK Landscape
Official SDKs
TypeScript SDK (github.com/modelcontextprotocol/typescript-sdk)
-
Status: ✅ Production-ready, fully featured
-
Best For: Node.js servers, edge deployments (Cloudflare Workers), full-stack apps
-
DX Highlights:
- Complete type safety with embedded JSON schemas
- Full autocompletion in VSCode/IDEs
- Implements complete MCP specification
- Excellent for I/O-bound workloads (non-blocking event loop)
- First-class support for Streamable HTTP transport (since v1.10.0, April 2025)
-
Example:
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; const server = new Server({ name: "example-server", version: "1.0.0" }, { capabilities: { tools: {}, resources: {}, prompts: {} } }); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [{ name: "echo", description: "Echo a message", inputSchema: {...} }] })); server.setRequestHandler(CallToolRequestSchema, async (request) => ({ content: [{ type: "text", text: `Echo: ${request.params.arguments.message}` }] }));
Python SDK (github.com/modelcontextprotocol/python-sdk)
-
Status: ✅ Production-ready
-
Best For: Data science, ML pipelines, Python-first teams
-
DX Highlights:
- Beginner-friendly, minimal boilerplate
- FastMCP framework for rapid development
- Decorator-based tool registration
- Great for CPU-bound workloads and Python ecosystem integration
-
Example:
from fastmcp import FastMCP mcp = FastMCP("example-server") @mcp.tool() def echo(message: str) -> str: """Echo a message back""" return f"Echo: {message}" @mcp.resource("config://settings") def get_settings(): """Return server settings""" return {"environment": "production"}
Go SDK (github.com/mark3labs/mcp-go)
- Status: 🟡 Community-maintained, growing
- Best For: High-performance servers, systems programming
- DX: Good for Go shops, leverages goroutines for concurrency
Alternative Frameworks
EasyMCP (TypeScript)
- Focuses on developer experience with minimal boilerplate
- Abstracts away protocol details for simple use cases
- Good for developers new to MCP
FastAPI + MCP (Python)
- Integrates MCP with FastAPI's async capabilities
- Good for teams already using FastAPI
Development Experience Comparison
| Aspect | TypeScript SDK | Python SDK | Go SDK | | ------------------ | --------------------------- | ------------------------- | -------------------- | | Type Safety | ⭐⭐⭐⭐⭐ Full | ⭐⭐⭐⭐ Good (with mypy) | ⭐⭐⭐⭐⭐ Full | | Learning Curve | ⭐⭐⭐ Moderate | ⭐⭐⭐⭐⭐ Easy | ⭐⭐⭐ Moderate | | Boilerplate | ⭐⭐⭐ Reasonable | ⭐⭐⭐⭐⭐ Minimal | ⭐⭐⭐ Reasonable | | Performance | ⭐⭐⭐⭐ Excellent (I/O) | ⭐⭐⭐ Good | ⭐⭐⭐⭐⭐ Excellent | | Deployment | ⭐⭐⭐⭐⭐ Anywhere | ⭐⭐⭐⭐ Most places | ⭐⭐⭐⭐ Containers | | Edge Support | ⭐⭐⭐⭐⭐ Yes (Cloudflare) | ⭐⭐ Limited | ⭐⭐⭐ Possible | | Ecosystem | ⭐⭐⭐⭐⭐ Vast (npm) | ⭐⭐⭐⭐⭐ Vast (PyPI) | ⭐⭐⭐⭐ Good |
Choosing Your Stack
Choose TypeScript if:
- Deploying to edge (Cloudflare Workers, Deno Deploy)
- Building full-stack app with JavaScript frontend
- Want best IDE support and type safety
- Server is primarily I/O-bound (API calls, database)
- Want widest deployment target support
Choose Python if:
- Team is Python-first
- Integrating with data science/ML tools
- Want fastest time to first prototype
- Server involves heavy compute (data processing, ML inference)
- Prefer decorator-based APIs
Choose Go if:
- Need maximum performance
- Building high-throughput server
- Systems programming background
- Want minimal runtime dependencies
- Deploying to Kubernetes/containers
Development Workflow
1. Scaffolding a New Server
TypeScript:
npx create-mcp-server my-server
cd my-server
npm install
Python:
pip install fastmcp
fastmcp init my-server
cd my-server
2. Local Development & Testing
Use MCP Inspector (official debugging tool):
npx @modelcontextprotocol/inspector node build/index.js
Opens web UI showing:
- Available tools, resources, prompts
- Request/response inspector
- OAuth flow testing
- Protocol compliance validation
3. Testing Integration with Claude
Local stdio testing:
// claude_desktop_config.json
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/path/to/my-server/build/index.js"]
}
}
}
Remote HTTP testing:
{
"mcpServers": {
"my-server": {
"url": "http://localhost:3000/mcp",
"transport": "streamable-http"
}
}
}
4. Deployment Options
| Platform | Best For | Complexity | OAuth Support | | ---------------------- | -------------------- | ---------- | ------------- | | Cloudflare Workers | Edge, global latency | Low | ✅ Built-in | | Vercel | TypeScript servers | Low | 🟡 DIY | | AWS Lambda | Event-driven | Medium | 🟡 DIY | | Railway/Fly.io | Stateful servers | Low | 🟡 DIY | | Self-hosted | Full control | High | 🟡 DIY |
Common Development Patterns
Pattern 1: Tool Registration
TypeScript (verbose but explicit):
server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case "search":
return await handleSearch(request.params.arguments);
case "create":
return await handleCreate(request.params.arguments);
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
});
Python (decorator-based):
@mcp.tool()
async def search(query: str, limit: int = 10):
"""Search for items"""
results = await db.search(query, limit)
return {"results": results}
@mcp.tool()
async def create(name: str, description: str):
"""Create a new item"""
item = await db.create(name, description)
return {"id": item.id}
Pattern 2: Resource Exposure
@mcp.resource("projects://{project_id}/tasks")
async def get_project_tasks(project_id: str):
"""List all tasks for a project"""
tasks = await db.get_tasks(project_id)
return {"tasks": [task.to_dict() for task in tasks]}
Pattern 3: Prompt Templates
@mcp.prompt()
def code_review_prompt():
"""Generate a code review prompt"""
return """You are a senior software engineer reviewing code.
Focus on: security, performance, maintainability.
Provide specific suggestions with code examples."""
Debugging Tips
Enable MCP Debug Logging:
TypeScript:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
const server = new Server({...}, {
capabilities: {...},
debug: true // Logs all protocol messages
});
Python:
import logging
logging.basicConfig(level=logging.DEBUG)
Common Issues:
-
Tool not appearing in Claude:
- Check
ListToolsRequestSchemahandler returns valid JSON Schema - Verify
inputSchemamatches expected format - Use MCP Inspector to validate tool registration
- Check
-
Authentication failures:
- Verify
resourceparameter matches server URI exactly - Check authorization server metadata URLs are accessible
- Confirm PKCE implementation uses S256 method
- Verify
-
Session management errors:
- Ensure
Mcp-Session-Idheader is returned and persisted - Verify session IDs are cryptographically secure
- Check session timeout configuration
- Ensure
Code Generation Options
Stainless API (TypeScript only):
- Generates fully-typed MCP servers from API specs
- Embeds JSON schemas for complete type safety
- Best for wrapping existing REST APIs as MCP servers
- Commercial offering with free tier
Manual Implementation:
- Full control over server behavior
- Best for custom business logic
- Most flexible but more boilerplate
Testing Strategies
Unit Testing:
import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js";
describe("Echo Tool", () => {
it("should echo message", async () => {
const request: CallToolRequest = {
method: "tools/call",
params: {
name: "echo",
arguments: { message: "test" },
},
};
const response = await server.handleRequest(request);
expect(response.content[0].text).toBe("Echo: test");
});
});
Integration Testing:
- Use MCP Inspector programmatically
- Test full OAuth flows in staging
- Validate against MCP spec compliance tests
Reference Implementations & Best Practices
Official Reference Servers
The Model Context Protocol team maintains a collection of reference implementations showcasing best practices: github.com/modelcontextprotocol/servers
Key Reference Servers:
1. Everything Server (Reference/Test)
- Purpose: Comprehensive protocol demonstration
- Features: All MCP features (tools, resources, prompts)
- Best For: Learning complete MCP implementation
- Key Lessons:
- How to structure multi-feature servers
- Proper error handling patterns
- Session management examples
2. Filesystem Server
- Purpose: Secure file operations
- Features: Read/write with permission controls
- Best For: Learning security patterns
- Key Lessons:
- Input validation and sanitization
- Path traversal prevention
- Permission model implementation
- Safe file I/O with bounded resources
3. Git Server
- Purpose: Repository operations
- Features: Read, search, manipulate git repos
- Best For: Learning complex tool orchestration
- Key Lessons:
- Multi-step operation patterns
- Error recovery and rollback
- Streaming large outputs
- Working with external processes
4. Memory Server
- Purpose: Knowledge graph-based persistent memory
- Features: Entity storage, relationship management
- Best For: Learning stateful MCP patterns
- Key Lessons:
- Persistent state management
- Graph data structures in MCP
- Complex query patterns
- Memory/context tradeoffs
5. Fetch Server
- Purpose: Web content retrieval
- Features: HTTP client with safety controls
- Best For: Learning API integration patterns
- Key Lessons:
- Rate limiting
- Content sanitization
- Error handling for external APIs
- Timeout management
Production Reference Implementations
GitHub MCP Server (github.com/github/github-mcp-server)
- Status: ✅ Official production implementation
- Stack: TypeScript
- Features:
- Repository operations
- Issue/PR management
- Code search
- GraphQL API integration
- Best Practices Demonstrated:
- OAuth flow with GitHub
- Pagination handling
- Rate limit respect
- Comprehensive error messages
- API versioning strategy
Atlassian MCP Server
- Purpose: Jira + Confluence integration
- Features:
- Ticket operations
- Documentation search
- Cross-product workflows
- Best Practices Demonstrated:
- Multi-tenant architecture
- Enterprise authentication patterns
- Audit logging
- Performance at scale
Cloudflare Remote MCP (blog.cloudflare.com/remote-model-context-protocol-servers-mcp)
- Purpose: Turnkey MCP hosting platform
- Features:
- Built-in OAuth + DCR
- Auto-scaling
- Global edge deployment
- Two-token security pattern
- Best Practices Demonstrated:
- Edge-optimized architecture
- Token encryption at rest
- Fine-grained access controls
- Monitoring and observability
Community Examples
Microsoft MCP for Beginners (github.com/microsoft/mcp-for-beginners)
- Purpose: Educational curriculum
- Languages: .NET, Java, TypeScript, JavaScript, Rust, Python
- Best For: Cross-language comparison
- Covers:
- Session management
- Service orchestration
- Security fundamentals
- Production deployment patterns
Learning Path
1. Start Here: Simple Echo Server
# Clone reference servers
git clone https://github.com/modelcontextprotocol/servers
cd servers/src/everything
# Study the implementation
# Run with Inspector
npx @modelcontextprotocol/inspector npm start
2. Next: Build Domain-Specific Server
- Pick a simple API (weather, dictionary, calculator)
- Implement 2-3 tools
- Add proper error handling
- Test with MCP Inspector
3. Then: Add Authentication
- Implement OAuth 2.1 flow
- Add resource indicators
- Test token refresh
- Implement permission scoping
4. Finally: Production Deployment
- Deploy to chosen platform
- Add monitoring/logging
- Register in MCP Directory
- Document for end users
Best Practice Patterns from References
Error Handling:
try {
const result = await externalAPI.call();
return { content: [{ type: "text", text: JSON.stringify(result) }] };
} catch (error) {
if (error.code === "RATE_LIMIT") {
throw new McpError(
ErrorCode.InternalError,
`Rate limited. Retry after ${error.retryAfter}s`,
);
}
throw new McpError(
ErrorCode.InternalError,
`Failed to fetch data: ${error.message}`,
);
}
Input Validation:
const schema = {
type: "object",
properties: {
path: { type: "string", pattern: "^[a-zA-Z0-9/_-]+$" },
limit: { type: "number", minimum: 1, maximum: 100 },
},
required: ["path"],
};
// SDK validates automatically, but double-check for security
if (!isValidPath(args.path)) {
throw new McpError(ErrorCode.InvalidParams, "Invalid path format");
}
Resource Management:
// Always limit output size
const MAX_OUTPUT_SIZE = 1_000_000; // 1MB
const content = await fetchLargeData();
if (content.length > MAX_OUTPUT_SIZE) {
return {
content: [
{
type: "text",
text: content.slice(0, MAX_OUTPUT_SIZE) + "\n[Truncated...]",
},
],
};
}
GraphQL as MCP Backend: Deep Analysis
The Opportunity
GraphQL is a natural fit for MCP servers because:
- Schema Introspection: GraphQL's type system can be exposed to AI agents dynamically
- Precise Data Fetching: Agents can request exactly what they need
- API Orchestration: Single query can aggregate multiple data sources
- Strongly Typed: Schema enforcement prevents invalid requests
- Existing Infrastructure: Many orgs already have GraphQL APIs
The Challenge: Context Bloat
The Fundamental Problem:
Both common approaches for exposing GraphQL to MCP suffer from the same issue—context window explosion:
-
Approach 1: Expose Full Schema
- Pro: Agent has complete API knowledge
- Con: Schema size proportional to entire API surface area
- Result: Massive context consumption for large APIs
-
Approach 2: Pre-define Operations
- Pro: Controlled, specific queries
- Con: Number of operations scales with use cases
- Result: Hundreds of "tools" for comprehensive coverage
Real-World Impact:
A typical production GraphQL API might have:
- 200+ types
- 500+ fields
- 1000+ possible queries
Serializing this schema → 100K+ tokens → leaves little room for actual task context.
Solution Architectures
1. Grafbase Three-Tool Pattern (Recommended)
Architecture:
Instead of exposing the schema or individual queries, expose three meta-tools:
{
tools: [
{
name: "graphql_search",
description: "Search for types, fields, or operations in the schema",
inputSchema: {
query: "string", // e.g., "user email"
filters: ["type" | "field" | "mutation"],
},
},
{
name: "graphql_introspect",
description:
"Get detailed schema information for a specific type or field",
inputSchema: {
path: "string", // e.g., "Query.user" or "User.posts"
},
},
{
name: "graphql_execute",
description: "Execute a GraphQL query or mutation",
inputSchema: {
query: "string",
variables: "object",
},
},
];
}
Agent Workflow:
User: "Show me the latest posts from user with email john@example.com"
Step 1: Agent calls graphql_search
→ Input: { query: "user posts", filters: ["field"] }
→ Output: {
results: [
"Query.user(email: String!): User",
"User.posts(limit: Int): [Post]",
"Post.title: String"
]
}
Step 2: Agent calls graphql_introspect
→ Input: { path: "Query.user" }
→ Output: {
field: "user",
type: "User",
args: [{ name: "email", type: "String!", required: true }],
returns: {
id: "ID!",
email: "String!",
posts: "[Post]"
}
}
Step 3: Agent constructs and executes query
→ Input: {
query: `
query GetUserPosts($email: String!) {
user(email: $email) {
posts(limit: 5) {
title
createdAt
}
}
}
`,
variables: { email: "john@example.com" }
}
→ Output: { data: { user: { posts: [...] } } }
Benefits:
- Constant Context: Always 3 tools, regardless of API size
- Progressive Discovery: Agent explores schema as needed
- Flexibility: Agent constructs custom queries
- Scalability: Works with any GraphQL API size
Implementation:
server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case "graphql_search":
const { query, filters } = request.params.arguments;
const results = searchSchema(graphqlSchema, query, filters);
return {
content: [
{
type: "text",
text: JSON.stringify({ results }, null, 2),
},
],
};
case "graphql_introspect":
const { path } = request.params.arguments;
const details = introspectPath(graphqlSchema, path);
return {
content: [
{
type: "text",
text: JSON.stringify(details, null, 2),
},
],
};
case "graphql_execute":
const { query, variables } = request.params.arguments;
const result = await graphqlClient.request(query, variables);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
});
2. Apollo Contracts + Persisted Queries Pattern
Architecture:
Combine two features:
- Contracts: Expose subset of schema to different audiences
- Persisted Queries: Pre-approved queries referenced by ID
{
tools: [
{
name: "execute_persisted_query",
description: "Execute a pre-approved query by ID",
inputSchema: {
queryId: "string", // e.g., "getUserPosts"
variables: "object",
},
},
{
name: "introspect_contract",
description: "Explore available queries in the AI contract",
inputSchema: {
focus: "string", // e.g., "user", "posts"
},
},
];
}
Benefits:
- Security: Only allow pre-vetted queries
- Performance: Queries pre-compiled and optimized
- Controlled Surface Area: Explicit about what AI can access
- Cost Control: Prevents expensive arbitrary queries
Drawbacks:
- Less Flexible: Agent can't construct novel queries
- Maintenance: Must add new persisted queries for new use cases
- Brittle: Schema changes may break persisted queries
Best For:
- Security-sensitive environments
- Well-defined, stable use cases
- Performance-critical applications
3. Progressive Introspection Pattern
Architecture:
Allow agent to navigate schema tree progressively:
{
tools: [
{
name: "graphql_introspect_from",
description: "Introspect schema starting from any node",
inputSchema: {
startPath: "string", // e.g., "Query", "Query.user", "User.posts"
depth: "number", // how deep to traverse (1-3)
},
},
{
name: "graphql_execute",
// Same as before
},
];
}
Agent Workflow:
Step 1: Introspect root
→ introspect_from("Query", depth=1)
→ Returns: { user: "...", posts: "...", ... }
Step 2: Drill into relevant path
→ introspect_from("Query.user", depth=2)
→ Returns: { args: [...], returns: { User: { fields: [...] } } }
Step 3: Execute
→ graphql_execute(query, vars)
Benefits:
- Context-Aware: Agent only loads relevant schema parts
- Flexible: Still allows custom queries
- Depth Control: Prevents overwhelming context
Security Considerations
1. Introspection in Production
GraphQL introspection is typically disabled in production for security. With MCP:
✅ DO:
- Create separate "AI contract" with limited schema
- Use authentication to control who can introspect
- Audit introspection requests
- Rate limit introspection calls
❌ DON'T:
- Expose full production schema to AI
- Allow unauthenticated introspection
- Skip query depth/complexity limits
2. Mutation Safety
Mutations are high-risk for AI agents:
// SAFE: Disable mutations by default
const ALLOWED_OPERATIONS = ['query']; // Not 'mutation'
if (operationType === 'mutation' && !user.hasPermission('ai:mutate')) {
throw new McpError(
ErrorCode.InvalidRequest,
'Mutations not allowed for AI agents'
);
}
// If mutations needed: require explicit approval
{
name: "graphql_execute_mutation",
description: "Execute a mutation (requires user confirmation)",
inputSchema: {
mutation: "string",
variables: "object",
confirmationToken: "string" // User must explicitly approve
}
}
3. Query Complexity Limits
Prevent expensive queries:
import { createComplexityLimitRule } from "graphql-validation-complexity";
const complexityLimit = createComplexityLimitRule(1000, {
onCost: (cost) => console.log("Query cost:", cost),
});
// Reject queries exceeding budget
if (queryCost > MAX_COST) {
throw new McpError(
ErrorCode.InvalidRequest,
`Query too complex (cost: ${queryCost}, max: ${MAX_COST})`,
);
}
Performance Optimization
1. Schema Caching
// Cache introspection results
const schemaCache = new LRUCache({
max: 100,
ttl: 1000 * 60 * 60, // 1 hour
});
function introspectPath(path: string) {
const cached = schemaCache.get(path);
if (cached) return cached;
const result = computeIntrospection(path);
schemaCache.set(path, result);
return result;
}
2. Batching & DataLoader
Use DataLoader pattern to batch GraphQL calls:
import DataLoader from "dataloader";
const queryLoader = new DataLoader(async (queries) => {
// Batch multiple agent queries into single GraphQL request
return await Promise.all(queries.map((q) => graphql.execute(q)));
});
3. Streaming for Large Results
For large datasets, use SSE streaming:
async function* streamQueryResults(query: string) {
const result = await graphqlClient.request(query);
// Stream results in chunks
for (const chunk of chunkArray(result.data, 10)) {
yield {
type: "text",
text: JSON.stringify(chunk),
};
}
}
Pros & Cons Summary
✅ Pros of GraphQL Backend:
- Self-Documenting: Schema introspection provides automatic API docs
- Precise Fetching: Agent requests exactly what it needs
- Type Safety: Strong types prevent malformed requests
- Aggregation: Single query across multiple data sources
- Existing Infrastructure: Leverage existing GraphQL investments
- Flexibility: Agent can construct novel queries for unexpected use cases
❌ Cons of GraphQL Backend:
- Context Bloat Risk: Full schema introspection explodes token usage
- Complexity: Requires sophisticated implementation (3-tool pattern)
- Security Risks: Introspection + mutations = potential for misuse
- Performance: Arbitrary queries can be expensive
- Learning Curve: Agent must learn GraphQL syntax
- Brittle Queries: Schema changes may break agent-constructed queries
Recommendation for QXO
If QXO has existing GraphQL API:
✅ Use GraphQL with Grafbase 3-tool pattern:
- Implement
search,introspect,executetools - Start with queries only (disable mutations)
- Create AI-specific contract with limited schema
- Add query complexity limits
- Monitor and iterate based on usage
If building new MCP server from scratch:
🤔 Consider REST/direct tool approach:
- Less complexity
- More explicit control
- Easier to reason about context usage
- But less flexible than GraphQL
Hybrid Approach:
Expose both:
- High-level domain-specific tools for common workflows
graphql_executetool for advanced/unexpected queries
{
tools: [
// Domain-specific (preferred for common tasks)
{ name: "get_project_tasks", ... },
{ name: "create_task", ... },
{ name: "update_task_status", ... },
// GraphQL escape hatch (for flexibility)
{ name: "graphql_search", ... },
{ name: "graphql_introspect", ... },
{ name: "graphql_execute", ... }
]
}
This gives you:
- Low context usage for common operations
- Flexibility for novel queries
- Clear audit trail of what agents are doing
Implementation Best Practices
Transport Layer
✅ DO:
- Use Streamable HTTP as primary transport
- Implement session management with cryptographically secure session IDs
- Support SSE streams for bidirectional communication (optional but recommended)
- Implement resumable streams using event IDs
- Support message batching for efficiency
- Use HTTPS exclusively
❌ DON'T:
- Use HTTP+SSE for new implementations (deprecated)
- Skip Origin header validation
- Use HTTP for remote servers (security risk)
- Assume persistent connections (design for statelessness)
Authentication & Authorization
✅ DO:
- Implement OAuth 2.1 with resource indicators (mandatory)
- Use DCR to simplify client onboarding
- Implement token refresh to avoid re-authentication
- Consider two-token architecture for enhanced security
- Provide fine-grained permission controls per user
- Encrypt tokens at rest
- Implement proper CORS policies
❌ DON'T:
- Skip resource indicators in token requests
- Allow token reuse across different MCP servers
- Store unencrypted upstream tokens
- Grant excessive permissions
Security Checklist
- [ ] Validate Origin headers (DNS rebinding prevention)
- [ ] Implement HTTPS exclusively
- [ ] Use OAuth 2.1 with resource indicators
- [ ] Implement DCR for client registration
- [ ] Support token refresh
- [ ] Encrypt tokens at rest
- [ ] Implement IP allowlists if needed
- [ ] Provide OAuth 2.0 Protected Resource Metadata (RFC 9728)
- [ ] Provide Authorization Server Metadata (RFC 8414)
- [ ] Implement fine-grained permission controls
- [ ] Consider two-token architecture
Testing & Validation
MCP Inspector Tool:
Use for validating:
- Auth flow completion
- Protocol implementation correctness
- Tool/prompt/resource exposure
- Session management
- Error handling
Platform Support
Claude Support
- Plans: Pro, Max, Team, Enterprise
- Platforms: Web, Desktop, iOS, Android
- Custom Connectors: Available via Remote MCP
Major Implementations
- Atlassian: Jira tickets and Confluence documentation access
- Cloudflare: Remote MCP hosting with built-in OAuth, autoscaling
- Various community servers in MCP Registry
Current State & Roadmap
Specification Version
- Current: 2025-03-26
- Next Release: 2025-11-25 (RC available 2025-11-11)
Active Development Areas
Statelessness/Scalability:
- Priority for enterprise-scale deployments
- Addressing rough edges in server startup and session handling
- Improving production deployment experience
Server Identity:
.well-knownURLs for server discovery- Enables clients to discover capabilities without connecting first
MCP Registry:
- Transitioning from preview (September 2025) to general availability
- Community-driven platform for discovering and sharing MCP servers
- Available at modelcontextprotocol.io
Reference Implementation:
- Planned validation showcasing authentication patterns
- Remote deployment best practices
Known Deprecations
- HTTP+SSE transport (migrate to Streamable HTTP)
- SSE may be deprecated in coming months (use Streamable HTTP)
Technical References
Specifications
- MCP Specification 2025-03-26: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports
- OAuth 2.1: RFC standards
- Resource Indicators: RFC 8707
- Protected Resource Metadata: RFC 9728
- Authorization Server Metadata: RFC 8414
Official Documentation
- Claude Remote MCP Docs: https://platform.claude.com/docs/en/agents-and-tools/remote-mcp-servers
- Building Custom Integrations: https://support.claude.com/en/articles/11503834
- Best Practices: https://support.anthropic.com/en/articles/11596040
- MCP Directory FAQ: https://support.anthropic.com/en/articles/11596036
Implementation Resources
- Cloudflare Remote MCP: https://blog.cloudflare.com/remote-model-context-protocol-servers-mcp/
- GitHub MCP Servers: https://github.com/modelcontextprotocol/servers
- MCP TypeScript SDK: https://github.com/modelcontextprotocol/typescript-sdk
- MCP Python SDK: https://github.com/modelcontextprotocol/python-sdk
Community Resources
- MCP Registry: https://modelcontextprotocol.io
- Anthropic Course: Introduction to Model Context Protocol (Skilljar)
Key Takeaways for QXO
-
Transport: Streamable HTTP is production-ready and future-proof. Skip HTTP+SSE entirely.
-
Authentication: OAuth 2.1 with resource indicators is non-negotiable for security. This is where implementation complexity lives.
-
Security: Two-token architecture pattern provides best defense-in-depth. Consider from day one.
-
Hosting: Cloudflare and similar platforms offer turnkey remote MCP hosting with OAuth built-in.
-
Testing: Use MCP Inspector throughout development to validate protocol compliance.
-
Timeline: Protocol is stable enough for production use. Next spec release in November 2025 is iterative, not disruptive.
-
Ecosystem: Strong momentum with Atlassian, Cloudflare, and growing registry. Good timing for QXO to enter space.
Questions for QXO Meeting
- What QXO functionality would be exposed via MCP? (tools, resources, prompts)
- Expected user base? (internal only vs. external customers)
- Authentication requirements? (existing OAuth provider or need to build?)
- Hosting preferences? (self-hosted vs. platform like Cloudflare)
- Security/compliance requirements? (SOC2, GDPR, etc.)
- Timeline for MVP?
- Integration targets? (Claude only or broader MCP ecosystem?)
Next Steps
- Review QXO's existing API architecture and OAuth infrastructure
- Prototype simple MCP server using Streamable HTTP + OAuth
- Test with MCP Inspector
- Deploy to staging environment
- Register in MCP Directory (optional but recommended for discovery)