Skip to main content
Glama
TedTschopp

MCP Server on Cloudflare Workers & Azure Functions

by TedTschopp
index.ts18.6 kB
/** * MCP Server on Cloudflare Workers * * This module implements a Model Context Protocol (MCP) server that can be deployed * to Cloudflare Workers. It provides both tools and prompts that can be accessed * via the MCP protocol or HTTP endpoints. * * @module index * @see https://modelcontextprotocol.io for MCP specification * @see https://developers.cloudflare.com/workers/ for Cloudflare Workers docs */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListResourceTemplatesRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { PROMPTS, generatePrompt } from './prompts.js'; import { RESOURCES, RESOURCE_TEMPLATES, generateResourceContent } from './resources.js'; /** * MCPServer class * * Main server implementation that handles MCP protocol requests for both * tools and prompts. This class is used when running the server in stdio * mode for local testing or direct MCP client connections. * * @class * @example * ```typescript * const server = new MCPServer(); * await server.run(); * ``` */ class MCPServer { /** The MCP Server instance from the SDK */ private server: Server; /** * Creates a new MCP server instance * * Initializes the server with metadata and capabilities, then sets up * all request handlers for tools and prompts. * * @constructor */ constructor() { // Initialize the MCP server with metadata this.server = new Server( { name: 'cloudflare-mcp-server', version: '1.0.0', }, { // Declare supported capabilities capabilities: { // Server features - what this server provides tools: { listChanged: true, // Can notify when tools change }, prompts: { listChanged: true, // Can notify when prompts change }, resources: { subscribe: true, // Clients can subscribe to resource changes listChanged: true, // Can notify when resources change }, // Logging support logging: {}, }, } ); // Set up all request handlers this.setupHandlers(); } /** * Sets up all MCP request handlers * * Configures handlers for: * - ListPromptsRequestSchema: Returns available prompt templates * - GetPromptRequestSchema: Generates a specific prompt with arguments * - ListToolsRequestSchema: Returns available tools and their schemas * - CallToolRequestSchema: Executes a tool with provided arguments * * @private */ private setupHandlers() { // ==================== PROMPT HANDLERS ==================== /** * Handler: List all available prompts * * Returns the list of prompt templates that can be invoked by MCP clients. * Prompts are defined in the prompts.ts file and imported here. * * @returns Promise resolving to object containing prompts array */ this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: PROMPTS, })); /** * Handler: Get a specific prompt * * Generates a prompt message based on the prompt name and provided arguments. * The actual prompt generation logic is delegated to the generatePrompt function * from prompts.ts for better separation of concerns. * * @param request - The MCP request containing prompt name and arguments * @returns Promise resolving to prompt messages * @throws Error if prompt name is unknown */ this.server.setRequestHandler(GetPromptRequestSchema, async (request) => { const { name, arguments: args } = request.params; return generatePrompt(name, args as Record<string, string>); }); // ==================== RESOURCE HANDLERS ==================== /** * Handler: List all available resources * * Resources provide contextual information to AI applications. * This example includes static resources, but in practice these could * be dynamically generated based on server state, database contents, etc. * * @returns Promise resolving to object containing resources array */ this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: RESOURCES, })); /** * Handler: Read a specific resource * * Retrieves the contents of a resource identified by its URI. * Resources can return text or binary (base64) content. * * @param request - The MCP request containing resource URI * @returns Promise resolving to resource contents * @throws Error if resource URI is unknown */ this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; // Find the resource to get its MIME type const resource = RESOURCES.find(r => r.uri === uri); if (!resource) { throw new Error(`Unknown resource: ${uri}`); } // Generate the content for this resource const content = generateResourceContent(uri); return { contents: [ { uri, mimeType: resource.mimeType, text: content, }, ], }; }); /** * Handler: List resource templates * * Resource templates allow parameterized resources using URI templates. * Clients can fill in template parameters to access dynamic resources. * * @returns Promise resolving to object containing resource templates array */ this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: RESOURCE_TEMPLATES, })); /** * Handler: Subscribe to resource updates * * Allows clients to subscribe to notifications when a specific resource changes. * In a real implementation, this would track subscriptions and send notifications * when resources are updated. * * @param request - The MCP request containing resource URI to subscribe to * @returns Promise resolving to empty object (acknowledgment) */ this.server.setRequestHandler(SubscribeRequestSchema, async (request) => { const { uri } = request.params; // In a production implementation, you would store subscriptions // and send notifications/resources/updated when the resource changes console.log(`Client subscribed to resource: ${uri}`); return {}; }); /** * Handler: Unsubscribe from resource updates * * Allows clients to unsubscribe from resource update notifications. * * @param request - The MCP request containing resource URI to unsubscribe from * @returns Promise resolving to empty object (acknowledgment) */ this.server.setRequestHandler(UnsubscribeRequestSchema, async (request) => { const { uri } = request.params; // In a production implementation, you would remove the subscription console.log(`Client unsubscribed from resource: ${uri}`); return {}; }); /** * Handler: Set logging level * * Allows clients to control the verbosity of server logging. * Supported levels: debug, info, notice, warning, error, critical, alert, emergency * * @param request - The MCP request containing desired log level * @returns Promise resolving to empty object (acknowledgment) */ this.server.setRequestHandler(SetLevelRequestSchema, async (request) => { const { level } = request.params; // In a production implementation, you would update the logging level console.log(`Logging level set to: ${level}`); return {}; }); // ==================== TOOL HANDLERS ==================== /** * Handler: List all available tools * * Returns metadata for all tools including their names, descriptions, * and input schemas. Each tool's schema follows JSON Schema format * to define expected parameters and their types. * * @returns Promise resolving to object containing tools array */ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ // Tool: get_time // Returns the current server time in ISO 8601 format // No parameters required { name: 'get_time', description: 'Get the current server time', inputSchema: { type: 'object', properties: {}, }, }, // Tool: echo // Simple echo tool that returns the provided message // Useful for testing MCP connectivity { name: 'echo', description: 'Echo back a message', inputSchema: { type: 'object', properties: { message: { type: 'string', description: 'Message to echo', }, }, required: ['message'], }, }, // Tool: add // Performs addition of two numbers // Demonstrates parameter validation and numeric operations { name: 'add', description: 'Add two numbers together', inputSchema: { type: 'object', properties: { a: { type: 'number', description: 'First number', }, b: { type: 'number', description: 'Second number', }, }, required: ['a', 'b'], }, }, ], })); /** * Handler: Execute a tool * * Processes tool execution requests by routing to the appropriate tool * implementation based on the tool name. Each tool validates its inputs * and returns results in the MCP content format. * * @param request - The MCP request containing tool name and arguments * @returns Promise resolving to tool execution result * @throws Error if tool is unknown or arguments are invalid */ this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; // Route to appropriate tool implementation switch (name) { // Tool Implementation: get_time // Returns current server time in ISO 8601 format case 'get_time': return { content: [ { type: 'text', text: `Current server time: ${new Date().toISOString()}`, }, ], }; // Tool Implementation: echo // Validates that message argument is provided and is a string, // then returns it with an "Echo:" prefix case 'echo': if (!args || typeof args.message !== 'string') { throw new Error('Invalid arguments: message is required'); } return { content: [ { type: 'text', text: `Echo: ${args.message}`, }, ], }; // Tool Implementation: add // Validates that both arguments are numbers, then performs addition case 'add': if (!args || typeof args.a !== 'number' || typeof args.b !== 'number') { throw new Error('Invalid arguments: a and b must be numbers'); } return { content: [ { type: 'text', text: `Result: ${args.a + args.b}`, }, ], }; // Unknown tool error default: throw new Error(`Unknown tool: ${name}`); } }); } /** * Starts the MCP server with stdio transport * * This method is used for local testing and direct MCP client connections. * It establishes a stdio-based transport layer for communication using * the MCP protocol over standard input/output streams. * * @async * @throws Error if connection fails * * @example * ```typescript * const server = new MCPServer(); * await server.run(); * ``` */ async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); } } // ==================== CLOUDFLARE WORKERS EXPORT ==================== /** * Cloudflare Workers default export * * This object handles all HTTP requests to the Worker. It provides: * - Health check endpoint for monitoring * - MCP protocol endpoint for HTTP-based MCP communication * - CORS support for cross-origin requests * - Default informational endpoint * * @see https://developers.cloudflare.com/workers/runtime-apis/fetch-event/ */ export default { /** * Fetch handler for Cloudflare Workers * * Main entry point for all HTTP requests to the Worker. Routes requests * to appropriate handlers based on pathname and HTTP method. * * @param request - The incoming HTTP request * @param env - Environment bindings (KV, D1, secrets, etc.) * @param ctx - Execution context for waitUntil and passThroughOnException * @returns Promise resolving to HTTP response * * @example * ```bash * # Health check * curl https://your-worker.workers.dev/health * * # MCP protocol * curl -X POST https://your-worker.workers.dev/mcp \ * -H "Content-Type: application/json" \ * -d '{"method": "tools/list"}' * ``` */ async fetch(request: Request, env: any, ctx: any): Promise<Response> { const url = new URL(request.url); // ==================== ENDPOINT: /health ==================== /** * Health check endpoint * * Returns server status and version information. Used for: * - Monitoring and uptime checks * - Deployment verification * - Load balancer health probes * * @returns 200 OK with JSON status object */ if (url.pathname === '/health') { return new Response( JSON.stringify({ status: 'ok', timestamp: new Date().toISOString(), version: '1.0.0', }), { headers: { 'Content-Type': 'application/json' }, status: 200, } ); } // ==================== ENDPOINT: /mcp ==================== /** * MCP protocol endpoint * * Accepts POST requests with MCP protocol messages. This endpoint * allows HTTP-based MCP communication for clients that don't support * stdio transport. Currently returns acknowledgment; full MCP protocol * handling can be added here. * * Expected request body format: * ```json * { * "method": "tools/list" | "tools/call" | "prompts/list" | "prompts/get", * "params": { ... } * } * ``` * * @returns 200 OK with MCP response, or 400 Bad Request for invalid JSON */ if (url.pathname === '/mcp' && request.method === 'POST') { try { const body = await request.json(); // TODO: Implement full MCP protocol handling here // For now, this acknowledges receipt of MCP messages return new Response( JSON.stringify({ message: 'MCP server running on Cloudflare Workers', timestamp: new Date().toISOString(), request: body, }), { headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', }, } ); } catch (error) { // Handle JSON parsing errors return new Response( JSON.stringify({ error: 'Invalid JSON', message: error instanceof Error ? error.message : 'Unknown error', }), { headers: { 'Content-Type': 'application/json' }, status: 400, } ); } } // ==================== CORS PREFLIGHT ==================== /** * CORS preflight handler * * Responds to OPTIONS requests for Cross-Origin Resource Sharing (CORS). * Allows the server to be accessed from web browsers across different origins. * * @returns 200 OK with CORS headers */ if (request.method === 'OPTIONS') { return new Response(null, { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }, }); } // ==================== DEFAULT ENDPOINT ==================== /** * Default informational endpoint * * Returns server information and available endpoints for any unmatched routes. * Useful for discovering the API surface and verifying deployment. * * @returns 200 OK with server metadata */ return new Response( JSON.stringify({ name: 'Cloudflare MCP Server', version: '1.0.0', endpoints: { health: '/health', mcp: '/mcp (POST)', }, }), { headers: { 'Content-Type': 'application/json' }, } ); }, }; // ==================== LOCAL TESTING BOOTSTRAP ==================== /** * Local testing entry point * * This code runs when the file is executed directly with Node.js, enabling * local testing of the MCP server using stdio transport. This is useful for: * - Testing MCP protocol implementation without deploying * - Debugging tool and prompt handlers * - Direct integration with MCP clients like Claude Desktop * * IMPORTANT: This code does NOT run in Cloudflare Workers. It only executes * in Node.js environments when this file is the main entry point. * * @example * ```bash * # Run locally for testing * node --loader ts-node/esm src/index.ts * ``` * * @see README.md for more information on local testing */ // @ts-ignore - Node.js specific code not available in Workers runtime if (typeof process !== 'undefined' && import.meta.url === `file://${process.argv[1]}`) { const server = new MCPServer(); server.run().catch(console.error); }

Latest Blog Posts

MCP directory API

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

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TedTschopp/MCP-Server-on-CloudFlare'

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