Initial commit
This commit is contained in:
231
templates/basic-mcp-server.ts
Normal file
231
templates/basic-mcp-server.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* Basic MCP Server (No Authentication)
|
||||
*
|
||||
* A simple Model Context Protocol server with basic tools.
|
||||
* Demonstrates the core McpAgent pattern without authentication.
|
||||
*
|
||||
* Perfect for: Internal tools, development, public APIs
|
||||
*
|
||||
* Based on: https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-authless
|
||||
*
|
||||
* ⚠️ CRITICAL URL CONFIGURATION:
|
||||
*
|
||||
* This template serves MCP at TWO base paths:
|
||||
* - SSE transport: /sse
|
||||
* - HTTP transport: /mcp
|
||||
*
|
||||
* Your client configuration MUST match:
|
||||
* {
|
||||
* "mcpServers": {
|
||||
* "my-mcp": {
|
||||
* "url": "https://YOUR-WORKER.workers.dev/sse" // ← Include /sse!
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Common mistakes:
|
||||
* ❌ "url": "https://YOUR-WORKER.workers.dev" // Missing /sse → 404
|
||||
* ❌ "url": "http://localhost:8788" // Wrong after deploy
|
||||
* ✅ "url": "https://YOUR-WORKER.workers.dev/sse" // Correct!
|
||||
*
|
||||
* After deploying:
|
||||
* 1. Test with: curl https://YOUR-WORKER.workers.dev/sse
|
||||
* 2. Update client config with exact URL from step 1
|
||||
* 3. Restart Claude Desktop
|
||||
*/
|
||||
|
||||
import { McpAgent } from "agents/mcp";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
|
||||
type Env = {
|
||||
// Add your environment bindings here
|
||||
// Example: MY_KV: KVNamespace;
|
||||
};
|
||||
|
||||
/**
|
||||
* MyMCP extends McpAgent to create a stateless MCP server
|
||||
*/
|
||||
export class MyMCP extends McpAgent<Env> {
|
||||
server = new McpServer({
|
||||
name: "My MCP Server",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize tools, resources, and prompts
|
||||
* Called automatically by McpAgent base class
|
||||
*/
|
||||
async init() {
|
||||
// Simple calculation tool
|
||||
this.server.tool(
|
||||
"add",
|
||||
"Add two numbers together",
|
||||
{
|
||||
a: z.number().describe("First number"),
|
||||
b: z.number().describe("Second number"),
|
||||
},
|
||||
async ({ a, b }) => ({
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `The sum of ${a} + ${b} = ${a + b}`,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// Calculator tool with operations
|
||||
this.server.tool(
|
||||
"calculate",
|
||||
"Perform basic arithmetic operations",
|
||||
{
|
||||
operation: z
|
||||
.enum(["add", "subtract", "multiply", "divide"])
|
||||
.describe("The arithmetic operation to perform"),
|
||||
a: z.number().describe("First operand"),
|
||||
b: z.number().describe("Second operand"),
|
||||
},
|
||||
async ({ operation, a, b }) => {
|
||||
let result: number;
|
||||
|
||||
switch (operation) {
|
||||
case "add":
|
||||
result = a + b;
|
||||
break;
|
||||
case "subtract":
|
||||
result = a - b;
|
||||
break;
|
||||
case "multiply":
|
||||
result = a * b;
|
||||
break;
|
||||
case "divide":
|
||||
if (b === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: Division by zero is not allowed",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
result = a / b;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Result: ${a} ${operation} ${b} = ${result}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Example resource (optional)
|
||||
this.server.resource({
|
||||
uri: "about://server",
|
||||
name: "About this server",
|
||||
description: "Information about this MCP server",
|
||||
mimeType: "text/plain",
|
||||
}, async () => ({
|
||||
contents: [{
|
||||
uri: "about://server",
|
||||
mimeType: "text/plain",
|
||||
text: "This is a basic MCP server running on Cloudflare Workers"
|
||||
}]
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Worker fetch handler
|
||||
* Supports both SSE and Streamable HTTP transports
|
||||
*
|
||||
* ⚠️ URL CONFIGURATION GUIDE:
|
||||
*
|
||||
* Option 1: Serve at /sse (current setup)
|
||||
* -----------------------------------------
|
||||
* Server code (below): MyMCP.serveSSE("/sse").fetch(...)
|
||||
* Client config: "url": "https://worker.dev/sse"
|
||||
* Tools available at: https://worker.dev/sse/tools/list
|
||||
*
|
||||
* Option 2: Serve at root / (alternative)
|
||||
* -----------------------------------------
|
||||
* Server code: MyMCP.serveSSE("/").fetch(...)
|
||||
* Client config: "url": "https://worker.dev"
|
||||
* Tools available at: https://worker.dev/tools/list
|
||||
*
|
||||
* The base path argument MUST match what the client expects!
|
||||
*
|
||||
* TESTING YOUR CONFIGURATION:
|
||||
* 1. Deploy: npx wrangler deploy
|
||||
* 2. Test: curl https://YOUR-WORKER.workers.dev/sse
|
||||
* 3. Configure: Use exact URL from step 2 in client config
|
||||
* 4. Restart: Restart Claude Desktop to load new config
|
||||
*/
|
||||
export default {
|
||||
async fetch(
|
||||
request: Request,
|
||||
env: Env,
|
||||
ctx: ExecutionContext
|
||||
): Promise<Response> {
|
||||
const { pathname } = new URL(request.url);
|
||||
|
||||
// Handle CORS preflight (for browser-based clients like MCP Inspector)
|
||||
if (request.method === "OPTIONS") {
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||
"Access-Control-Max-Age": "86400",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// SSE transport (legacy, but widely supported)
|
||||
// ⚠️ IMPORTANT: Use pathname.startsWith() to match sub-paths like /sse/tools/list
|
||||
if (pathname.startsWith("/sse")) {
|
||||
return MyMCP.serveSSE("/sse").fetch(request, env, ctx);
|
||||
// ↑ Base path "/sse" means client URL must be: https://worker.dev/sse
|
||||
}
|
||||
|
||||
// Streamable HTTP transport (2025 standard)
|
||||
// ⚠️ IMPORTANT: Use pathname.startsWith() to match sub-paths like /mcp/tools/list
|
||||
if (pathname.startsWith("/mcp")) {
|
||||
return MyMCP.serve("/mcp").fetch(request, env, ctx);
|
||||
// ↑ Base path "/mcp" means client URL must be: https://worker.dev/mcp
|
||||
}
|
||||
|
||||
// Health check endpoint (useful for debugging connection issues)
|
||||
// Test with: curl https://YOUR-WORKER.workers.dev/
|
||||
if (pathname === "/" || pathname === "/health") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
name: "My MCP Server",
|
||||
version: "1.0.0",
|
||||
transports: {
|
||||
sse: "/sse",
|
||||
http: "/mcp",
|
||||
},
|
||||
status: "ok",
|
||||
timestamp: new Date().toISOString(),
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return new Response("Not Found", { status: 404 });
|
||||
},
|
||||
};
|
||||
22
templates/claude_desktop_config.json
Normal file
22
templates/claude_desktop_config.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://modelcontextprotocol.io/schemas/client-config.json",
|
||||
"mcpServers": {
|
||||
"my-mcp-server-local": {
|
||||
"comment": "Local MCP server (development)",
|
||||
"url": "http://localhost:8788/sse"
|
||||
},
|
||||
"my-mcp-server-remote": {
|
||||
"comment": "Remote MCP server (production)",
|
||||
"url": "https://my-mcp-server.your-account.workers.dev/sse"
|
||||
},
|
||||
"my-mcp-oauth-server": {
|
||||
"comment": "MCP server with OAuth authentication",
|
||||
"url": "https://my-mcp-oauth.your-account.workers.dev/sse",
|
||||
"auth": {
|
||||
"type": "oauth",
|
||||
"authorizationUrl": "https://my-mcp-oauth.your-account.workers.dev/authorize",
|
||||
"tokenUrl": "https://my-mcp-oauth.your-account.workers.dev/token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
384
templates/mcp-bearer-auth.ts
Normal file
384
templates/mcp-bearer-auth.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
/**
|
||||
* MCP Server with Bearer Token Authentication
|
||||
*
|
||||
* Demonstrates Bearer token authentication pattern for custom auth systems.
|
||||
* Shows middleware pattern for validating Authorization headers.
|
||||
*
|
||||
* Based on: https://github.com/cloudflare/ai/tree/main/demos/mcp-server-bearer-auth
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
* 🔐 BEARER TOKEN AUTHENTICATION
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*
|
||||
* This pattern is for:
|
||||
* - Custom authentication systems
|
||||
* - API key validation
|
||||
* - Service-to-service communication
|
||||
* - Integration with existing auth backends
|
||||
*
|
||||
* NOT for:
|
||||
* - OAuth (use OAuth Proxy pattern instead)
|
||||
* - Public APIs (use authless pattern)
|
||||
* - Enterprise SSO (use Auth0/Okta integrations)
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
* 📋 HOW IT WORKS
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 1. Client sends request with Authorization header:
|
||||
* Authorization: Bearer YOUR_TOKEN_HERE
|
||||
*
|
||||
* 2. Worker validates token (check against database, external API, etc.)
|
||||
*
|
||||
* 3. If valid: Pass token to MCP server via ctx.props
|
||||
*
|
||||
* 4. If invalid: Return 401 Unauthorized
|
||||
*
|
||||
* 5. MCP tools can access token via this.props.bearerToken
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
* 🔧 CONFIGURATION
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*
|
||||
* Option 1: Static token list (simple, for development)
|
||||
* Option 2: Check against KV store (production)
|
||||
* Option 3: Validate with external API (most flexible)
|
||||
*
|
||||
* This template shows all three approaches.
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import { McpAgent } from "agents/mcp";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
|
||||
type Env = {
|
||||
// Optional: KV for token storage
|
||||
AUTH_TOKENS?: KVNamespace;
|
||||
|
||||
// Optional: API endpoint for token validation
|
||||
AUTH_API_URL?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Props passed to MCP server after authentication
|
||||
*/
|
||||
type Props = {
|
||||
bearerToken: string; // The validated token
|
||||
userId?: string; // Optional: User ID from token
|
||||
};
|
||||
|
||||
/**
|
||||
* MCP Server with bearer token authentication
|
||||
*/
|
||||
export class MyMCP extends McpAgent<Env, Record<string, never>, Props> {
|
||||
server = new McpServer({
|
||||
name: "Bearer Auth MCP Server",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
async init() {
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// TOOL 1: Echo with Auth Info
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Demonstrates accessing bearer token from props
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
this.server.tool(
|
||||
"echo_auth",
|
||||
"Echo back your message along with auth info",
|
||||
{
|
||||
message: z.string().describe("Message to echo"),
|
||||
},
|
||||
async ({ message }) => {
|
||||
// Access authenticated user info
|
||||
const token = this.props?.bearerToken || "none";
|
||||
const userId = this.props?.userId || "unknown";
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Message: ${message}\n\nAuth Info:\n- Token: ${token.substring(0, 10)}...\n- User ID: ${userId}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// TOOL 2: Protected Tool Example
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Only accessible to authenticated users
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
this.server.tool(
|
||||
"protected_action",
|
||||
"Perform a protected action (requires auth)",
|
||||
{
|
||||
action: z.string().describe("Action to perform"),
|
||||
},
|
||||
async ({ action }) => {
|
||||
// Verify authentication (should always pass if middleware worked)
|
||||
if (!this.props?.bearerToken) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: Unauthenticated. This should never happen if middleware is working.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Perform protected action
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Protected action "${action}" performed successfully by user ${this.props.userId}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate bearer token
|
||||
*
|
||||
* Three validation strategies (choose one):
|
||||
* 1. Static list (development)
|
||||
* 2. KV store lookup (production)
|
||||
* 3. External API validation (most flexible)
|
||||
*/
|
||||
async function validateToken(
|
||||
token: string,
|
||||
env: Env
|
||||
): Promise<{ valid: boolean; userId?: string }> {
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Strategy 1: Static Token List (Development Only!)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// ⚠️ DON'T use in production! Tokens exposed in code.
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
const VALID_TOKENS = {
|
||||
"dev-token-123": "user-1",
|
||||
"dev-token-456": "user-2",
|
||||
};
|
||||
|
||||
if (VALID_TOKENS[token]) {
|
||||
return { valid: true, userId: VALID_TOKENS[token] };
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Strategy 2: KV Store Lookup (Production)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Store tokens in KV: { "token-abc123": "user-id" }
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
if (env.AUTH_TOKENS) {
|
||||
const userId = await env.AUTH_TOKENS.get(token);
|
||||
if (userId) {
|
||||
return { valid: true, userId };
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Strategy 3: External API Validation (Most Flexible)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Validate token with external auth service
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
if (env.AUTH_API_URL) {
|
||||
try {
|
||||
const response = await fetch(`${env.AUTH_API_URL}/validate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
return { valid: true, userId: data.userId };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Token validation error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Token Invalid
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
return { valid: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Worker fetch handler with bearer auth middleware
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
* 🔐 AUTHENTICATION FLOW
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 1. Extract Authorization header
|
||||
* 2. Validate bearer token
|
||||
* 3. If valid: Pass to MCP server with user context
|
||||
* 4. If invalid: Return 401 Unauthorized
|
||||
*
|
||||
* Client configuration:
|
||||
* {
|
||||
* "mcpServers": {
|
||||
* "my-mcp": {
|
||||
* "url": "https://YOUR-WORKER.workers.dev/sse",
|
||||
* "headers": {
|
||||
* "Authorization": "Bearer YOUR_TOKEN_HERE"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*/
|
||||
export default {
|
||||
async fetch(
|
||||
request: Request,
|
||||
env: Env,
|
||||
ctx: ExecutionContext
|
||||
): Promise<Response> {
|
||||
const { pathname } = new URL(request.url);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Handle CORS Preflight (no auth required)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
if (request.method === "OPTIONS") {
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||
"Access-Control-Max-Age": "86400",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Health Check (no auth required)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
if (pathname === "/" || pathname === "/health") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
name: "Bearer Auth MCP Server",
|
||||
version: "1.0.0",
|
||||
transports: {
|
||||
sse: "/sse",
|
||||
http: "/mcp",
|
||||
},
|
||||
auth: "Bearer token required",
|
||||
status: "ok",
|
||||
timestamp: new Date().toISOString(),
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Authentication Middleware
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Extract and validate bearer token before serving MCP
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
// 1. Extract Authorization header
|
||||
const authHeader = request.headers.get("Authorization");
|
||||
|
||||
if (!authHeader) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "Unauthorized",
|
||||
message: "Missing Authorization header",
|
||||
hint: 'Include header: Authorization: Bearer YOUR_TOKEN',
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"WWW-Authenticate": 'Bearer realm="MCP Server"',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Check Bearer format
|
||||
if (!authHeader.startsWith("Bearer ")) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "Unauthorized",
|
||||
message: "Invalid Authorization header format",
|
||||
hint: 'Use format: Authorization: Bearer YOUR_TOKEN',
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"WWW-Authenticate": 'Bearer realm="MCP Server"',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Extract token
|
||||
const token = authHeader.substring(7); // Remove "Bearer " prefix
|
||||
|
||||
// 4. Validate token
|
||||
const { valid, userId } = await validateToken(token, env);
|
||||
|
||||
if (!valid) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "Unauthorized",
|
||||
message: "Invalid bearer token",
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"WWW-Authenticate": 'Bearer realm="MCP Server"',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Authentication successful! Set props and pass to MCP server
|
||||
const props: Props = {
|
||||
bearerToken: token,
|
||||
userId,
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// SSE Transport (with auth)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
if (pathname.startsWith("/sse")) {
|
||||
return MyMCP.serveSSE("/sse").fetch(request, env, {
|
||||
...ctx,
|
||||
props,
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// HTTP Transport (with auth)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
if (pathname.startsWith("/mcp")) {
|
||||
return MyMCP.serve("/mcp").fetch(request, env, {
|
||||
...ctx,
|
||||
props,
|
||||
});
|
||||
}
|
||||
|
||||
return new Response("Not Found", { status: 404 });
|
||||
},
|
||||
};
|
||||
210
templates/mcp-http-fundamentals.ts
Normal file
210
templates/mcp-http-fundamentals.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* MCP HTTP Fundamentals - Minimal Example
|
||||
*
|
||||
* The SIMPLEST working MCP server demonstrating ONLY URL configuration.
|
||||
* Perfect for understanding how base paths work before adding features.
|
||||
*
|
||||
* This template focuses on THE #1 MISTAKE: URL path mismatches
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
* ⚠️ CRITICAL: URL CONFIGURATION EXPLAINED
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*
|
||||
* CONCEPT: The base path you use in serveSSE() determines the client URL
|
||||
*
|
||||
* Example A: Serving at /sse
|
||||
* ---------------------------
|
||||
* Code: MyMCP.serveSSE("/sse").fetch(...)
|
||||
* Client URL: "https://worker.dev/sse" ✅
|
||||
* Wrong URL: "https://worker.dev" ❌ 404!
|
||||
*
|
||||
* Example B: Serving at root /
|
||||
* ----------------------------
|
||||
* Code: MyMCP.serveSSE("/").fetch(...)
|
||||
* Client URL: "https://worker.dev" ✅
|
||||
* Wrong URL: "https://worker.dev/sse" ❌ 404!
|
||||
*
|
||||
* Example C: Serving at /api/mcp
|
||||
* -------------------------------
|
||||
* Code: MyMCP.serveSSE("/api/mcp").fetch(...)
|
||||
* Client URL: "https://worker.dev/api/mcp" ✅
|
||||
* Wrong URL: "https://worker.dev/sse" ❌ 404!
|
||||
*
|
||||
* The pattern: pathname.startsWith("/sse") matches ALL paths like:
|
||||
* - /sse
|
||||
* - /sse/tools/list
|
||||
* - /sse/tools/call
|
||||
* - /sse/resources/list
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
* 📋 POST-DEPLOYMENT CHECKLIST
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*
|
||||
* After running `npx wrangler deploy`:
|
||||
*
|
||||
* 1. Note the deployed URL (e.g., https://my-mcp.my-account.workers.dev)
|
||||
*
|
||||
* 2. Test the endpoint:
|
||||
* curl https://my-mcp.my-account.workers.dev/sse
|
||||
* Should return: {"name":"My MCP Server", ...} (not 404!)
|
||||
*
|
||||
* 3. Update Claude Desktop config with EXACT URL from step 2:
|
||||
* ~/.config/claude/claude_desktop_config.json:
|
||||
* {
|
||||
* "mcpServers": {
|
||||
* "my-mcp": {
|
||||
* "url": "https://my-mcp.my-account.workers.dev/sse"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* 4. Restart Claude Desktop
|
||||
*
|
||||
* 5. Verify connection in Claude Desktop (check for tools)
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import { McpAgent } from "agents/mcp";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
|
||||
type Env = {};
|
||||
|
||||
/**
|
||||
* Minimal MCP server with ONE simple tool
|
||||
*/
|
||||
export class MyMCP extends McpAgent<Env> {
|
||||
server = new McpServer({
|
||||
name: "My MCP Server",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
async init() {
|
||||
// One simple tool to verify connection
|
||||
this.server.tool(
|
||||
"echo",
|
||||
"Echo back the provided message (useful for testing connection)",
|
||||
{
|
||||
message: z.string().describe("The message to echo back"),
|
||||
},
|
||||
async ({ message }) => ({
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Echo: ${message}`,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Worker fetch handler demonstrating URL configuration
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
* 🔍 HOW THIS WORKS
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*
|
||||
* Request flow:
|
||||
* 1. Client sends: https://worker.dev/sse
|
||||
* 2. Worker receives request
|
||||
* 3. Extract pathname: new URL(request.url).pathname === "/sse"
|
||||
* 4. Check: pathname.startsWith("/sse") → TRUE
|
||||
* 5. Call: MyMCP.serveSSE("/sse").fetch(...) → Handle MCP request
|
||||
* 6. MCP tools available at:
|
||||
* - /sse/tools/list
|
||||
* - /sse/tools/call
|
||||
* - etc.
|
||||
*
|
||||
* If client sends: https://worker.dev (missing /sse):
|
||||
* 1. pathname === "/"
|
||||
* 2. Check: pathname.startsWith("/sse") → FALSE
|
||||
* 3. Falls through to 404
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*/
|
||||
export default {
|
||||
async fetch(
|
||||
request: Request,
|
||||
env: Env,
|
||||
ctx: ExecutionContext
|
||||
): Promise<Response> {
|
||||
const { pathname } = new URL(request.url);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// SSE Transport at /sse
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// This matches:
|
||||
// - /sse (initial connection)
|
||||
// - /sse/tools/list (list available tools)
|
||||
// - /sse/tools/call (execute tool)
|
||||
// - /sse/resources/list (list resources)
|
||||
// - etc.
|
||||
//
|
||||
// Client URL MUST be: https://worker.dev/sse
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
if (pathname.startsWith("/sse")) {
|
||||
return MyMCP.serveSSE("/sse").fetch(request, env, ctx);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Health Check Endpoint
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Test with: curl https://YOUR-WORKER.workers.dev/
|
||||
// Useful for:
|
||||
// - Verifying Worker is deployed
|
||||
// - Debugging connection issues
|
||||
// - Discovering available transports
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
if (pathname === "/" || pathname === "/health") {
|
||||
return new Response(
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "My MCP Server",
|
||||
version: "1.0.0",
|
||||
transports: {
|
||||
sse: "/sse",
|
||||
},
|
||||
status: "ok",
|
||||
timestamp: new Date().toISOString(),
|
||||
help: {
|
||||
clientConfig: {
|
||||
url: `${new URL(request.url).origin}/sse`,
|
||||
},
|
||||
testCommand: `curl ${new URL(request.url).origin}/sse`,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 404 Not Found
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// If you're seeing this:
|
||||
// - Check client URL includes /sse
|
||||
// - Try: curl https://YOUR-WORKER.workers.dev/ to see available paths
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: "Not Found",
|
||||
requestedPath: pathname,
|
||||
availablePaths: ["/sse", "/", "/health"],
|
||||
hint: "Client URL must be: https://YOUR-WORKER.workers.dev/sse",
|
||||
}),
|
||||
{
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
329
templates/mcp-oauth-proxy.ts
Normal file
329
templates/mcp-oauth-proxy.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
/**
|
||||
* MCP Server with OAuth Proxy (GitHub Example)
|
||||
*
|
||||
* Uses Cloudflare's workers-oauth-provider to proxy OAuth to GitHub.
|
||||
* This pattern lets you integrate with GitHub, Google, Azure, etc.
|
||||
*
|
||||
* Perfect for: Authenticated API access, user-scoped tools
|
||||
*
|
||||
* Based on: https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-github-oauth
|
||||
*
|
||||
* ⚠️ CRITICAL OAuth URL CONFIGURATION:
|
||||
*
|
||||
* ALL OAuth URLs must use the SAME domain and protocol!
|
||||
*
|
||||
* Client configuration after deployment:
|
||||
* {
|
||||
* "mcpServers": {
|
||||
* "github-mcp": {
|
||||
* "url": "https://YOUR-WORKER.workers.dev/sse",
|
||||
* "auth": {
|
||||
* "type": "oauth",
|
||||
* "authorizationUrl": "https://YOUR-WORKER.workers.dev/authorize",
|
||||
* "tokenUrl": "https://YOUR-WORKER.workers.dev/token"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Common mistakes:
|
||||
* ❌ Mixed protocols: http://... and https://...
|
||||
* ❌ Missing /sse in main URL
|
||||
* ❌ Wrong domain after deployment (still using localhost)
|
||||
* ❌ Typos in endpoint paths (/authorize vs /auth)
|
||||
*
|
||||
* Post-deployment checklist:
|
||||
* 1. Deploy: npx wrangler deploy
|
||||
* 2. Note deployed URL
|
||||
* 3. Update ALL three URLs in client config
|
||||
* 4. Test OAuth flow in Claude Desktop
|
||||
*/
|
||||
|
||||
import { McpAgent } from "agents/mcp";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { OAuthProvider, GitHubHandler } from "@cloudflare/workers-oauth-provider";
|
||||
import { z } from "zod";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
|
||||
type Env = {
|
||||
OAUTH_KV: KVNamespace; // Required for OAuth token storage
|
||||
GITHUB_CLIENT_ID?: string; // Optional: pre-register client
|
||||
GITHUB_CLIENT_SECRET?: string; // Optional: pre-register client
|
||||
};
|
||||
|
||||
/**
|
||||
* Props passed to MCP server after OAuth authentication
|
||||
* Contains user info and access token
|
||||
*/
|
||||
type Props = {
|
||||
login: string; // GitHub username
|
||||
name: string; // User's display name
|
||||
email: string; // User's email
|
||||
accessToken: string; // GitHub API access token
|
||||
};
|
||||
|
||||
/**
|
||||
* Allowlist for sensitive operations (optional)
|
||||
* Replace with your GitHub usernames
|
||||
*/
|
||||
const ALLOWED_USERNAMES = new Set(["your-github-username"]);
|
||||
|
||||
/**
|
||||
* MyMCP extends McpAgent with OAuth-authenticated context
|
||||
*/
|
||||
export class MyMCP extends McpAgent<Env, Record<string, never>, Props> {
|
||||
server = new McpServer({
|
||||
name: "GitHub MCP Server",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize tools with authenticated context
|
||||
* this.props contains user info and accessToken
|
||||
*/
|
||||
async init() {
|
||||
// Create Octokit client with user's access token
|
||||
const octokit = new Octokit({
|
||||
auth: this.props!.accessToken,
|
||||
});
|
||||
|
||||
// Tool: List user's repositories
|
||||
this.server.tool(
|
||||
"list_repos",
|
||||
"List GitHub repositories for the authenticated user",
|
||||
{
|
||||
visibility: z
|
||||
.enum(["all", "public", "private"])
|
||||
.default("all")
|
||||
.describe("Filter by repository visibility"),
|
||||
sort: z
|
||||
.enum(["created", "updated", "pushed", "full_name"])
|
||||
.default("updated")
|
||||
.describe("Sort order"),
|
||||
per_page: z.number().min(1).max(100).default(30).describe("Results per page"),
|
||||
},
|
||||
async ({ visibility, sort, per_page }) => {
|
||||
try {
|
||||
const { data: repos } = await octokit.rest.repos.listForAuthenticatedUser({
|
||||
visibility,
|
||||
sort,
|
||||
per_page,
|
||||
});
|
||||
|
||||
const repoList = repos
|
||||
.map(
|
||||
(repo) =>
|
||||
`- ${repo.full_name} (${repo.visibility}) - ${repo.description || "No description"}`
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Found ${repos.length} repositories:\n\n${repoList}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error fetching repositories: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Tool: Get repository details
|
||||
this.server.tool(
|
||||
"get_repo",
|
||||
"Get detailed information about a GitHub repository",
|
||||
{
|
||||
owner: z.string().describe("Repository owner (username or org)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
},
|
||||
async ({ owner, repo }) => {
|
||||
try {
|
||||
const { data: repository } = await octokit.rest.repos.get({
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `# ${repository.full_name}
|
||||
|
||||
${repository.description || "No description"}
|
||||
|
||||
**Stars**: ${repository.stargazers_count}
|
||||
**Forks**: ${repository.forks_count}
|
||||
**Open Issues**: ${repository.open_issues_count}
|
||||
**Language**: ${repository.language || "Not specified"}
|
||||
**License**: ${repository.license?.name || "None"}
|
||||
**Homepage**: ${repository.homepage || "None"}
|
||||
**Created**: ${repository.created_at}
|
||||
**Updated**: ${repository.updated_at}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error fetching repository: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Tool: Create GitHub issue (requires write permissions)
|
||||
this.server.tool(
|
||||
"create_issue",
|
||||
"Create a new issue in a GitHub repository",
|
||||
{
|
||||
owner: z.string().describe("Repository owner"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
title: z.string().describe("Issue title"),
|
||||
body: z.string().optional().describe("Issue description"),
|
||||
labels: z.array(z.string()).optional().describe("Issue labels"),
|
||||
},
|
||||
async ({ owner, repo, title, body, labels }) => {
|
||||
try {
|
||||
const { data: issue } = await octokit.rest.issues.create({
|
||||
owner,
|
||||
repo,
|
||||
title,
|
||||
body,
|
||||
labels,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Created issue #${issue.number}: ${issue.title}\n\nURL: ${issue.html_url}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error creating issue: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Optional: Allowlist-protected tool
|
||||
// Only registers if user is in ALLOWED_USERNAMES
|
||||
if (ALLOWED_USERNAMES.has(this.props!.login)) {
|
||||
this.server.tool(
|
||||
"delete_repo",
|
||||
"Delete a repository (DANGEROUS - restricted to allowlisted users)",
|
||||
{
|
||||
owner: z.string().describe("Repository owner"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
},
|
||||
async ({ owner, repo }) => {
|
||||
try {
|
||||
await octokit.rest.repos.delete({ owner, repo });
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Successfully deleted repository ${owner}/${repo}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error deleting repository: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth Provider configuration
|
||||
* Handles GitHub OAuth flow and token management
|
||||
*/
|
||||
export default new OAuthProvider({
|
||||
/**
|
||||
* OAuth endpoints
|
||||
*/
|
||||
authorizeEndpoint: "/authorize",
|
||||
tokenEndpoint: "/token",
|
||||
clientRegistrationEndpoint: "/register",
|
||||
|
||||
/**
|
||||
* GitHub OAuth handler
|
||||
* Automatically handles OAuth flow with GitHub
|
||||
*/
|
||||
defaultHandler: new GitHubHandler({
|
||||
// Optional: Pre-configure client credentials
|
||||
// If not set, uses Dynamic Client Registration
|
||||
clientId: (env: Env) => env.GITHUB_CLIENT_ID,
|
||||
clientSecret: (env: Env) => env.GITHUB_CLIENT_SECRET,
|
||||
|
||||
// Scopes: What permissions to request
|
||||
scopes: ["repo", "user:email", "read:org"],
|
||||
|
||||
// Context: Extract user info to pass to MCP server
|
||||
context: async (accessToken: string) => {
|
||||
const octokit = new Octokit({ auth: accessToken });
|
||||
const { data: user } = await octokit.rest.users.getAuthenticated();
|
||||
|
||||
return {
|
||||
login: user.login,
|
||||
name: user.name || user.login,
|
||||
email: user.email || `${user.login}@github.com`,
|
||||
accessToken,
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* KV namespace for token storage
|
||||
* Must be bound in wrangler.jsonc
|
||||
*/
|
||||
kv: (env: Env) => env.OAUTH_KV,
|
||||
|
||||
/**
|
||||
* API handlers: MCP transport endpoints
|
||||
*/
|
||||
apiHandlers: {
|
||||
"/sse": MyMCP.serveSSE("/sse"),
|
||||
"/mcp": MyMCP.serve("/mcp"),
|
||||
},
|
||||
|
||||
/**
|
||||
* Security settings
|
||||
*/
|
||||
allowConsentScreen: true, // Show consent screen (required for production)
|
||||
allowDynamicClientRegistration: true, // Allow clients to register on-the-fly
|
||||
});
|
||||
351
templates/mcp-stateful-do.ts
Normal file
351
templates/mcp-stateful-do.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
/**
|
||||
* Stateful MCP Server with Durable Objects
|
||||
*
|
||||
* Uses Durable Objects to maintain per-session state.
|
||||
* Each MCP client gets its own DO instance with persistent storage.
|
||||
*
|
||||
* Perfect for: Stateful applications, games, conversation history
|
||||
*
|
||||
* Based on: https://developers.cloudflare.com/agents/model-context-protocol/mcp-agent-api
|
||||
*/
|
||||
|
||||
import { McpAgent } from "agents/mcp";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
|
||||
type Env = {
|
||||
MY_MCP: DurableObjectNamespace; // Binding to this DO class
|
||||
};
|
||||
|
||||
/**
|
||||
* Stateful MCP Server using Durable Objects
|
||||
* Each instance has its own SQL database and persistent storage
|
||||
*/
|
||||
export class MyMCP extends McpAgent<Env> {
|
||||
server = new McpServer({
|
||||
name: "Stateful MCP Server",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize tools that use persistent state
|
||||
*/
|
||||
async init() {
|
||||
// Tool: Store a value
|
||||
this.server.tool(
|
||||
"store_value",
|
||||
"Store a key-value pair in persistent storage",
|
||||
{
|
||||
key: z.string().describe("Storage key"),
|
||||
value: z.string().describe("Value to store"),
|
||||
},
|
||||
async ({ key, value }) => {
|
||||
try {
|
||||
// Use Durable Objects storage API for persistence
|
||||
await this.state.storage.put(key, value);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Stored "${key}" = "${value}"`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error storing value: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Tool: Retrieve a value
|
||||
this.server.tool(
|
||||
"get_value",
|
||||
"Retrieve a stored value by key",
|
||||
{
|
||||
key: z.string().describe("Storage key"),
|
||||
},
|
||||
async ({ key }) => {
|
||||
try {
|
||||
const value = await this.state.storage.get<string>(key);
|
||||
|
||||
if (value === undefined) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `No value found for key "${key}"`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `"${key}" = "${value}"`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error retrieving value: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Tool: List all stored keys
|
||||
this.server.tool(
|
||||
"list_keys",
|
||||
"List all stored keys",
|
||||
{},
|
||||
async () => {
|
||||
try {
|
||||
const keys = await this.state.storage.list();
|
||||
const keyList = Array.from(keys.keys()).join(", ");
|
||||
|
||||
if (keys.size === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "No keys stored",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Stored keys (${keys.size}): ${keyList}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error listing keys: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Tool: Delete a key
|
||||
this.server.tool(
|
||||
"delete_key",
|
||||
"Delete a stored key-value pair",
|
||||
{
|
||||
key: z.string().describe("Storage key to delete"),
|
||||
},
|
||||
async ({ key }) => {
|
||||
try {
|
||||
const existed = await this.state.storage.delete(key);
|
||||
|
||||
if (!existed) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Key "${key}" did not exist`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Deleted key "${key}"`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error deleting key: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Example: Counter with persistent state
|
||||
this.server.tool(
|
||||
"increment_counter",
|
||||
"Increment a persistent counter",
|
||||
{
|
||||
counter_name: z.string().default("default").describe("Counter name"),
|
||||
},
|
||||
async ({ counter_name }) => {
|
||||
try {
|
||||
const key = `counter:${counter_name}`;
|
||||
const current = (await this.state.storage.get<number>(key)) || 0;
|
||||
const newValue = current + 1;
|
||||
|
||||
await this.state.storage.put(key, newValue);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Counter "${counter_name}" incremented: ${current} → ${newValue}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error incrementing counter: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Example: Store structured data (JSON)
|
||||
this.server.tool(
|
||||
"store_json",
|
||||
"Store structured JSON data",
|
||||
{
|
||||
key: z.string().describe("Storage key"),
|
||||
data: z.record(z.any()).describe("JSON data to store"),
|
||||
},
|
||||
async ({ key, data }) => {
|
||||
try {
|
||||
await this.state.storage.put(key, data);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Stored JSON data under key "${key}"`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error storing JSON: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Tool: Get JSON data
|
||||
this.server.tool(
|
||||
"get_json",
|
||||
"Retrieve stored JSON data",
|
||||
{
|
||||
key: z.string().describe("Storage key"),
|
||||
},
|
||||
async ({ key }) => {
|
||||
try {
|
||||
const data = await this.state.storage.get<Record<string, any>>(key);
|
||||
|
||||
if (data === undefined) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `No data found for key "${key}"`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(data, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error retrieving JSON: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Worker fetch handler
|
||||
* Routes requests to Durable Objects
|
||||
*/
|
||||
export default {
|
||||
async fetch(
|
||||
request: Request,
|
||||
env: Env,
|
||||
ctx: ExecutionContext
|
||||
): Promise<Response> {
|
||||
const { pathname } = new URL(request.url);
|
||||
|
||||
// Health check
|
||||
if (pathname === "/") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
name: "Stateful MCP Server",
|
||||
version: "1.0.0",
|
||||
transports: ["/sse", "/mcp"],
|
||||
stateful: true,
|
||||
}),
|
||||
{
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Route MCP requests to Durable Objects
|
||||
if (pathname.startsWith("/sse") || pathname.startsWith("/mcp")) {
|
||||
return MyMCP.serveSSE("/sse").fetch(request, env, ctx);
|
||||
}
|
||||
|
||||
return new Response("Not Found", { status: 404 });
|
||||
},
|
||||
};
|
||||
587
templates/mcp-with-d1.ts
Normal file
587
templates/mcp-with-d1.ts
Normal file
@@ -0,0 +1,587 @@
|
||||
/**
|
||||
* MCP Server with D1 Database Integration
|
||||
*
|
||||
* Demonstrates D1 (Cloudflare's SQL database) integration for persistent data storage.
|
||||
* Shows CRUD operations, SQL queries, and error handling.
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
* 💾 D1 DATABASE INTEGRATION
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*
|
||||
* This template shows:
|
||||
* 1. D1 binding configuration
|
||||
* 2. Schema creation and migrations
|
||||
* 3. CRUD operations (Create, Read, Update, Delete)
|
||||
* 4. SQL query patterns
|
||||
* 5. Error handling for database operations
|
||||
* 6. Prepared statements (SQL injection prevention)
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
* 📋 REQUIRED SETUP
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 1. Create D1 database:
|
||||
* npx wrangler d1 create my-database
|
||||
*
|
||||
* 2. Add binding to wrangler.jsonc:
|
||||
* {
|
||||
* "d1_databases": [
|
||||
* {
|
||||
* "binding": "DB",
|
||||
* "database_name": "my-database",
|
||||
* "database_id": "YOUR_DATABASE_ID"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* 3. Create schema (run locally or in wrangler):
|
||||
* npx wrangler d1 execute my-database --local --file=schema.sql
|
||||
*
|
||||
* schema.sql:
|
||||
* ```sql
|
||||
* CREATE TABLE IF NOT EXISTS users (
|
||||
* id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
* name TEXT NOT NULL,
|
||||
* email TEXT UNIQUE NOT NULL,
|
||||
* created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* 4. Deploy:
|
||||
* npx wrangler deploy
|
||||
*
|
||||
* Pricing: https://developers.cloudflare.com/d1/platform/pricing/
|
||||
* - Free tier: 5 GB storage, 5 million reads/day
|
||||
* - Pay-as-you-go after free tier
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import { McpAgent } from "agents/mcp";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
|
||||
type Env = {
|
||||
DB: D1Database; // D1 binding (configured in wrangler.jsonc)
|
||||
};
|
||||
|
||||
/**
|
||||
* User type (matches database schema)
|
||||
*/
|
||||
type User = {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* MCP Server with D1 database tools
|
||||
*/
|
||||
export class MyMCP extends McpAgent<Env> {
|
||||
server = new McpServer({
|
||||
name: "D1 Database MCP Server",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
async init() {
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// TOOL 1: Create User
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Demonstrates: INSERT with prepared statements
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
this.server.tool(
|
||||
"create_user",
|
||||
"Create a new user in the database",
|
||||
{
|
||||
name: z.string().describe("User's full name"),
|
||||
email: z.string().email().describe("User's email address"),
|
||||
},
|
||||
async ({ name, email }) => {
|
||||
try {
|
||||
// Use prepared statement to prevent SQL injection
|
||||
const result = await this.env.DB.prepare(
|
||||
"INSERT INTO users (name, email) VALUES (?, ?)"
|
||||
)
|
||||
.bind(name, email)
|
||||
.run();
|
||||
|
||||
// Check if insert was successful
|
||||
if (!result.success) {
|
||||
throw new Error("Failed to insert user");
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `User created successfully!\nID: ${result.meta.last_row_id}\nName: ${name}\nEmail: ${email}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
// Handle duplicate email error (UNIQUE constraint)
|
||||
if (error.message.includes("UNIQUE constraint failed")) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: Email "${email}" is already registered.`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error creating user: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// TOOL 2: Get User by ID
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Demonstrates: SELECT with WHERE clause
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
this.server.tool(
|
||||
"get_user",
|
||||
"Get a user by their ID",
|
||||
{
|
||||
id: z.number().int().positive().describe("User ID"),
|
||||
},
|
||||
async ({ id }) => {
|
||||
try {
|
||||
const user = await this.env.DB.prepare(
|
||||
"SELECT * FROM users WHERE id = ?"
|
||||
)
|
||||
.bind(id)
|
||||
.first<User>();
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `User with ID ${id} not found.`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(user, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error fetching user: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// TOOL 3: List All Users
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Demonstrates: SELECT all rows with pagination
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
this.server.tool(
|
||||
"list_users",
|
||||
"List all users (with optional pagination)",
|
||||
{
|
||||
limit: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.max(100)
|
||||
.default(10)
|
||||
.optional()
|
||||
.describe("Maximum number of users to return (default 10, max 100)"),
|
||||
offset: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.default(0)
|
||||
.optional()
|
||||
.describe("Number of users to skip (for pagination, default 0)"),
|
||||
},
|
||||
async ({ limit = 10, offset = 0 }) => {
|
||||
try {
|
||||
// Get users with pagination
|
||||
const { results: users } = await this.env.DB.prepare(
|
||||
"SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
||||
)
|
||||
.bind(limit, offset)
|
||||
.all<User>();
|
||||
|
||||
// Get total count
|
||||
const { count } = await this.env.DB.prepare(
|
||||
"SELECT COUNT(*) as count FROM users"
|
||||
).first<{ count: number }>();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(
|
||||
{
|
||||
users,
|
||||
pagination: {
|
||||
total: count,
|
||||
limit,
|
||||
offset,
|
||||
showing: users.length,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error listing users: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// TOOL 4: Update User
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Demonstrates: UPDATE with prepared statements
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
this.server.tool(
|
||||
"update_user",
|
||||
"Update a user's information",
|
||||
{
|
||||
id: z.number().int().positive().describe("User ID to update"),
|
||||
name: z.string().optional().describe("New name (optional)"),
|
||||
email: z.string().email().optional().describe("New email (optional)"),
|
||||
},
|
||||
async ({ id, name, email }) => {
|
||||
try {
|
||||
// Build dynamic UPDATE query based on provided fields
|
||||
const updates: string[] = [];
|
||||
const values: (string | number)[] = [];
|
||||
|
||||
if (name !== undefined) {
|
||||
updates.push("name = ?");
|
||||
values.push(name);
|
||||
}
|
||||
if (email !== undefined) {
|
||||
updates.push("email = ?");
|
||||
values.push(email);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "No fields to update. Provide name or email.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Add ID to values array
|
||||
values.push(id);
|
||||
|
||||
// Execute UPDATE
|
||||
const result = await this.env.DB.prepare(
|
||||
`UPDATE users SET ${updates.join(", ")} WHERE id = ?`
|
||||
)
|
||||
.bind(...values)
|
||||
.run();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error("Failed to update user");
|
||||
}
|
||||
|
||||
// Fetch updated user
|
||||
const updatedUser = await this.env.DB.prepare(
|
||||
"SELECT * FROM users WHERE id = ?"
|
||||
)
|
||||
.bind(id)
|
||||
.first<User>();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `User updated successfully!\n\n${JSON.stringify(updatedUser, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.message.includes("UNIQUE constraint failed")) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: Email "${email}" is already in use.`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error updating user: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// TOOL 5: Delete User
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Demonstrates: DELETE with confirmation
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
this.server.tool(
|
||||
"delete_user",
|
||||
"Delete a user from the database (⚠️ permanent!)",
|
||||
{
|
||||
id: z.number().int().positive().describe("User ID to delete"),
|
||||
},
|
||||
async ({ id }) => {
|
||||
try {
|
||||
// Get user before deleting (for confirmation message)
|
||||
const user = await this.env.DB.prepare(
|
||||
"SELECT * FROM users WHERE id = ?"
|
||||
)
|
||||
.bind(id)
|
||||
.first<User>();
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `User with ID ${id} not found.`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Delete user
|
||||
const result = await this.env.DB.prepare(
|
||||
"DELETE FROM users WHERE id = ?"
|
||||
)
|
||||
.bind(id)
|
||||
.run();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error("Failed to delete user");
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `User deleted successfully!\n\nDeleted: ${user.name} (${user.email})`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error deleting user: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// TOOL 6: Search Users
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Demonstrates: LIKE queries for text search
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
this.server.tool(
|
||||
"search_users",
|
||||
"Search users by name or email",
|
||||
{
|
||||
query: z.string().describe("Search term (name or email)"),
|
||||
},
|
||||
async ({ query }) => {
|
||||
try {
|
||||
const searchPattern = `%${query}%`;
|
||||
|
||||
const { results: users } = await this.env.DB.prepare(
|
||||
"SELECT * FROM users WHERE name LIKE ? OR email LIKE ? ORDER BY created_at DESC"
|
||||
)
|
||||
.bind(searchPattern, searchPattern)
|
||||
.all<User>();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(
|
||||
{
|
||||
query,
|
||||
results: users.length,
|
||||
users,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error searching users: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Worker fetch handler
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
* 🔧 SETUP CHECKLIST
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 1. Create D1 database:
|
||||
* npx wrangler d1 create my-database
|
||||
*
|
||||
* 2. Note the database_id from output
|
||||
*
|
||||
* 3. Add to wrangler.jsonc:
|
||||
* {
|
||||
* "d1_databases": [{
|
||||
* "binding": "DB",
|
||||
* "database_name": "my-database",
|
||||
* "database_id": "YOUR_ID_HERE"
|
||||
* }]
|
||||
* }
|
||||
*
|
||||
* 4. Create schema:
|
||||
* npx wrangler d1 execute my-database --local --command \
|
||||
* "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"
|
||||
*
|
||||
* 5. Deploy:
|
||||
* npx wrangler deploy
|
||||
*
|
||||
* 6. Client URL:
|
||||
* "url": "https://YOUR-WORKER.workers.dev/sse"
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*/
|
||||
export default {
|
||||
async fetch(
|
||||
request: Request,
|
||||
env: Env,
|
||||
ctx: ExecutionContext
|
||||
): Promise<Response> {
|
||||
const { pathname } = new URL(request.url);
|
||||
|
||||
// Handle CORS preflight
|
||||
if (request.method === "OPTIONS") {
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||
"Access-Control-Max-Age": "86400",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// SSE transport
|
||||
if (pathname.startsWith("/sse")) {
|
||||
return MyMCP.serveSSE("/sse").fetch(request, env, ctx);
|
||||
}
|
||||
|
||||
// HTTP transport
|
||||
if (pathname.startsWith("/mcp")) {
|
||||
return MyMCP.serve("/mcp").fetch(request, env, ctx);
|
||||
}
|
||||
|
||||
// Health check with DB binding info
|
||||
if (pathname === "/" || pathname === "/health") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
name: "D1 Database MCP Server",
|
||||
version: "1.0.0",
|
||||
transports: {
|
||||
sse: "/sse",
|
||||
http: "/mcp",
|
||||
},
|
||||
features: {
|
||||
database: !!env.DB,
|
||||
},
|
||||
tools: [
|
||||
"create_user",
|
||||
"get_user",
|
||||
"list_users",
|
||||
"update_user",
|
||||
"delete_user",
|
||||
"search_users",
|
||||
],
|
||||
status: "ok",
|
||||
timestamp: new Date().toISOString(),
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return new Response("Not Found", { status: 404 });
|
||||
},
|
||||
};
|
||||
325
templates/mcp-with-workers-ai.ts
Normal file
325
templates/mcp-with-workers-ai.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* MCP Server with Workers AI Integration
|
||||
*
|
||||
* Demonstrates Workers AI integration for image and text generation.
|
||||
* Shows how to use AI binding in MCP tools.
|
||||
*
|
||||
* Based on: https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-github-oauth
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
* 🤖 WORKERS AI INTEGRATION
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*
|
||||
* This template shows:
|
||||
* 1. AI binding configuration (wrangler.jsonc)
|
||||
* 2. Image generation with Flux
|
||||
* 3. Text generation with Llama
|
||||
* 4. Error handling for AI requests
|
||||
* 5. Streaming responses
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
* 📋 REQUIRED CONFIGURATION
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*
|
||||
* wrangler.jsonc:
|
||||
* {
|
||||
* "ai": {
|
||||
* "binding": "AI"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* No API keys required! Workers AI is built into Cloudflare Workers.
|
||||
*
|
||||
* Pricing: https://developers.cloudflare.com/workers-ai/platform/pricing/
|
||||
* - Free tier: 10,000 Neurons per day
|
||||
* - Image generation: ~500 Neurons per image
|
||||
* - Text generation: Varies by token count
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import { McpAgent } from "agents/mcp";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
|
||||
type Env = {
|
||||
AI: Ai; // Workers AI binding (configured in wrangler.jsonc)
|
||||
};
|
||||
|
||||
/**
|
||||
* MCP Server with Workers AI tools
|
||||
*/
|
||||
export class MyMCP extends McpAgent<Env> {
|
||||
server = new McpServer({
|
||||
name: "Workers AI MCP Server",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
async init() {
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// TOOL 1: Generate Image with Flux
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Model: @cf/black-forest-labs/flux-1-schnell
|
||||
// Fast image generation model (2-4 seconds)
|
||||
// Input: Text prompt → Output: Base64-encoded PNG
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
this.server.tool(
|
||||
"generate_image",
|
||||
"Generate an image from a text prompt using Flux AI model",
|
||||
{
|
||||
prompt: z
|
||||
.string()
|
||||
.describe("Detailed description of the image to generate"),
|
||||
num_steps: z
|
||||
.number()
|
||||
.min(1)
|
||||
.max(8)
|
||||
.default(4)
|
||||
.optional()
|
||||
.describe("Number of inference steps (1-8, default 4). Higher = better quality but slower"),
|
||||
},
|
||||
async ({ prompt, num_steps = 4 }) => {
|
||||
try {
|
||||
// Call Workers AI
|
||||
const response = await this.env.AI.run(
|
||||
"@cf/black-forest-labs/flux-1-schnell",
|
||||
{
|
||||
prompt,
|
||||
num_steps,
|
||||
}
|
||||
);
|
||||
|
||||
// Response is a base64-encoded PNG image
|
||||
const imageBase64 = (response as { image: string }).image;
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Generated image from prompt: "${prompt}"`,
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
data: imageBase64,
|
||||
mimeType: "image/png",
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error generating image: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// TOOL 2: Generate Text with Llama
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Model: @cf/meta/llama-3.1-8b-instruct
|
||||
// Fast text generation model
|
||||
// Input: User message → Output: AI-generated text
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
this.server.tool(
|
||||
"generate_text",
|
||||
"Generate text using Llama AI model",
|
||||
{
|
||||
prompt: z.string().describe("The prompt or question for the AI"),
|
||||
max_tokens: z
|
||||
.number()
|
||||
.min(1)
|
||||
.max(2048)
|
||||
.default(512)
|
||||
.optional()
|
||||
.describe("Maximum number of tokens to generate (default 512)"),
|
||||
},
|
||||
async ({ prompt, max_tokens = 512 }) => {
|
||||
try {
|
||||
// Call Workers AI
|
||||
const response = await this.env.AI.run(
|
||||
"@cf/meta/llama-3.1-8b-instruct",
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
max_tokens,
|
||||
}
|
||||
);
|
||||
|
||||
// Extract generated text
|
||||
const text = (response as { response: string }).response;
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error generating text: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// TOOL 3: List Available AI Models
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Shows all models available in Workers AI
|
||||
// Useful for discovering what's available
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
this.server.tool(
|
||||
"list_ai_models",
|
||||
"List all available Workers AI models",
|
||||
{},
|
||||
async () => {
|
||||
// This is a static list, but you could dynamically fetch from CF API
|
||||
const models = [
|
||||
{
|
||||
name: "@cf/black-forest-labs/flux-1-schnell",
|
||||
type: "Image Generation",
|
||||
description: "Fast image generation (2-4s)",
|
||||
},
|
||||
{
|
||||
name: "@cf/meta/llama-3.1-8b-instruct",
|
||||
type: "Text Generation",
|
||||
description: "Fast text generation",
|
||||
},
|
||||
{
|
||||
name: "@cf/meta/llama-3.1-70b-instruct",
|
||||
type: "Text Generation",
|
||||
description: "High-quality text generation (slower)",
|
||||
},
|
||||
{
|
||||
name: "@cf/openai/whisper",
|
||||
type: "Speech Recognition",
|
||||
description: "Audio transcription",
|
||||
},
|
||||
{
|
||||
name: "@cf/baai/bge-base-en-v1.5",
|
||||
type: "Text Embeddings",
|
||||
description: "Generate embeddings for semantic search",
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(models, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Worker fetch handler
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
* 🔧 CONFIGURATION NOTES
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 1. AI Binding Setup (wrangler.jsonc):
|
||||
* {
|
||||
* "ai": {
|
||||
* "binding": "AI"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* 2. Deploy:
|
||||
* npx wrangler deploy
|
||||
*
|
||||
* 3. Test Tools:
|
||||
* - generate_image: Creates PNG images from prompts
|
||||
* - generate_text: Generates text responses
|
||||
* - list_ai_models: Shows available models
|
||||
*
|
||||
* 4. Client URL:
|
||||
* "url": "https://YOUR-WORKER.workers.dev/sse"
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════
|
||||
*/
|
||||
export default {
|
||||
async fetch(
|
||||
request: Request,
|
||||
env: Env,
|
||||
ctx: ExecutionContext
|
||||
): Promise<Response> {
|
||||
const { pathname } = new URL(request.url);
|
||||
|
||||
// Handle CORS preflight
|
||||
if (request.method === "OPTIONS") {
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||
"Access-Control-Max-Age": "86400",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// SSE transport
|
||||
if (pathname.startsWith("/sse")) {
|
||||
return MyMCP.serveSSE("/sse").fetch(request, env, ctx);
|
||||
}
|
||||
|
||||
// HTTP transport
|
||||
if (pathname.startsWith("/mcp")) {
|
||||
return MyMCP.serve("/mcp").fetch(request, env, ctx);
|
||||
}
|
||||
|
||||
// Health check with AI binding info
|
||||
if (pathname === "/" || pathname === "/health") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
name: "Workers AI MCP Server",
|
||||
version: "1.0.0",
|
||||
transports: {
|
||||
sse: "/sse",
|
||||
http: "/mcp",
|
||||
},
|
||||
features: {
|
||||
ai: !!env.AI,
|
||||
},
|
||||
models: [
|
||||
"flux-1-schnell (image generation)",
|
||||
"llama-3.1-8b-instruct (text generation)",
|
||||
],
|
||||
status: "ok",
|
||||
timestamp: new Date().toISOString(),
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return new Response("Not Found", { status: 404 });
|
||||
},
|
||||
};
|
||||
24
templates/package.json
Normal file
24
templates/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "my-mcp-server",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wrangler dev",
|
||||
"deploy": "wrangler deploy",
|
||||
"test": "npx @modelcontextprotocol/inspector"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20250122.0",
|
||||
"wrangler": "^3.103.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.21.0",
|
||||
"agents": "^0.2.20",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@cloudflare/workers-oauth-provider": "^0.0.13",
|
||||
"@octokit/rest": "^21.0.2"
|
||||
}
|
||||
}
|
||||
44
templates/wrangler-basic.jsonc
Normal file
44
templates/wrangler-basic.jsonc
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Basic MCP Server Configuration (No Authentication)
|
||||
*
|
||||
* This configuration supports a simple MCP server without:
|
||||
* - Authentication
|
||||
* - Durable Objects
|
||||
* - External bindings
|
||||
*
|
||||
* Perfect for: Internal tools, development, public APIs
|
||||
*/
|
||||
{
|
||||
"name": "my-mcp-server",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-01-01",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
|
||||
/**
|
||||
* Account ID (required for deployment)
|
||||
* Get your account ID: npx wrangler whoami
|
||||
*/
|
||||
"account_id": "YOUR_ACCOUNT_ID_HERE",
|
||||
|
||||
/**
|
||||
* Environment variables
|
||||
* Use .dev.vars for local secrets
|
||||
*/
|
||||
"vars": {
|
||||
"ENVIRONMENT": "production"
|
||||
},
|
||||
|
||||
/**
|
||||
* Node.js compatibility
|
||||
* Required for @modelcontextprotocol/sdk
|
||||
*/
|
||||
"node_compat": true,
|
||||
|
||||
/**
|
||||
* Custom domains (optional)
|
||||
* Replace with your domain
|
||||
*/
|
||||
// "routes": [
|
||||
// { "pattern": "mcp.example.com", "custom_domain": true }
|
||||
// ]
|
||||
}
|
||||
90
templates/wrangler-oauth.jsonc
Normal file
90
templates/wrangler-oauth.jsonc
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* MCP Server with OAuth Configuration
|
||||
*
|
||||
* This configuration supports:
|
||||
* - OAuth authentication via workers-oauth-provider
|
||||
* - KV namespace for token storage
|
||||
* - Durable Objects for stateful sessions (optional)
|
||||
* - Environment variables for OAuth credentials
|
||||
*
|
||||
* Perfect for: GitHub, Google, Azure OAuth integrations
|
||||
*/
|
||||
{
|
||||
"name": "my-mcp-oauth-server",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-01-01",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
|
||||
/**
|
||||
* Account ID (required for deployment)
|
||||
* Get your account ID: npx wrangler whoami
|
||||
*/
|
||||
"account_id": "YOUR_ACCOUNT_ID_HERE",
|
||||
|
||||
/**
|
||||
* Environment variables
|
||||
* IMPORTANT: Never commit secrets to version control!
|
||||
* Use .dev.vars for local development
|
||||
*/
|
||||
"vars": {
|
||||
"ENVIRONMENT": "production",
|
||||
|
||||
/**
|
||||
* Optional: Pre-configured OAuth client credentials
|
||||
* If not set, Dynamic Client Registration is used
|
||||
*/
|
||||
// "GITHUB_CLIENT_ID": "your-client-id",
|
||||
// "GOOGLE_CLIENT_ID": "your-client-id",
|
||||
},
|
||||
|
||||
/**
|
||||
* KV namespace for OAuth token storage
|
||||
* REQUIRED for workers-oauth-provider
|
||||
*
|
||||
* Create KV namespace: npx wrangler kv namespace create OAUTH_KV
|
||||
*/
|
||||
"kv_namespaces": [
|
||||
{
|
||||
"binding": "OAUTH_KV",
|
||||
"id": "YOUR_KV_NAMESPACE_ID_HERE",
|
||||
"preview_id": "YOUR_PREVIEW_KV_NAMESPACE_ID_HERE"
|
||||
}
|
||||
],
|
||||
|
||||
/**
|
||||
* Durable Objects configuration (optional, for stateful servers)
|
||||
* Uncomment if your MCP server needs persistent state
|
||||
*/
|
||||
// "durable_objects": {
|
||||
// "bindings": [
|
||||
// {
|
||||
// "name": "MY_MCP",
|
||||
// "class_name": "MyMCP",
|
||||
// "script_name": "my-mcp-oauth-server"
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
|
||||
/**
|
||||
* Durable Objects migrations (required on first deployment if using DOs)
|
||||
*/
|
||||
// "migrations": [
|
||||
// {
|
||||
// "tag": "v1",
|
||||
// "new_classes": ["MyMCP"]
|
||||
// }
|
||||
// ],
|
||||
|
||||
/**
|
||||
* Node.js compatibility
|
||||
* Required for @modelcontextprotocol/sdk and OAuth libraries
|
||||
*/
|
||||
"node_compat": true,
|
||||
|
||||
/**
|
||||
* Custom domains (optional)
|
||||
*/
|
||||
// "routes": [
|
||||
// { "pattern": "mcp.example.com", "custom_domain": true }
|
||||
// ]
|
||||
}
|
||||
Reference in New Issue
Block a user