MCP (Model Context Protocol) crossed from experimental to essential in 2026. According to Stacklok's 2026 software report, 41% of surveyed engineering organizations are running MCP servers in limited or broad production. Official servers now ship from Google, Microsoft, Stripe, and Vercel. It's screened in mid/senior AI engineer interviews. If you build agent systems and haven't shipped an MCP server yet, this is the tutorial.
1. What MCP Actually Does
MCP solves the integration combinatorics problem. Without it, every AI agent needs a custom integration for every tool it might use — N agents times M tools equals a maintenance nightmare. MCP is the USB-C of AI: you write the server once, expose your tools via a standard protocol, and any MCP-compatible client (Claude Desktop, Claude Code, your own agent) can pick them up.
For most practical agent work, you'll only use Tools. That's what this tutorial covers.
2. Project Setup
Node.js 22+ required (we use the built-in node:sqlite module — no native compilation needed). Initialize with ESM from the start; MCP SDK is ESM-only.
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk
Edit package.json to add "type": "module". This is the step that trips up most people — without it, the ESM imports in the MCP SDK will fail.
{
"name": "my-mcp-server",
"type": "module",
"scripts": { "start": "node src/server.js" },
"dependencies": { "@modelcontextprotocol/sdk": "^1.0.0" }
}
3. The Minimal Server
An MCP server has three parts: the McpServer instance (registers tools), the transport (how clients connect), and the handler loop. Here's the minimal working version:
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: "my-mcp-server",
version: "1.0.0"
});
server.tool(
"add",
"Add two numbers together.",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
4. Adding Persistent State with SQLite
The real power comes when your tools maintain state between calls. Node.js 22's built-in node:sqlite gives you a synchronous, zero-dependency SQLite — no native compilation, no extra packages.
import { DatabaseSync } from "node:sqlite";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
export function openDb(dbPath = join(__dirname, "..", "data", "store.db")) {
const db = new DatabaseSync(dbPath);
db.exec(`
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT UNIQUE NOT NULL,
body TEXT NOT NULL,
ts INTEGER DEFAULT (unixepoch())
);
`);
return db;
}
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { openDb } from "./db.js";
const server = new McpServer({ name: "notes-server", version: "1.0.0" });
const db = openDb();
server.tool(
"note_write",
"Save a note under a key. Overwrites if key exists.",
{ key: z.string(), body: z.string() },
async ({ key, body }) => {
db.prepare(
"INSERT INTO notes (key, body) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET body=excluded.body"
).run(key, body);
return { content: [{ type: "text", text: `Saved: ${key}` }] };
}
);
server.tool(
"note_read",
"Read a note by key.",
{ key: z.string() },
async ({ key }) => {
const row = db.prepare("SELECT body FROM notes WHERE key=?").get(key);
return {
content: [{ type: "text", text: row ? row.body : "Not found." }]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
5. Wire It Into Claude Desktop
Find your Claude Desktop config file:
- Mac:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"notes-server": {
"command": "node",
"args": ["/absolute/path/to/my-mcp-server/src/server.js"]
}
}
}
Restart Claude Desktop. Your tools will appear in the tool picker. If they don't show, run npx @modelcontextprotocol/inspector node src/server.js to test and debug your server interactively before touching the config again.
6. Tool Description Quality Matters More Than You Think
The model selects tools entirely based on your description strings. A vague description like "Write something" leads to incorrect tool selection. Be specific about what the tool does, what the parameters mean, and when to use it vs. alternatives.
"Write a note"Good:
"Save a note under a unique key. Use this to persist information across conversation turns. Overwrites any existing note at that key. Use note_read to retrieve it later."
7. Production Considerations
For local developer tools, stdio is production-ready as-is. For shared or remote servers, the 2026 MCP roadmap addresses the main gaps:
- OAuth 2.1 — in preview for remote server auth. The current workaround is API key headers.
- Stateful sessions vs. load balancers — HTTP transport's open problem. For now, sticky sessions or single-instance deployment.
- Audit trails — on the roadmap. For now, log every tool call with timestamps on the server side.
- Version pinning — pin your MCP SDK version. The protocol is still evolving and minor updates occasionally change behavior.
What I Built
My MCP Agent Toolkit extends this pattern to expose three production agent-kernel patterns as MCP tools: shared blackboard state (agents communicate without coupling), SCAR failure memory (fix the same error once, never twice), and LLM response cache (SHA-256 keyed, stops you paying for identical requests). 13 tests, all passing. Plug it into any Claude or GPT agent in under five minutes.