ГлавнаяБлогTech UpdatesСоздание MCP‑серверов на TypeScript, которые не разваливаются
Tech Updates24 мая 2026 г.7 мин

Создание MCP‑серверов на TypeScript, которые не разваливаются

Ваш MCP сервер работает отлично, когда на нём три инструмента. К двенадцатому инструменту он превращается в груду `switch`-конструкций

Создание MCP-серверов на TypeScript, которые не разваливаются

Ваш MCP сервер работает отлично, когда на нём три инструмента. К двенадцатому инструменту он превращается в груду switch-конструкций, которых вы боитесь касаться. Вот архитектура на TypeScript, которая позволяет сохранить чистоту кода по мере роста — одолженная прямо из Domain-Driven Design.

Проблема плоских MCP серверов

Большинство туториалов по MCP серверам начинаются примерно так:

import { Server } from \"@modelcontextprotocol/sdk/server/index.js\";
import { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";

const server = new Server(
  {
    name: \"my-server\",
    version: \"1.0.0\"
  },
  {
    capabilities: {
      tools: {}
    }
  }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: \"get_user\",
      description: \"Get a user by ID\",
      inputSchema: { ... }
    },
    {
      name: \"create_order\",
      description: \"Create an order\",
      inputSchema: { ... }
    },
    {
      name: \"send_email\",
      description: \"Send an email\",
      inputSchema: { ... }
    },
    {
      name: \"get_product\", 
      description: \"Get product details\",
      inputSchema: { ... }
    },
    // ...ещё 8 инструментов
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  switch (request.params.name) {
    case \"get_user\":
      return handleGetUser(request.params.arguments);
    case \"create_order\":
      return handleCreateOrder(request.params.arguments);
    case \"send_email\":
      return handleSendEmail(request.params.arguments);
    // ...
  }
});

Знакомо? Проблема не в том, что этот код неправильный. Он работает отлично — для одного разработчика, поддерживающего его в течение трёх недель.

Как только вы сталкиваетесь с реальными требованиями продакшена — разные владельцы логики пользователей и заказов, разные контексты аутентификации, состояние ресурсов — вам нужна структура.

Именно здесь Domain-Driven Design (DDD) доказывает свою ценность.

Три концепции DDD и их MCP отображение

Вам не нужно полное погружение в DDD для получения выгод. Вот три ключевые идеи и как они ложатся на MCP серверы:

1. Сущности (Entities) и Объекты-значения (Value Objects)

В DDD Сущности имеют идентификатор и жизненный цикл (например, пользователь). Объекты-значения неизменяемы и описывают характеристику (например, адрес).

В MCP контексте это означает:

// Сущность: User
interface User {
  id: string;
  email: string;
  createdAt: Date;
}

// Объекты-значения
interface Address {
  street: string;
  city: string;
  zipCode: string;
}

interface UserPreferences {
  theme: \'light\' | \'dark\';
  notifications: boolean;
}

2. Агрегаты (Aggregates)

Агрегат — это кластер из связанных объектов, которые мы рассматриваем как единое целое для изменений.

Пример с заказом:

interface Order {
  id: string;
  customerId: string;
  items: OrderItem[];
  status: OrderStatus;
  totalAmount: number;
}

interface OrderItem {
  productId: string;
  quantity: number;
  unitPrice: number;
}

type OrderStatus = \'pending\' | \'confirmed\' | \'shipped\' | \'delivered\';

3. Сервисы домена (Domain Services)

Когда операция не принадлежит естественным образом одной сущности, вы создаёте сервис.

Пример:

class OrderService {
  constructor(
    private orderRepository: OrderRepository,
    private inventoryService: InventoryService,
    private paymentService: PaymentService
  ) {}

