Build an MCP Server in 50 Lines
MCP (Model Context Protocol) lets you give AI agents new capabilities. In this section, you'll build a working MCP server from scratch.
MCP Recap
MCP is the standard protocol for connecting AI agents to tools:
Agent (Client) ←→ MCP Server (Tool)
↕ ↕
LLM Brain Your Code
Decides what Executes the
tool to use actual work
What an MCP server provides:
- Tools — Functions the agent can call (search, calculate, query)
- Resources — Data the agent can read (files, databases, configs)
- Prompts — Template prompts for common tasks
When you build an MCP server, any MCP-compatible agent (Claude Code, OpenCode, Cursor, etc.) can use it immediately.
Your First MCP Server
A weather lookup tool in under 50 lines:
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",
version: "1.0.0",
});
// Define a tool
server.tool(
"get_weather",
"Get current weather for a city",
{
city: z.string().describe("City name, e.g. 'London'"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
},
async ({ city, units }) => {
// In production, call a real weather API
const temp = Math.round(Math.random() * 30 + 5);
const conditions = ["sunny", "cloudy", "rainy", "snowy"];
const condition = conditions[Math.floor(Math.random() * 4)];
return {
content: [{
type: "text",
text: `Weather in ${city}: ${temp}°${units === "celsius" ? "C" : "F"}, ${condition}`,
}],
};
}
);
// Start the server
const transport = new StdioServerTransport();
await server.connect(transport);
That's it. 30 lines of actual code, and any MCP agent can now check the weather.
Setting Up the Project
Step-by-step setup:
mkdir mcp-weather && cd mcp-weather
npm init -y
npm install @modelcontextprotocol/sdk zod
Add to package.json:
{
"type": "module",
"scripts": {
"start": "node index.js"
}
}
Create index.js with the code from the previous slide.
Register with your agent (e.g., Claude Code config):
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/path/to/mcp-weather/index.js"]
}
}
}
Now when you ask Claude Code "What's the weather in Berlin?", it will call your MCP server.
Adding Resources
Resources let agents read data from your server:
// Add a resource that provides city data
server.resource(
"cities",
"cities://list",
async (uri) => ({
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify([
{ name: "London", country: "UK", timezone: "GMT" },
{ name: "Berlin", country: "DE", timezone: "CET" },
{ name: "Tokyo", country: "JP", timezone: "JST" },
]),
}],
})
);
Resources are useful for:
- Configuration files
- Database schemas
- Documentation
- Any reference data the agent might need
Real-World MCP Servers
Practical MCP servers you can build:
Database Query Tool:
server.tool("query_db", "Run a read-only SQL query", {
sql: z.string().describe("SQL SELECT query"),
}, async ({ sql }) => {
// Safety: only allow SELECT statements
if (!sql.trim().toUpperCase().startsWith("SELECT")) {
return { content: [{ type: "text", text: "Error: Only SELECT queries allowed" }] };
}
const results = await db.query(sql);
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
});
Jira Integration:
server.tool("create_ticket", "Create a Jira ticket", {
project: z.string(),
title: z.string(),
description: z.string(),
priority: z.enum(["P0", "P1", "P2", "P3"]),
}, async ({ project, title, description, priority }) => {
const ticket = await jira.createIssue({ project, title, description, priority });
return { content: [{ type: "text", text: `Created ${ticket.key}: ${ticket.url}` }] };
});
Monitoring Dashboard:
server.tool("get_metrics", "Get application metrics", {
service: z.string(),
period: z.enum(["1h", "24h", "7d"]),
}, async ({ service, period }) => {
const metrics = await prometheus.query(service, period);
return { content: [{ type: "text", text: formatMetrics(metrics) }] };
});
MCP Security Best Practices
Your MCP server has real power — secure it:
1. Input validation (already handled by Zod schemas)
// Zod validates types and constraints automatically
city: z.string().max(100).describe("City name")
2. Read-only by default
- Start with read-only tools (queries, lookups)
- Add write operations only when needed
- Use confirmation prompts for destructive actions
3. Rate limiting
const rateLimiter = new Map();
function checkRate(toolName) {
const count = rateLimiter.get(toolName) || 0;
if (count > 100) throw new Error("Rate limit exceeded");
rateLimiter.set(toolName, count + 1);
// Reset every minute
}
4. Audit logging
// Log every tool call
server.tool("query_db", ..., async (args) => {
console.error(`[AUDIT] query_db called with: ${JSON.stringify(args)}`);
// ... execute
});
Connecting Multiple MCP Servers
Agents become powerful when they can use multiple tools:
Agent (Claude Code)
├── MCP: File System (built-in)
├── MCP: GitHub (issues, PRs)
├── MCP: Database (queries)
├── MCP: Monitoring (metrics)
└── MCP: Your Custom Server
Example workflow with multiple MCP servers:
You: "Check if there are any open P0 tickets, then look at
the related code and monitoring metrics to diagnose them"
Agent:
→ [Jira MCP] Get open P0 tickets → 2 found
→ [File MCP] Read the affected code files
→ [Monitoring MCP] Get error rates for those services
→ Synthesize: "Ticket PROJ-123 is caused by a null check
missing in user-service.js:42. Error rate spiked at 3am."
Each MCP server does one thing well. The agent orchestrates them together to solve complex problems.
---quiz question: What are the three types of capabilities an MCP server can provide? options:
- { text: "Read, write, and delete", correct: false }
- { text: "Tools (functions to call), Resources (data to read), and Prompts (template prompts)", correct: true }
- { text: "Input, processing, and output", correct: false } feedback: MCP servers provide Tools (executable functions like queries or API calls), Resources (readable data like configs or schemas), and Prompts (reusable prompt templates for common tasks).
---quiz question: Why should MCP servers start with read-only tools? options:
- { text: "Because write operations are slower", correct: false }
- { text: "To minimize risk — read-only operations can't accidentally delete or modify critical data", correct: true }
- { text: "Because MCP doesn't support write operations", correct: false } feedback: Read-only tools (queries, lookups, status checks) are safe to experiment with. Write operations (create, update, delete) carry risk and should be added carefully with proper validation, confirmation, and audit logging.
---quiz question: What makes MCP powerful compared to custom tool integrations? options:
- { text: "MCP is faster than custom integrations", correct: false }
- { text: "Any MCP-compatible agent can use any MCP server — it's a universal standard", correct: true }
- { text: "MCP servers are always free", correct: false } feedback: MCP is a universal protocol. Once you build an MCP server, every MCP-compatible agent (Claude Code, OpenCode, Cursor, etc.) can use it immediately — no custom integration per agent needed.