In this article
What MCP servers are
The Model Context Protocol is an open standard from Anthropic. It defines how AI models communicate with external tools and data sources.
An MCP server is a program that exposes capabilities to AI clients. It can provide tools (actions the model can call), resources (data the model can read), and prompts (reusable templates). The server runs locally or on a remote machine. The AI client connects to it over stdio or HTTP.
Think of it like building an API, but for AI models instead of frontends. You define what the model can do, and the protocol handles the rest.
Prerequisites
You need three things before starting:
- Node.js 18+ installed on your machine
- TypeScript (we will set this up in step 1)
- An MCP client for testing: Claude Desktop, Claude Code, or Cursor
The official SDK is @modelcontextprotocol/sdk. It handles transport, serialization, and protocol compliance. You write the logic. The SDK handles the plumbing.
Step 1: Project setup
Create a new directory and initialize the project. We will use TypeScript with ESM modules.
mkdir my-mcp-server && cd my-mcp-server npm init -y npm install @modelcontextprotocol/sdk zod npm install -D typescript @types/node
Zod is for input validation. The SDK uses it to define tool parameter schemas. Now create your tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}Update your package.json with these fields:
{
"type": "module",
"bin": {
"my-mcp-server": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}Step 2: Define your server
Create src/index.ts. This is the entry point. It creates an MCP server instance and connects it to a transport layer.
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "weather-server",
version: "1.0.0",
});
// Tools and resources go here (steps 3 and 4)
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Weather MCP server running on stdio");
}
main().catch(console.error);The StdioServerTransport communicates over stdin/stdout. This is what Claude Desktop and most clients expect. The shebang line (#!/usr/bin/env node) makes the compiled file executable via npx.
Note: console.error is intentional. Stdout is reserved for MCP protocol messages. Use stderr for logging.
Step 3: Add tools
Tools are the core of most MCP servers. Each tool has a name, description, a Zod schema for parameters, and a handler function. Here is a tool that fetches weather data:
import { z } from "zod";
server.tool(
"get-weather",
"Get current weather for a city",
{
city: z.string().describe("City name, e.g. Amsterdam"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
},
async ({ city, units }) => {
const response = await fetch(
`https://wttr.in/${encodeURIComponent(city)}?format=j1`
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Failed to fetch weather for ${city}` },
],
};
}
const data = await response.json();
const current = data.current_condition[0];
const tempC = current.temp_C;
const tempF = current.temp_F;
const temp = units === "celsius" ? `${tempC}°C` : `${tempF}°F`;
const desc = current.weatherDesc[0].value;
const humidity = current.humidity;
return {
content: [
{
type: "text",
text: `Weather in ${city}: ${temp}, ${desc}. Humidity: ${humidity}%.`,
},
],
};
}
);The four arguments to server.tool() are:
- Name: A unique identifier. The AI client uses this to call your tool.
- Description: What the tool does. The AI model reads this to decide when to use it.
- Schema: A Zod object defining the parameters. The SDK validates inputs automatically.
- Handler: An async function that runs when the tool is called. Returns content blocks.
You can add multiple tools to one server. Here is a second tool that fetches a 3-day forecast:
server.tool(
"get-forecast",
"Get 3-day weather forecast for a city",
{
city: z.string().describe("City name"),
},
async ({ city }) => {
const response = await fetch(
`https://wttr.in/${encodeURIComponent(city)}?format=j1`
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Failed to fetch forecast for ${city}` },
],
};
}
const data = await response.json();
const forecast = data.weather.map((day: any) => {
const date = day.date;
const maxC = day.maxtempC;
const minC = day.mintempC;
const desc = day.hourly[4].weatherDesc[0].value;
return `${date}: ${minC}°C - ${maxC}°C, ${desc}`;
});
return {
content: [
{
type: "text",
text: `3-day forecast for ${city}:\n${forecast.join("\n")}`,
},
],
};
}
);Step 4: Add resources
Resources expose read-only data. They are useful for giving the AI model context without requiring an explicit tool call. Each resource has a URI, a name, and a handler.
server.resource(
"supported-cities",
"weather://cities",
{
description: "List of cities with reliable weather data",
mimeType: "application/json",
},
async () => {
const cities = [
"Amsterdam", "London", "New York", "Tokyo",
"Berlin", "Paris", "Sydney", "Toronto",
];
return {
contents: [
{
uri: "weather://cities",
text: JSON.stringify(cities, null, 2),
},
],
};
}
);Resources use URI schemes. You pick the scheme (here weather://). The AI client can read these resources to understand what data is available before calling tools.
Common resource patterns: database schemas, configuration files, API documentation, or lists of available entities.
Step 5: Test locally
Build the project first:
npm run build
Now add it to your MCP client. For Claude Desktop, edit claude_desktop_config.json:
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/absolute/path/to/my-mcp-server/dist/index.js"]
}
}
}The config file location depends on your OS:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json
Restart Claude Desktop after saving. Your tools will appear in the tools menu. Ask Claude something like "What is the weather in Amsterdam?" to test.
For Cursor, add the same config to .cursor/mcp.json in your project root. For Claude Code, use .claude/settings.json.
Debugging tip
If the server does not appear in Claude Desktop, check stderr output. Run node dist/index.js directly in your terminal to see error messages. Common issues: missing dependencies, incorrect file paths, or syntax errors in the compiled output.
Step 6: Publish to npm
Once your server works locally, publishing lets anyone use it with npx. Update your package.json:
{
"name": "@yourscope/mcp-server-weather",
"version": "1.0.0",
"type": "module",
"bin": {
"mcp-server-weather": "./dist/index.js"
},
"files": ["dist"],
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
}
}Make sure the compiled file has the shebang line. Then publish:
npm run build npm publish --access public
After publishing, anyone can add your server to their config:
{
"mcpServers": {
"weather": {
"command": "npx",
"args": ["-y", "@yourscope/mcp-server-weather"]
}
}
}Real examples to study
The best way to learn is to read production MCP servers. Here are a few worth studying:
GitHub MCP server
View detailsDozens of tools for repos, issues, PRs, and code search. Good example of a large, multi-tool server.
Supabase MCP server
View detailsDatabase queries, table management, and migrations. Shows how to handle authenticated connections.
Brave Search MCP server
View detailsSimple single-tool server. Good starting point if you want a minimal reference.
Puppeteer MCP server
View detailsBrowser automation with screenshots and navigation. Shows how to manage stateful resources.
Python alternative
If Python is your language, the mcp package provides the same functionality. The API is similar but uses decorators.
pip install mcp
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather-server")
@mcp.tool()
async def get_weather(city: str) -> str:
"""Get current weather for a city."""
# Your logic here
return f"Weather in {city}: 18°C, partly cloudy"
@mcp.resource("weather://cities")
async def list_cities() -> str:
"""List supported cities."""
return '["Amsterdam", "London", "Tokyo"]'
if __name__ == "__main__":
mcp.run()Python servers use uvx instead of npx in the client config. The protocol messages are identical. Both TypeScript and Python servers work with every MCP client.
Want to see what others have built?
We track over 2,500 MCP servers across every category. Browse the full catalog for inspiration.
Browse MCP serversFrequently asked questions
What language should I use to build an MCP server?
TypeScript and Python are the two officially supported languages. The @modelcontextprotocol/sdk package covers TypeScript. The mcp Python package covers Python. Both support tools, resources, and prompts. Pick whichever your team already uses.
Can MCP servers access remote APIs?
Yes. Your tool handlers are regular async functions. You can call any REST API, query a database, read files, or run shell commands inside them. The MCP protocol handles serialization and transport between your server and the AI client.
How do I distribute my MCP server to other people?
Publish it to npm. Users install it with npx -y your-package-name in their claude_desktop_config.json or .cursor/mcp.json. No extra setup needed. The -y flag auto-confirms the install prompt.
What is the difference between tools and resources in MCP?
Tools are actions the AI can execute, like running a query or sending a message. Resources are read-only data the AI can access, like a configuration file or a database schema. Tools take parameters and return results. Resources have a URI and return content.
Find out how AI fits into your stack
The free AI Scan analyzes your current tools and shows where MCP servers and AI agents can save you time.