MCP Server Tutorial: Build with TypeScript from Scratch
Building a Model Context Protocol (MCP) server with TypeScript has become increasingly important for developers working with AI applications. This comprehensive guide will walk you through creating a production-ready MCP server from the ground up, complete with working code examples and best practices.
What is MCP and Why Use TypeScript?
Model Context Protocol (MCP) is a standardized communication protocol that enables AI models to interact with external tools, resources, and data sources. Building an MCP server with TypeScript offers several advantages:
- Type Safety: Catch errors at compile time
- Better Developer Experience: IntelliSense and autocomplete
- Maintainability: Easier to refactor and scale
- Integration: Seamless integration with modern tooling
Prerequisites and Setup
Before building your MCP server, ensure you have:
- Node.js 18+ installed
- TypeScript knowledge (intermediate level)
- Understanding of async/await patterns
- Basic knowledge of JSON-RPC protocols
Getting Started
git clone https://github.com/ajeetraina/mcp-typescript-server.git
cd mcp-typescript-server
npm start
npm start
> mcp-typescript-server@1.0.1 start
> node dist/server.js
[INFO] Initialized 4 tools
[INFO] Initialized 3 resources
[INFO] MCP TypeScript Server started successfully
^C
Received SIGINT, shutting down gracefully...
[INFO] Stopping MCP TypeScript Server...
Using Docker Compose
docker compose up -d
[+] Running 1/1
✔ Container mcp-typescript-server Started

