Join our Discord Server
Collabnix Team The Collabnix Team is a diverse collective of Docker, Kubernetes, and IoT experts united by a passion for cloud-native technologies. With backgrounds spanning across DevOps, platform engineering, cloud architecture, and container orchestration, our contributors bring together decades of combined experience from various industries and technical domains.

MCP Server Tutorial: Build with TypeScript from Scratch

12 min read

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

  1. “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 }
  2. Path validation errors typescript// Check allowed paths configuration private validatePath(userPath: string): string { const resolvedPath = resolve(userPath); console.log('Validating path:', resolvedPath); // ... validation logic }
  3. 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

  1. Add more tools specific to your use case
  2. Implement authentication for secure access
  3. Add metrics and monitoring for production observability
  4. Create a web interface for easier management
  5. 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.

Have Queries? Join https://launchpass.com/collabnix

Collabnix Team The Collabnix Team is a diverse collective of Docker, Kubernetes, and IoT experts united by a passion for cloud-native technologies. With backgrounds spanning across DevOps, platform engineering, cloud architecture, and container orchestration, our contributors bring together decades of combined experience from various industries and technical domains.

Getting Started with NVIDIA Jetson AGX Thor Developer Kit:…

If you’re building robots, you’re going to want to hear about this. NVIDIA just released Jetson Thor, and it’s a beast. We’re talking 2,070 teraflops...
Ajeet Raina
8 min read
Join our Discord Server
Index