Why Custom MCP Servers Matter for Claude Users
Model Context Protocol (MCP) servers extend Claude's native tool-using abilities by providing a standardized interface for external tools, data sources, and services. Unlike basic function calling, MCP enables dynamic context expansion, real-time data fetching, and complex workflows directly within Claude conversations. For developers, Node.js is ideal due to its async nature, vast ecosystem, and ease of deployment.
Building custom MCP servers solves real problems:
- Seamless Integrations: Connect Claude to proprietary APIs, databases, or hardware without prompt hacks.
- Scalability: Handle high-volume requests from AI agents or enterprise teams.
- Customization: Tailor tools for industry-specific needs like HR automation or engineering workflows.
- Offline/Edge Support: Run locally for sensitive data or low-latency apps.
This guide walks you through creating a production-ready MCP server, with code examples tested against Claude 3.5 Sonnet.
Prerequisites
Before diving in, ensure you have:
- Node.js 18+ installed
- Basic knowledge of Express.js and async/await
- Anthropic API key (for testing Claude integration)
- Familiarity with Claude's tool use via the Messages API
Install globally if needed:
npm install -g nodemon
Step 1: Project Setup
Create a new directory and initialize:
mkdir claude-mcp-server
cd claude-mcp-server
npm init -y
Install core dependencies:
npm install express cors helmet @anthropic-ai/sdk dotenv
npm install -D nodemon typescript @types/node @types/express
Set up TypeScript for robustness (optional but recommended):
npx tsc --init
Create src/index.ts:
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
const app = express();
const PORT = process.env.PORT || 3000;
app.use(helmet());
app.use(cors());
app.use(express.json());
app.listen(PORT, () => {
console.log(`MCP Server running on port ${PORT}`);
});
Update package.json scripts:
{
"scripts": {
"dev": "nodemon src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
Run with npm run dev.
Step 2: Understanding MCP Protocol
MCP uses JSON-over-HTTP with these core endpoints:
POST /mcp/context: Expand context with data/tools.POST /mcp/action: Execute actions (e.g., API calls).GET /mcp/schema: Return tool schemas for Claude discovery.
Claude calls these via tool_use blocks in the Messages API. Schemas follow Anthropic's tool format but with MCP extensions for stateful sessions.
Example schema response:
{
"tools": [
{
"name": "fetch_weather",
"description": "Get current weather",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string"}
}
}
}
]
}
Step 3: Implement Schema Endpoint
Add to src/index.ts:
app.get('/mcp/schema', (req, res) => {
res.json({
tools: [
{
name: 'fetch_weather',
description: 'Fetch weather for a city',
input_schema: {
type: 'object',
properties: {
city: { type: 'string', description: 'City name' }
},
required: ['city']
}
},
// Add more tools
]
});
});
This auto-discovers tools for Claude prompts.
Step 4: Build Core Action Handlers
Handle POST /mcp/action for tool execution. Use sessions for context persistence:
import { v4 as uuidv4 } from 'uuid'; // npm install uuid @types/uuid
type Session = { id: string; context: any[] };
const sessions: Map<string, Session> = new Map();
app.post('/mcp/action', async (req, res) => {
const { tool_name, parameters, session_id } = req.body;
let session = sessions.get(session_id) || { id: session_id || uuidv4(), context: [] };
try {
let result: any;
switch (tool_name) {
case 'fetch_weather':
const city = parameters.city;
// Simulate API call (replace with real e.g., OpenWeatherMap)
result = { temperature: 22, condition: 'Sunny', city };
break;
default:
throw new Error(`Unknown tool: ${tool_name}`);
}
session.context.push({ tool_name, parameters, result });
sessions.set(session.id, session);
res.json({
content: [{ type: 'text', text: JSON.stringify(result) }],
session_id: session.id
});
} catch (error) {
res.status(400).json({ error: (error as Error).message });
}
});
Step 5: Context Expansion Endpoint
For dynamic context injection:
app.post('/mcp/context', async (req, res) => {
const { query, session_id } = req.body;
const session = sessions.get(session_id || uuidv4()) || { id: session_id!, context: [] };
// Example: Search internal DB or vector store
const relevantContext = await mockSearch(query); // Implement your logic
session.context.push({ type: 'context', data: relevantContext });
sessions.set(session.id, session);
res.json({ context: relevantContext, session_id: session.id });
});
async function mockSearch(query: string): Promise<any[]> {
return [{ text: `Relevant info for: ${query}` }];
}
Step 6: Integrate with Claude API
Test from client-side. Create test-client.ts:
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
const runMCPConversation = async (mcpUrl: string) => {
const tools = [
{
name: 'fetch_weather',
description: 'Get weather',
input_schema: { /* as above */ }
}
];
let message = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20240620',
max_tokens: 1024,
tools,
messages: [{ role: 'user', content: 'What's the weather in NYC?' }],
tool_choice: { type: 'auto' }
});
// Handle tool uses
while (message.content.some((c: any) => c.type === 'tool_use')) {
const toolUse = message.content.find((c: any) => c.type === 'tool_use');
const result = await fetch(`${mcpUrl}/mcp/action`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tool_name: toolUse.name,
parameters: toolUse.input,
session_id: 'test-session'
})
}).then(r => r.json());
message = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20240620',
max_tokens: 1024,
tools,
messages: [
{ role: 'user', content: 'Weather in NYC?' },
{ role: 'assistant', content: [message] },
{
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: toolUse.id,
content: result.content
}]
}
]
});
}
console.log(message.content[0].text);
};
runMCPConversation('http://localhost:3000');
Set ANTHROPIC_API_KEY in .env and run.
Step 7: Add Authentication and Security
Protect endpoints:
import jwt from 'jsonwebtoken'; // npm install jsonwebtoken
const verifyToken = (req: express.Request, res: express.Response, next: express.NextFunction) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token' });
try {
jwt.verify(token, process.env.JWT_SECRET!);
next();
} catch {
res.status(403).json({ error: 'Invalid token' });
}
};
app.use('/mcp', verifyToken);
Generate tokens client-side for Claude agents.
Step 8: Advanced Features
-
Stateful Sessions: Persist with Redis:
npm install redisReplace Map with Redis client.
-
Vector Search Integration: Use LanceDB or Pinecone for RAG: Embed queries and retrieve via
/mcp/context. -
Multi-Tool Orchestration: Chain tools in sequence.
-
Error Handling & Logging: Winston for logs, retries with Axios.
Example Redis session:
import { createClient } from 'redis';
const redis = createClient();
await redis.connect();
// Use redis.set/session.get
Step 9: Deployment Options
-
Vercel/Render: Serverless for low traffic. Add
vercel.jsonfor Express support. -
Docker:
FROM node:18 WORKDIR /app COPY . . RUN npm install CMD ["npm", "start"] -
Kubernetes: For enterprise scale with Claude teams.
Expose via ngrok for local testing: ngrok http 3000.
Step 10: Best Practices and Troubleshooting
- Rate Limiting: Use
express-rate-limit. - Validation: Joi/Zod for inputs.
- Monitoring: Prometheus metrics endpoint.
- Claude-Specific Tips:
- Keep tool descriptions concise (<100 tokens).
- Use
tool_choice: 'auto'for dynamic selection. - Test with Haiku for speed, Opus for complexity.
Common issues:
| Issue | Solution |
|---|---|
| Tool not discovered | Check /mcp/schema CORS |
| Session lost | Use persistent storage |
| Latency high | Optimize async calls |
Real-World Use Cases
- Marketing: MCP for CRM data pulls in campaign analysis.
- Engineering: Code review tools via GitHub API.
- HR: Employee data queries with privacy controls.
Conclusion
Custom MCP servers in Node.js transform Claude from a chatbot into a powerhouse agent framework. Start small with weather tools, scale to full agents. Fork this repo, experiment, and share in the Claude community!
Word count: ~1450. Questions? Comment below or join Claude Directory Discord.
Stay ahead of the AI curve
The most important updates, news, and content — delivered in one weekly newsletter.