Chapter 3: Implementation Guide
Building MCP Servers and Clients
This chapter provides comprehensive guidance for implementing MCP servers and clients, from basic examples to production-ready systems. We'll cover best practices, common patterns, and real-world implementation strategies.
Project Setup and Dependencies
Setting Up the Development Environment
# Create a new MCP project
mkdir awesome-mcp-server
cd awesome-mcp-server
# Initialize package.json
npm init -y
# Install MCP SDK and dependencies
npm install @modelcontextprotocol/sdk
npm install --save-dev typescript @types/node ts-node nodemon
# Install additional utilities
npm install zod ajv dotenv winstonProject Structure
awesome-mcp-server/
├── src/
│ ├── server/
│ │ ├── index.ts
│ │ ├── handlers/
│ │ │ ├── tools.ts
│ │ │ ├── resources.ts
│ │ │ └── prompts.ts
│ │ ├── services/
│ │ │ ├── database.ts
│ │ │ ├── filesystem.ts
│ │ │ └── external-api.ts
│ │ └── utils/
│ │ ├── validation.ts
│ │ ├── permissions.ts
│ │ └── logging.ts
│ ├── client/
│ │ ├── index.ts
│ │ └── examples/
│ └── types/
│ └── index.ts
├── config/
│ ├── server.json
│ └── permissions.json
├── tests/
├── package.json
└── tsconfig.jsonTypeScript Configuration
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}Building a Comprehensive MCP Server
Core Server Implementation
// src/server/index.ts
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 { z } from "zod";
import { ToolHandlers } from "./handlers/tools.js";
import { ResourceHandlers } from "./handlers/resources.js";
import { PromptHandlers } from "./handlers/prompts.js";
import { Logger } from "./utils/logging.js";
import { PermissionManager } from "./utils/permissions.js";
import { ValidationManager } from "./utils/validation.js";
export class AwesomeMCPServer {
private server: Server;
private logger: Logger;
private permissions: PermissionManager;
private validation: ValidationManager;
private toolHandlers: ToolHandlers;
private resourceHandlers: ResourceHandlers;
private promptHandlers: PromptHandlers;
constructor() {
this.logger = new Logger("AwesomeMCPServer");
this.permissions = new PermissionManager();
this.validation = new ValidationManager();
this.server = new Server(
{
name: "awesome-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: { listChanged: true },
resources: {
subscribe: true,
listChanged: true,
},
prompts: { listChanged: true },
logging: {},
},
}
);
this.toolHandlers = new ToolHandlers(this.logger, this.permissions);
this.resourceHandlers = new ResourceHandlers(this.logger, this.permissions);
this.promptHandlers = new PromptHandlers(this.logger);
this.setupHandlers();
}
private setupHandlers(): void {
// Tool handlers
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: await this.toolHandlers.listTools(),
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// Validate permissions
if (!this.permissions.canExecuteTool(name)) {
throw new Error(`Permission denied for tool: ${name}`);
}
// Execute tool
return await this.toolHandlers.callTool(name, args || {});
});
// Resource handlers
this.server.setRequestHandler({ method: "resources/list" }, async () => ({
resources: await this.resourceHandlers.listResources(),
}));
this.server.setRequestHandler(
{ method: "resources/read" },
async (request) => {
const { uri } = request.params as { uri: string };
if (!this.permissions.canAccessResource(uri)) {
throw new Error(`Permission denied for resource: ${uri}`);
}
return {
contents: await this.resourceHandlers.readResource(uri),
};
}
);
// Prompt handlers
this.server.setRequestHandler({ method: "prompts/list" }, async () => ({
prompts: await this.promptHandlers.listPrompts(),
}));
this.server.setRequestHandler(
{ method: "prompts/get" },
async (request) => {
const { name, arguments: args } = request.params as {
name: string;
arguments?: Record<string, any>;
};
return await this.promptHandlers.getPrompt(name, args || {});
}
);
// Logging handler
this.server.setNotificationHandler(
{ method: "notifications/message" },
async (notification) => {
const { level, logger, data } = notification.params as {
level: string;
logger?: string;
data: any;
};
this.logger.log(level as any, `[${logger}] ${JSON.stringify(data)}`);
}
);
}
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
this.logger.info("MCP Server started successfully");
}
async stop(): Promise<void> {
await this.server.close();
this.logger.info("MCP Server stopped");
}
}
// Start the server
if (require.main === module) {
const server = new AwesomeMCPServer();
process.on("SIGINT", async () => {
console.log("Received SIGINT, shutting down gracefully...");
await server.stop();
process.exit(0);
});
server.start().catch(console.error);
}Tool Handlers Implementation
// src/handlers/tools.ts
import {
Tool,
ToolResult,
TextContent,
} from "@modelcontextprotocol/sdk/types.js";
import { DatabaseService } from "../services/database.js";
import { FileSystemService } from "../services/filesystem.js";
import { ExternalAPIService } from "../services/external-api.js";
import { Logger } from "../utils/logging.js";
import { PermissionManager } from "../utils/permissions.js";
export class ToolHandlers {
private dbService: DatabaseService;
private fsService: FileSystemService;
private apiService: ExternalAPIService;
constructor(private logger: Logger, private permissions: PermissionManager) {
this.dbService = new DatabaseService();
this.fsService = new FileSystemService();
this.apiService = new ExternalAPIService();
}
async listTools(): Promise<Tool[]> {
return [
{
name: "query_database",
description: "Execute SQL queries against the database",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "SQL query to execute",
},
limit: {
type: "number",
description: "Maximum number of results",
default: 100,
},
},
required: ["query"],
},
},
{
name: "search_files",
description: "Search for files matching a pattern",
inputSchema: {
type: "object",
properties: {
pattern: {
type: "string",
description: "Glob pattern to search for",
},
directory: {
type: "string",
description: "Directory to search in",
default: ".",
},
recursive: {
type: "boolean",
description: "Search recursively",
default: true,
},
},
required: ["pattern"],
},
},
{
name: "http_request",
description: "Make HTTP requests to external APIs",
inputSchema: {
type: "object",
properties: {
url: {
type: "string",
description: "URL to request",
},
method: {
type: "string",
enum: ["GET", "POST", "PUT", "DELETE"],
default: "GET",
},
headers: {
type: "object",
description: "HTTP headers",
},
body: {
type: "string",
description: "Request body",
},
},
required: ["url"],
},
},
{
name: "analyze_code",
description: "Analyze code for issues and suggestions",
inputSchema: {
type: "object",
properties: {
code: {
type: "string",
description: "Source code to analyze",
},
language: {
type: "string",
description: "Programming language",
},
checks: {
type: "array",
items: {
type: "string",
enum: ["syntax", "security", "performance", "style"],
},
description: "Types of analysis to perform",
},
},
required: ["code"],
},
},
];
}
async callTool(name: string, args: Record<string, any>): Promise<ToolResult> {
this.logger.info(`Calling tool: ${name}`, { args });
try {
switch (name) {
case "query_database":
return await this.handleDatabaseQuery(args);
case "search_files":
return await this.handleFileSearch(args);
case "http_request":
return await this.handleHttpRequest(args);
case "analyze_code":
return await this.handleCodeAnalysis(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
this.logger.error(`Tool execution failed: ${name}`, { error, args });
return {
content: [
{
type: "text",
text: `Error executing tool ${name}: ${error.message}`,
},
],
isError: true,
};
}
}
private async handleDatabaseQuery(
args: Record<string, any>
): Promise<ToolResult> {
const { query, limit = 100 } = args;
// Validate SQL query (basic security check)
if (!this.isValidQuery(query)) {
throw new Error("Invalid or potentially dangerous SQL query");
}
const results = await this.dbService.executeQuery(query, limit);
return {
content: [
{
type: "text",
text:
`Query executed successfully. Found ${results.length} results:\n\n` +
JSON.stringify(results, null, 2),
},
],
};
}
private async handleFileSearch(
args: Record<string, any>
): Promise<ToolResult> {
const { pattern, directory = ".", recursive = true } = args;
const files = await this.fsService.searchFiles(
pattern,
directory,
recursive
);
return {
content: [
{
type: "text",
text:
`Found ${files.length} files matching "${pattern}":\n\n` +
files.map((file) => `- ${file}`).join("\n"),
},
],
};
}
private async handleHttpRequest(
args: Record<string, any>
): Promise<ToolResult> {
const { url, method = "GET", headers = {}, body } = args;
// Validate URL
if (!this.isValidUrl(url)) {
throw new Error("Invalid URL provided");
}
const response = await this.apiService.makeRequest({
url,
method,
headers,
body,
});
return {
content: [
{
type: "text",
text:
`HTTP ${method} ${url}\n` +
`Status: ${response.status}\n` +
`Headers: ${JSON.stringify(response.headers, null, 2)}\n\n` +
`Response:\n${response.data}`,
},
],
};
}
private async handleCodeAnalysis(
args: Record<string, any>
): Promise<ToolResult> {
const { code, language, checks = ["syntax", "security", "style"] } = args;
const analysis = await this.analyzeCode(code, language, checks);
return {
content: [
{
type: "text",
text:
`Code Analysis Results:\n\n` +
`Language: ${language}\n` +
`Checks performed: ${checks.join(", ")}\n\n` +
`Issues found: ${analysis.issues.length}\n\n` +
analysis.issues
.map(
(issue) =>
`- ${issue.severity}: ${issue.message} (Line ${issue.line})`
)
.join("\n"),
},
],
};
}
private isValidQuery(query: string): boolean {
// Basic SQL injection protection
const dangerous = [
/DROP\s+TABLE/i,
/DELETE\s+FROM/i,
/UPDATE\s+.*\s+SET/i,
/INSERT\s+INTO/i,
/CREATE\s+TABLE/i,
/ALTER\s+TABLE/i,
/TRUNCATE/i,
/EXEC/i,
/EXECUTE/i,
];
return !dangerous.some((pattern) => pattern.test(query));
}
private isValidUrl(url: string): boolean {
try {
const parsed = new URL(url);
// Only allow HTTP and HTTPS
return ["http:", "https:"].includes(parsed.protocol);
} catch {
return false;
}
}
private async analyzeCode(
code: string,
language: string,
checks: string[]
): Promise<{
issues: Array<{ severity: string; message: string; line: number }>;
}> {
// Simplified code analysis - in practice, integrate with real linters
const issues = [];
if (checks.includes("syntax")) {
// Basic syntax checks
if (language === "javascript" || language === "typescript") {
const lines = code.split("\n");
lines.forEach((line, index) => {
if (line.includes("eval(")) {
issues.push({
severity: "error",
message: "Use of eval() is dangerous",
line: index + 1,
});
}
});
}
}
if (checks.includes("security")) {
// Security checks
if (code.includes("innerHTML") && language === "javascript") {
issues.push({
severity: "warning",
message: "Potential XSS vulnerability with innerHTML",
line: 1,
});
}
}
return { issues };
}
}Resource Handlers Implementation
// src/handlers/resources.ts
import { Resource, ResourceContents } from "@modelcontextprotocol/sdk/types.js";
import { FileSystemService } from "../services/filesystem.js";
import { DatabaseService } from "../services/database.js";
import { Logger } from "../utils/logging.js";
import { PermissionManager } from "../utils/permissions.js";
export class ResourceHandlers {
private fsService: FileSystemService;
private dbService: DatabaseService;
constructor(private logger: Logger, private permissions: PermissionManager) {
this.fsService = new FileSystemService();
this.dbService = new DatabaseService();
}
async listResources(): Promise<Resource[]> {
const resources: Resource[] = [];
// Add file system resources
const files = await this.fsService.listAccessibleFiles();
resources.push(
...files.map((file) => ({
uri: `file://${file.path}`,
name: file.name,
description: `File: ${file.path}`,
mimeType: file.mimeType,
}))
);
// Add database resources
const tables = await this.dbService.listTables();
resources.push(
...tables.map((table) => ({
uri: `db://table/${table.name}`,
name: `Table: ${table.name}`,
description: `Database table with ${table.rowCount} rows`,
mimeType: "application/json",
}))
);
return resources;
}
async readResource(uri: string): Promise<ResourceContents[]> {
this.logger.info(`Reading resource: ${uri}`);
const url = new URL(uri);
switch (url.protocol) {
case "file:":
return await this.readFileResource(url.pathname);
case "db:":
return await this.readDatabaseResource(url.pathname);
default:
throw new Error(`Unsupported protocol: ${url.protocol}`);
}
}
private async readFileResource(path: string): Promise<ResourceContents[]> {
const content = await this.fsService.readFile(path);
const mimeType = this.fsService.getMimeType(path);
if (mimeType.startsWith("text/") || mimeType === "application/json") {
return [
{
uri: `file://${path}`,
mimeType,
text: content.toString(),
},
];
} else {
return [
{
uri: `file://${path}`,
mimeType,
blob: content.toString("base64"),
},
];
}
}
private async readDatabaseResource(
tablePath: string
): Promise<ResourceContents[]> {
// Parse table name from path like "/table/users"
const tableName = tablePath.split("/")[2];
if (!tableName) {
throw new Error("Invalid database resource path");
}
const data = await this.dbService.getTableData(tableName);
return [
{
uri: `db://table/${tableName}`,
mimeType: "application/json",
text: JSON.stringify(
{
table: tableName,
rows: data.length,
data: data,
},
null,
2
),
},
];
}
}Service Layer Implementation
// src/services/database.ts
import { Pool } from "pg";
import { Logger } from "../utils/logging.js";
export class DatabaseService {
private pool: Pool;
private logger: Logger;
constructor() {
this.logger = new Logger("DatabaseService");
this.pool = new Pool({
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT || "5432"),
database: process.env.DB_NAME || "myapp",
user: process.env.DB_USER || "user",
password: process.env.DB_PASSWORD || "password",
max: 10, // Maximum number of connections
idleTimeoutMillis: 30000,
});
}
async executeQuery(query: string, limit?: number): Promise<any[]> {
const client = await this.pool.connect();
try {
let finalQuery = query;
if (limit) {
finalQuery = `${query} LIMIT ${limit}`;
}
this.logger.info("Executing query", { query: finalQuery });
const result = await client.query(finalQuery);
return result.rows;
} finally {
client.release();
}
}
async listTables(): Promise<Array<{ name: string; rowCount: number }>> {
const client = await this.pool.connect();
try {
const tablesQuery = `
SELECT
table_name,
(SELECT COUNT(*) FROM information_schema.tables WHERE table_name = t.table_name) as row_count
FROM information_schema.tables t
WHERE table_schema = 'public'
ORDER BY table_name
`;
const result = await client.query(tablesQuery);
return result.rows.map((row) => ({
name: row.table_name,
rowCount: parseInt(row.row_count) || 0,
}));
} finally {
client.release();
}
}
async getTableData(tableName: string, limit: number = 100): Promise<any[]> {
// Validate table name to prevent SQL injection
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
throw new Error("Invalid table name");
}
return await this.executeQuery(`SELECT * FROM ${tableName}`, limit);
}
async close(): Promise<void> {
await this.pool.end();
}
}
// src/services/filesystem.ts
import { promises as fs } from "fs";
import { join, extname, basename } from "path";
import { glob } from "glob";
import { Logger } from "../utils/logging.js";
export class FileSystemService {
private logger: Logger;
private allowedPaths: string[];
constructor() {
this.logger = new Logger("FileSystemService");
this.allowedPaths = process.env.ALLOWED_PATHS?.split(",") || ["."];
}
async searchFiles(
pattern: string,
directory: string,
recursive: boolean
): Promise<string[]> {
// Validate directory is allowed
if (!this.isPathAllowed(directory)) {
throw new Error(`Access denied to directory: ${directory}`);
}
const globPattern = recursive
? join(directory, "**", pattern)
: join(directory, pattern);
try {
const files = await glob(globPattern);
return files.filter((file) => this.isPathAllowed(file));
} catch (error) {
this.logger.error("File search failed", { pattern, directory, error });
throw error;
}
}
async readFile(path: string): Promise<Buffer> {
if (!this.isPathAllowed(path)) {
throw new Error(`Access denied to file: ${path}`);
}
try {
return await fs.readFile(path);
} catch (error) {
this.logger.error("File read failed", { path, error });
throw error;
}
}
async listAccessibleFiles(): Promise<
Array<{ path: string; name: string; mimeType: string }>
> {
const files = [];
for (const allowedPath of this.allowedPaths) {
try {
const globPattern = join(allowedPath, "**/*");
const paths = await glob(globPattern);
for (const path of paths) {
const stats = await fs.stat(path);
if (stats.isFile()) {
files.push({
path,
name: basename(path),
mimeType: this.getMimeType(path),
});
}
}
} catch (error) {
this.logger.warn("Failed to list files in path", {
path: allowedPath,
error,
});
}
}
return files;
}
getMimeType(filePath: string): string {
const ext = extname(filePath).toLowerCase();
const mimeTypes: Record<string, string> = {
".txt": "text/plain",
".md": "text/markdown",
".json": "application/json",
".js": "text/javascript",
".ts": "text/typescript",
".py": "text/x-python",
".html": "text/html",
".css": "text/css",
".pdf": "application/pdf",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
};
return mimeTypes[ext] || "application/octet-stream";
}
private isPathAllowed(path: string): boolean {
// Normalize path and check against allowed paths
const normalizedPath = join(".", path);
return this.allowedPaths.some((allowedPath) =>
normalizedPath.startsWith(join(".", allowedPath))
);
}
}Building MCP Clients
Basic MCP Client
// src/client/index.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { Logger } from "../utils/logging.js";
export class MCPClient {
private client: Client;
private transport: StdioClientTransport;
private logger: Logger;
constructor(serverCommand: string, serverArgs: string[] = []) {
this.logger = new Logger("MCPClient");
this.transport = new StdioClientTransport({
command: serverCommand,
args: serverArgs,
});
this.client = new Client(
{
name: "awesome-mcp-client",
version: "1.0.0",
},
{
capabilities: {
sampling: {},
},
}
);
}
async connect(): Promise<void> {
try {
await this.client.connect(this.transport);
this.logger.info("Connected to MCP server");
} catch (error) {
this.logger.error("Failed to connect to MCP server", { error });
throw error;
}
}
async disconnect(): Promise<void> {
try {
await this.client.close();
this.logger.info("Disconnected from MCP server");
} catch (error) {
this.logger.error("Failed to disconnect from MCP server", { error });
throw error;
}
}
async listTools(): Promise<any[]> {
try {
const response = await this.client.request(
{ method: "tools/list" },
{ tools: [] }
);
return response.tools;
} catch (error) {
this.logger.error("Failed to list tools", { error });
throw error;
}
}
async callTool(name: string, args?: Record<string, any>): Promise<any> {
try {
const response = await this.client.request(
{
method: "tools/call",
params: { name, arguments: args },
},
{ content: [] }
);
return response;
} catch (error) {
this.logger.error("Failed to call tool", { name, args, error });
throw error;
}
}
async listResources(): Promise<any[]> {
try {
const response = await this.client.request(
{ method: "resources/list" },
{ resources: [] }
);
return response.resources;
} catch (error) {
this.logger.error("Failed to list resources", { error });
throw error;
}
}
async readResource(uri: string): Promise<any> {
try {
const response = await this.client.request(
{
method: "resources/read",
params: { uri },
},
{ contents: [] }
);
return response.contents;
} catch (error) {
this.logger.error("Failed to read resource", { uri, error });
throw error;
}
}
async listPrompts(): Promise<any[]> {
try {
const response = await this.client.request(
{ method: "prompts/list" },
{ prompts: [] }
);
return response.prompts;
} catch (error) {
this.logger.error("Failed to list prompts", { error });
throw error;
}
}
async getPrompt(name: string, args?: Record<string, any>): Promise<any> {
try {
const response = await this.client.request(
{
method: "prompts/get",
params: { name, arguments: args },
},
{ messages: [] }
);
return response;
} catch (error) {
this.logger.error("Failed to get prompt", { name, args, error });
throw error;
}
}
}
// Example usage
async function example() {
const client = new MCPClient("node", ["dist/server/index.js"]);
try {
await client.connect();
// List available tools
const tools = await client.listTools();
console.log(
"Available tools:",
tools.map((t) => t.name)
);
// Execute a tool
const result = await client.callTool("search_files", {
pattern: "*.ts",
directory: "./src",
});
console.log("Search results:", result.content[0].text);
// List and read resources
const resources = await client.listResources();
console.log("Available resources:", resources.length);
if (resources.length > 0) {
const content = await client.readResource(resources[0].uri);
console.log("Resource content:", content[0].text?.substring(0, 200));
}
} finally {
await client.disconnect();
}
}Advanced Client with Connection Management
// src/client/connection-manager.ts
export class MCPConnectionManager {
private connections: Map<string, MCPClient> = new Map();
private logger: Logger;
constructor() {
this.logger = new Logger("MCPConnectionManager");
}
async createConnection(
name: string,
config: {
command: string;
args?: string[];
env?: Record<string, string>;
timeout?: number;
}
): Promise<MCPClient> {
if (this.connections.has(name)) {
throw new Error(`Connection ${name} already exists`);
}
const client = new MCPClient(config.command, config.args);
// Set timeout for connection
const connectPromise = client.connect();
const timeoutPromise = new Promise((_, reject) => {
setTimeout(
() => reject(new Error("Connection timeout")),
config.timeout || 30000
);
});
await Promise.race([connectPromise, timeoutPromise]);
this.connections.set(name, client);
this.logger.info(`Created connection: ${name}`);
return client;
}
getConnection(name: string): MCPClient {
const connection = this.connections.get(name);
if (!connection) {
throw new Error(`Connection ${name} not found`);
}
return connection;
}
async closeConnection(name: string): Promise<void> {
const connection = this.connections.get(name);
if (connection) {
await connection.disconnect();
this.connections.delete(name);
this.logger.info(`Closed connection: ${name}`);
}
}
async closeAllConnections(): Promise<void> {
await Promise.all(
Array.from(this.connections.entries()).map(async ([name, client]) => {
try {
await client.disconnect();
this.logger.info(`Closed connection: ${name}`);
} catch (error) {
this.logger.error(`Failed to close connection: ${name}`, { error });
}
})
);
this.connections.clear();
}
listConnections(): string[] {
return Array.from(this.connections.keys());
}
}Configuration and Deployment
Server Configuration
// config/server.json
{
"server": {
"name": "awesome-mcp-server",
"version": "1.0.0",
"description": "Comprehensive MCP server with database, filesystem, and API capabilities"
},
"capabilities": {
"tools": {
"listChanged": true
},
"resources": {
"subscribe": true,
"listChanged": true
},
"prompts": {
"listChanged": true
},
"logging": {}
},
"database": {
"host": "localhost",
"port": 5432,
"database": "myapp",
"user": "user",
"password": "password",
"maxConnections": 10
},
"filesystem": {
"allowedPaths": ["./data", "./docs", "./src"],
"maxFileSize": "10MB"
},
"api": {
"timeout": 30000,
"maxRetries": 3,
"allowedDomains": ["api.example.com", "httpbin.org"]
},
"security": {
"enablePermissions": true,
"logLevel": "info"
}
}Permission Configuration
// config/permissions.json
{
"tools": {
"allowed": ["*"],
"denied": ["system_admin", "delete_files"],
"limits": {
"query_database": {
"maxExecutionTime": 30000,
"maxRows": 1000
},
"http_request": {
"maxResponseSize": "1MB",
"allowedMethods": ["GET", "POST"],
"blockedDomains": ["internal.company.com"]
}
}
},
"resources": {
"allowed": ["file://./data/**", "file://./docs/**", "db://table/*"],
"denied": ["file://./config/**", "file://.env*", "db://table/admin_*"],
"limits": {
"maxResourceSize": "5MB"
}
}
}Docker Deployment
# Dockerfile
FROM node:18-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci --only=production
# Copy source code
COPY dist/ ./dist/
COPY config/ ./config/
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S mcp -u 1001
USER mcp
# Expose health check endpoint
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node -e "console.log('Health check passed')" || exit 1
CMD ["node", "dist/server/index.js"]# docker-compose.yml
version: "3.8"
services:
mcp-server:
build: .
environment:
- NODE_ENV=production
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=mcpdb
- DB_USER=mcp
- DB_PASSWORD=secretpassword
volumes:
- ./data:/app/data:ro
- ./logs:/app/logs
depends_on:
- postgres
restart: unless-stopped
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_DB=mcpdb
- POSTGRES_USER=mcp
- POSTGRES_PASSWORD=secretpassword
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
restart: unless-stopped
volumes:
postgres_data:Testing and Validation
Unit Testing
// tests/handlers/tools.test.ts
import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
import { ToolHandlers } from "../../src/handlers/tools.js";
import { Logger } from "../../src/utils/logging.js";
import { PermissionManager } from "../../src/utils/permissions.js";
describe("ToolHandlers", () => {
let toolHandlers: ToolHandlers;
let mockLogger: Logger;
let mockPermissions: PermissionManager;
beforeEach(() => {
mockLogger = new Logger("test");
mockPermissions = new PermissionManager();
toolHandlers = new ToolHandlers(mockLogger, mockPermissions);
});
describe("listTools", () => {
it("should return available tools", async () => {
const tools = await toolHandlers.listTools();
expect(tools).toHaveLength(4);
expect(tools.map((t) => t.name)).toContain("query_database");
expect(tools.map((t) => t.name)).toContain("search_files");
expect(tools.map((t) => t.name)).toContain("http_request");
expect(tools.map((t) => t.name)).toContain("analyze_code");
});
});
describe("callTool", () => {
it("should execute search_files tool successfully", async () => {
const result = await toolHandlers.callTool("search_files", {
pattern: "*.test.ts",
directory: "./tests",
});
expect(result.content).toHaveLength(1);
expect(result.content[0].type).toBe("text");
expect(result.content[0].text).toContain("Found");
});
it("should handle unknown tools", async () => {
const result = await toolHandlers.callTool("unknown_tool", {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("Unknown tool");
});
});
});Integration Testing
// tests/integration/server.test.ts
import { describe, it, expect, beforeAll, afterAll } from "@jest/globals";
import { AwesomeMCPServer } from "../../src/server/index.js";
import { MCPClient } from "../../src/client/index.js";
describe("MCP Server Integration", () => {
let server: AwesomeMCPServer;
let client: MCPClient;
beforeAll(async () => {
// Start server in test mode
server = new AwesomeMCPServer();
await server.start();
// Connect client
client = new MCPClient("node", ["dist/server/index.js"]);
await client.connect();
});
afterAll(async () => {
await client.disconnect();
await server.stop();
});
it("should list tools via client", async () => {
const tools = await client.listTools();
expect(tools.length).toBeGreaterThan(0);
});
it("should execute tools via client", async () => {
const result = await client.callTool("search_files", {
pattern: "*.ts",
directory: "./src",
});
expect(result.content).toBeDefined();
expect(result.content[0].type).toBe("text");
});
it("should list and read resources", async () => {
const resources = await client.listResources();
expect(Array.isArray(resources)).toBe(true);
if (resources.length > 0) {
const content = await client.readResource(resources[0].uri);
expect(content).toBeDefined();
expect(Array.isArray(content)).toBe(true);
}
});
});Production Considerations
Monitoring and Observability
// src/utils/monitoring.ts
import { createPrometheusMetrics } from "prometheus-client";
export class MCPMonitoring {
private metrics = {
toolCalls: createPrometheusMetrics.counter({
name: "mcp_tool_calls_total",
help: "Total number of tool calls",
labelNames: ["tool_name", "status"],
}),
resourceReads: createPrometheusMetrics.counter({
name: "mcp_resource_reads_total",
help: "Total number of resource reads",
labelNames: ["protocol", "status"],
}),
requestDuration: createPrometheusMetrics.histogram({
name: "mcp_request_duration_seconds",
help: "Request duration in seconds",
labelNames: ["method"],
}),
activeConnections: createPrometheusMetrics.gauge({
name: "mcp_active_connections",
help: "Number of active connections",
}),
};
recordToolCall(toolName: string, status: "success" | "error"): void {
this.metrics.toolCalls.inc({ tool_name: toolName, status });
}
recordResourceRead(protocol: string, status: "success" | "error"): void {
this.metrics.resourceReads.inc({ protocol, status });
}
recordRequestDuration(method: string, duration: number): void {
this.metrics.requestDuration.observe({ method }, duration);
}
updateActiveConnections(count: number): void {
this.metrics.activeConnections.set(count);
}
}Health Checks
// src/utils/health.ts
export class HealthChecker {
private checks: Map<string, () => Promise<boolean>> = new Map();
addCheck(name: string, check: () => Promise<boolean>): void {
this.checks.set(name, check);
}
async runHealthCheck(): Promise<{
status: "healthy" | "unhealthy";
checks: Record<string, boolean>;
}> {
const results: Record<string, boolean> = {};
let allHealthy = true;
for (const [name, check] of this.checks) {
try {
results[name] = await check();
if (!results[name]) allHealthy = false;
} catch (error) {
results[name] = false;
allHealthy = false;
}
}
return {
status: allHealthy ? "healthy" : "unhealthy",
checks: results,
};
}
}
// Usage in server
const healthChecker = new HealthChecker();
healthChecker.addCheck("database", async () => {
try {
await dbService.executeQuery("SELECT 1");
return true;
} catch {
return false;
}
});
healthChecker.addCheck("filesystem", async () => {
try {
await fs.access("./data");
return true;
} catch {
return false;
}
});Key Takeaways
- Modular architecture enables maintainable and extensible MCP servers
- Type safety with TypeScript prevents runtime errors and improves developer experience
- Security considerations are crucial for production deployments
- Comprehensive testing ensures reliability and correctness
- Monitoring and observability are essential for production operations
- Configuration management allows for flexible deployment scenarios
- Error handling provides graceful degradation and debugging information
This implementation guide provides a solid foundation for building production-ready MCP servers and clients that can scale and integrate seamlessly with AI applications.
Next Steps
Chapter 4 will explore Agent Integration, showing how to integrate MCP servers with various AI agents and frameworks for building sophisticated multi-tool workflows.