The Model Context Protocol (MCP) is the open standard that lets AI assistants like Claude call external tools, query databases, and interact with APIs. Instead of copy-pasting context into chat windows, you give the model direct access to your systems through a lightweight server.
In this tutorial, you will build a fully functional MCP server in TypeScript that exposes a custom tool to Claude. By the end, you will have a working server you can extend with any capability you need.
What You Will Build
A local MCP server that provides a get-weather tool. When Claude asks about weather, it will call your server, which fetches real data and returns it. The same pattern works for databases, internal APIs, file systems, or any service you want to expose.
Prerequisites
Before starting, make sure you have:
- Node.js 18 or later installed
- A package manager (npm or pnpm)
- Claude Desktop or Claude Code installed
- Basic familiarity with TypeScript
Step 1: Initialize the Project
Create a new directory and initialize a TypeScript project:
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --initThe @modelcontextprotocol/sdk package provides the server framework. zod handles input validation for your tools.
Step 2: Configure TypeScript
Update your tsconfig.json to use modern module resolution:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}Also update your package.json to add the module type and build script:
{
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}Step 3: Write the Server
Create src/index.ts with the following code:
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",
"Get current weather for a city",
{
city: z.string().describe("City name, e.g. 'London' or 'Ho Chi Minh City'"),
},
async ({ city }) => {
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 summary = [
`Weather in ${city}:`,
`Temperature: ${current.temp_C}C / ${current.temp_F}F`,
`Condition: ${current.weatherDesc[0].value}`,
`Humidity: ${current.humidity}%`,
`Wind: ${current.windspeedKmph} km/h ${current.winddir16Point}`,
].join("\n");
return {
content: [{ type: "text", text: summary }],
};
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Weather MCP server running on stdio");
}
main().catch(console.error);This server does three things:
- Creates an MCP server instance with a name and version
- Registers a
get-weathertool with a schema (city name) and handler function - Connects via stdio transport, which is how Claude communicates with local servers
Step 4: Build and Test
Compile the TypeScript:
npm run buildTest that the server starts without errors:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}' | node dist/index.jsYou should see a JSON response with server capabilities. Press Ctrl+C to stop.
Step 5: Connect to Claude
Create or edit your MCP configuration file. For Claude Desktop, edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/absolute/path/to/my-mcp-server/dist/index.js"]
}
}
}For Claude Code, add the server to your project's .mcp.json:
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["./my-mcp-server/dist/index.js"]
}
}
}Restart Claude after saving the configuration. The weather tool should now appear in the available tools list.
Step 6: Verify It Works
Open Claude and ask: "What is the weather in Tokyo right now?" Claude should invoke your get-weather tool and return real-time weather data.
Adding More Tools
The power of MCP is composability. Add as many tools as you need:
server.tool(
"search-docs",
"Search internal documentation",
{
query: z.string().describe("Search query"),
limit: z.number().optional().default(5).describe("Max results"),
},
async ({ query, limit }) => {
// Your search logic here
const results = await searchDatabase(query, limit);
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
}
);Each tool gets its own schema validation, description for the AI model, and async handler.
Practical Tips
-
Use descriptive tool names and descriptions. Claude uses these to decide when to call your tool. Be specific about what the tool does and when it should be used.
-
Validate inputs with zod. The schema is not just documentation; it prevents malformed requests from reaching your handler.
-
Log to stderr, not stdout. The stdio transport uses stdout for protocol messages. Use
console.error()for debugging output. -
Handle errors gracefully. Return informative error messages in the content array rather than throwing exceptions. Claude can then explain the issue to the user.
-
Keep tools focused. One tool should do one thing well. Prefer multiple small tools over one tool with complex branching logic.
-
Test outside Claude first. Use the MCP Inspector (
npx @modelcontextprotocol/inspector) to debug your server interactively before connecting it to Claude.
Common Issues
Server not appearing in Claude: Double-check the absolute path in your config. Ensure the compiled JS file exists at that path.
Tool calls failing silently: Check stderr output. Run the server manually and send test requests to isolate the problem.
TypeScript compilation errors: Make sure "type": "module" is set in package.json and your imports use the .js extension.
Next Steps
From here, you can extend your server to connect to databases, wrap REST APIs, interact with file systems, or orchestrate other services. The MCP ecosystem is growing rapidly, and building custom servers is the fastest way to make Claude work with your specific tools and data.
Try claude-code
Claude Code is the fastest way to develop and test MCP servers. It connects to local servers automatically and provides real-time feedback as you build.