  async placeOrder(userId: string, items: CartItem[]): Promise {
    // Проверка доступности
    const available = await this.inventoryService.checkAvailability(items);
    if (!available) {
      throw new Error(\"Some items are out of stock\");
    }

    // Создание заказа
    const order = await this.orderRepository.create(userId, items);
    
    // Обработка платежа
    await this.paymentService.process(order);
    
    return order;
  }
}

Реструктуризация MCP сервера с помощью DDD

Давайте преобразуем наш плоский сервер в модульную структуру:

// src/infrastructure/mcp/mcp-server.ts
import { Server } from \"@modelcontextprotocol/sdk/server/index.js\";
import { ToolRegistry } from \"./tool-registry.js\";

export class McpServer {
  private server: Server;
  private toolRegistry: ToolRegistry;

  constructor(
    private userHandlers: UserHandlers,
    private orderHandlers: OrderHandlers,
    private emailHandlers: EmailHandlers
  ) {
    this.server = new Server(
      { name: \"my-server\", version: \"1.0.0\" },
      { capabilities: { tools: {} } }
    );
    this.toolRegistry = new ToolRegistry();
    
    this.setupHandlers();
  }

  private setupHandlers() {
    // Регистрация инструментов
    this.server.setRequestHandler(
      ListToolsRequestSchema,
      async () => this.toolRegistry.listTools()
    );

    // Маршрутизация запросов инструментов
    this.server.setRequestHandler(
      CallToolRequestSchema, 
      async (request) => this.toolRegistry.handle(request)
    );
  }
}

// src/infrastructure/mcp/tool-registry.ts
export class ToolRegistry {
  private tools: Tool[] = [];
  private handlers: Map = new Map();

  registerTool(tool: Tool, handler: ToolHandler) {
    this.tools.push(tool);
    this.handlers.set(tool.name, handler);
  }

  async listTools(): Promise {
    return { tools: this.tools };
  }

  async handle(request: CallToolRequest): Promise {
    const handler = this.handlers.get(request.params.name);
    if (!handler) {
      throw new Error(`Unknown tool: ${request.params.name}`);
    }
    return handler(request.params.arguments);
  }
}

Модульная структура проекта

// src/users/user.ts (Сущности и Value Objects)
export interface User {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
}

export interface UserPreferences {
  theme: \'light\' | \'dark\';
  language: string;
  timezone: string;
}

// src/users/user-service.ts (Доменный сервис)
export class UserService {
  constructor(
    private userRepository: UserRepository,
    private emailService: EmailService
  ) {}

  async getUser(userId: string): Promise {
    return this.userRepository.findById(userId);
  }

  async createUser(userData: CreateUserRequest): Promise {
    const user = await this.userRepository.create(userData);
    await this.emailService.sendWelcomeEmail(user.email);
    return user;
  }
}

// src/users/mcp-handlers.ts (MCP адаптер)
export class UserHandlers {
  constructor(
    private userService: UserService,
    private toolRegistry: ToolRegistry
  ) {
    this.registerToolHandlers();
  }

