Building a Custom MCP Server
Build your own MCP server from scratch. Complete guide covering TypeScript setup, tool implementation, testing, security, and publishing to share with others.
WHAT YOU'LL BUILD
- A working MCP server with multiple tools
- Proper TypeScript setup with the MCP SDK
- Input validation and error handling
- Tests for your server functionality
- Documentation and publishing setup
Quick Note
This is a blog post overview. For the complete interactive tutorial with more detail, visit /build-mcp-server.
Why Build a Custom MCP Server?
While there are already 70+ MCP servers available, you might want to build your own for:
- Proprietary APIs: Integrate internal company tools or private services
- Specialized workflows: Create domain-specific tools for your industry
- Missing integrations: Connect to services that don't have MCP servers yet
- Custom business logic: Implement unique operations specific to your needs
- Learning: Understand MCP internals and contribute to the ecosystem
Example: Weather API Server
We'll build a simple weather MCP server that demonstrates core concepts. This server will fetch weather data from an API and expose it to AI assistants.
1. Project Setup
Initialize Project
# Create project directory mkdir weather-mcp-server cd weather-mcp-server # Initialize npm project npm init -y # Install dependencies npm install @modelcontextprotocol/sdk zod axios npm install -D typescript @types/node tsx # Initialize TypeScript npx tsc --init
2. Configure TypeScript
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
3. Implement the Server
src/index.ts
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
import { z } from "zod";
// Environment variable validation
const API_KEY = process.env.WEATHER_API_KEY;
if (!API_KEY) {
throw new Error("WEATHER_API_KEY environment variable is required");
}
// Tool input schemas
const GetWeatherSchema = z.object({
location: z.string().describe("City name, e.g., 'San Francisco' or 'London'"),
units: z.enum(["metric", "imperial"]).default("metric").describe("Temperature units"),
});
const GetForecastSchema = z.object({
location: z.string().describe("City name"),
days: z.number().min(1).max(7).default(3).describe("Number of days (1-7)"),
});
// Server implementation
class WeatherServer {
private server: Server;
constructor() {
this.server = new Server(
{
name: "weather-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupToolHandlers();
// Error handling
this.server.onerror = (error) => console.error("[MCP Error]", error);
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
private setupToolHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "get_current_weather",
description: "Get current weather for a location",
inputSchema: {
type: "object",
properties: {
location: {
type: "string",
description: "City name, e.g., 'San Francisco' or 'London'",
},
units: {
type: "string",
enum: ["metric", "imperial"],
description: "Temperature units (default: metric)",
default: "metric",
},
},
required: ["location"],
},
},
{
name: "get_forecast",
description: "Get weather forecast for multiple days",
inputSchema: {
type: "object",
properties: {
location: {
type: "string",
description: "City name",
},
days: {
type: "number",
description: "Number of days (1-7, default: 3)",
minimum: 1,
maximum: 7,
default: 3,
},
},
required: ["location"],
},
},
],
}));
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === "get_current_weather") {
const { location, units } = GetWeatherSchema.parse(args);
return await this.getCurrentWeather(location, units);
} else if (name === "get_forecast") {
const { location, days } = GetForecastSchema.parse(args);
return await this.getForecast(location, days);
} else {
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid arguments: ${error.message}`);
}
throw error;
}
});
}
private async getCurrentWeather(location: string, units: string) {
try {
const response = await axios.get(
`https://api.openweathermap.org/data/2.5/weather`,
{
params: {
q: location,
appid: API_KEY,
units: units,
},
}
);
const data = response.data;
const tempUnit = units === "metric" ? "°C" : "°F";
return {
content: [
{
type: "text",
text: JSON.stringify(
{
location: data.name,
temperature: `${data.main.temp}${tempUnit}`,
feels_like: `${data.main.feels_like}${tempUnit}`,
conditions: data.weather[0].description,
humidity: `${data.main.humidity}%`,
wind_speed: `${data.wind.speed} m/s`,
},
null,
2
),
},
],
};
} catch (error: any) {
if (error.response?.status === 404) {
throw new Error(`Location not found: ${location}`);
}
throw new Error(`Weather API error: ${error.message}`);
}
}
private async getForecast(location: string, days: number) {
try {
const response = await axios.get(
`https://api.openweathermap.org/data/2.5/forecast`,
{
params: {
q: location,
appid: API_KEY,
units: "metric",
cnt: days * 8, // API returns 3-hour intervals
},
}
);
const forecasts = response.data.list.map((item: any) => ({
datetime: item.dt_txt,
temperature: `${item.main.temp}°C`,
conditions: item.weather[0].description,
}));
return {
content: [
{
type: "text",
text: JSON.stringify(
{
location: response.data.city.name,
forecast: forecasts,
},
null,
2
),
},
],
};
} catch (error: any) {
throw new Error(`Forecast API error: ${error.message}`);
}
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Weather MCP server running on stdio");
}
}
// Start server
const server = new WeatherServer();
server.run().catch(console.error);
4. Add Build Scripts
package.json
{
"name": "weather-mcp-server",
"version": "1.0.0",
"description": "MCP server for weather data",
"type": "module",
"bin": {
"weather-mcp-server": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts",
"prepare": "npm run build"
},
"keywords": ["mcp", "weather", "api"],
"author": "Your Name",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^0.5.0",
"axios": "^1.6.0",
"zod": "^3.22.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.7.0",
"typescript": "^5.3.0"
}
}
5. Test Your Server
Test Locally
# Set API key export WEATHER_API_KEY="your_openweather_api_key" # Build the server npm run build # Run in development mode npm run dev
6. Configure in Claude Desktop
claude_desktop_config.json
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/absolute/path/to/weather-mcp-server/dist/index.js"],
"env": {
"WEATHER_API_KEY": "your_api_key_here"
}
}
}
}Restart Claude Desktop and test:
"What's the current weather in Tokyo?"
Key Concepts
1. Tool Definition
Tools are the core of MCP servers. Each tool needs:
- Name: Unique identifier (use snake_case)
- Description: Clear explanation for the AI model
- Input schema: JSON Schema defining parameters
- Handler: Function that executes the tool logic
2. Input Validation
Always validate inputs using libraries like Zod:
Why Validation Matters
AI models may pass unexpected or malicious inputs. Validation prevents errors, security issues, and ensures type safety. See our security best practices guide.
3. Error Handling
Provide helpful error messages that guide the AI (and users) on how to fix issues:
// ❌ Bad error message
throw new Error("Invalid");
// ✓ Good error message
throw new Error("Location not found: '${location}'. Please provide a valid city name.");
4. Response Format
Tool responses should return content in a structured format:
return {
content: [
{
type: "text",
text: "Your response here" // Can be plain text or JSON
}
]
};
Advanced Features
1. Resources (Optional)
Resources allow servers to expose data that the AI can read. Example: a documentation server might expose docs as resources.
2. Prompts (Optional)
Servers can provide pre-defined prompts to guide users:
this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
prompts: [
{
name: "weather_report",
description: "Generate a detailed weather report",
arguments: [
{
name: "location",
description: "City name",
required: true,
},
],
},
],
}));
3. Sampling (Advanced)
Servers can request the client to perform LLM sampling. Useful for complex workflows where the server needs AI assistance.
Testing Your Server
Unit Tests
tests/weather.test.ts
import { describe, it, expect, beforeAll } from "vitest";
import { WeatherServer } from "../src/index";
describe("WeatherServer", () => {
beforeAll(() => {
process.env.WEATHER_API_KEY = "test_key";
});
it("should validate location parameter", async () => {
// Test input validation
await expect(
server.getCurrentWeather("", "metric")
).rejects.toThrow("Invalid arguments");
});
it("should handle API errors gracefully", async () => {
// Test error handling
await expect(
server.getCurrentWeather("InvalidCity123", "metric")
).rejects.toThrow("Location not found");
});
});
Integration Testing
Test your server with the MCP Inspector tool (official debugging tool from Anthropic):
# Install MCP Inspector npm install -g @modelcontextprotocol/inspector # Run your server with inspector npx @modelcontextprotocol/inspector node dist/index.js
Publishing Your Server
1. Prepare for Publishing
- Add a comprehensive README.md
- Include setup instructions and examples
- Document all tools and their parameters
- Add a LICENSE file (MIT is common)
- Create a .npmignore file
2. Publish to npm
# Login to npm npm login # Publish package npm publish # Users can then install with: # npx your-package-name
3. Submit to MCP Server Directory
Submit your server to the official directory so others can discover it:
- Fork the MCP servers repository
- Add your server to the directory
- Submit a pull request
Best Practices
Security
- Never hardcode API keys (use environment variables)
- Validate all inputs with schemas
- Sanitize user-provided data before using in queries
- Implement rate limiting for expensive operations
- Use HTTPS for external API calls
See our security guide for detailed security patterns.
Performance
- Cache frequently accessed data
- Set reasonable timeouts for API calls
- Implement pagination for large datasets
- Use connection pooling for databases
User Experience
- Write clear, concise tool descriptions
- Provide helpful examples in documentation
- Return structured, easy-to-parse responses
- Include error recovery suggestions in error messages
Real-World Examples
Database Query Server
A server that executes safe SQL queries against a database. Check the PostgreSQL MCP guide for inspiration.
CRM Integration
Connect to Salesforce, HubSpot, or other CRM systems. Tools for searching contacts, creating leads, updating deals.
Internal Tools
Expose company-specific APIs, deploy systems, or monitoring dashboards to AI assistants.
Common Pitfalls
| Pitfall | Solution |
|---|---|
| No input validation | Use Zod or similar validation library |
| Vague error messages | Provide specific, actionable errors |
| Missing documentation | Write README with examples |
| Blocking operations | Use async/await, set timeouts |
| Hardcoded credentials | Use environment variables |
Next Steps
CONTINUE LEARNING
This blog post covers the essentials. For a complete, step-by-step interactive tutorial with more examples and advanced topics, visit /build-mcp-server.
FULL TUTORIAL
Complete interactive guide to building MCP servers
SECURITY GUIDE
Secure your custom servers
EXPLORE SERVERS
Study existing server implementations
Resources
- MCP SDK Documentation: github.com/modelcontextprotocol/sdk
- Example Servers: github.com/modelcontextprotocol/servers
- MCP Specification: spec.modelcontextprotocol.io
- Community Discord: discord.gg/anthropic
Ready to Build?
Start with the full interactive tutorial or join the community on GitHub and Discord for help.