Building Custom MCP Servers: From Concept to Claude Integration
Model Context Protocol (MCP) servers extend Claude with custom tools. Learn the architecture, implement resource/tool patterns, and handle the bidirectional JSON-RPC communication protocol.
Model Context Protocol (MCP) connects Claude to external systems via custom servers. Unlike simple function calling, MCP enables bidirectional communication with resources (data) and tools (actions). Here's how to build production-ready MCP servers.
MCP Architecture: More Than Function Calling
MCP uses JSON-RPC 2.0 over stdio/transport with three core concepts:
// MCP server structure
interface MCPServer {
resources: Resource[]; // Data Claude can read
tools: Tool[]; // Actions Claude can execute
prompts: Prompt[]; // Templated instructions
}Resources are read-only data sources—files, database records, API responses. Tools are executable functions that modify state. Prompts provide templated instructions with placeholders.
Unlike OpenAI function calling (stateless, single request), MCP maintains persistent connections with streaming updates and state synchronization.
Implementing the MCP Protocol
Start with the official TypeScript SDK:
import {
Server,
StdioServerTransport,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListToolsRequestSchema,
CallToolRequestSchema
} from '@modelcontextprotocol/sdk/server';
class DatabaseMCPServer {
private server = new Server(
{ name: 'database-server', version: '1.0.0' },
{ capabilities: { resources: {}, tools: {} } }
);
constructor() {
this.setupResourceHandlers();
this.setupToolHandlers();
}
private setupResourceHandlers() {
// List available database tables as resources
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: 'db://tables/users',
name: 'Users Table',
description: 'User account data',
mimeType: 'application/json'
},
{
uri: 'db://tables/orders',
name: 'Orders Table',
description: 'Order transaction data',
mimeType: 'application/json'
}
]
}));
// Read specific resource content
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
if (uri === 'db://tables/users') {
const users = await this.queryDatabase('SELECT * FROM users LIMIT 10');
return {
contents: [{
uri,
mimeType: 'application/json',
text: JSON.stringify(users, null, 2)
}]
};
}
throw new Error(`Unknown resource: ${uri}`);
});
}Tool Implementation: Actions with Side Effects
Tools execute actions that modify external state:
private setupToolHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'create_user',
description: 'Create a new user account',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Full name' },
email: { type: 'string', format: 'email' },
role: {
type: 'string',
enum: ['user', 'admin', 'moderator'],
default: 'user'
}
},
required: ['name', 'email']
}
},
{
name: 'send_notification',
description: 'Send notification to user',
inputSchema: {
type: 'object',
properties: {
userId: { type: 'number' },
message: { type: 'string' },
channel: {
type: 'string',
enum: ['email', 'sms', 'push'],
default: 'email'
}
},
required: ['userId', 'message']
}
}
]
}));
// Execute tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case 'create_user':
return await this.createUser(args as CreateUserArgs);
case 'send_notification':
return await this.sendNotification(args as NotificationArgs);
default:
throw new Error(`Unknown tool: ${name}`);
}
});
}
private async createUser(args: CreateUserArgs) {
try {
const userId = await this.database.query(
'INSERT INTO users (name, email, role) VALUES ($1, $2, $3) RETURNING id',
[args.name, args.email, args.role || 'user']
);
return {
content: [{
type: 'text',
text: `Successfully created user with ID ${userId}. \n\nDetails:\n- Name: ${args.name}\n- Email: ${args.email}\n- Role: ${args.role || 'user'}`
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: `Failed to create user: ${error.message}`
}],
isError: true
};
}
}Advanced Patterns: Streaming and State Management
MCP supports server-initiated notifications for real-time updates:
class RealtimeMCPServer {
private subscriptions = new MapError Handling and Recovery
MCP servers must handle failures gracefully:
// Implement retry logic for flaky external APIs
private async withRetryProduction Deployment and Configuration
Package your MCP server for Claude Desktop integration:
// package.json - Make it executable
{
"name": "my-mcp-server",
"version": "1.0.0",
"bin": {
"my-mcp-server": "./dist/index.js"
},
"files": ["dist/"],
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
// index.ts - Server entry point
#!/usr/bin/env node
async function main() {
const server = new DatabaseMCPServer();
// Connect via stdio transport for Claude Desktop
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Database MCP Server started'); // stderr for logging
}
main().catch(console.error);Claude Desktop configuration (claude_desktop_config.json):
{
"mcpServers": {
"database": {
"command": "npx",
"args": ["my-mcp-server"],
"env": {
"DATABASE_URL": "postgresql://localhost/myapp"
}
}
}
}Security and Access Control
MCP servers have full system access—implement security layers:
// Environment-based access control
class SecureMCPServer {
private readonly allowedOperations: SetSecurity best practices:
- Validate all inputs using JSON Schema
- Use environment variables for sensitive configuration
- Implement rate limiting for destructive operations
- Log all tool executions for audit trails
- Sanitize resource content to prevent injection attacks
Key architectural insight: MCP servers are persistent processes that maintain state and bidirectional communication with Claude, unlike stateless function calls. This enables sophisticated workflows but requires careful resource management and error recovery.
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Advertisement