  private registerToolHandlers() {
    this.toolRegistry.registerTool(
      {
        name: \"get_user\",
        description: \"Get user details by ID\",
        inputSchema: {
          type: \"object\",
          properties: {
            userId: {
              type: \"string\",
              description: \"User ID to retrieve\"
            }
          }
        }
      },
      async (args) => {
        const user = await this.userService.getUser(args.userId);
        return {
          content: [
            {
              type: \"text\",
              text: JSON.stringify(user)
            }
          ]
        };
      }
    );

    this.toolRegistry.registerTool(
      {
        name: \"create_user\",
        description: \"Create new user\",
        inputSchema: {
          type: \"object\",
          properties: {
            email: {
              type: \"string\",
              description: \"User email address\"
            },
            name: {
              type: \"string\",
              description: \"User name\"
            }
          }
        }
      },
      async (args) => {
        const user = await this.userService.createUser({
          email: args.email,
          name: args.name
        });
        return {
          content: [
            {
              type: \"text\",
              text: JSON.stringify(user)
            }
          ]
        };
      }
    );
  }
}

Практические советы по структуре

1. Слой за слоем

src/
├── domain/            # Сущности, Value Objects, Domain Services (без зависимостей от фреймворков)
├── application/       # Сценарии использования - прикладные сервисы
├── infrastructure/    # Реализации репозиториев, внешние API, MCP адаптеры
└── presentation/      # MCP серверы, HTTP контроллеры, если нужно

2. Принцип единой ответственности для инструментов

// src/users/mcp-handlers.ts - отвечает только за пользователей
export class UserHandlers {
  // ...
}

// src/orders/mcp-handlers.ts - отвечает только за заказы
export class OrderHandlers {
  // ...
}

// src/emails/mcp-handlers.ts - отвечает только за email
export class EmailHandlers {
  // ...
}

3. Реализация репозиториев для тестирования

export interface UserRepository {
  findById(id: string): Promise;
  create(data: CreateUserRequest): Promise;
  save(user: User): Promise;
}

export class InMemoryUserRepository implements UserRepository {
  private users: Map = new Map();

  async findById(id: string): Promise {
    return this.users.get(id) || null;
  }

  async create(data: CreateUserRequest): Promise {
    const user = {
      id: uuid(),
      email: data.email,
      name: data.name,
      createdAt: new Date()
    };
    this.users.set(user.id, user);
    return user;
  }

  async save(user: User): Promise {
    this.users.set(user.id, user);
  }
}

export class PostgresUserRepository implements UserRepository {
  constructor(private pool: Pool) {}

  async findById(id: string): Promise {
    const result = await this.pool.query(
      \'SELECT * FROM users WHERE id = $1\',
      [id]
    );
    return result.rows[0] || null;
  }

  // ...
}

4. Настройка зависимостей с помощью DI контейнера

import { container } from \'tsyringe\';

// Регистрация зависимостей
container.register(\'UserRepository\', {
  useClass: process.env.NODE_ENV === \'test\' 
    ? InMemoryUserRepository 
    : PostgresUserRepository
});

container.register(\'UserService\', {
  useClass: UserService
});

// Красивое разрешение в сервере
export class McpServer {
  constructor() {
    const userService = container.resolve(\'UserService\');
    const userHandlers = new UserHandlers(userService);
    
    // ...
  }
}

5. Логирование и обработка ошибок в структуре DDD

export class UserHandlers {
  constructor(
    private userService: UserService,
    private logger: Logger,
    private toolRegistry: ToolRegistry
  ) {
    this.registerToolHandlers();
  }

  private registerToolHandlers() {
    this.toolRegistry.registerTool(
      { name: \"get_user\", /* ... */ },
      async (args) => {
        try {
          this.logger.info(\'Getting user\', { userId: args.userId });
          const user = await this.userService.getUser(args.userId);
          if (!user) {
            throw new NotFoundError(`User ${args.userId} not found`);
          }
          return {
            content: [{ type: \"text\", text: JSON.stringify(user) }]
          };
        } catch (error) {
          this.logger.error(\'Error getting user\', { error });
          throw error;
        }
      }
    );
  }
}

Чего вы достигнете

Эта структура даёт вам:

  • Четкую разделяемость ответственности: разные члены команды могут работать над разными модулями.
  • Тестируемость: модули домена не зависят от внешних фреймворков, вы можете легко тестировать бизнес-логику изолированно.
  • Эволюционируемость: добавление нового инструмента MCP означает добавление нового метода в доменный сервис, а не переписывание центрального switch.
  • Простое поддержание: когда вы запускаете новый экземпляр сервера или инструмента, вы знаете, где искать код.

Как ваши клиенты, когда их модель MCP вызывает ваш сервер, они видят только инструменты и схемы. Им всё равно, что у вас лежит под капотом — модульная структура или гигантский switch.

Но для вас, разработчика, который добавляет 13-й инструмент, это имеет огромное значение.