Skip to main content
21nauts
MCPTypeScriptDevelopment

Building Custom MCP Servers with TypeScript

Step-by-step guide to creating MCP servers using the official TypeScript SDK. Learn to expose resources, tools, and prompts through standardized interfaces that work with any MCP client.

January 8, 2025
18 min read
21nauts Team

Building Custom MCP Servers with TypeScript

The Model Context Protocol (MCP) TypeScript SDK provides a robust foundation for building custom servers that can integrate with any MCP-compatible AI application. This comprehensive guide will walk you through creating production-ready MCP servers using TypeScript.

Why TypeScript for MCP?

TypeScript offers several advantages for MCP server development:

  • Type Safety: Catch errors at compile time
  • IntelliSense: Better IDE support and autocomplete
  • Ecosystem: Rich npm ecosystem and tooling
  • Performance: Node.js runtime performance
  • Maintainability: Easier to maintain and refactor

Setting Up Your Development Environment

Prerequisites

Before starting, ensure you have:

  • Node.js 18+ installed
  • TypeScript knowledge
  • Basic understanding of MCP concepts

Project Initialization

Create a new TypeScript project:

# Create project directory
mkdir my-mcp-server
cd my-mcp-server

# Initialize npm project
npm init -y

# Install MCP TypeScript SDK
npm install @modelcontextprotocol/sdk

# Install development dependencies
npm install -D typescript @types/node tsx
Bash

TypeScript Configuration

Create a tsconfig.json file:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
JSON

MCP Server Architecture

An MCP server consists of three main components:

1. Resources

Static or dynamic data that the server provides:

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';

class MyServer {
  private server: McpServer;

  constructor() {
    this.server = new McpServer(
      { name: "my-server", version: "1.0.0" },
      { capabilities: { resources: {} } }
    );

    this.setupResourceHandlers();
  }

  private setupResourceHandlers() {
    // List available resources
    this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
      return {
        resources: [
          {
            uri: "file://config.json",
            name: "Application Configuration",
            mimeType: "application/json"
          },
          {
            uri: "file://logs/latest.log",
            name: "Latest Log File",
            mimeType: "text/plain"
          }
        ]
      };
    });

    // Read specific resource
    this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
      const { uri } = request.params;

      switch (uri) {
        case "file://config.json":
          return {
            contents: [
              {
                uri,
                mimeType: "application/json",
                text: JSON.stringify({
                  environment: "production",
                  debug: false,
                  apiUrl: "https://api.example.com"
                }, null, 2)
              }
            ]
          };

        case "file://logs/latest.log":
          return {
            contents: [
              {
                uri,
                mimeType: "text/plain",
                text: "2025-01-08 10:00:00 INFO Application started\n2025-01-08 10:01:00 INFO Processing request"
              }
            ]
          };

        default:
          throw new Error(`Resource not found: ${uri}`);
      }
    });
  }
}
TYPESCRIPT

2. Tools

Functions that clients can invoke:

import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';

class MyServer {
  private setupToolHandlers() {
    // List available tools
    this.server.setRequestHandler(ListToolsRequestSchema, async () => {
      return {
        tools: [
          {
            name: "calculate",
            description: "Perform mathematical calculations",
            inputSchema: {
              type: "object",
              properties: {
                expression: {
                  type: "string",
                  description: "Mathematical expression to evaluate"
                }
              },
              required: ["expression"]
            }
          },
          {
            name: "fetch_data",
            description: "Fetch data from external API",
            inputSchema: {
              type: "object",
              properties: {
                endpoint: {
                  type: "string",
                  description: "API endpoint to fetch data from"
                },
                method: {
                  type: "string",
                  enum: ["GET", "POST"],
                  default: "GET"
                }
              },
              required: ["endpoint"]
            }
          }
        ]
      };
    });

    // Handle tool calls
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;

      switch (name) {
        case "calculate":
          return await this.handleCalculate(args);
        case "fetch_data":
          return await this.handleFetchData(args);
        default:
          throw new Error(`Unknown tool: ${name}`);
      }
    });
  }

  private async handleCalculate(args: any) {
    try {
      const { expression } = args;

      // Simple expression evaluation (use a proper parser in production)
      const result = eval(expression);

      return {
        content: [
          {
            type: "text",
            text: `Result: ${result}`
          }
        ]
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Error: ${error.message}`
          }
        ],
        isError: true
      };
    }
  }

  private async handleFetchData(args: any) {
    try {
      const { endpoint, method = "GET" } = args;

      const response = await fetch(endpoint, { method });
      const data = await response.text();

      return {
        content: [
          {
            type: "text",
            text: `Data from ${endpoint}:\n${data}`
          }
        ]
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Error fetching data: ${error.message}`
          }
        ],
        isError: true
      };
    }
  }
}
TYPESCRIPT

