Skip to main content
Glama
slide.md26.7 kB
--- theme: default class: text-center highlighter: shiki lineNumbers: false colorSchema: auto info: | ## From the Edge and Back Turn Any REST API into an MCP Server drawings: persist: false transition: fade title: From the Edge and Back - REST API to MCP Server mdc: true --- # From the Edge and Back ## Turn Any REST API into an MCP Server Deploy AI-ready APIs to Cloudflare Workers <div class="pt-12"> <span @click="$slidev.nav.next" class="px-2 py-1 rounded cursor-pointer" hover="bg-white bg-opacity-10"> Press Space for next page <carbon:arrow-right class="inline"/> </span> </div> --- layout: center --- <img src="/aidd.png" class="mx-auto max-h-[500px]" /> --- layout: two-cols --- # What We're Building A real-world example: Products API + MCP Server ::right:: ## What You'll Learn - 🔧 **MCP Protocol** - Model Context Protocol basics - 🌐 **REST Translation** - Converting REST to MCP tools - ⚡ **Edge Deployment** - Cloudflare Workers at scale - 🔄 **Dual Interface** - One API, two protocols - 📦 **Real Code** - Actual working implementation <br> ### Code-Heavy Session Working code you can deploy today --- layout: section --- # The Problem Why wrap REST APIs with MCP? --- # AI Agents Need Context <div class="grid grid-cols-2 gap-4"> <div> ## Traditional Approach ```typescript // AI has to know your API const response = await fetch( 'https://api.example.com/products?category=Electronics' ); // AI has to parse response const data = await response.json(); // AI has to handle errors if (!response.ok) { // what now? } ``` - Manual integration for each API - No standardization - Limited discoverability - Error handling varies </div> <div> ## MCP Approach ```typescript // AI discovers available tools const tools = await mcp.listTools(); // Returns: list_products, get_product, etc. // AI calls tool with schema validation const result = await mcp.callTool( 'list_products', { category: 'Electronics' } ); // Structured error handling built-in ``` - Standard protocol - Self-describing - Type-safe - Consistent errors </div> </div> --- layout: section --- # Our Example: Products API Let's see what we're building --- # The REST API A simple products CRUD API ```typescript // src/api.ts - In-memory product store interface Product { id: string; name: string; price: number; description: string; category: string; inStock: boolean; } const products: Product[] = [ { id: '1', name: 'Wireless Headphones', price: 99.99, description: 'High-quality wireless headphones with noise cancellation', category: 'Electronics', inStock: true, }, // ... more products ]; ``` --- # REST Endpoints Standard CRUD operations ```typescript // GET /api/products - List all (with filtering) apiRoutes.get('/products', (c) => { const category = c.req.query('category'); const inStock = c.req.query('inStock'); // ... filter logic }); // GET /api/products/:id - Get single product apiRoutes.get('/products/:id', (c) => { /* ... */ }); // POST /api/products - Create new product apiRoutes.post('/products', async (c) => { /* ... */ }); // PUT /api/products/:id - Update product apiRoutes.put('/products/:id', async (c) => { /* ... */ }); // DELETE /api/products/:id - Delete product apiRoutes.delete('/products/:id', (c) => { /* ... */ }); ``` --- # Testing the REST API ```bash # List all products curl http://localhost:8787/api/products # Filter by category curl http://localhost:8787/api/products?category=Electronics # Get single product curl http://localhost:8787/api/products/1 # Create a product curl -X POST http://localhost:8787/api/products \ -H "Content-Type: application/json" \ -d '{ "name": "Laptop", "price": 999, "description": "Powerful laptop", "category": "Electronics" }' ``` --- layout: section --- # Adding MCP Wrapping REST with Model Context Protocol --- # What is MCP? Model Context Protocol - A standard for AI-API communication ```mermaid graph LR A[AI Agent<br/>Claude] -->|MCP Protocol| B[MCP Server<br/>Your Worker] B -->|Internal Call| C[REST API<br/>Products] C -->|Response| B B -->|Structured Result| A style B fill:#f96,stroke:#333,stroke-width:4px style A fill:#9cf,stroke:#333,stroke-width:2px style C fill:#fc9,stroke:#333,stroke-width:2px ``` **Key Benefit**: AI agents can discover and use your API without custom integration --- # MCP Tool Definitions Each REST endpoint becomes a tool ```typescript {all|3-6|7-20|all} // src/mcp.ts const tools = [ { name: 'list_products', description: 'List all products with optional filtering by category or stock status', inputSchema: { type: 'object', properties: { category: { type: 'string', description: 'Filter by product category (e.g., Electronics, Home, Sports)', }, inStock: { type: 'boolean', description: 'Filter by stock availability', }, }, }, }, // ... more tools ]; ``` --- # More MCP Tools ```typescript { name: 'get_product', description: 'Get details of a specific product by ID', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'The product ID' } }, required: ['id'] } }, { name: 'create_product', description: 'Create a new product', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Product name' }, price: { type: 'number', description: 'Product price' }, description: { type: 'string' }, category: { type: 'string' }, inStock: { type: 'boolean' } }, required: ['name', 'price', 'description', 'category'] } } ``` --- # Executing MCP Tools Tools call the REST API internally ```typescript {all|1-5|7-17|19-25|all} async function executeTool(name: string, args: any, baseUrl: string) { let response: Response; switch (name) { case 'list_products': { const params = new URLSearchParams(); if (args?.category) params.append('category', String(args.category)); if (args?.inStock !== undefined) params.append('inStock', String(args.inStock)); const url = `${baseUrl}/api/products${ params.toString() ? '?' + params.toString() : '' }`; response = await fetch(url); break; } case 'get_product': { response = await fetch(`${baseUrl}/api/products/${args?.id}`); break; } case 'create_product': { response = await fetch(`${baseUrl}/api/products`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(args), }); break; } // ... more cases } return await response.json(); } ``` --- # MCP Request Handler Handling MCP protocol requests ```typescript {all|1-8|10-15|17-30|all} export async function mcpHandler(c: Context) { const body = await c.req.json(); const requestId = body.id !== undefined ? body.id : 1; const url = new URL(c.req.url); const baseUrl = `${url.protocol}//${url.host}`; // Handle tools/list request if (body.method === 'tools/list') { return c.json({ jsonrpc: '2.0', id: requestId, result: { tools } }); } // Handle tools/call request if (body.method === 'tools/call') { const { name, arguments: args } = body.params; try { const result = await executeTool(name, args, baseUrl); return c.json({ jsonrpc: '2.0', id: requestId, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } }); } catch (error) { return c.json({ jsonrpc: '2.0', id: requestId, error: { code: -32603, message: error.message } }); } } // Handle initialize (MCP handshake) if (body.method === 'initialize') { const response = { jsonrpc: '2.0', id: requestId, result: { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'cf-mcp-server', version: '1.0.0' } } }; return c.json(response, 200, { 'Mcp-Session-Id': crypto.randomUUID() }); } } ``` --- # Main Application Bringing it all together ```typescript {all|1-4|6-8|10-12|14-16|18-29|all} // src/index.ts import { Hono } from 'hono'; import { cors } from 'hono/cors'; import { apiRoutes } from './api'; import { mcpHandler } from './mcp'; const app = new Hono(); app.use('/*', cors()); // REST API routes app.route('/api', apiRoutes); // MCP endpoint app.post('/mcp', mcpHandler); // Root endpoint app.get('/', (c) => { return c.json({ message: 'CF-MCP: REST API + MCP Server', endpoints: { rest: { products: 'GET /api/products', product: 'GET /api/products/:id', create: 'POST /api/products', update: 'PUT /api/products/:id', delete: 'DELETE /api/products/:id', }, mcp: 'POST /mcp (Model Context Protocol endpoint)', }, }); }); export default app; ``` --- # Project Structure Clean separation of concerns ``` cf-mcp/ ├── src/ │ ├── index.ts # Main Hono app + routing │ ├── api.ts # REST API endpoints + data │ └── mcp.ts # MCP protocol handler ├── examples/ │ ├── rest-api.sh # REST API test script │ └── mcp-api.sh # MCP test script ├── wrangler.toml # Cloudflare config ├── package.json └── tsconfig.json ``` **Key Insight**: REST and MCP share the same business logic --- layout: section --- # Testing the MCP Server --- # MCP Protocol Tests ```bash # List available tools curl -X POST http://localhost:8787/mcp \ -H "Content-Type: application/json" \ -d '{ "method": "tools/list", "params": {} }' # Response: { "tools": [ { "name": "list_products", "description": "List all products with optional filtering...", "inputSchema": { ... } }, ... ] } ``` --- # Calling MCP Tools ```bash # Call list_products tool curl -X POST http://localhost:8787/mcp \ -H "Content-Type: application/json" \ -d '{ "method": "tools/call", "params": { "name": "list_products", "arguments": { "category": "Electronics" } } }' # Response: { "content": [ { "type": "text", "text": "{\n \"success\": true,\n \"data\": [...],\n \"count\": 1\n}" } ] } ``` --- # Using with Claude Code Configure Claude Code to use your MCP server ```bash # Add MCP server (local development) claude mcp add --transport http products-api http://localhost:8788/mcp # Or for production deployment claude mcp add --transport http products-api https://cf-mcp.your-subdomain.workers.dev/mcp # Verify it's connected claude mcp list ``` Then in Claude: - "List all products in the Electronics category" - "Create a new product called Gaming Mouse for $49.99" - "What products are out of stock?" Claude will use your MCP tools automatically! --- layout: section --- # Key Patterns REST to MCP Translation --- # Pattern 1: CRUD → Tools Each REST operation becomes a tool <div class="grid grid-cols-2 gap-4"> <div> ## REST ```http GET /api/products GET /api/products/:id POST /api/products PUT /api/products/:id DELETE /api/products/:id ``` </div> <div> ## MCP Tools ```typescript - list_products - get_product - create_product - update_product - delete_product ``` </div> </div> **Pattern**: Use `verb_noun` naming for tools --- # Pattern 2: Query Params → Arguments REST query parameters become tool arguments <div class="grid grid-cols-2 gap-4"> <div> ## REST ```http GET /api/products?category=Electronics&inStock=true ``` Query parameters: - `category` (string) - `inStock` (boolean) </div> <div> ## MCP Tool ```typescript { name: 'list_products', inputSchema: { properties: { category: { type: 'string' }, inStock: { type: 'boolean' } } } } ``` </div> </div> **Pattern**: Map query params to typed schema properties --- # Pattern 3: Path Params → Required Args URL path parameters become required arguments <div class="grid grid-cols-2 gap-4"> <div> ## REST ```http GET /api/products/:id ``` Path parameter: - `id` (required) </div> <div> ## MCP Tool ```typescript { name: 'get_product', inputSchema: { properties: { id: { type: 'string', description: 'Product ID' } }, required: ['id'] } } ``` </div> </div> **Pattern**: Path params always go in `required` array --- # Pattern 4: Request Body → Arguments POST/PUT body becomes tool arguments <div class="grid grid-cols-2 gap-4"> <div> ## REST ```http POST /api/products Content-Type: application/json { "name": "Laptop", "price": 999, "description": "...", "category": "Electronics" } ``` </div> <div> ## MCP Tool ```typescript { name: 'create_product', inputSchema: { properties: { name: { type: 'string' }, price: { type: 'number' }, description: { type: 'string' }, category: { type: 'string' } }, required: ['name', 'price', 'description', 'category'] } } ``` </div> </div> **Pattern**: Flatten JSON body into schema properties --- # Pattern 5: Response Format Wrap REST responses in MCP content format ```typescript // REST API returns: { "success": true, "data": [{ ... }], "count": 3 } // MCP wraps it: { "content": [ { "type": "text", "text": "{\"success\":true,\"data\":[{...}],\"count\":3}" } ] } ``` **Pattern**: Always return `content` array with `type` and `text` --- # Pattern 6: Error Handling Consistent JSON-RPC error responses ```typescript try { const result = await executeTool(name, args, baseUrl); return c.json({ jsonrpc: '2.0', id: requestId, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } }); } catch (error) { return c.json({ jsonrpc: '2.0', id: requestId, error: { code: -32603, // Internal error message: error.message } }); } ``` **Pattern**: Use JSON-RPC error format with proper error codes --- layout: section --- # Deployment Getting to the edge --- # Cloudflare Workers Configuration ```toml # wrangler.toml name = "cf-mcp" main = "src/index.ts" compatibility_date = "2024-11-01" [observability] enabled = true ``` Simple and minimal - that's all you need! --- # Local Development ```bash # Install dependencies npm install # Start dev server npm run dev # Server runs at http://localhost:8787 ``` Test both interfaces: - REST: `http://localhost:8787/api/products` - MCP: `http://localhost:8787/mcp` --- # Deploy to Cloudflare ```bash # Deploy to production npm run deploy # Output: # Deployed to: https://cf-mcp.your-subdomain.workers.dev ``` Your API is now: - ✅ Globally distributed on 300+ edge locations - ✅ Automatically scaled - ✅ Low latency worldwide - ✅ Serving both REST and MCP --- # Testing Deployed MCP ```bash # Replace with your deployed URL WORKER_URL="https://cf-mcp.your-subdomain.workers.dev" # Test MCP endpoint curl -X POST $WORKER_URL/mcp \ -H "Content-Type: application/json" \ -d '{ "method": "tools/call", "params": { "name": "list_products", "arguments": {} } }' ``` Update Claude Desktop config with your production URL! --- layout: section --- # Architecture Deep Dive How it all works together --- # Request Flow - REST ```mermaid sequenceDiagram participant Client participant Worker participant API Client->>Worker: GET /api/products?category=Electronics Worker->>API: Route to apiRoutes API->>API: Filter products API->>Worker: JSON response Worker->>Client: { success: true, data: [...] } ``` Direct REST request → Direct REST response --- # Request Flow - MCP ```mermaid sequenceDiagram participant Claude participant Worker participant MCP participant API Claude->>Worker: POST /mcp (tools/call) Worker->>MCP: Route to mcpHandler MCP->>MCP: Parse tool name & args MCP->>API: Internal fetch to /api/products API->>MCP: REST response MCP->>MCP: Wrap in MCP format MCP->>Worker: MCP response Worker->>Claude: { content: [{type: text, ...}] } ``` MCP wraps the REST API internally --- # Architecture Diagram ```mermaid graph TB A[Client/Browser] -->|HTTP REST| B[Cloudflare Worker] C[Claude Desktop] -->|MCP Protocol| B B --> D{Router} D -->|/api/*| E[REST API Handler] D -->|/mcp| F[MCP Handler] E --> G[(Products Data)] F -->|Internal Fetch| E style B fill:#f96,stroke:#333,stroke-width:4px style E fill:#9cf,stroke:#333,stroke-width:2px style F fill:#fc9,stroke:#333,stroke-width:2px style G fill:#9f9,stroke:#333,stroke-width:2px ``` **One codebase, two interfaces, shared data** --- layout: section --- # Real-World Demo Let's see it in action --- # Demo Checklist What we'll show: 1. **Start the local server** - `npm run dev` 2. **Test REST API** - List products - Filter by category - Create a product 3. **Test MCP endpoint** - List available tools - Call a tool - See structured response 4. **Use with Claude Desktop** - Configure MCP server - Ask Claude to list products - Ask Claude to create a product 5. **Deploy to Cloudflare** - `npm run deploy` - Test production endpoint --- layout: section --- # Advanced Topics Going beyond the basics --- # Adding Authentication Secure your MCP server ```typescript // Simple API key authentication export async function mcpHandler(c: Context) { const apiKey = c.req.header('X-API-Key'); if (apiKey !== c.env.API_KEY) { return c.json({ error: 'Unauthorized' }, 401); } // ... rest of handler } ``` Store API key in Cloudflare secrets: ```bash wrangler secret put API_KEY ``` --- # Adding Persistent Storage Use Cloudflare D1 (SQLite) ```typescript // wrangler.toml [[d1_databases]] binding = "DB" database_name = "products-db" database_id = "your-database-id" // src/api.ts apiRoutes.get('/products', async (c) => { const { results } = await c.env.DB.prepare( 'SELECT * FROM products' ).all(); return c.json({ success: true, data: results }); }); ``` --- # Adding Rate Limiting Protect your API from abuse ```typescript // Using Cloudflare KV for rate limiting async function checkRateLimit(c: Context, key: string) { const count = await c.env.RATE_LIMIT.get(key); if (count && parseInt(count) > 100) { return c.json({ error: 'Rate limit exceeded' }, 429); } await c.env.RATE_LIMIT.put( key, String((parseInt(count || '0') + 1)), { expirationTtl: 60 } // 1 minute window ); } ``` --- # Adding Caching Speed up repeated requests ```typescript // Cache product list for 5 minutes apiRoutes.get('/products', async (c) => { const cache = caches.default; const cacheKey = new Request(c.req.url); let response = await cache.match(cacheKey); if (!response) { const data = getProducts(); response = new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=300' } }); await cache.put(cacheKey, response.clone()); } return response; }); ``` --- layout: section --- # Best Practices Lessons learned --- # Design Principles <div class="grid grid-cols-2 gap-4"> <div> ## REST API - Clear resource naming - RESTful conventions - Consistent responses - Proper HTTP status codes - Filter with query params - Paginate large results </div> <div> ## MCP Tools - Descriptive tool names - Rich input schemas - Clear descriptions - Type validation - Proper error messages - Document all fields </div> </div> **Key**: Make both interfaces intuitive --- # Tool Naming Conventions Good vs bad tool names ✅ **Good Names** - `list_products` - Clear verb + noun - `get_product` - Specific action - `create_product` - Obvious intent - `update_product_stock` - Descriptive ❌ **Bad Names** - `products` - Missing verb - `fetch` - Too generic - `doStuff` - Meaningless - `api_call` - Not descriptive **Pattern**: Always use `verb_noun` or `verb_noun_qualifier` --- # Schema Best Practices Write helpful schemas ```typescript // ✅ Good schema { name: 'list_products', description: 'List all products with optional filtering by category or stock status', inputSchema: { type: 'object', properties: { category: { type: 'string', description: 'Filter by product category (e.g., Electronics, Home, Sports)', }, inStock: { type: 'boolean', description: 'Filter by stock availability (true = in stock, false = out of stock)', } } } } ``` **Tip**: AI agents read descriptions - make them helpful! --- # Error Handling Best Practices ```typescript // ✅ Good error handling try { const result = await executeTool(name, args, baseUrl); return c.json({ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }); } catch (error) { console.error('Tool execution failed:', error); return c.json({ content: [{ type: 'text', text: error instanceof Error ? `Error: ${error.message}` : 'Unknown error occurred' }], isError: true }); } ``` **Always**: Log errors and return user-friendly messages --- # Testing Strategy Test both interfaces ```bash # Create test scripts # examples/rest-api.sh curl http://localhost:8787/api/products curl http://localhost:8787/api/products/1 # ... more tests # examples/mcp-api.sh curl -X POST http://localhost:8787/mcp \ -d '{"method":"tools/list"}' curl -X POST http://localhost:8787/mcp \ -d '{"method":"tools/call","params":{"name":"list_products"}}' # ... more tests ``` **Run tests**: Before every deploy! --- layout: section --- # Common Challenges And how to solve them --- # Challenge: CORS Issues Problem: Browser requests blocked ```typescript // ❌ Without CORS const app = new Hono(); app.post('/mcp', mcpHandler); // ✅ With CORS import { cors } from 'hono/cors'; const app = new Hono(); app.use('/*', cors()); // Enable for all routes app.post('/mcp', mcpHandler); ``` **Solution**: Always enable CORS for edge APIs --- # Challenge: Large Responses Problem: MCP responses too big ```typescript // ❌ Returning everything const products = await getAllProducts(); // 10,000 items! return c.json({ content: [{ type: 'text', text: JSON.stringify(products) }] }); // ✅ Paginate results const limit = args.limit || 50; const offset = args.offset || 0; const products = await getProducts(limit, offset); return c.json({ content: [{ type: 'text', text: JSON.stringify({ data: products, pagination: { limit, offset, total } }) }] }); ``` **Solution**: Always paginate large datasets --- # Challenge: Type Safety Problem: Runtime errors from invalid data ```typescript // ❌ No validation async function executeTool(name: string, args: any) { const response = await fetch(`/api/products/${args.id}`); // What if args.id is undefined? } // ✅ With validation async function executeTool(name: string, args: any) { if (!args?.id) { throw new Error('Product ID is required'); } const response = await fetch(`/api/products/${args.id}`); } ``` **Solution**: Validate inputs before using them --- # Challenge: Testing MCP Locally Problem: Hard to test MCP without Claude ```typescript // Create a simple test client async function testMCP() { // Test tools/list const listResponse = await fetch('http://localhost:8787/mcp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ method: 'tools/list', params: {} }) }); console.log('Tools:', await listResponse.json()); // Test tools/call const callResponse = await fetch('http://localhost:8787/mcp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ method: 'tools/call', params: { name: 'list_products', arguments: {} } }) }); console.log('Result:', await callResponse.json()); } ``` **Solution**: Write simple test scripts --- layout: section --- # Next Steps Where to go from here --- # Extend This Project Ideas for enhancement: 1. **Add More Entities** - Users, orders, categories - Relationships between entities 2. **Add Search** - Full-text search tool - Complex filtering 3. **Add Analytics** - Track tool usage - Monitor performance 4. **Add Webhooks** - Notify on events - Real-time updates 5. **Add File Uploads** - Product images - Use Cloudflare R2 --- # Wrap Other APIs Apply this pattern to: - **GitHub API** → MCP tools for repos, issues, PRs - **Stripe API** → MCP tools for payments, customers - **SendGrid API** → MCP tools for emails - **Weather API** → MCP tools for forecasts - **Your internal APIs** → Make them AI-ready **The pattern works for any REST API!** --- # Resources <div class="grid grid-cols-2 gap-4"> <div> ## This Project - [GitHub: cf-mcp](https://github.com/heygarrison/cf-mcp) - [Documentation](https://github.com/heygarrison/cf-mcp#readme) - [Examples](https://github.com/heygarrison/cf-mcp/tree/main/examples) ## MCP Resources - [MCP Specification](https://modelcontextprotocol.io/) - [MCP Servers](https://github.com/modelcontextprotocol/servers) - [Claude Desktop](https://claude.ai/desktop) </div> <div> ## Cloudflare Resources - [Workers Docs](https://developers.cloudflare.com/workers/) - [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/) - [Hono Framework](https://hono.dev) - [D1 Database](https://developers.cloudflare.com/d1/) ## Community - [Cloudflare Discord](https://discord.gg/cloudflare) - [Hono Discord](https://discord.gg/hono) </div> </div> --- layout: center class: text-center --- # Questions? Let's discuss your use cases <div class="pt-12"> <span class="text-6xl">🚀</span> </div> --- layout: center --- <img src="/aidd.png" class="mx-auto max-h-[500px]" /> --- layout: center class: text-center --- # Thank You! ## Get Started Today ```bash git clone https://github.com/heygarrison/cf-mcp cd cf-mcp npm install npm run dev ``` <div class="pt-8"> Deploy your first MCP server in minutes </div> <div class="pt-8 text-sm opacity-75"> From the Edge and Back: Turn Any REST API into an MCP Server </div>

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/HeyGarrison/cf-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server