Build your own MCP server in an evening
Model Context Protocol is the way to give Claude access to your own tools. Not just functions in a prompt, but a full server that plugs into any MCP-compatible client: Claude Desktop, Claude Code, Cursor, and others.
An evening is enough to ship one for a real task. Step by step.
Why you'd want this
Simple example. A company has an internal knowledge base: Confluence, Notion, or something homegrown. You want Claude to search and read it without you copy-pasting every article into chat. Solution: an MCP server that wraps your base's API and hands it to Claude.
Another example — local scripts. You have a set of utilities: run tests a specific way, build the project with custom flags, run a linter with a non-standard config. MCP packages those into tools Claude can call directly.
The minimal server structure
TypeScript and the official SDK.
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
index.ts:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{ name: "my-tools", version: "0.1.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "search_docs",
description: "Search internal documentation",
inputSchema: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
if (req.params.name === "search_docs") {
const q = (req.params.arguments as { query: string }).query;
const results = await doActualSearch(q);
return { content: [{ type: "text", text: results }] };
}
throw new Error("Unknown tool");
});
const transport = new StdioServerTransport();
await server.connect(transport);
That's the whole working server. It runs as a stdio process — the client talks to it over stdin/stdout in JSON-RPC.
Wiring it into Claude Code
In ~/.config/claude-code/mcp_settings.json:
{
"mcpServers": {
"my-tools": {
"command": "node",
"args": ["/absolute/path/to/my-mcp-server/index.js"]
}
}
}
Restart Claude Code. The search_docs tool shows up in chat. Claude decides when to call it.
What matters in a real server
Types via zod. Describe input/output schemas explicitly. Claude reads the description and uses it to shape the call. A vague description produces vague calls.
Meaningful errors. If a tool fails, don't return Error: failed — return Failed to fetch from Notion API: token expired, re-auth with <url>. Claude uses that message to figure out what happened and how to fix it.
Idempotency. Claude might retry if the response looks incomplete. A tool that writes state needs to survive that.
Granularity. One giant do_everything tool — bad. Ten narrow ones — good. The model picks what it needs.
When MCP is overkill
If your task is a one-off automation in a 50-line script, skip MCP. Just run claude directly and ask.
MCP earns its keep when the same toolset is needed constantly, by different agents (you via Claude Desktop + teammates via Cursor + a CI job via the SDK). That's when it pays to extract the logic into a server and share it.
Where to go next
The protocol has prompts and resources too, not just tools. Prompts are reusable templates the client can offer to users. Resources are files/data Claude can read as context. For big knowledge bases, resources are nicer than tools.
If your server turns out useful — publish it to npm, add it to the official MCP registry. More visibility and other AI agents can use it too.
Need a custom MCP server for your stack? Get in touch, I'll build it.