Project setup
First, create a new project and install dependencies:
Install the required dependencies:
And the dev dependencies:
Here's what each package does:
@modelcontextprotocol/sdk: The official MCP SDK for building servers
agents: Cloudflare's Agents SDK for building MCP servers on Workers
zod: Schema validation for tool input parameters
wrangler: Cloudflare's CLI for local development and deployment
TypeScript configuration
Create a tsconfig.json:
Wrangler configuration
Create a wrangler.toml file to configure your Cloudflare Worker:
A few things to note:
- The
rules section above tells Wrangler to bundle any .md files as text, which is useful if you want to include documentation directly in your server
- The Durable Objects configuration gives each MCP server instance its own persistent state
- You can define separate environments (e.g.,
[env.dev] and [env.prod]) if you want isolated deployments for development versus production
The MCP server
Now let's create the server. Create src/index.ts and we'll build it step by step.
Imports and types
Start with the imports and a state interface for your server:
The Agent class provides the foundation for stateful Workers. The createMcpHandler function generates an HTTP handler that speaks the MCP protocol. If you need to persist data across requests, you can add properties to the State interface.
The Agent class
Next, create your server class that extends Agent:
The McpServer instance holds your server's metadata and handles tool and resource registration. Clients see this name and description when they connect.
Important: The description field should guide AI clients on how to use your server effectively. Include which tools to call first, any prerequisites, and the typical workflow sequence. AI clients use this to understand the intended usage pattern, so prioritize the most important information at the beginning.
Registering tools
The onStart() lifecycle method is where you register all your tools and resources.
Here's an example of a tool that fetches component metadata from our API.
First, define the tool metadata with input and output schemas:
Then implement the handler that fetches and returns the metadata:
Tools have three parts: a name, metadata with input/output schemas, and an async handler. The outputSchema defines the structure of structuredContent, which AI clients can parse programmatically alongside the human-readable content.
This example demonstrates the basic pattern for tools that fetch external data: validate input, make the API request, handle errors with isError: true, and return both human-readable text and structured data.
Tool description best practices: Keep descriptions concise (1-2 sentences) and prioritize the most important information first. Specify when/why to use the tool, any prerequisites, and its place in the workflow. This helps AI clients understand the intended usage pattern.
Registering resources
Resources expose data that clients can read on demand. Extend the onStart() method to also register resources:
Resources are identified by a custom URI scheme (like mdc://docs/components). Unlike tools which AI calls on demand, resources are typically read once when the client needs reference material. Resource descriptions work the same way: be concise, specify when to use the resource, and mention any prerequisites or workflow context.
Handling MCP requests
Add a method to your MyMcpServer class to handle incoming MCP protocol requests:
The createMcpHandler function takes your McpServer instance and returns an HTTP handler that implements the MCP protocol. This handles the bidirectional messaging between clients and your server.
The fetch handler
Finally, export a default handler that routes requests to your MCP server:
The getAgentByName function retrieves (or creates) a Durable Object instance for each session. This gives each connected client its own isolated state. The session ID is in the mcp-session-id header, or you can generate one for new connections.
What's happening under the hood
To summarize the architecture:
- Agent class: Extends
Agent from the Cloudflare Agents SDK, giving you access to Durable Object state and lifecycle hooks
- McpServer instance: Holds your server metadata and registered tools/resources
- onStart() lifecycle: Called when the Agent initializes, perfect for registering capabilities
- createMcpHandler(): Generates an HTTP handler that implements the MCP protocol
- getAgentByName(): Retrieves the Agent instance by session ID, enabling per-client state
Adding persistent session state
The basic example above works well for stateless servers, but production MCP servers often need to persist session state across requests. For this, you can add a WorkerTransport with Durable Object storage.
First, update your imports:
Then add a transport instance to your Agent class:
Finally, update your onMcpRequest() method to pass the transport to createMcpHandler:
The WorkerTransport uses your Durable Object's storage to persist session state between requests. This enables advanced MCP features like elicitation (asking the user for input) and sampling (requesting AI completions from the client). For most servers, the basic stateless approach is sufficient, but adding WorkerTransport gives you the full capabilities of the MCP protocol.
Adding more sophisticated tools
Tools can also perform complex operations like parsing and validation. Here's a MDC validator that checks for syntax errors, validates component structures, and ensures nested components are properly formed.
First, define the tool metadata with schemas for validation results:
Then implement the validation logic using a stack to track nested components:
This validator demonstrates more sophisticated tool logic:
- Uses a stack to track nested components and ensure proper nesting
- Validates that opening and closing tags have matching colon counts
- Returns structured error data with specific line numbers
- Provides both human-readable summaries and programmatic access via
structuredContent
Organizing tools and resources
As your MCP server grows, you'll want to organize tools and resources into separate files. A pattern that works well is creating a Registrable interface:
Then each tool or resource becomes its own class:
And in your main file:
This keeps your code organized and makes it easy to add or remove functionality.
Seeing it in action
Once your MCP server is connected to a client like Claude or Cursor, here's what happens. A user might ask:
The AI client automatically reads your component_docs resource to learn about page-hero and card components, generates the MDC content, and then calls your validate_mdc_syntax tool to ensure the syntax is correct before presenting it to the user. The user never needs to know which tools were called or how the MCP protocol works behind the scenes.