Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:24:23 +08:00
commit 5f996ee003
23 changed files with 6697 additions and 0 deletions

View 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 });
},
};

View 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"
}
}
}
}

View 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 });
},
};

View 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" },
}
);
},
};

View 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
});

View 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
View 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 });
},
};

View 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
View 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"
}
}

View 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 }
// ]
}

View 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 }
// ]
}