Building Custom MCP Servers for Claude Code
This guide provides a comprehensive walkthrough for building custom MCP (Model Context Protocol) servers that extend Claude Code’s capabilities. Learn how to create powerful integrations that connect Claude to your specific tools, APIs, and workflows.
Table of Contents
- Overview
- Getting Started
- TypeScript MCP Server Development
- JavaScript Implementation
- Advanced Server Features
- Testing and Debugging
- Deployment Strategies
- Real-World Examples
- Best Practices
Overview
MCP servers are lightweight programs that expose specific capabilities to Claude Code. By building custom servers, you can:
- Integrate proprietary systems not covered by existing servers
- Create domain-specific tools tailored to your workflow
- Expose complex APIs through simple, natural language interfaces
- Build reusable components that can be shared across teams
Why Build Custom MCP Servers?
- Tailored Functionality: Create tools specific to your organization’s needs
- Security Control: Keep sensitive operations within your infrastructure
- Performance Optimization: Optimize for your specific use cases
- Workflow Integration: Seamlessly connect Claude to your existing tools
Getting Started
Prerequisites
# For TypeScript development
npm install -g typescript @modelcontextprotocol/sdk
# For JavaScript development
npm install @modelcontextprotocol/sdk
# Development tools
npm install -g tsx nodemonProject Setup
# Create new MCP server project
mkdir my-mcp-server && cd my-mcp-server
npm init -y
# Install dependencies
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node tsx
# Initialize TypeScript
npx tsc --initBasic Configuration
// package.json
{
"name": "my-mcp-server",
"version": "1.0.0",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts",
"test": "jest"
},
"bin": {
"my-mcp-server": "./dist/index.js"
}
}TypeScript MCP Server Development
Basic Server Structure
// src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// Initialize server with metadata
const server = new Server(
{
name: "my-mcp-server",
version: "1.0.0",
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
// Error handling helper
const handleError = (error: unknown): string => {
if (error instanceof Error) {
return `Error: ${error.message}`;
}
return "An unknown error occurred";
};
// Define tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "process_data",
description: "Process data with custom business logic",
inputSchema: {
type: "object",
properties: {
input: {
type: "string",
description: "Data to process",
},
options: {
type: "object",
properties: {
format: {
type: "string",
enum: ["json", "csv", "xml"],
description: "Output format",
},
validate: {
type: "boolean",
description: "Validate input data",
},
},
},
},
required: ["input"],
},
},
],
};
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "process_data": {
const { input, options } = args as {
input: string;
options?: { format?: string; validate?: boolean };
};
// Custom processing logic
const result = await processData(input, options);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: "text",
text: handleError(error),
},
],
};
}
});
// Define resources
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "config://settings",
name: "Server Configuration",
description: "Current server configuration and settings",
mimeType: "application/json",
},
],
};
});
// Handle resource reads
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
switch (uri) {
case "config://settings":
return {
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify(getServerConfig(), null, 2),
},
],
};
default:
throw new Error(`Unknown resource: ${uri}`);
}
});
// Helper functions
async function processData(
input: string,
options?: { format?: string; validate?: boolean }
): Promise<any> {
// Implement your custom logic here
if (options?.validate) {
validateInput(input);
}
// Process data
const processed = transformData(input);
// Format output
return formatOutput(processed, options?.format || "json");
}
function validateInput(input: string): void {
// Validation logic
if (!input || input.trim().length === 0) {
throw new Error("Input cannot be empty");
}
}
function transformData(input: string): any {
// Transform logic
return {
original: input,
processed: input.toUpperCase(),
timestamp: new Date().toISOString(),
};
}
function formatOutput(data: any, format: string): any {
switch (format) {
case "csv":
return convertToCSV(data);
case "xml":
return convertToXML(data);
default:
return data;
}
}
function getServerConfig(): any {
return {
version: "1.0.0",
environment: process.env.NODE_ENV || "development",
features: {
validation: true,
formats: ["json", "csv", "xml"],
},
};
}
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP server running on stdio");
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});Advanced TypeScript Features
// src/advanced-server.ts
import { Server } from "@modelcontextprotocol/sdk/server";
import { z } from "zod";
import { EventEmitter } from "events";
// Type-safe configuration
interface ServerConfig {
apiKey: string;
baseUrl: string;
timeout: number;
retryAttempts: number;
}
// Input validation schemas
const ProcessingOptionsSchema = z.object({
priority: z.enum(["low", "medium", "high"]).default("medium"),
async: z.boolean().default(false),
callback: z.string().optional(),
});
type ProcessingOptions = z.infer<typeof ProcessingOptionsSchema>;
// Event-driven architecture
class MCPServerWithEvents extends EventEmitter {
private server: Server;
private config: ServerConfig;
private activeJobs: Map<string, Job>;
constructor(config: ServerConfig) {
super();
this.config = config;
this.activeJobs = new Map();
this.server = new Server(
{
name: "advanced-mcp-server",
version: "2.0.0",
},
{
capabilities: {
resources: {},
tools: {},
prompts: {},
},
}
);
this.setupHandlers();
}
private setupHandlers() {
// Tool with streaming response
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "process_stream") {
return this.handleStreamProcessing(args);
}
return this.handleStandardTool(name, args);
});
// Dynamic prompt generation
this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: await this.generateDynamicPrompts(),
};
});
}
private async handleStreamProcessing(args: any) {
const jobId = generateJobId();
const job = new Job(jobId, args);
this.activeJobs.set(jobId, job);
this.emit("job:started", job);
// Return immediately with job ID
return {
content: [
{
type: "text",
text: JSON.stringify({
jobId,
status: "processing",
message: "Job started. Use check_job tool to monitor progress.",
}),
},
],
};
}
private async generateDynamicPrompts() {
// Generate prompts based on current state
const prompts = [];
if (this.activeJobs.size > 0) {
prompts.push({
name: "check_all_jobs",
description: "Check status of all active jobs",
arguments: [],
});
}
// Add context-aware prompts
const recentErrors = await this.getRecentErrors();
if (recentErrors.length > 0) {
prompts.push({
name: "diagnose_errors",
description: "Analyze recent errors and suggest fixes",
arguments: [
{
name: "error_count",
description: "Number of recent errors to analyze",
required: false,
},
],
});
}
return prompts;
}
}
// Job management
class Job {
id: string;
status: "pending" | "processing" | "completed" | "failed";
progress: number;
result?: any;
error?: Error;
constructor(id: string, params: any) {
this.id = id;
this.status = "pending";
this.progress = 0;
}
updateProgress(progress: number) {
this.progress = Math.min(100, Math.max(0, progress));
if (this.progress === 100) {
this.status = "completed";
}
}
}JavaScript Implementation
For those preferring JavaScript:
// index.js
import { Server } from "@modelcontextprotocol/sdk/server";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server(
{
name: "js-mcp-server",
version: "1.0.0",
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
// Define tools with JSDoc for better IDE support
/**
* @typedef {Object} AnalyzeOptions
* @property {boolean} [detailed] - Include detailed analysis
* @property {string[]} [metrics] - Specific metrics to calculate
*/
server.setRequestHandler("tools/list", async () => {
return {
tools: [
{
name: "analyze_text",
description: "Analyze text and return insights",
inputSchema: {
type: "object",
properties: {
text: {
type: "string",
description: "Text to analyze",
},
options: {
type: "object",
properties: {
detailed: {
type: "boolean",
description: "Include detailed analysis",
},
metrics: {
type: "array",
items: { type: "string" },
description: "Specific metrics to calculate",
},
},
},
},
required: ["text"],
},
},
],
};
});
// Handle tool calls
server.setRequestHandler("tools/call", async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "analyze_text":
return handleTextAnalysis(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
});
/**
* Analyze text and return insights
* @param {Object} params
* @param {string} params.text - Text to analyze
* @param {AnalyzeOptions} [params.options] - Analysis options
*/
async function handleTextAnalysis({ text, options = {} }) {
const analysis = {
length: text.length,
wordCount: text.split(/\s+/).length,
lineCount: text.split('\n').length,
};
if (options.detailed) {
analysis.sentences = text.split(/[.!?]+/).length - 1;
analysis.avgWordLength =
text.replace(/\s/g, '').length / analysis.wordCount;
}
if (options.metrics && options.metrics.includes('readability')) {
analysis.readabilityScore = calculateReadability(text);
}
return {
content: [
{
type: "text",
text: JSON.stringify(analysis, null, 2),
},
],
};
}
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("JavaScript MCP server started");
}
main().catch(console.error);Advanced Server Features
1. Authentication and Security
// src/auth.ts
import jwt from "jsonwebtoken";
import { createHash } from "crypto";
export class AuthManager {
private readonly secret: string;
private readonly validTokens: Set<string> = new Set();
constructor(secret: string) {
this.secret = secret;
}
generateToken(userId: string, permissions: string[]): string {
const token = jwt.sign(
{
userId,
permissions,
iat: Date.now(),
},
this.secret,
{ expiresIn: "24h" }
);
this.validTokens.add(this.hashToken(token));
return token;
}
validateToken(token: string): jwt.JwtPayload | null {
try {
const hashedToken = this.hashToken(token);
if (!this.validTokens.has(hashedToken)) {
return null;
}
return jwt.verify(token, this.secret) as jwt.JwtPayload;
} catch {
return null;
}
}
private hashToken(token: string): string {
return createHash("sha256").update(token).digest("hex");
}
}
// Integration in server
server.setRequestHandler(CallToolRequestSchema, async (request) => {
// Extract auth token from request
const authToken = request.params.authToken;
if (!authToken) {
return {
content: [
{
type: "text",
text: "Authentication required",
},
],
};
}
const payload = authManager.validateToken(authToken);
if (!payload) {
return {
content: [
{
type: "text",
text: "Invalid or expired token",
},
],
};
}
// Check permissions
const requiredPermission = getRequiredPermission(request.params.name);
if (!payload.permissions.includes(requiredPermission)) {
return {
content: [
{
type: "text",
text: "Insufficient permissions",
},
],
};
}
// Process request...
});2. Database Integration
// src/database.ts
import { Pool } from "pg";
import { Redis } from "ioredis";
export class DatabaseManager {
private pgPool: Pool;
private redis: Redis;
constructor(config: DatabaseConfig) {
this.pgPool = new Pool(config.postgres);
this.redis = new Redis(config.redis);
}
async queryWithCache(
query: string,
params: any[],
ttl: number = 300
): Promise<any> {
const cacheKey = this.getCacheKey(query, params);
// Check cache
const cached = await this.redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Execute query
const result = await this.pgPool.query(query, params);
// Cache result
await this.redis.setex(
cacheKey,
ttl,
JSON.stringify(result.rows)
);
return result.rows;
}
private getCacheKey(query: string, params: any[]): string {
return `query:${createHash("md5")
.update(query + JSON.stringify(params))
.digest("hex")}`;
}
}
// Database tool
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "query_database",
description: "Execute a database query with natural language",
inputSchema: {
type: "object",
properties: {
description: {
type: "string",
description: "Natural language description of the query",
},
table: {
type: "string",
description: "Table to query",
},
filters: {
type: "object",
description: "Filter conditions",
},
},
required: ["description", "table"],
},
},
],
};
});3. File System Operations
// src/filesystem.ts
import fs from "fs/promises";
import path from "path";
import chokidar from "chokidar";
export class FileSystemManager {
private basePath: string;
private watcher: chokidar.FSWatcher;
private fileChangeCallbacks: Map<string, Function[]> = new Map();
constructor(basePath: string) {
this.basePath = basePath;
this.watcher = chokidar.watch(basePath, {
persistent: true,
ignoreInitial: true,
});
this.setupWatcher();
}
private setupWatcher() {
this.watcher.on("change", (filePath) => {
const callbacks = this.fileChangeCallbacks.get(filePath) || [];
callbacks.forEach((cb) => cb(filePath));
});
}
async safeReadFile(filePath: string): Promise<string> {
const fullPath = this.resolvePath(filePath);
await this.validatePath(fullPath);
return fs.readFile(fullPath, "utf-8");
}
async safeWriteFile(filePath: string, content: string): Promise<void> {
const fullPath = this.resolvePath(filePath);
await this.validatePath(fullPath);
// Create backup
const backupPath = `${fullPath}.backup`;
try {
await fs.copyFile(fullPath, backupPath);
} catch {
// File doesn't exist yet
}
await fs.writeFile(fullPath, content, "utf-8");
}
watchFile(filePath: string, callback: Function) {
const fullPath = this.resolvePath(filePath);
const callbacks = this.fileChangeCallbacks.get(fullPath) || [];
callbacks.push(callback);
this.fileChangeCallbacks.set(fullPath, callbacks);
}
private resolvePath(filePath: string): string {
const resolved = path.resolve(this.basePath, filePath);
return resolved;
}
private async validatePath(fullPath: string): Promise<void> {
// Ensure path is within basePath
if (!fullPath.startsWith(this.basePath)) {
throw new Error("Path traversal attempt detected");
}
// Check if path exists and is accessible
try {
await fs.access(fullPath);
} catch {
// Create directory if it doesn't exist
await fs.mkdir(path.dirname(fullPath), { recursive: true });
}
}
}4. HTTP/API Integration
// src/api-client.ts
import axios, { AxiosInstance } from "axios";
import pRetry from "p-retry";
export class APIClient {
private client: AxiosInstance;
private rateLimiter: RateLimiter;
constructor(config: APIConfig) {
this.client = axios.create({
baseURL: config.baseURL,
timeout: config.timeout || 10000,
headers: {
"User-Agent": "MCP-Server/1.0",
...config.headers,
},
});
this.rateLimiter = new RateLimiter(
config.rateLimit || { requests: 100, per: 60000 }
);
this.setupInterceptors();
}
private setupInterceptors() {
// Request interceptor for rate limiting
this.client.interceptors.request.use(async (config) => {
await this.rateLimiter.acquire();
return config;
});
// Response interceptor for error handling
this.client.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 429) {
// Rate limited, wait and retry
const retryAfter = error.response.headers["retry-after"] || 60;
await new Promise((resolve) =>
setTimeout(resolve, retryAfter * 1000)
);
return this.client.request(error.config);
}
throw error;
}
);
}
async makeRequest(
method: string,
endpoint: string,
data?: any
): Promise<any> {
return pRetry(
async () => {
const response = await this.client.request({
method,
url: endpoint,
data,
});
return response.data;
},
{
retries: 3,
onFailedAttempt: (error) => {
console.error(
`Attempt ${error.attemptNumber} failed: ${error.message}`
);
},
}
);
}
}
class RateLimiter {
private requests: number[];
private maxRequests: number;
private windowMs: number;
constructor({ requests, per }: { requests: number; per: number }) {
this.requests = [];
this.maxRequests = requests;
this.windowMs = per;
}
async acquire(): Promise<void> {
const now = Date.now();
this.requests = this.requests.filter(
(time) => now - time < this.windowMs
);
if (this.requests.length >= this.maxRequests) {
const oldestRequest = this.requests[0];
const waitTime = this.windowMs - (now - oldestRequest);
await new Promise((resolve) => setTimeout(resolve, waitTime));
return this.acquire();
}
this.requests.push(now);
}
}Testing and Debugging
Unit Testing
// src/__tests__/server.test.ts
import { Server } from "@modelcontextprotocol/sdk/server";
import { MemoryTransport } from "./test-utils";
describe("MCP Server", () => {
let server: Server;
let transport: MemoryTransport;
beforeEach(() => {
server = new Server({
name: "test-server",
version: "1.0.0",
});
transport = new MemoryTransport();
});
test("should list available tools", async () => {
server.setRequestHandler("tools/list", async () => ({
tools: [
{
name: "test_tool",
description: "A test tool",
inputSchema: {
type: "object",
properties: {
input: { type: "string" },
},
},
},
],
}));
await server.connect(transport);
const response = await transport.request("tools/list", {});
expect(response.tools).toHaveLength(1);
expect(response.tools[0].name).toBe("test_tool");
});
test("should handle tool execution", async () => {
server.setRequestHandler("tools/call", async (request) => {
const { name, arguments: args } = request.params;
if (name === "echo") {
return {
content: [
{
type: "text",
text: args.message,
},
],
};
}
throw new Error("Unknown tool");
});
await server.connect(transport);
const response = await transport.request("tools/call", {
name: "echo",
arguments: { message: "Hello, World!" },
});
expect(response.content[0].text).toBe("Hello, World!");
});
});Integration Testing
// src/__tests__/integration.test.ts
import { spawn } from "child_process";
import { Client } from "@modelcontextprotocol/sdk/client";
describe("MCP Server Integration", () => {
let serverProcess: any;
let client: Client;
beforeAll(async () => {
// Start server
serverProcess = spawn("node", ["dist/index.js"], {
stdio: ["pipe", "pipe", "pipe"],
});
// Create client
client = new Client();
await client.connect({
stdin: serverProcess.stdout,
stdout: serverProcess.stdin,
});
});
afterAll(() => {
serverProcess.kill();
});
test("end-to-end tool execution", async () => {
const tools = await client.listTools();
expect(tools.length).toBeGreaterThan(0);
const result = await client.callTool(
tools[0].name,
{ input: "test data" }
);
expect(result).toBeDefined();
});
});Debugging Tools
// src/debug.ts
export class MCPDebugger {
private logs: LogEntry[] = [];
private maxLogs: number = 1000;
log(level: "info" | "warn" | "error", message: string, data?: any) {
const entry: LogEntry = {
timestamp: new Date().toISOString(),
level,
message,
data,
stack: new Error().stack,
};
this.logs.push(entry);
if (this.logs.length > this.maxLogs) {
this.logs.shift();
}
// Also log to console in development
if (process.env.NODE_ENV === "development") {
console.log(`[${level.toUpperCase()}] ${message}`, data || "");
}
}
getRecentLogs(count: number = 100): LogEntry[] {
return this.logs.slice(-count);
}
clearLogs() {
this.logs = [];
}
exportLogs(): string {
return JSON.stringify(this.logs, null, 2);
}
}
// Use in server
const debugger = new MCPDebugger();
server.setRequestHandler(CallToolRequestSchema, async (request) => {
debugger.log("info", "Tool called", {
tool: request.params.name,
args: request.params.arguments,
});
try {
const result = await processToolCall(request);
debugger.log("info", "Tool completed", {
tool: request.params.name,
success: true,
});
return result;
} catch (error) {
debugger.log("error", "Tool failed", {
tool: request.params.name,
error: error.message,
});
throw error;
}
});Deployment Strategies
1. Local Development
# Development with hot reload
npm run dev
# Test with Claude Desktop
claude mcp add my-server --transport stdio -- npm run start2. Production Deployment
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist/ ./dist/
COPY .env.production .env
EXPOSE 3000
CMD ["node", "dist/index.js"]3. Cloud Deployment
# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-server
spec:
replicas: 3
selector:
matchLabels:
app: mcp-server
template:
metadata:
labels:
app: mcp-server
spec:
containers:
- name: mcp-server
image: myregistry/mcp-server:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"Real-World Examples
1. Database Query Assistant
// Complete database query MCP server
import { Server } from "@modelcontextprotocol/sdk/server";
import { Pool } from "pg";
import natural from "natural";
class DatabaseQueryServer {
private server: Server;
private db: Pool;
private classifier: any;
constructor() {
this.server = new Server({
name: "db-query-assistant",
version: "1.0.0",
});
this.db = new Pool({
connectionString: process.env.DATABASE_URL,
});
this.setupNLP();
this.registerTools();
}
private setupNLP() {
// Train classifier for query intent
this.classifier = new natural.BayesClassifier();
// Training data
this.classifier.addDocument("show all users", "SELECT");
this.classifier.addDocument("find customers from", "SELECT");
this.classifier.addDocument("update user email", "UPDATE");
this.classifier.addDocument("delete old records", "DELETE");
this.classifier.addDocument("add new customer", "INSERT");
this.classifier.train();
}
private registerTools() {
this.server.setRequestHandler("tools/list", async () => ({
tools: [
{
name: "natural_query",
description: "Execute database queries using natural language",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Natural language query description",
},
limit: {
type: "number",
description: "Maximum number of results",
default: 100,
},
},
required: ["query"],
},
},
],
}));
this.server.setRequestHandler("tools/call", async (request) => {
if (request.params.name === "natural_query") {
return this.handleNaturalQuery(request.params.arguments);
}
throw new Error("Unknown tool");
});
}
private async handleNaturalQuery({ query, limit = 100 }) {
try {
// Classify query intent
const intent = this.classifier.classify(query);
// Generate SQL based on intent
const sql = await this.generateSQL(query, intent);
// Add safety checks
if (intent !== "SELECT" && !query.includes("confirm")) {
return {
content: [
{
type: "text",
text: `This appears to be a ${intent} operation. Please add "confirm" to your query to proceed.`,
},
],
};
}
// Execute query
const result = await this.db.query({
text: sql,
values: [],
rowMode: "object",
});
return {
content: [
{
type: "text",
text: JSON.stringify({
query: sql,
rowCount: result.rowCount,
rows: result.rows.slice(0, limit),
}, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error.message}`,
},
],
};
}
}
private async generateSQL(naturalQuery: string, intent: string): Promise<string> {
// Simple SQL generation logic
// In production, use a more sophisticated NLP model
const patterns = {
SELECT: {
pattern: /(?:show|find|get|list) (?:all )?(\w+)(?: where (.+))?/i,
template: "SELECT * FROM $1 WHERE $2",
},
UPDATE: {
pattern: /update (\w+) set (.+) where (.+)/i,
template: "UPDATE $1 SET $2 WHERE $3",
},
};
const patternConfig = patterns[intent];
if (!patternConfig) {
throw new Error(`Unsupported query type: ${intent}`);
}
const match = naturalQuery.match(patternConfig.pattern);
if (!match) {
throw new Error("Could not parse query");
}
// Build SQL from template
let sql = patternConfig.template;
match.slice(1).forEach((group, index) => {
if (group) {
sql = sql.replace(`$${index + 1}`, group);
}
});
return sql;
}
}2. CI/CD Pipeline Assistant
// CI/CD automation MCP server
class CICDServer {
private githubClient: Octokit;
private jenkinsClient: Jenkins;
constructor() {
this.githubClient = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
this.jenkinsClient = new Jenkins({
baseUrl: process.env.JENKINS_URL,
crumbIssuer: true,
promisify: true,
});
this.registerTools();
}
private registerTools() {
this.server.setRequestHandler("tools/list", async () => ({
tools: [
{
name: "deploy_application",
description: "Deploy application to specified environment",
inputSchema: {
type: "object",
properties: {
application: {
type: "string",
description: "Application name",
},
environment: {
type: "string",
enum: ["dev", "staging", "production"],
description: "Target environment",
},
version: {
type: "string",
description: "Version to deploy (tag or branch)",
},
},
required: ["application", "environment", "version"],
},
},
{
name: "rollback_deployment",
description: "Rollback to previous deployment",
inputSchema: {
type: "object",
properties: {
application: {
type: "string",
description: "Application name",
},
environment: {
type: "string",
enum: ["dev", "staging", "production"],
},
},
required: ["application", "environment"],
},
},
],
}));
}
private async deployApplication({ application, environment, version }) {
// Pre-deployment checks
const checks = await this.runPreDeploymentChecks(
application,
environment,
version
);
if (!checks.passed) {
return {
content: [
{
type: "text",
text: `Pre-deployment checks failed:\n${checks.failures.join("\n")}`,
},
],
};
}
// Trigger deployment
const job = await this.jenkinsClient.job.build({
name: `deploy-${application}-${environment}`,
parameters: {
VERSION: version,
ENVIRONMENT: environment,
},
});
// Monitor deployment
const result = await this.monitorDeployment(job.queueId);
return {
content: [
{
type: "text",
text: JSON.stringify({
status: result.status,
duration: result.duration,
logs: result.logs.slice(-1000), // Last 1000 chars
url: result.url,
}, null, 2),
},
],
};
}
}Best Practices
1. Error Handling
// Comprehensive error handling
class ErrorHandler {
static handle(error: unknown): ErrorResponse {
if (error instanceof ValidationError) {
return {
type: "validation_error",
message: error.message,
details: error.details,
};
}
if (error instanceof AuthenticationError) {
return {
type: "authentication_error",
message: "Authentication failed",
// Don't expose sensitive details
};
}
if (error instanceof RateLimitError) {
return {
type: "rate_limit_error",
message: "Rate limit exceeded",
retryAfter: error.retryAfter,
};
}
// Log unexpected errors
console.error("Unexpected error:", error);
return {
type: "internal_error",
message: "An unexpected error occurred",
};
}
}2. Input Validation
// Strict input validation
import { z } from "zod";
const ToolInputSchema = z.object({
name: z.string().min(1).max(100),
arguments: z.record(z.unknown()),
metadata: z.object({
userId: z.string().uuid(),
timestamp: z.string().datetime(),
}).optional(),
});
function validateToolInput(input: unknown): ToolInput {
try {
return ToolInputSchema.parse(input);
} catch (error) {
if (error instanceof z.ZodError) {
throw new ValidationError(
"Invalid input",
error.errors.map(e => ({
path: e.path.join("."),
message: e.message,
}))
);
}
throw error;
}
}3. Performance Optimization
// Performance monitoring and optimization
class PerformanceMonitor {
private metrics: Map<string, Metric[]> = new Map();
async measure<T>(
operation: string,
fn: () => Promise<T>
): Promise<T> {
const start = process.hrtime.bigint();
try {
const result = await fn();
const duration = Number(process.hrtime.bigint() - start) / 1e6; // ms
this.recordMetric(operation, {
duration,
success: true,
timestamp: Date.now(),
});
return result;
} catch (error) {
const duration = Number(process.hrtime.bigint() - start) / 1e6;
this.recordMetric(operation, {
duration,
success: false,
timestamp: Date.now(),
});
throw error;
}
}
private recordMetric(operation: string, metric: Metric) {
const metrics = this.metrics.get(operation) || [];
metrics.push(metric);
// Keep only last 1000 metrics
if (metrics.length > 1000) {
metrics.shift();
}
this.metrics.set(operation, metrics);
}
getStats(operation: string): Stats {
const metrics = this.metrics.get(operation) || [];
if (metrics.length === 0) {
return null;
}
const durations = metrics.map(m => m.duration);
const successes = metrics.filter(m => m.success).length;
return {
count: metrics.length,
successRate: successes / metrics.length,
avgDuration: durations.reduce((a, b) => a + b) / durations.length,
minDuration: Math.min(...durations),
maxDuration: Math.max(...durations),
p95Duration: this.percentile(durations, 0.95),
p99Duration: this.percentile(durations, 0.99),
};
}
private percentile(values: number[], p: number): number {
const sorted = values.sort((a, b) => a - b);
const index = Math.ceil(sorted.length * p) - 1;
return sorted[index];
}
}4. Configuration Management
// Environment-based configuration
class ConfigManager {
private config: Config;
constructor() {
this.config = this.loadConfig();
this.validateConfig();
}
private loadConfig(): Config {
const env = process.env.NODE_ENV || "development";
const baseConfig = {
server: {
name: process.env.MCP_SERVER_NAME || "my-mcp-server",
version: process.env.MCP_SERVER_VERSION || "1.0.0",
port: parseInt(process.env.PORT || "3000"),
},
features: {
authentication: process.env.ENABLE_AUTH === "true",
rateLimit: process.env.ENABLE_RATE_LIMIT === "true",
caching: process.env.ENABLE_CACHE === "true",
},
limits: {
maxRequestSize: parseInt(process.env.MAX_REQUEST_SIZE || "1048576"),
maxConcurrent: parseInt(process.env.MAX_CONCURRENT || "100"),
timeout: parseInt(process.env.TIMEOUT || "30000"),
},
};
// Environment-specific overrides
const envConfig = this.loadEnvConfig(env);
return deepMerge(baseConfig, envConfig);
}
private validateConfig() {
const required = [
"server.name",
"server.version",
];
for (const path of required) {
if (!this.get(path)) {
throw new Error(`Missing required config: ${path}`);
}
}
}
get<T>(path: string): T {
return path.split(".").reduce((obj, key) => obj?.[key], this.config) as T;
}
}Conclusion
Building custom MCP servers for Claude Code opens up endless possibilities for extending its capabilities. By following the patterns and best practices outlined in this guide, you can create robust, secure, and performant integrations that seamlessly connect Claude to your specific tools and workflows.
Remember to:
- Start simple and iterate
- Focus on security and validation
- Test thoroughly
- Monitor performance
- Document your tools well
The MCP ecosystem is rapidly evolving, with new features and improvements being added regularly. Stay connected with the community and keep your servers updated to take advantage of the latest capabilities.