I spent a weekend trying to get Claude to talk to my company's internal API. Tried prompt engineering. Tried copy-pasting JSON into the chat window. Tried asking it politely. None of it worked well.
Then I found MCP, the Model Context Protocol, and built a working server in about 25 minutes. Claude could suddenly pull live data from our database, run searches, and format reports without me having to do the whole copy-paste dance. It felt like giving Claude a pair of hands.
If you write code for a living and you use Claude (or want to), this tutorial will walk you through building your own MCP server from scratch. No fluff, just working code you can adapt.
What is MCP and why should you care?
MCP stands for Model Context Protocol. It is an open standard that Anthropic published in late 2024 to let AI models like Claude connect to external tools and data sources. Think of it as a USB port for AI. You plug in a server, and Claude gets access to whatever that server exposes: tools, data, prompts, anything.
Before MCP, if you wanted Claude to work with your stuff, you had to build custom integrations, deal with function calling schemas, and handle all the plumbing yourself. MCP standardizes all of that.
+------------------+ +------------------+ +------------------+
| | JSON- | | Your | |
| Claude / | RPC | MCP Server | Logic | Database / |
| AI Client | -------> | (Your Code) | -------> | API / Files |
| | <------- | | <------- | |
+------------------+ +------------------+ +------------------+
Sends tool Handles request, Your existing
call requests returns results infrastructure
The protocol uses JSON-RPC 2.0 over standard I/O (stdin/stdout) or HTTP with server-sent events. You pick whichever transport fits your use case. For local tools, stdio is the simplest option and what we will use here.
What you will need
This tutorial assumes you have Node.js 18+ and npm installed. If you are reading this on CodeBrainery, you probably already do. You should also have Claude Desktop or Claude Code installed so you can test the server when it is done.
That is it. No Docker, no cloud deployment, no complicated setup.
Step 1: Scaffold the project
Open your terminal and create a new directory. We are building a weather tool because it is a clean example with a free API, but the same structure works for anything: database queries, file search, Jira tickets, whatever.
# Create project and initialize
mkdir mcp-weather-server && cd mcp-weather-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
# Set up TypeScript
npx tsc --init --target es2022 --module nodenext \
--moduleResolution nodenext --outDir dist --rootDir src
The @modelcontextprotocol/sdk package does most of the heavy lifting. It handles protocol negotiation, message parsing, and transport. The zod library is for input validation, which MCP uses to tell Claude what parameters your tools accept.
Your project structure should look like this:
mcp-weather-server/
src/
index.ts
package.json
tsconfig.json
Quick thing: open package.json and add "type": "module" so Node handles ES modules properly. Also add a build script.
{
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
Step 2: Write the MCP server
Here is where the actual work happens. Create src/index.ts with the following code. I have added comments to explain each piece, but the whole thing is under 80 lines.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// 1. Create the server instance
const server = new McpServer({
name: "weather-server",
version: "1.0.0",
description: "Gets current weather for any city"
});
// 2. Define a tool that Claude can call
server.tool(
"get_weather",
"Get current weather conditions for a city",
{
city: z.string().describe("City name, e.g. 'London' or 'Tokyo'"),
units: z.enum(["celsius", "fahrenheit"])
.default("celsius")
.describe("Temperature units")
},
async ({ city, units }) => {
// Call a free weather API (no key needed for wttr.in)
const unitParam = units === "fahrenheit" ? "u" : "m";
const url = `https://wttr.in/${encodeURIComponent(city)}?format=j1&${unitParam}`;
const res = await fetch(url);
if (!res.ok) {
return {
content: [{ type: "text", text: `Could not fetch weather for "${city}".` }],
isError: true
};
}
const data = await res.json();
const current = data.current_condition[0];
const tempField = units === "fahrenheit" ? "temp_F" : "temp_C";
const summary = [
`Weather in ${city}:`,
`Temperature: ${current[tempField]}° ${units}`,
`Condition: ${current.weatherDesc[0].value}`,
`Humidity: ${current.humidity}%`,
`Wind: ${current.windspeedKmph} km/h ${current.winddir16Point}`
].join("\n");
return {
content: [{ type: "text", text: summary }]
};
}
);
// 3. Connect via stdio and start listening
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Weather MCP server running on stdio");
That is the whole server. Let me break down what each section does.
The McpServer constructor takes a name and version. These get sent to the client during the handshake so Claude knows what it is talking to.
The server.tool() call registers a tool. The first argument is the tool name (what Claude will call), the second is a human-readable description (Claude reads this to decide when to use the tool), and the third is a Zod schema that defines the input parameters. The fourth argument is the handler function that runs when Claude calls the tool.
The transport layer uses stdio, which means the server reads JSON-RPC messages from stdin and writes responses to stdout. That is why the log goes to console.error instead of console.log, so it does not interfere with the protocol messages.
Step 3: Build and test locally
npm run build
You can do a quick sanity check by piping a JSON-RPC message directly into the server. But the real test is connecting it to Claude.
Step 4: Connect it to Claude Desktop
Open your Claude Desktop config file. On macOS it is at ~/Library/Application Support/Claude/claude_desktop_config.json. On Windows, check %APPDATA%\Claude\. Add your server to the mcpServers section:
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/full/path/to/mcp-weather-server/dist/index.js"]
}
}
}
Restart Claude Desktop. You should see a small hammer icon in the chat input area. Click it, and you will see your get_weather tool listed. Now type something like "What is the weather in Berlin right now?" and Claude will call your server.
Tip: If you are using Claude Code instead of Claude Desktop, you can register MCP servers with claude mcp add weather node /full/path/to/dist/index.js and it works the same way.
How the request flows
When you ask Claude about the weather, here is what happens under the hood:
User: "What's the weather in Tokyo?"
|
v
Claude reads tool descriptions
|
v
Claude decides to call "get_weather" with { city: "Tokyo", units: "celsius" }
|
v
JSON-RPC request ---> Your MCP Server ---> wttr.in API
|
Claude formats Your server returns API responds
the answer <--- weather data <--- with JSON
|
v
User sees: "It's currently 18°C in Tokyo with clear skies..."
The beauty of this setup is that Claude figures out when to call your tool on its own. You do not need to write routing logic or intent detection. Claude reads the tool description and the parameter schema and makes the call when it is relevant.
Going further: add more tools
One server can expose multiple tools. Here is how you would add a forecast tool alongside the current weather tool:
server.tool(
"get_forecast",
"Get a 3-day weather forecast for a city",
{
city: z.string().describe("City name")
},
async ({ city }) => {
const url = `https://wttr.in/${encodeURIComponent(city)}?format=j1`;
const res = await fetch(url);
const data = await res.json();
const forecast = data.weather.map((day: any) =>
`${day.date}: ${day.mintempC}-${day.maxtempC}°C, ${day.hourly[4].weatherDesc[0].value}`
).join("\n");
return {
content: [{ type: "text", text: `3-day forecast for ${city}:\n${forecast}` }]
};
}
);
You can keep adding tools. Each one gets its own name, description, schema, and handler. I have seen people build MCP servers that connect to Postgres databases, search Elasticsearch indexes, read from S3 buckets, and manage Kubernetes deployments. The pattern is always the same.
Common mistakes to avoid
After building a few of these and helping others debug theirs, here are the things that trip people up most often.
First, do not write to stdout for logging. Anything you print to stdout gets interpreted as a protocol message and will crash the connection. Use console.error for all your debug output.
Second, make your tool descriptions specific. Claude uses these descriptions to decide which tool to call. A vague description like "does stuff with data" means Claude will not know when to use it. Write it like you are explaining the tool to a coworker who has never seen it before.
Third, always return the expected response shape. Every tool handler must return an object with a content array containing objects with type and text fields. If you return something else, the SDK will throw an error and Claude will see a failure.
Fourth, handle errors gracefully. If your API call fails or data is missing, return an error message in the content rather than throwing an exception. Set isError: true in the response so Claude knows something went wrong and can tell the user.
Where to go from here
This weather server is a starting point. The real power shows up when you connect MCP to your own tools and data. Some ideas worth exploring:
Build a server that queries your company's database so Claude can answer business questions in natural language. Build one that wraps your CI/CD pipeline so you can ask Claude to check build status or trigger deployments. Build one that reads your local file system so Claude can help you search through codebases or documentation.
The MCP specification also supports resources (read-only data that Claude can pull in as context) and prompts (reusable prompt templates). We will cover those in upcoming tutorials on CodeBrainery.
If you want to dig deeper into the protocol itself, the official documentation is at modelcontextprotocol.io and the SDK source is on GitHub under the modelcontextprotocol organization.
Wrapping up
MCP is one of those things that feels obvious once you see it working. Instead of awkwardly pasting data into chat windows, you give Claude direct access to your systems through a clean, standardized protocol. The 30 minutes you spend building your first server will save you hours of copy-pasting later.
The full source code for this tutorial is ready to clone and run. Swap out the weather API for whatever service you actually need, keep the same structure, and you are good to go.
Want more tutorials like this?
CodeBrainery publishes hands-on technical guides for engineers every week. Join the community, share what you are building, and write articles of your own.
Found this useful? Share it with a developer who could use it. And if you have built something cool with MCP, we would love to feature your work on CodeBrainery.
© 2026 CodeBrainery · Built for engineers, by engineers