3. Prompts

Predefined templates for common interactions:

import { GetPromptRequestSchema, ListPromptsRequestSchema } from '@modelcontextprotocol/sdk/types.js';

class MyServer {
  private setupPromptHandlers() {
    // List available prompts
    this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
      return {
        prompts: [
          {
            name: "code_review",
            description: "Generate a comprehensive code review",
            arguments: [
              {
                name: "code",
                description: "Code to review",
                required: true
              },
              {
                name: "language",
                description: "Programming language",
                required: false
              }
            ]
          },
          {
            name: "documentation",
            description: "Generate API documentation",
            arguments: [
              {
                name: "endpoint",
                description: "API endpoint to document",
                required: true
              },
              {
                name: "method",
                description: "HTTP method",
                required: true
              }
            ]
          }
        ]
      };
    });

    // Handle prompt requests
    this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;

      switch (name) {
        case "code_review":
          return this.generateCodeReviewPrompt(args);
        case "documentation":
          return this.generateDocumentationPrompt(args);
        default:
          throw new Error(`Unknown prompt: ${name}`);
      }
    });
  }

  private generateCodeReviewPrompt(args: any) {
    const { code, language = "javascript" } = args;

    return {
      description: `Code review for ${language} code`,
      messages: [
        {
          role: "user" as const,
          content: {
            type: "text",
            text: `Please review the following ${language} code and provide feedback on:

1. Code quality and best practices
2. Potential bugs or issues
3. Performance considerations
4. Security concerns
5. Suggestions for improvement

Code to review:
\`\`\`${language}
${code}
\`\`\`

Please provide a comprehensive review with specific recommendations.`
          }
        }
      ]
    };
  }

  private generateDocumentationPrompt(args: any) {
    const { endpoint, method } = args;

    return {
      description: `API documentation for ${method} ${endpoint}`,
      messages: [
        {
          role: "user" as const,
          content: {
            type: "text",
            text: `Generate comprehensive API documentation for the following endpoint:

**Endpoint:** ${method} ${endpoint}

Please include:
1. Description of what this endpoint does
2. Request parameters (path, query, body)
3. Response format and status codes
4. Example request and response
5. Error handling
6. Authentication requirements
7. Rate limiting information

Format the documentation in a clear, professional manner suitable for developers.`
          }
        }
      ]
    };
  }
}
TYPESCRIPT

Advanced Server Features

Error Handling and Logging

Implement comprehensive error handling:

import { Logger } from './logger.js';

class MyServer {
  private logger: Logger;

  constructor() {
    this.logger = new Logger("MyServer");
    this.server = new McpServer(
      { name: "my-server", version: "1.0.0" },
      { capabilities: { resources: {}, tools: {}, prompts: {} } }
    );

    this.setupErrorHandling();
  }

  private setupErrorHandling() {
    this.server.onerror = (error) => {
      this.logger.error("Server error:", error);
    };

    // Wrap handlers with error handling
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      try {
        return await this.handleToolCall(request);
      } catch (error) {
        this.logger.error(`Tool call error: ${error.message}`, { request });
        throw error;
      }
    });
  }
}
TYPESCRIPT

Configuration Management

Create a configuration system:

interface ServerConfig {
  port: number;
  debug: boolean;
  apiKeys: {
    [service: string]: string;
  };
  rateLimits: {
    requestsPerMinute: number;
  };
}

class ConfigManager {
  private config: ServerConfig;

  constructor() {
    this.config = this.loadConfig();
  }

  private loadConfig(): ServerConfig {
    return {
      port: parseInt(process.env.PORT || "3000"),
      debug: process.env.DEBUG === "true",
      apiKeys: {
        openai: process.env.OPENAI_API_KEY || "",
        weather: process.env.WEATHER_API_KEY || ""
      },
      rateLimits: {
        requestsPerMinute: parseInt(process.env.RATE_LIMIT || "60")
      }
    };
  }

  public get<T extends keyof ServerConfig>(key: T): ServerConfig[T] {
    return this.config[key];
  }
}
TYPESCRIPT

Rate Limiting

Implement rate limiting for production use:

class RateLimiter {
  private requests: Map<string, number[]> = new Map();

