You can scaffold an MCP server in an afternoon. You can ship one a model will use well in a week. The gap between those two states is not protocol detail -- the spec is short, the SDK does the heavy lifting -- but four early decisions that compound: which transport, which primitives to expose, how to name them, and where the server runs.

This is the skeleton. If you want the war stories from running fourteen of them in production, that is what MCP in Production is for.

Decision 1: stdio or HTTP?

MCP supports two transports:

Most servers should start stdio. You can wrap a stdio server in an HTTP layer later if the need shows up; you cannot easily revert an HTTP server back to stdio without breaking everyone's install. Start narrow.

Decision 2: tools, resources, or prompts?

MCP exposes three primitive types. They overlap, and the protocol does not stop you from misusing them -- but the model behaves differently for each:

Decision 3: naming and shape

The model picks tools by reading tools/list output. Names and descriptions are the firing predicates. list_orders with a 30-character description is invisible next to create_invoice_for_customer with a useful one.

Three rules pay off:

The tool-list size problem is real: at 40,000 tokens the model degrades on selection. See the schema design guide for the patterns that keep the surface small as the server grows.

Decision 4: where it runs

A stdio server can run from npx your-package, from a local binary, or as a launched subprocess your tool ships. An HTTP server needs hosting -- and the hosting choice has cost, latency, auth, and reliability implications. See the hosting guide for the six realistic shapes and when each one fits.

The minimal skeleton

A working Node-based stdio server is ~30 lines using the official SDK:

// server.mjs import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; const server = new Server( { name: "my-server", version: "0.1.0" }, { capabilities: { tools: {} } } ); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [{ name: "echo", description: "Echo the input back. Use when the user wants to test the server.", inputSchema: { type: "object", properties: { message: { type: "string" } }, required: ["message"] } }] })); server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "echo") { return { content: [{ type: "text", text: request.params.arguments.message }] }; } throw new Error("Unknown tool"); }); const transport = new StdioServerTransport(); await server.connect(transport);

Wire "bin" in your package.json so npx your-server works. Ship to npm. Add it to a Claude Code config. The model can now call your tool.

What's not in the skeleton

Auth. Error handling that the model can act on. Pagination for lists that will grow. Idempotency for write tools. Testing that survives a non-deterministic consumer. Hosting if you outgrow stdio. Security review.

Each of these is one chapter of MCP in Production -- the book is what comes after the tutorial works.

MCP in Production

The MCP server book. Twelve chapters from shipping fourteen @yawlabs/* servers. PDF + EPUB. Free updates as the spec moves. $39 one-time, secure checkout.

Read more & buy $39

Published by Yaw Labs.

Related