Building an MCP server in an afternoon
The Model Context Protocol looks intimidating from the outside — a new protocol, a spec, an ecosystem of clients. It’s smaller than it looks. MCP is just a standard way to hand an AI client a set of tools and resources to call. I shipped a working server between lunch and dinner, and the surprising part was how little there is to it.
Strip away the branding and MCP answers two questions a client asks your server: what can you do (tools), and what can you read (resources). That’s most of it.
The shape of a server
A server registers tools (functions the model can invoke, each with a name, a description, and a typed input schema) and resources (addressable data the model can read). The transport is usually stdio for local servers — the client launches your process and talks JSON-RPC over stdin/stdout. You don’t implement the protocol; an SDK does. You implement the tools.
The whole mental model: a typed RPC server whose schema doubles as documentation for a language model. The descriptions aren’t comments — they’re how the model decides when to call you, so they’re load-bearing prose.
A minimal server
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "repo-tools", version: "1.0.0" });
server.tool(
"search_files",
"Search the workspace for files matching a glob. Use before editing.",
{ pattern: z.string().describe("a glob, e.g. src/**/*.ts") },
async ({ pattern }) => ({
content: [{ type: "text", text: (await glob(pattern)).join("\n") }],
}),
);
await server.connect(new StdioServerTransport());
That’s a real, working server. The z schema gives the client a typed contract and gives you validated input for free — parse, don’t trust. Add more server.tool(...) calls and you’ve got a toolkit.
Wiring it to a client
A client (Claude Desktop, an IDE extension, your own agent) is told how to launch the server — a command and args in a small config block — and from then on it lists your tools and calls them when the model decides to. Local dev is just “point the client at node ./server.js.”
{
"mcpServers": {
"repo-tools": { "command": "node", "args": ["./dist/server.js"] },
},
}
What surprised me
- The descriptions are the API. A vague tool description means the model calls your tool at the wrong time or not at all. Writing them well is the actual work, and it’s prompt-engineering wearing a type signature.
- Schemas pay double. The same Zod schema validates input and documents the tool to the model. Strict types at the boundary, again — the theme that follows me everywhere.
- It’s just a server. All the instincts transfer: validate input, keep handlers small, put side effects behind ports so you can test the tool logic without a client attached.
MCP isn’t a new skill so much as an old one with a new socket. If you can write a small typed RPC server, you can write an MCP server — in an afternoon.
I went in expecting to learn a protocol and left having written a handful of well-described functions. Sometimes the intimidating new thing is a familiar thing in a fresh wrapper, and the fastest way to find out is to build the smallest version and see what’s actually hard. Here, almost nothing was.
Filed under dev tools. Built an MCP server with a clever tool surface? Show me.