What MCP actually is
Model Context Protocol is Anthropic's open standard for giving AI assistants structured access to external tools and data. Think of it as a well-typed API contract between your model host (Claude Code, Cursor, etc.) and any capability you want to expose: a database, a web scraper, a file watcher, a Slack bot, a CI trigger.
When you build an MCP server, you're building a bridge. The model calls your tools by name with structured arguments; your server executes and returns structured results. No more "browse to this URL and tell me what you see" — just typed function calls.
Prerequisites
- Node.js 22+
- A Claude Code or Cursor setup that supports MCP
- 20 minutes
Step 1: Scaffold the server
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"strict": true
}
}
Step 2: Write your first tool
Create src/index.ts:
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-first-mcp",
version: "0.1.0",
});
// Tool: fetch and summarize a URL's title
server.tool(
"get_page_title",
"Fetches the <title> tag from a given URL",
{ url: z.string().url().describe("The URL to fetch") },
async ({ url }) => {
try {
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
const html = await res.text();
const match = html.match(/<title[^>]*>(.*?)<\/title>/is);
const title = match?.[1]?.trim() ?? "No title found";
return { content: [{ type: "text", text: title }] };
} catch (err) {
return {
content: [{ type: "text", text: `Error: ${String(err)}` }],
isError: true,
};
}
}
);
// Tool: simple math evaluator
server.tool(
"calculate",
"Evaluates a safe arithmetic expression",
{ expression: z.string().describe("e.g. '2 + 2 * 10'") },
async ({ expression }) => {
// Only allow digits, operators, spaces, parens
if (!/^[0-9+\-*/.() ]+$/.test(expression)) {
return {
content: [{ type: "text", text: "Invalid expression" }],
isError: true,
};
}
const result = Function(`"use strict"; return (${expression})`)();
return { content: [{ type: "text", text: String(result) }] };
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch(console.error);
Step 3: Wire it into Claude Code
Add to your Claude Code MCP config (usually ~/.claude/mcp.json or the workspace .mcp.json):
{
"mcpServers": {
"my-first-mcp": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/my-mcp-server/src/index.ts"]
}
}
}
Restart Claude Code. Open the tool palette — your two tools should appear.
Step 4: Test it without a host
Use the MCP inspector:
npx @modelcontextprotocol/inspector tsx src/index.ts
This opens a local UI where you can invoke your tools, inspect schemas, and see raw JSON-RPC messages. Essential for debugging before wiring into a real host.
What to build next
Once you have the pattern down, MCP servers shine for:
- Persistent memory — read/write a local JSON store so your agent remembers context between sessions
- Project-aware search — expose semantic search over your codebase via embeddings
- Internal APIs — wrap any company API behind a clean typed interface the model can call
- Browser control — wrap Playwright for structured web interaction
- CI hooks — trigger builds, read test results, open PRs from inside your agent loop
The key insight
MCP turns your agent from a chat interface into a programmable runtime. The model stops describing what it would do and starts actually doing it — via your typed, auditable, logged function calls. That's the leap from vibecoding to building real agentic systems.
20 minutes to your first tool. The rest is scope.
