The press · AI Agent Development · filed 2026-05-14 · updated 2026-07-10
Build an MCP Server in a Weekend (Without the Production Surprises)
A working MCP server on Cloudflare Workers in two days. Tools, resources, prompts, OAuth, and the production hardening most weekend tutorials skip.
The problem
An MCP server is a small program that exposes tools, resources, and prompts to an AI client over JSON-RPC. By Sunday evening you can have one running on Cloudflare Workers, secured with OAuth, and reachable from Claude Desktop, ChatGPT, Gemini, Copilot, or any other MCP-compatible client. The protocol is one year old, has surpassed 8 million SDK downloads as of April 2025, and roughly 28% of Fortune 500 companies have deployed servers into their AI stacks.
Most weekend tutorials get you to “hello world on stdio” and stop. That code is useful for an afternoon. It breaks the moment a real client connects over Streamable HTTP, refuses authentication, hits a rate-limited tool, or asks for a resource that doesn’t exist yet. The interesting work is the rest of the weekend — the part that turns a localhost demo into a server you can actually point at Claude Desktop in front of a customer.
This walks through that part: the three primitives that matter, the OAuth wiring that survives a real client, the rate-limiting pattern that prevents your free-tier Worker from being abused, and the deployment steps that put the whole thing on the global Cloudflare edge for $0 a month.
What most people get wrong
Mistake one: treating MCP as another API framework. MCP isn’t a thinner OpenAPI. It’s a control-plane definition. Tools, resources, and prompts are not three flavors of endpoint — they map to three different actors:
- Tools are model-controlled. The AI decides when to call them based on conversation context.
- Resources are application-controlled. The client (Claude Desktop, an agent runtime, your own UI) decides which resources to expose to the model.
- Prompts are user-controlled. The human selects which prompt template to activate.
If you treat all three as “endpoints the AI can call,” the surface gets confused. You end up with tools that should have been resources (read-only data exposed as a side-effecting action), resources that should have been prompts (workflow templates returned as inert JSON), and prompts that get ignored because nobody knows to invoke them. The three primitives correspond to three different control planes; designing them as one collapses the abstraction the protocol depends on.
Mistake two: shipping stdio when you needed Streamable HTTP. The original MCP specification used stdio (the server runs as a subprocess of the client) and Server-Sent Events for remote servers. The June 2025 specification replaced SSE with Streamable HTTP — a single endpoint that supports bidirectional messaging. stdio is still valid for local servers that ship inside a desktop extension. Streamable HTTP is what you want for a server you deploy to Cloudflare Workers and connect to remotely from a half-dozen different clients. Picking stdio when the deployment shape calls for Streamable HTTP means rewriting the transport layer on Sunday morning when you discover Claude Desktop’s “remote server” config requires the HTTP variant.
This article is the short version — Building Your First MCP Server: Zero to Production in a Weekend is the full playbook.
Get the ebook — $19A working approach
The two-day arc:
- Saturday morning — scaffold the project, build the first tool, verify it from the MCP Inspector.
- Saturday afternoon — add resources and prompts. Now you have all three primitives.
- Sunday morning — wire OAuth, add API-key fallback, implement rate limiting and input validation.
- Sunday afternoon — deploy to Cloudflare Workers, connect Claude Desktop, smoke-test end-to-end.
The scaffolding step is short:
npm create mcp-server@latest -- --template cloudflare-workers-auth
cd my-mcp-server
npm install
The first tool is a product search. It demonstrates the inputSchema pattern that every tool needs:
this.server.tool(
"search_products",
"Search the product catalog by keyword or category. " +
"Returns matching products with prices and availability status.",
{
query: {
type: "string",
description: "Search keywords to match against product names and descriptions"
},
category: {
type: "string",
description: "Filter by product category",
enum: ["electronics", "clothing", "food", "home", "office"]
},
inStockOnly: {
type: "boolean",
description: "If true, only return products currently in stock"
}
},
async ({ query, category, inStockOnly }) => {
let results = PRODUCTS;
if (query) {
const q = query.toLowerCase();
results = results.filter(
p => p.name.toLowerCase().includes(q)
|| p.description.toLowerCase().includes(q)
);
}
if (category) results = results.filter(p => p.category === category);
if (inStockOnly) results = results.filter(p => p.inStock);
return {
content: [{
type: "text",
text: JSON.stringify({ count: results.length, products: results }, null, 2)
}]
};
}
);
Two patterns are doing work here. First, the tool returns structured JSON inside a text content block — not a pre-formatted human-readable string. Let the AI parse the JSON and present the result in whatever shape the conversation needs. Pre-formatted text strips the model’s flexibility. Second, the enum on category constrains the model’s input at schema time, so an invalid category never reaches your filter logic.
Once a tool works, the rest of the architecture falls into place. Resources are URI-addressable read-only data:
this.server.resource("config://store", async () => ({
contents: [{
uri: "config://store",
mimeType: "application/json",
text: JSON.stringify({
currency: "USD",
shippingCountries: ["US", "CA", "UK"],
returnWindow: 30
})
}]
}));
Prompts are reusable conversation templates with parameters:
this.server.prompt(
"compare_products",
"Compare two products from the catalog",
{ productIds: { type: "array", items: { type: "string" } } },
({ productIds }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `Compare these products and recommend one: ${productIds.join(", ")}`
}
}]
})
);
OAuth wiring on Cloudflare Workers uses the @modelcontextprotocol/sdk/auth module and a KV namespace to store sessions:
import { OAuthProvider } from "@modelcontextprotocol/sdk/server/oauth.js";
const oauth = new OAuthProvider({
issuer: "https://my-mcp-server.workers.dev",
authorizationEndpoint: "/oauth/authorize",
tokenEndpoint: "/oauth/token",
sessionStore: env.SESSIONS // KV namespace
});
Rate limiting is a sliding window backed by KV — 100 requests per minute per authenticated user is a sane default. Input validation runs on every tool call against the inputSchema (the SDK handles the JSON Schema enforcement; you supplement with business-rule checks).
Deployment is one command:
wrangler deploy
Then in Claude Desktop’s claude_desktop_config.json:
{
"mcpServers": {
"my-store": {
"url": "https://my-mcp-server.workers.dev/mcp",
"transport": "streamable-http"
}
}
}
Restart Claude Desktop. The server appears in the connected-servers list. The tools, resources, and prompts you wrote are now available to the model, the data lives at the edge across 300+ Cloudflare locations, and the whole thing runs inside the Workers free tier.
This article is the short version — Building Your First MCP Server: Zero to Production in a Weekend is the full playbook.
Get the ebook — $19Where this scales
The article above is the spine. The full book covers four more layers:
- Production hardening — structured error handling that doesn’t leak stack traces to the model, idempotency for state-changing tools, a
/healthzendpoint for monitoring, structured logging with request-scoped IDs, and graceful degradation when upstream services are slow. - Multi-tool patterns — domain-organized modules, tool composition (a higher-level tool calls a lower-level tool inside the same server), and the trade-offs between many small tools and fewer larger ones.
- Real data sources — connecting Cloudflare D1 for SQL, R2 for blob storage, KV for cache, and external APIs for data the server doesn’t own.
- Distribution — listing the server on the MCP Registry, listing on agent marketplaces, and the cross-listing pattern that gets your server discovered by client developers, not just end users.
The book is built around what survives production, not what compiles. Every code example has been tested against Claude Desktop, ChatGPT’s MCP support, and Gemini’s MCP support — three different clients with three different quirks. The patterns that work across all three are the ones in the chapters.
Included with the book
mcp-server-starter.ts— a 360-line production-ready MCP server skeleton for Cloudflare Workers: typedEnvbindings for D1, KV, and R2, JSON-RPC request/response types, and starter tool definitions (search,get_details,create_order) with full inputSchema declarations. Replace the example tools with your own business logic.schema.sql— a D1 database schema withitemsandorderstables, an FTS5 full-text search index with sync triggers, indexes on category/price/status/created_at, and an idempotency-key column on orders. Apply withwrangler d1 execute.wrangler.toml— a filled-in wrangler configuration with D1, KV, and R2 bindings pre-declared, plus the secrets list (API_KEY,SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY) the starter expects. Replace the placeholder IDs and deploy.
Get the full picture
Building Your First MCP Server: Zero to Production in a Weekend — everything this article compresses, worked through end to end.
Get the ebook — $19Readers of this also chose
Questions readers ask
Do I need anything Anthropic-specific to build an MCP server?
No. MCP is an open protocol with SDKs in TypeScript, Python, and several other languages. Anthropic shipped the first reference implementation, but the protocol is governed by the Linux Foundation as of 2025 and supported by OpenAI, Google, Microsoft, and other vendors.
Does this run on the Cloudflare Workers free tier?
Yes. The Workers free tier covers the request volume of a typical first-generation MCP server. The KV, D1, and R2 free tiers cover the storage and bindings the starter uses.
I already have an HTTP API. Do I need a separate MCP server?
Usually yes. MCP is built around how models discover and invoke capabilities, so mapping it onto an existing REST API means writing an adapter that handles JSON-RPC framing, capability negotiation, and the tools/resources/prompts split — not a proxy that forwards requests one-to-one.
Does a user see a login screen for OAuth?
The OAuth 2.1 flow shows a one-time consent screen the first time a client connects. After that, the client holds an access token and reuses it for every subsequent call. API-key authentication is a simpler alternative for first-party servers where you control both ends.
What's the difference between a tool, a resource, and a prompt?
Tools are model-controlled — the AI decides when to call them. Resources are application-controlled — the client decides which ones to expose. Prompts are user-controlled — a human selects which template to activate. Treating all three as one kind of endpoint is the most common design mistake in a first server.