The Model Context Protocol (MCP) is an open standard that lets AI models talk to external tools and data sources through a clean, well-defined interface. Instead of stuffing tool logic into your prompt or bolting on ad hoc function-calling glue code, MCP gives you a proper server-client architecture: your code lives in a server, the AI client connects to it, and the model invokes your tools by name when it needs them.
This guide walks you through building a minimal MCP server from scratch, exposing one real tool, and connecting it to a Claude client that can actually call it. By the end you will have a working foundation you can extend into anything from database queries to file operations to external API wrappers.
What You Need Before You Start
- Node.js 18 or later (or Python 3.10+ if you prefer the Python path)
- An Anthropic API key stored in the
ANTHROPIC_API_KEYenvironment variable - Basic familiarity with async JavaScript or Python
This guide uses TypeScript and the official @anthropic-ai/sdk package for the client side. The MCP server itself can be written in either language; we will use TypeScript throughout for consistency.
Understanding the MCP Architecture
Before writing any code, it helps to have the mental model straight.
An MCP server is a lightweight process that advertises a list of tools. Each tool has a name, a description, and a JSON Schema defining its input parameters. When a Claude model decides it needs to call a tool, the client sends a structured request to the MCP server, the server runs the corresponding function, and returns a result. The model never executes arbitrary code on your machine directly. It asks; your server decides what to do.
This separation matters for safety. You control exactly what surface area the model can touch. Nothing outside your explicitly registered tools is reachable.
Setting Up the Project
mkdir mcp-demo && cd mcp-demo
npm init -y
npm install @anthropic-ai/sdk @modelcontextprotocol/sdk
npm install -D typescript tsx @types/node
npx tsc --initThe @modelcontextprotocol/sdk package provides the server primitives. The @anthropic-ai/sdk package is for the client that talks to Claude. Make sure your tsconfig.json targets ES2020 or later and has "module": "NodeNext" or "module": "commonjs" set to whatever suits your project.
Writing the MCP Server
Create a file called server.ts. This server will expose one tool: a simple weather stub that accepts a city name and returns a canned forecast. In production you would replace the stub with a real API call.
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: "weather-server",
version: "1.0.0",
});
server.tool(
"get_weather",
"Returns the current weather for a given city.",
{
city: z.string().describe("The name of the city to look up."),
},
async ({ city }) => {
// Replace this stub with a real weather API call.
const forecast = `The weather in ${city} is 22°C and sunny.`;
return {
content: [
{ type: "text", text: forecast },
],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Weather MCP server running on stdio.");A few things to note here. The server communicates over standard I/O. This is the simplest transport for local development and for spawning the server as a subprocess from your client. The tool registration takes a name, a human-readable description (which the model uses to decide when to call the tool), a Zod schema for validation, and an async handler. The return value must be an MCP-formatted content array.
Install Zod if you have not already: npm install zod.
Writing the Client
Create client.ts. This file spawns the MCP server as a child process, discovers its tools, and passes them to Claude so the model can call them during a conversation.
import Anthropic from "@anthropic-ai/sdk";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
const anthropic = new Anthropic();
// Spawn the MCP server as a subprocess.
const transport = new StdioClientTransport({
command: "npx",
args: ["tsx", "server.ts"],
});
const mcpClient = new Client({ name: "demo-client", version: "1.0.0" });
await mcpClient.connect(transport);
// Discover tools from the server.
const { tools } = await mcpClient.listTools();
// Convert MCP tool definitions into Anthropic tool format.
const anthropicTools = tools.map((tool) => ({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
}));
// Send a message and handle any tool calls.
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: "What is the weather like in Tokyo right now?" },
];
let response = await anthropic.messages.create({
model: "claude-fable-5",
max_tokens: 1024,
tools: anthropicTools,
messages,
});
// Agentic loop: keep going until the model stops calling tools.
while (response.stop_reason === "tool_use") {
const toolUseBlock = response.content.find((b) => b.type === "tool_use");
if (!toolUseBlock || toolUseBlock.type !== "tool_use") break;
// Call the tool on the MCP server.
const toolResult = await mcpClient.callTool({
name: toolUseBlock.name,
arguments: toolUseBlock.input as Record,
});
// Append the assistant turn and the tool result to the conversation.
messages.push({ role: "assistant", content: response.content });
messages.push({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: toolUseBlock.id,
content: toolResult.content as Anthropic.ToolResultBlockParam["content"],
},
],
});
response = await anthropic.messages.create({
model: "claude-fable-5",
max_tokens: 1024,
tools: anthropicTools,
messages,
});
}
// Print the final text response.
const finalText = response.content
.filter((b) => b.type === "text")
.map((b) => (b.type === "text" ? b.text : ""))
.join("");
console.log(finalText);
await mcpClient.close(); This client uses claude-fable-5, the most capable Claude model, which carries a 1M-token context window. If you want a faster or cheaper run during development, swap in claude-haiku-4-5 (200K context) or claude-sonnet-4-6 (1M context). The tool-calling logic is identical across all of them.
Running It
npx tsx client.tsThe client spawns the server, lists available tools, sends the user message to Claude, receives a tool_use response, forwards the call to your server, and feeds the result back. Claude then produces a final natural-language answer. You should see something like: “The weather in Tokyo is 22°C and sunny.”
Extending Your Server Safely
Once the plumbing works, adding more tools is straightforward. Each new call to server.tool() registers another capability. A few principles worth holding onto as you grow the surface area:
- Validate all inputs. Zod handles this by default in the pattern above. Never pass raw model output directly to a database query or shell command without sanitizing it first.
- Return structured errors. If a tool fails, return an error message in the content array rather than throwing. This lets the model handle the failure gracefully instead of crashing the loop.
- Keep tools narrowly scoped. A tool that does one thing is easier to reason about, easier to test, and harder for a confused model to misuse. Avoid building Swiss-army-knife tools.
- Log tool invocations. Write every tool call and its arguments to a log. When something goes wrong you will want that audit trail.
Switching Transports for Production
Stdio transport is perfect for local development and for tightly coupled subprocess deployments. For a networked or multi-client setup, MCP also supports HTTP with Server-Sent Events transport. The server and tool registration code stays the same; only the transport layer changes. Consult the MCP SDK documentation for the HTTP transport options once you outgrow the subprocess model.
Takeaway
MCP gives you a clean boundary between AI reasoning and your business logic. The model expresses intent; your server executes it under your rules. The pattern you built here, a subprocess server with validated tool handlers and an agentic client loop, scales from a weekend project to a production agent with minimal structural change. Start with one real tool, get it right, then add the next one.
