Главная→Блог→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-й инструмент, это имеет огромное значение.