/**
* 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);
}