  public checkLimit(clientId: string, limit: number, windowMs: number): boolean {
    const now = Date.now();
    const requests = this.requests.get(clientId) || [];

    // Remove old requests outside the window
    const validRequests = requests.filter(time => now - time < windowMs);

    if (validRequests.length >= limit) {
      return false;
    }

    validRequests.push(now);
    this.requests.set(clientId, validRequests);
    return true;
  }
}

class MyServer {
  private rateLimiter = new RateLimiter();

  private async handleToolCall(request: any) {
    const clientId = request.meta?.clientId || "unknown";

    if (!this.rateLimiter.checkLimit(clientId, 60, 60000)) {
      throw new Error("Rate limit exceeded");
    }

    // Process request...
  }
}
TYPESCRIPT

Testing Your MCP Server

Unit Testing Setup

Create comprehensive tests:

// tests/server.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { MyServer } from '../src/server.js';

describe('MyServer', () => {
  let server: MyServer;

  beforeEach(() => {
    server = new MyServer();
  });

  describe('Tools', () => {
    it('should handle calculate tool correctly', async () => {
      const result = await server.handleToolCall({
        params: {
          name: "calculate",
          arguments: { expression: "2 + 2" }
        }
      });

      expect(result.content[0].text).toBe("Result: 4");
    });

    it('should handle invalid expressions', async () => {
      const result = await server.handleToolCall({
        params: {
          name: "calculate",
          arguments: { expression: "invalid" }
        }
      });

      expect(result.isError).toBe(true);
    });
  });

  describe('Resources', () => {
    it('should list available resources', async () => {
      const result = await server.listResources();

      expect(result.resources).toHaveLength(2);
      expect(result.resources[0].uri).toBe("file://config.json");
    });
  });
});
TYPESCRIPT

Integration Testing

Test with real MCP clients:

// tests/integration.test.ts
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { MyServer } from '../src/server.js';

describe('Integration Tests', () => {
  it('should work with stdio transport', async () => {
    const server = new MyServer();
    const transport = new StdioServerTransport();

    await server.connect(transport);

    // Test server connection and basic functionality
    expect(server.isConnected()).toBe(true);
  });
});
TYPESCRIPT

Deployment and Production

Docker Deployment

Create a Dockerfile:

FROM node:18-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./
RUN npm ci --only=production

# Copy source code
COPY dist/ ./dist/

# Set environment
ENV NODE_ENV=production
EXPOSE 3000

# Run server
CMD ["node", "dist/index.js"]
Dockerfile

Environment Configuration

Set up environment variables:

# .env.production
NODE_ENV=production
PORT=3000
DEBUG=false
OPENAI_API_KEY=your_openai_key
WEATHER_API_KEY=your_weather_key
RATE_LIMIT=100
Bash

Monitoring and Observability

Add monitoring capabilities:

import { createPrometheusMetrics } from './metrics.js';

class MyServer {
  private metrics = createPrometheusMetrics();

  private async handleToolCall(request: any) {
    const startTime = Date.now();

    try {
      const result = await this.processToolCall(request);
      this.metrics.toolCallDuration.observe(Date.now() - startTime);
      this.metrics.toolCallsTotal.inc({ status: 'success' });
      return result;
    } catch (error) {
      this.metrics.toolCallsTotal.inc({ status: 'error' });
      throw error;
    }
  }
}
TYPESCRIPT

Best Practices

Security Considerations

  1. Input Validation: Always validate input parameters
  2. API Key Management: Use environment variables, never hardcode
  3. Rate Limiting: Implement proper rate limiting
  4. Error Messages: Don't expose sensitive information in errors

Performance Optimization

  1. Caching: Cache frequently accessed data
  2. Connection Pooling: Reuse database connections
  3. Async Operations: Use async/await for I/O operations
  4. Memory Management: Monitor memory usage and implement cleanup

Code Organization

  1. Modular Design: Separate concerns into different modules
  2. Type Safety: Use TypeScript types and interfaces
  3. Error Handling: Implement comprehensive error handling
  4. Testing: Write thorough unit and integration tests

Conclusion

Building MCP servers with TypeScript provides a robust, type-safe foundation for creating powerful AI integrations. By following these patterns and best practices, you can create production-ready servers that integrate seamlessly with any MCP-compatible AI application.

The TypeScript SDK's rich feature set, combined with proper architecture and testing, enables you to build scalable, maintainable MCP servers that enhance AI applications with external data and tools.


Ready to deploy your MCP server? Check out our deployment guides and production best practices for scaling your MCP infrastructure.