Building from Scratch
Initial Project Setup
First, let’s create a new TypeScript project:
mkdir mcp-typescript-server
cd mcp-typescript-server
npm init -y
npm install typescript @types/node ts-node nodemon
npm install @modelcontextprotocol/sdk
Create the basic TypeScript configuration:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Project Structure and Initial Setup
Organize your project with a clean, scalable structure:
mcp-typescript-server/
├── src/
│ ├── server.ts # Main server file
│ ├── types/ # Type definitions
│ │ └── index.ts
│ ├── tools/ # Tool implementations
│ │ ├── calculator.ts
│ │ └── fileManager.ts
│ ├── resources/ # Resource handlers
│ │ └── dataProvider.ts
│ └── utils/ # Utility functions
│ └── logger.ts
├── tests/ # Test files
├── package.json
└── tsconfig.json
Setting Up Package.json Scripts
{
"scripts": {
"dev": "nodemon --exec ts-node src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"test": "jest"
}
}
Core MCP Server Implementation
Let’s start by creating the foundational types and interfaces:
// src/types/index.ts
export interface ServerCapabilities {
tools?: {
listChanged?: boolean;
};
resources?: {
subscribe?: boolean;
listChanged?: boolean;
};
logging?: {};
}
export interface ToolDefinition {
name: string;
description: string;
inputSchema: {
type: string;
properties: Record<string, any>;
required?: string[];
};
}
export interface Resource {
uri: string;
name: string;
description?: string;
mimeType?: string;
}
export interface ToolResult {
content: Array<{
type: 'text' | 'image' | 'resource';
text?: string;
data?: string;
mimeType?: string;
}>;
isError?: boolean;
}
Now, let’s create the main server implementation:
// src/server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { ToolDefinition, ToolResult, Resource, ServerCapabilities } from './types/index.js';
import { CalculatorTool } from './tools/calculator.js';
import { FileManagerTool } from './tools/fileManager.js';
import { Logger } from './utils/logger.js';
export class MCPTypeScriptServer {
private server: Server;
private tools: Map<string, any> = new Map();
private resources: Map<string, Resource> = new Map();
private logger: Logger;
constructor() {
this.logger = new Logger();
this.server = new Server(
{
name: 'typescript-mcp-server',
version: '1.0.0',
},
{
capabilities: this.getServerCapabilities(),
}
);
this.initializeTools();
this.initializeResources();
this.setupHandlers();
}
private getServerCapabilities(): ServerCapabilities {
return {
tools: {
listChanged: true,
},
resources: {
subscribe: true,
listChanged: true,
},
logging: {},
};
}
private initializeTools(): void {
// Register calculator tool
const calculatorTool = new CalculatorTool();
this.tools.set('calculate', calculatorTool);
// Register file manager tool
const fileManagerTool = new FileManagerTool();
this.tools.set('read_file', fileManagerTool);
this.tools.set('write_file', fileManagerTool);
this.tools.set('list_directory', fileManagerTool);
this.logger.info(`Initialized ${this.tools.size} tools`);
}
private initializeResources(): void {
// Add sample resources
this.resources.set('config://server.json', {
uri: 'config://server.json',
name: 'Server Configuration',
description: 'Current server configuration',
mimeType: 'application/json',
});
this.resources.set('logs://recent.txt', {
uri: 'logs://recent.txt',
name: 'Recent Logs',
description: 'Recent server logs',
mimeType: 'text/plain',
});
this.logger.info(`Initialized ${this.resources.size} resources`);
}
private setupHandlers(): void {
// Handle tool listing
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
const toolDefinitions: ToolDefinition[] = [];
this.tools.forEach((tool, name) => {
if (tool.getDefinition) {
toolDefinitions.push(tool.getDefinition());
}
});
return { tools: toolDefinitions };
});
// Handle tool execution
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
this.logger.info(`Executing tool: ${name}`, args);
try {
const tool = this.tools.get(name);
if (!tool) {
throw new Error(`Tool '${name}' not found`);
}
let result: ToolResult;
// Handle different tool methods
switch (name) {
case 'calculate':
result = await tool.calculate(args);
break;
case 'read_file':
result = await tool.readFile(args);
break;
case 'write_file':
result = await tool.writeFile(args);
break;
case 'list_directory':
result = await tool.listDirectory(args);
break;
default:
throw new Error(`Unknown tool method: ${name}`);
}
this.logger.info(`Tool ${name} executed successfully`);
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`Tool execution failed: ${errorMessage}`);
return {
content: [
{
type: 'text' as const,
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
});
// Handle resource listing
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
const resourceList = Array.from(this.resources.values());
return { resources: resourceList };
});
// Handle resource reading
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
const resource = this.resources.get(uri);
if (!resource) {
throw new Error(`Resource not found: ${uri}`);
}
// Simulate resource content based on URI
let content: string;
switch (uri) {
case 'config://server.json':
content = JSON.stringify({
name: 'typescript-mcp-server',
version: '1.0.0',
capabilities: this.getServerCapabilities(),
}, null, 2);
break;
case 'logs://recent.txt':
content = this.logger.getRecentLogs();
break;
default:
content = `Content for ${uri}`;
}
return {
contents: [
{
uri,
mimeType: resource.mimeType || 'text/plain',
text: content,
},
],
};
});
}
public async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
this.logger.info('MCP TypeScript Server started successfully');
}
}
// Start the server if this file is run directly
if (require.main === module) {
const server = new MCPTypeScriptServer();
server.start().catch((error) => {
console.error('Failed to start server:', error);
process.exit(1);
});
}
Adding Tools and Resources
Creating a Calculator Tool
// src/tools/calculator.ts
import { ToolDefinition, ToolResult } from '../types/index.js';
export class CalculatorTool {
getDefinition(): ToolDefinition {
return {
name: 'calculate',
description: 'Perform mathematical calculations with support for basic operations',
inputSchema: {
type: 'object',
properties: {
expression: {
type: 'string',
description: 'Mathematical expression to evaluate (e.g., "2 + 3 * 4")',
},
},
required: ['expression'],
},
};
}
async calculate(args: { expression: string }): Promise<ToolResult> {
try {
const { expression } = args;
// Basic validation
if (!expression || typeof expression !== 'string') {
throw new Error('Expression must be a non-empty string');
}
// Sanitize the expression to prevent code injection
const sanitized = expression.replace(/[^0-9+\-*/.() ]/g, '');
if (sanitized !== expression) {
throw new Error('Expression contains invalid characters');
}
// Evaluate the expression safely
const result = this.evaluateExpression(sanitized);
return {
content: [
{
type: 'text',
text: `Result: ${expression} = ${result}`,
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : 'Calculation failed';
return {
content: [
{
type: 'text',
text: `Error: ${message}`,
},
],
isError: true,
};
}
}
private evaluateExpression(expression: string): number {
// Simple expression evaluator - in production, use a proper math parser
try {
// Using Function constructor as a safer alternative to eval
const fn = new Function('return (' + expression + ')');
const result = fn();
if (typeof result !== 'number' || !isFinite(result)) {
throw new Error('Invalid mathematical expression');
}
return result;
} catch {
throw new Error('Failed to evaluate expression');
}
}
}
Creating a File Manager Tool
// src/tools/fileManager.ts
import { promises as fs } from 'fs';
import { join, resolve, dirname } from 'path';
import { ToolDefinition, ToolResult } from '../types/index.js';
export class FileManagerTool {
private readonly allowedPaths: string[];
constructor() {
// Define allowed paths for security
this.allowedPaths = [
resolve(process.cwd(), 'data'),
resolve(process.cwd(), 'temp'),
];
}
getDefinition(): ToolDefinition {
return {
name: 'file_operations',
description: 'Manage files and directories with read, write, and list operations',
inputSchema: {
type: 'object',
properties: {
operation: {
type: 'string',
enum: ['read', 'write', 'list'],
description: 'Operation to perform',
},
path: {
type: 'string',
description: 'File or directory path',
},
content: {
type: 'string',
description: 'Content to write (required for write operation)',
},
},
required: ['operation', 'path'],
},
};
}
async readFile(args: { path: string }): Promise<ToolResult> {
try {
const { path } = args;
const safePath = this.validatePath(path);
const content = await fs.readFile(safePath, 'utf-8');
const stats = await fs.stat(safePath);
return {
content: [
{
type: 'text',
text: `File: ${path}\nSize: ${stats.size} bytes\nModified: ${stats.mtime.toISOString()}\n\nContent:\n${content}`,
},
],
};
} catch (error) {
return this.handleError(error, `reading file: ${args.path}`);
}
}
async writeFile(args: { path: string; content: string }): Promise<ToolResult> {
try {
const { path, content } = args;
const safePath = this.validatePath(path);
// Ensure directory exists
await fs.mkdir(dirname(safePath), { recursive: true });
await fs.writeFile(safePath, content, 'utf-8');
return {
content: [
{
type: 'text',
text: `Successfully wrote ${content.length} characters to ${path}`,
},
],
};
} catch (error) {
return this.handleError(error, `writing file: ${args.path}`);
}
}
async listDirectory(args: { path: string }): Promise<ToolResult> {
try {
const { path } = args;
const safePath = this.validatePath(path);
const entries = await fs.readdir(safePath, { withFileTypes: true });
const items = await Promise.all(
entries.map(async (entry) => {
const fullPath = join(safePath, entry.name);
const stats = await fs.stat(fullPath);
return {
name: entry.name,
type: entry.isDirectory() ? 'directory' : 'file',
size: stats.size,
modified: stats.mtime.toISOString(),
};
})
);
const summary = `Directory: ${path}\nTotal items: ${items.length}\n\n` +
items.map(item =>
`${item.type === 'directory' ? '📁' : '📄'} ${item.name} (${item.size} bytes, ${item.modified})`
).join('\n');
return {
content: [
{
type: 'text',
text: summary,
},
],
};
} catch (error) {
return this.handleError(error, `listing directory: ${args.path}`);
}
}
private validatePath(userPath: string): string {
const resolvedPath = resolve(userPath);
// Check if path is within allowed directories
const isAllowed = this.allowedPaths.some(allowedPath =>
resolvedPath.startsWith(allowedPath)
);
if (!isAllowed) {
throw new Error(`Access denied: Path ${userPath} is not in allowed directories`);
}
return resolvedPath;
}
private handleError(error: unknown, operation: string): ToolResult {
const message = error instanceof Error ? error.message : 'Unknown error';
return {
content: [
{
type: 'text',
text: `Error ${operation}: ${message}`,
},
],
isError: true,
};
}
}
Creating a Logger Utility
// src/utils/logger.ts
export class Logger {
private logs: Array<{ level: string; message: string; timestamp: Date; data?: any }> = [];
private maxLogs = 1000;
info(message: string, data?: any): void {
this.log('INFO', message, data);
console.log(`[INFO] ${message}`, data ? JSON.stringify(data) : '');
}
error(message: string, data?: any): void {
this.log('ERROR', message, data);
console.error(`[ERROR] ${message}`, data ? JSON.stringify(data) : '');
}
warn(message: string, data?: any): void {
this.log('WARN', message, data);
console.warn(`[WARN] ${message}`, data ? JSON.stringify(data) : '');
}
debug(message: string, data?: any): void {
this.log('DEBUG', message, data);
if (process.env.NODE_ENV === 'development') {
console.log(`[DEBUG] ${message}`, data ? JSON.stringify(data) : '');
}
}
private log(level: string, message: string, data?: any): void {
this.logs.push({
level,
message,
data,
timestamp: new Date(),
});
// Keep only the most recent logs
if (this.logs.length > this.maxLogs) {
this.logs = this.logs.slice(-this.maxLogs);
}
}
getRecentLogs(count: number = 50): string {
return this.logs
.slice(-count)
.map(log => `[${log.timestamp.toISOString()}] ${log.level}: ${log.message}`)
.join('\n');
}
getLogs(): Array<{ level: string; message: string; timestamp: Date; data?: any }> {
return [...this.logs];
}
}
Testing Your MCP Server
Create comprehensive tests to ensure your MCP server works correctly:
// tests/server.test.ts
import { MCPTypeScriptServer } from '../src/server';
import { CalculatorTool } from '../src/tools/calculator';
import { FileManagerTool } from '../src/tools/fileManager';
describe('MCP TypeScript Server', () => {
let server: MCPTypeScriptServer;
beforeAll(() => {
server = new MCPTypeScriptServer();
});
describe('Calculator Tool', () => {
let calculator: CalculatorTool;
beforeEach(() => {
calculator = new CalculatorTool();
});
it('should perform basic arithmetic', async () => {
const result = await calculator.calculate({ expression: '2 + 3' });
expect(result.content[0].text).toContain('= 5');
expect(result.isError).toBeFalsy();
});
it('should handle complex expressions', async () => {
const result = await calculator.calculate({ expression: '(10 + 5) * 2 - 8' });
expect(result.content[0].text).toContain('= 22');
expect(result.isError).toBeFalsy();
});
it('should reject invalid expressions', async () => {
const result = await calculator.calculate({ expression: 'alert("hack")' });
expect(result.isError).toBeTruthy();
expect(result.content[0].text).toContain('Error');
});
it('should handle division by zero', async () => {
const result = await calculator.calculate({ expression: '5 / 0' });
expect(result.isError).toBeTruthy();
});
});
describe('File Manager Tool', () => {
let fileManager: FileManagerTool;
beforeEach(() => {
fileManager = new FileManagerTool();
});
it('should read existing files', async () => {
// This test requires setting up test files
const result = await fileManager.readFile({ path: 'test-data/sample.txt' });
expect(result.isError).toBeFalsy();
});
it('should reject paths outside allowed directories', async () => {
const result = await fileManager.readFile({ path: '/etc/passwd' });
expect(result.isError).toBeTruthy();
expect(result.content[0].text).toContain('Access denied');
});
});
});
Integration Testing Script
// tests/integration.test.ts
import { spawn } from 'child_process';
import { join } from 'path';
describe('MCP Server Integration', () => {
it('should start server and handle basic requests', async () => {
const serverPath = join(__dirname, '../dist/server.js');
const server = spawn('node', [serverPath]);
let output = '';
server.stdout.on('data', (data) => {
output += data.toString();
});
// Send a basic MCP request
const request = JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {},
});
server.stdin.write(request + '\n');
await new Promise(resolve => setTimeout(resolve, 1000));
expect(output).toContain('typescript-mcp-server');
server.kill();
}, 10000);
});
Load Testing
// tests/load.test.ts
import { MCPTypeScriptServer } from '../src/server';
import { CalculatorTool } from '../src/tools/calculator';
describe('Load Testing', () => {
it('should handle concurrent requests', async () => {
const calculator = new CalculatorTool();
const promises = [];
// Create 100 concurrent calculation requests
for (let i = 0; i < 100; i++) {
promises.push(
calculator.calculate({ expression: `${i} + ${i * 2}` })
);
}
const results = await Promise.all(promises);
expect(results).toHaveLength(100);
results.forEach((result, index) => {
expect(result.isError).toBeFalsy();
expect(result.content[0].text).toContain(`= ${index + index * 2}`);
});
});
});
Advanced Features and Error Handling {#advanced-features}
Adding Middleware Support
// src/middleware/index.ts
export interface MiddlewareContext {
method: string;
params: any;
timestamp: Date;
}
export type MiddlewareFunction = (
context: MiddlewareContext,
next: () => Promise<any>
) => Promise<any>;
export class MiddlewareManager {
private middlewares: MiddlewareFunction[] = [];
use(middleware: MiddlewareFunction): void {
this.middlewares.push(middleware);
}
async execute(context: MiddlewareContext, handler: () => Promise<any>): Promise<any> {
let index = 0;
const next = async (): Promise<any> => {
if (index >= this.middlewares.length) {
return handler();
}
const middleware = this.middlewares[index++];
return middleware(context, next);
};
return next();
}
}
// Example middleware implementations
export const loggingMiddleware: MiddlewareFunction = async (context, next) => {
const start = Date.now();
console.log(`[${context.timestamp.toISOString()}] Starting ${context.method}`);
try {
const result = await next();
const duration = Date.now() - start;
console.log(`[${context.timestamp.toISOString()}] Completed ${context.method} in ${duration}ms`);
return result;
} catch (error) {
const duration = Date.now() - start;
console.error(`[${context.timestamp.toISOString()}] Failed ${context.method} in ${duration}ms:`, error);
throw error;
}
};
export const rateLimitMiddleware = (requestsPerMinute: number): MiddlewareFunction => {
const requests = new Map<string, number[]>();
return async (context, next) => {
const now = Date.now();
const windowStart = now - 60000; // 1 minute window
const clientId = 'default'; // In real implementation, extract from context
if (!requests.has(clientId)) {
requests.set(clientId, []);
}
const clientRequests = requests.get(clientId)!;
// Remove old requests outside the window
const recentRequests = clientRequests.filter(time => time > windowStart);
requests.set(clientId, recentRequests);
if (recentRequests.length >= requestsPerMinute) {
throw new Error('Rate limit exceeded');
}
recentRequests.push(now);
return next();
};
};
Enhanced Error Handling
// src/utils/errorHandler.ts
export class MCPError extends Error {
constructor(
message: string,
public code: number = -1,
public data?: any
) {
super(message);
this.name = 'MCPError';
}
}
export class ErrorHandler {
static handle(error: unknown): { message: string; code: number; data?: any } {
if (error instanceof MCPError) {
return {
message: error.message,
code: error.code,
data: error.data,
};
}
if (error instanceof Error) {
return {
message: error.message,
code: -32603, // Internal error
};
}
return {
message: 'Unknown error occurred',
code: -32603,
};
}
static createValidationError(message: string, field?: string): MCPError {
return new MCPError(message, -32602, { field });
}
static createNotFoundError(resource: string): MCPError {
return new MCPError(`Resource not found: ${resource}`, -32601);
}
static createInternalError(message: string): MCPError {
return new MCPError(message, -32603);
}
}
Configuration Management
// src/config/index.ts
import { readFileSync } from 'fs';
import { join } from 'path';
export interface ServerConfig {
server: {
name: string;
version: string;
port?: number;
};
logging: {
level: 'debug' | 'info' | 'warn' | 'error';
maxLogs: number;
};
security: {
allowedPaths: string[];
rateLimitRpm: number;
};
tools: {
calculator: {
enabled: boolean;
maxExpressionLength: number;
};
fileManager: {
enabled: boolean;
allowedExtensions: string[];
};
};
}
export class ConfigManager {
private config: ServerConfig;
constructor(configPath?: string) {
this.config = this.loadConfig(configPath);
}
private loadConfig(configPath?: string): ServerConfig {
const defaultConfig: ServerConfig = {
server: {
name: 'typescript-mcp-server',
version: '1.0.0',
},
logging: {
level: 'info',
maxLogs: 1000,
},
security: {
allowedPaths: ['./data', './temp'],
rateLimitRpm: 60,
},
tools: {
calculator: {
enabled: true,
maxExpressionLength: 100,
},
fileManager: {
enabled: true,
allowedExtensions: ['.txt', '.json', '.md'],
},
},
};
if (configPath) {
try {
const configFile = readFileSync(configPath, 'utf-8');
const fileConfig = JSON.parse(configFile);
return { ...defaultConfig, ...fileConfig };
} catch (error) {
console.warn('Failed to load config file, using defaults:', error);
}
}
return defaultConfig;
}
getConfig(): ServerConfig {
return this.config;
}
get<T>(path: string): T {
const keys = path.split('.');
let value: any = this.config;
for (const key of keys) {
value = value?.[key];
}
return value as T;
}
}
Deployment and Production Considerations {#deployment}
Docker Configuration
# Dockerfile
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
RUN npm ci --only=production
# Copy source code
COPY dist/ ./dist/
# Create data directories
RUN mkdir -p /app/data /app/temp
# Set environment variables
ENV NODE_ENV=production
# Run as non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S mcp -u 1001
USER mcp
EXPOSE 3000
CMD ["node", "dist/server.js"]
Docker Compose for Development
# docker-compose.yml
services:
mcp-server:
build: .
ports:
- "3000:3000"
volumes:
- ./data:/app/data
- ./config.json:/app/config.json:ro
environment:
- NODE_ENV=development
restart: unless-stopped
mcp-server-test:
build: .
command: npm test
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=test
Production Deployment Script
bash
#!/bin/bash
# deploy.sh
set -e
echo "Building MCP TypeScript Server..."
# Build the TypeScript code
npm run build
# Run tests
npm test
# Build Docker image
docker build -t mcp-typescript-server:latest .
# Tag for production
docker tag mcp-typescript-server:latest mcp-typescript-server:$(date +%Y%m%d-%H%M%S)
echo "Deployment complete!"
Health Check Endpoint
// src/health.ts
export interface HealthStatus {
status: 'healthy' | 'unhealthy';
timestamp: string;
uptime: number;
memory: {
used: number;
total: number;
};
tools: {
[key: string]: boolean;
};
}
export class HealthChecker {
private startTime = Date.now();
getHealthStatus(): HealthStatus {
const now = Date.now();
const memUsage = process.memoryUsage();
return {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: now - this.startTime,
memory: {
used: memUsage.heapUsed,
total: memUsage.heapTotal,
},
tools: {
calculator: true,
fileManager: true,
},
};
}
}
Troubleshooting Common Issues {#troubleshooting}
Common Errors and Solutions
- “Tool not found” errors typescript
// Ensure tools are properly registered private initializeTools(): void { const tool = new YourTool(); this.tools.set(tool.name, tool); // Make sure name matches } - Path validation errors typescript
// Check allowed paths configuration private validatePath(userPath: string): string { const resolvedPath = resolve(userPath); console.log('Validating path:', resolvedPath); // ... validation logic } - Type errors in production bash
# Ensure proper TypeScript compilation npm run build node dist/server.js # Run compiled version
Debug Mode Configuration
// src/debug.ts
export class DebugManager {
private isDebugMode: boolean;
constructor() {
this.isDebugMode = process.env.NODE_ENV === 'development' ||
process.env.DEBUG === 'true';
}
log(message: string, data?: any): void {
if (this.isDebugMode) {
console.log(`[DEBUG] ${message}`, data);
}
}
trace(error: Error): void {
if (this.isDebugMode) {
console.trace('Debug trace:', error);
}
}
dumpRequest(method: string, params: any): void {
if (this.isDebugMode) {
console.log(`[REQUEST] ${method}:`, JSON.stringify(params, null, 2));
}
}
}
Performance Optimization Tips
Memory Management
// src/utils/memoryManager.ts
export class MemoryManager {
private memoryThreshold = 100 * 1024 * 1024; // 100MB
checkMemoryUsage(): void {
const usage = process.memoryUsage();
if (usage.heapUsed > this.memoryThreshold) {
console.warn('High memory usage detected:', usage);
// Trigger garbage collection if possible
if (global.gc) {
global.gc();
}
}
}
startMemoryMonitoring(): void {
setInterval(() => {
this.checkMemoryUsage();
}, 30000); // Check every 30 seconds
}
}
Caching Implementation
// src/utils/cache.ts
export class LRUCache<T> {
private cache = new Map<string, { value: T; timestamp: number }>();
private maxSize: number;
private ttl: number;
constructor(maxSize = 100, ttl = 300000) { // 5 minutes TTL
this.maxSize = maxSize;
this.ttl = ttl;
}
get(key: string): T | undefined {
const item = this.cache.get(key);
if (!item) return undefined;
if (Date.now() - item.timestamp > this.ttl) {
this.cache.delete(key);
return undefined;
}
// Move to end (most recently used)
this.cache.delete(key);
this.cache.set(key, item);
return item.value;
}
set(key: string, value: T): void {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.maxSize) {
// Remove least recently used
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, { value, timestamp: Date.now() });
}
}
Conclusion
You’ve now built a comprehensive, production-ready MCP server using TypeScript from scratch. This implementation includes:
- Type-safe architecture with comprehensive error handling
- Modular tool system that’s easy to extend
- Security features including path validation and rate limiting
- Comprehensive testing with unit, integration, and load tests
- Production deployment configurations and monitoring
- Performance optimizations with caching and memory management
Your MCP TypeScript server is now ready to integrate with AI applications and can be easily extended with additional tools and resources. The modular architecture ensures maintainability as your requirements grow.
Next Steps
- Add more tools specific to your use case
- Implement authentication for secure access
- Add metrics and monitoring for production observability
- Create a web interface for easier management
- Implement clustering for high availability
Key Takeaways
- Start simple: Begin with basic functionality and build complexity gradually
- Type safety: Leverage TypeScript’s type system for better code quality
- Test thoroughly: Comprehensive testing prevents production issues
- Monitor performance: Keep track of memory usage and response times
- Security first: Always validate inputs and restrict access appropriately
This MCP TypeScript server foundation will serve you well for building robust AI integrations and can be adapted for various use cases from simple calculations to complex data processing workflows.