# Руководство по реализации сервера MCP на Node/TypeScript ## Обзор Этот документ содержит лучшие практики и примеры для Node/TypeScript по реализации серверов MCP с помощью MCP TypeScript SDK. Он охватывает структуру проекта, настройку сервера, паттерны регистрации инструментов, проверку входных данных с помощью Zod, обработку ошибок и полные работающие примеры. --- ## Краткий справочник ### Основные импорты ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import express from "express"; import { z } from "zod"; ``` ### Инициализация сервера ```typescript const server = new McpServer({ name: "service-mcp-server", version: "1.0.0" }); ``` ### Паттерн регистрации инструментов ```typescript server.registerTool( "tool_name", { title: "Tool Display Name", description: "What the tool does", inputSchema: { param: z.string() }, outputSchema: { result: z.string() } }, async ({ param }) => { const output = { result: `Processed: ${param}` }; return { content: [{ type: "text", text: JSON.stringify(output) }], structuredContent: output // Modern pattern for structured data }; } ); ``` --- ## MCP TypeScript SDK Официальный MCP TypeScript SDK предоставляет: - Класс `McpServer` для инициализации сервера - Метод `registerTool` для регистрации инструментов - Интеграцию схем Zod для проверки входных данных во время выполнения - Типобезопасные реализации обработчиков инструментов **ВАЖНО — используй только современные API:** - **ИСПОЛЬЗУЙ**: `server.registerTool()`, `server.registerResource()`, `server.registerPrompt()` - **НЕ ИСПОЛЬЗУЙ**: старые устаревшие API, такие как `server.tool()`, `server.setRequestHandler(ListToolsRequestSchema, ...)`, или ручную регистрацию обработчиков - Методы `register*` обеспечивают лучшую типобезопасность, автоматическую обработку схем и являются рекомендуемым подходом Полные сведения смотри в документации MCP SDK в справочных материалах. ## Соглашение об именовании серверов Серверы MCP на Node/TypeScript должны следовать такому шаблону именования: - **Формат**: `{service}-mcp-server` (нижний регистр с дефисами) - **Примеры**: `github-mcp-server`, `jira-mcp-server`, `stripe-mcp-server` Имя должно быть: - Общим (не привязанным к конкретным функциям) - Описывающим интегрируемый сервис/API - Легко выводимым из описания задачи - Без номеров версий или дат ## Структура проекта Создай следующую структуру для серверов MCP на Node/TypeScript: ``` {service}-mcp-server/ ├── package.json ├── tsconfig.json ├── README.md ├── src/ │ ├── index.ts # Основная точка входа с инициализацией McpServer │ ├── types.ts # Определения типов и интерфейсы TypeScript │ ├── tools/ # Реализации инструментов (один файл на предметную область) │ ├── services/ # API-клиенты и общие утилиты │ ├── schemas/ # Схемы проверки Zod │ └── constants.ts # Общие константы (API_URL, CHARACTER_LIMIT и т. д.) └── dist/ # Собранные JavaScript-файлы (точка входа: dist/index.js) ``` ## Реализация инструментов ### Именование инструментов Используй snake_case для имён инструментов (например, "search_users", "create_project", "get_channel_info") с ясными именами, ориентированными на действия. **Избегай конфликтов имён**: включай контекст сервиса, чтобы предотвратить совпадения: - Используй "slack_send_message" вместо просто "send_message" - Используй "github_create_issue" вместо просто "create_issue" - Используй "asana_list_tasks" вместо просто "list_tasks" ### Структура инструмента Инструменты регистрируются методом `registerTool` со следующими требованиями: - Используй схемы Zod для проверки входных данных во время выполнения и типобезопасности - Поле `description` должно быть задано явно — комментарии JSDoc НЕ извлекаются автоматически - Явно задавай `title`, `description`, `inputSchema` и `annotations` - `inputSchema` должна быть объектом схемы Zod (а не JSON-схемой) - Явно типизируй все параметры и возвращаемые значения ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; const server = new McpServer({ name: "example-mcp", version: "1.0.0" }); // Zod schema for input validation const UserSearchInputSchema = z.object({ query: z.string() .min(2, "Query must be at least 2 characters") .max(200, "Query must not exceed 200 characters") .describe("Search string to match against names/emails"), limit: z.number() .int() .min(1) .max(100) .default(20) .describe("Maximum results to return"), offset: z.number() .int() .min(0) .default(0) .describe("Number of results to skip for pagination"), response_format: z.nativeEnum(ResponseFormat) .default(ResponseFormat.MARKDOWN) .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable") }).strict(); // Type definition from Zod schema type UserSearchInput = z.infer; server.registerTool( "example_search_users", { title: "Search Example Users", description: `Search for users in the Example system by name, email, or team. This tool searches across all user profiles in the Example platform, supporting partial matches and various search filters. It does NOT create or modify users, only searches existing ones. Args: - query (string): Search string to match against names/emails - limit (number): Maximum results to return, between 1-100 (default: 20) - offset (number): Number of results to skip for pagination (default: 0) - response_format ('markdown' | 'json'): Output format (default: 'markdown') Returns: For JSON format: Structured data with schema: { "total": number, // Total number of matches found "count": number, // Number of results in this response "offset": number, // Current pagination offset "users": [ { "id": string, // User ID (e.g., "U123456789") "name": string, // Full name (e.g., "John Doe") "email": string, // Email address "team": string, // Team name (optional) "active": boolean // Whether user is active } ], "has_more": boolean, // Whether more results are available "next_offset": number // Offset for next page (if has_more is true) } Examples: - Use when: "Find all marketing team members" -> params with query="team:marketing" - Use when: "Search for John's account" -> params with query="john" - Don't use when: You need to create a user (use example_create_user instead) Error Handling: - Returns "Error: Rate limit exceeded" if too many requests (429 status) - Returns "No users found matching ''" if search returns empty`, inputSchema: UserSearchInputSchema, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async (params: UserSearchInput) => { try { // Input validation is handled by Zod schema // Make API request using validated parameters const data = await makeApiRequest( "users/search", "GET", undefined, { q: params.query, limit: params.limit, offset: params.offset } ); const users = data.users || []; const total = data.total || 0; if (!users.length) { return { content: [{ type: "text", text: `No users found matching '${params.query}'` }] }; } // Prepare structured output const output = { total, count: users.length, offset: params.offset, users: users.map((user: any) => ({ id: user.id, name: user.name, email: user.email, ...(user.team ? { team: user.team } : {}), active: user.active ?? true })), has_more: total > params.offset + users.length, ...(total > params.offset + users.length ? { next_offset: params.offset + users.length } : {}) }; // Format text representation based on requested format let textContent: string; if (params.response_format === ResponseFormat.MARKDOWN) { const lines = [`# User Search Results: '${params.query}'`, "", `Found ${total} users (showing ${users.length})`, ""]; for (const user of users) { lines.push(`## ${user.name} (${user.id})`); lines.push(`- **Email**: ${user.email}`); if (user.team) lines.push(`- **Team**: ${user.team}`); lines.push(""); } textContent = lines.join("\n"); } else { textContent = JSON.stringify(output, null, 2); } return { content: [{ type: "text", text: textContent }], structuredContent: output // Modern pattern for structured data }; } catch (error) { return { content: [{ type: "text", text: handleApiError(error) }] }; } } ); ``` ## Схемы Zod для проверки входных данных Zod обеспечивает проверку типов во время выполнения: ```typescript import { z } from "zod"; // Basic schema with validation const CreateUserSchema = z.object({ name: z.string() .min(1, "Name is required") .max(100, "Name must not exceed 100 characters"), email: z.string() .email("Invalid email format"), age: z.number() .int("Age must be a whole number") .min(0, "Age cannot be negative") .max(150, "Age cannot be greater than 150") }).strict(); // Use .strict() to forbid extra fields // Enums enum ResponseFormat { MARKDOWN = "markdown", JSON = "json" } const SearchSchema = z.object({ response_format: z.nativeEnum(ResponseFormat) .default(ResponseFormat.MARKDOWN) .describe("Output format") }); // Optional fields with defaults const PaginationSchema = z.object({ limit: z.number() .int() .min(1) .max(100) .default(20) .describe("Maximum results to return"), offset: z.number() .int() .min(0) .default(0) .describe("Number of results to skip") }); ``` ## Варианты формата ответа Для гибкости поддерживай несколько форматов вывода: ```typescript enum ResponseFormat { MARKDOWN = "markdown", JSON = "json" } const inputSchema = z.object({ query: z.string(), response_format: z.nativeEnum(ResponseFormat) .default(ResponseFormat.MARKDOWN) .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable") }); ``` **Формат Markdown**: - Используй заголовки, списки и форматирование для ясности - Преобразовывай временные метки в удобочитаемый формат - Показывай отображаемые имена с идентификаторами в скобках - Опускай многословные метаданные - Логически группируй связанную информацию **Формат JSON**: - Возвращай полные структурированные данные, подходящие для программной обработки - Включай все доступные поля и метаданные - Используй единообразные имена и типы полей ## Реализация пагинации Для инструментов, перечисляющих ресурсы: ```typescript const ListSchema = z.object({ limit: z.number().int().min(1).max(100).default(20), offset: z.number().int().min(0).default(0) }); async function listItems(params: z.infer) { const data = await apiRequest(params.limit, params.offset); const response = { total: data.total, count: data.items.length, offset: params.offset, items: data.items, has_more: data.total > params.offset + data.items.length, next_offset: data.total > params.offset + data.items.length ? params.offset + data.items.length : undefined }; return JSON.stringify(response, null, 2); } ``` ## Ограничения количества символов и усечение Добавь константу CHARACTER_LIMIT, чтобы предотвратить чрезмерно большие ответы: ```typescript // At module level in constants.ts export const CHARACTER_LIMIT = 25000; // Maximum response size in characters async function searchTool(params: SearchInput) { let result = generateResponse(data); // Check character limit and truncate if needed if (result.length > CHARACTER_LIMIT) { const truncatedData = data.slice(0, Math.max(1, data.length / 2)); response.data = truncatedData; response.truncated = true; response.truncation_message = `Response truncated from ${data.length} to ${truncatedData.length} items. ` + `Use 'offset' parameter or add filters to see more results.`; result = JSON.stringify(response, null, 2); } return result; } ``` ## Обработка ошибок Предоставляй ясные сообщения об ошибках, подсказывающие действия: ```typescript import axios, { AxiosError } from "axios"; function handleApiError(error: unknown): string { if (error instanceof AxiosError) { if (error.response) { switch (error.response.status) { case 404: return "Error: Resource not found. Please check the ID is correct."; case 403: return "Error: Permission denied. You don't have access to this resource."; case 429: return "Error: Rate limit exceeded. Please wait before making more requests."; default: return `Error: API request failed with status ${error.response.status}`; } } else if (error.code === "ECONNABORTED") { return "Error: Request timed out. Please try again."; } } return `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`; } ``` ## Общие утилиты Выноси общую функциональность в переиспользуемые функции: ```typescript // Shared API request function async function makeApiRequest( endpoint: string, method: "GET" | "POST" | "PUT" | "DELETE" = "GET", data?: any, params?: any ): Promise { try { const response = await axios({ method, url: `${API_BASE_URL}/${endpoint}`, data, params, timeout: 30000, headers: { "Content-Type": "application/json", "Accept": "application/json" } }); return response.data; } catch (error) { throw error; } } ``` ## Лучшие практики Async/Await Всегда используй async/await для сетевых запросов и операций ввода-вывода: ```typescript // Good: Async network request async function fetchData(resourceId: string): Promise { const response = await axios.get(`${API_URL}/resource/${resourceId}`); return response.data; } // Bad: Promise chains function fetchData(resourceId: string): Promise { return axios.get(`${API_URL}/resource/${resourceId}`) .then(response => response.data); // Harder to read and maintain } ``` ## Лучшие практики TypeScript 1. **Используй строгий TypeScript**: включи строгий режим в tsconfig.json 2. **Определяй интерфейсы**: создавай ясные определения интерфейсов для всех структур данных 3. **Избегай `any`**: используй корректные типы или `unknown` вместо `any` 4. **Zod для проверки во время выполнения**: используй схемы Zod для проверки внешних данных 5. **Защитники типов**: создавай функции проверки типов для сложных проверок 6. **Обработка ошибок**: всегда используй try-catch с корректной проверкой типа ошибки 7. **Безопасность относительно null**: используй optional chaining (`?.`) и nullish coalescing (`??`) ```typescript // Good: Type-safe with Zod and interfaces interface UserResponse { id: string; name: string; email: string; team?: string; active: boolean; } const UserSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email(), team: z.string().optional(), active: z.boolean() }); type User = z.infer; async function getUser(id: string): Promise { const data = await apiCall(`/users/${id}`); return UserSchema.parse(data); // Runtime validation } // Bad: Using any async function getUser(id: string): Promise { return await apiCall(`/users/${id}`); // No type safety } ``` ## Конфигурация пакета ### package.json ```json { "name": "{service}-mcp-server", "version": "1.0.0", "description": "MCP server for {Service} API integration", "type": "module", "main": "dist/index.js", "scripts": { "start": "node dist/index.js", "dev": "tsx watch src/index.ts", "build": "tsc", "clean": "rm -rf dist" }, "engines": { "node": ">=18" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.6.1", "axios": "^1.7.9", "zod": "^3.23.8" }, "devDependencies": { "@types/node": "^22.10.0", "tsx": "^4.19.2", "typescript": "^5.7.2" } } ``` ### tsconfig.json ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "declaration": true, "declarationMap": true, "sourceMap": true, "allowSyntheticDefaultImports": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` ## Полный пример ```typescript #!/usr/bin/env node /** * MCP Server for Example Service. * * This server provides tools to interact with Example API, including user search, * project management, and data export capabilities. */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import axios, { AxiosError } from "axios"; // Constants const API_BASE_URL = "https://api.example.com/v1"; const CHARACTER_LIMIT = 25000; // Enums enum ResponseFormat { MARKDOWN = "markdown", JSON = "json" } // Zod schemas const UserSearchInputSchema = z.object({ query: z.string() .min(2, "Query must be at least 2 characters") .max(200, "Query must not exceed 200 characters") .describe("Search string to match against names/emails"), limit: z.number() .int() .min(1) .max(100) .default(20) .describe("Maximum results to return"), offset: z.number() .int() .min(0) .default(0) .describe("Number of results to skip for pagination"), response_format: z.nativeEnum(ResponseFormat) .default(ResponseFormat.MARKDOWN) .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable") }).strict(); type UserSearchInput = z.infer; // Shared utility functions async function makeApiRequest( endpoint: string, method: "GET" | "POST" | "PUT" | "DELETE" = "GET", data?: any, params?: any ): Promise { try { const response = await axios({ method, url: `${API_BASE_URL}/${endpoint}`, data, params, timeout: 30000, headers: { "Content-Type": "application/json", "Accept": "application/json" } }); return response.data; } catch (error) { throw error; } } function handleApiError(error: unknown): string { if (error instanceof AxiosError) { if (error.response) { switch (error.response.status) { case 404: return "Error: Resource not found. Please check the ID is correct."; case 403: return "Error: Permission denied. You don't have access to this resource."; case 429: return "Error: Rate limit exceeded. Please wait before making more requests."; default: return `Error: API request failed with status ${error.response.status}`; } } else if (error.code === "ECONNABORTED") { return "Error: Request timed out. Please try again."; } } return `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`; } // Create MCP server instance const server = new McpServer({ name: "example-mcp", version: "1.0.0" }); // Register tools server.registerTool( "example_search_users", { title: "Search Example Users", description: `[Full description as shown above]`, inputSchema: UserSearchInputSchema, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } }, async (params: UserSearchInput) => { // Implementation as shown above } ); // Main function // For stdio (local): async function runStdio() { if (!process.env.EXAMPLE_API_KEY) { console.error("ERROR: EXAMPLE_API_KEY environment variable is required"); process.exit(1); } const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP server running via stdio"); } // For streamable HTTP (remote): async function runHTTP() { if (!process.env.EXAMPLE_API_KEY) { console.error("ERROR: EXAMPLE_API_KEY environment variable is required"); process.exit(1); } const app = express(); app.use(express.json()); app.post('/mcp', async (req, res) => { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true }); res.on('close', () => transport.close()); await server.connect(transport); await transport.handleRequest(req, res, req.body); }); const port = parseInt(process.env.PORT || '3000'); app.listen(port, () => { console.error(`MCP server running on http://localhost:${port}/mcp`); }); } // Choose transport based on environment const transport = process.env.TRANSPORT || 'stdio'; if (transport === 'http') { runHTTP().catch(error => { console.error("Server error:", error); process.exit(1); }); } else { runStdio().catch(error => { console.error("Server error:", error); process.exit(1); }); } ``` --- ## Расширенные возможности MCP ### Регистрация ресурсов Предоставляй данные как ресурсы для эффективного доступа на основе URI: ```typescript import { ResourceTemplate } from "@modelcontextprotocol/sdk/types.js"; // Register a resource with URI template server.registerResource( { uri: "file://documents/{name}", name: "Document Resource", description: "Access documents by name", mimeType: "text/plain" }, async (uri: string) => { // Extract parameter from URI const match = uri.match(/^file:\/\/documents\/(.+)$/); if (!match) { throw new Error("Invalid URI format"); } const documentName = match[1]; const content = await loadDocument(documentName); return { contents: [{ uri, mimeType: "text/plain", text: content }] }; } ); // List available resources dynamically server.registerResourceList(async () => { const documents = await getAvailableDocuments(); return { resources: documents.map(doc => ({ uri: `file://documents/${doc.name}`, name: doc.name, mimeType: "text/plain", description: doc.description })) }; }); ``` **Когда использовать ресурсы, а когда инструменты:** - **Ресурсы**: для доступа к данным с простыми параметрами на основе URI - **Инструменты**: для сложных операций, требующих проверки и бизнес-логики - **Ресурсы**: когда данные относительно статичны или основаны на шаблонах - **Инструменты**: когда у операций есть побочные эффекты или сложные рабочие процессы ### Варианты транспорта TypeScript SDK поддерживает два основных транспортных механизма: #### Потоковый HTTP (рекомендуется для удалённых серверов) ```typescript import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import express from "express"; const app = express(); app.use(express.json()); app.post('/mcp', async (req, res) => { // Create new transport for each request (stateless, prevents request ID collisions) const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true }); res.on('close', () => transport.close()); await server.connect(transport); await transport.handleRequest(req, res, req.body); }); app.listen(3000); ``` #### stdio (для локальных интеграций) ```typescript import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; const transport = new StdioServerTransport(); await server.connect(transport); ``` **Выбор транспорта:** - **Потоковый HTTP**: веб-сервисы, удалённый доступ, несколько клиентов - **stdio**: инструменты командной строки, локальная разработка, интеграция подпроцессов ### Поддержка уведомлений Уведомляй клиентов об изменениях состояния сервера: ```typescript // Notify when tools list changes server.notification({ method: "notifications/tools/list_changed" }); // Notify when resources change server.notification({ method: "notifications/resources/list_changed" }); ``` Используй уведомления умеренно — только когда возможности сервера действительно меняются. --- ## Лучшие практики кода ### Компонуемость и переиспользуемость кода В твоей реализации ОБЯЗАТЕЛЬНО должны быть приоритетными компонуемость и повторное использование кода: 1. **Выноси общую функциональность**: - Создавай переиспользуемые вспомогательные функции для операций, используемых несколькими инструментами - Создавай общие API-клиенты для HTTP-запросов вместо дублирования кода - Централизуй логику обработки ошибок в служебных функциях - Выноси бизнес-логику в отдельные функции, которые можно компоновать - Выноси общую функциональность выбора и форматирования полей Markdown или JSON 2. **Избегай дублирования**: - НИКОГДА не копируй сходный код между инструментами - Если пишешь сходную логику второй раз, вынеси её в функцию - Общие операции, такие как пагинация, фильтрация, выбор полей и форматирование, должны переиспользоваться - Логика аутентификации/авторизации должна быть централизованной ## Сборка и запуск Всегда собирай код TypeScript перед запуском: ```bash # Build the project npm run build # Run the server npm start # Development with auto-reload npm run dev ``` Всегда убеждайся, что `npm run build` завершается успешно, прежде чем считать реализацию завершённой. ## Контрольный список качества Перед завершением реализации сервера MCP на Node/TypeScript убедись в следующем: ### Стратегическое проектирование - [ ] Инструменты обеспечивают полные рабочие процессы, а не только обёртки над конечными точками API - [ ] Имена инструментов отражают естественное деление задач - [ ] Форматы ответов оптимизированы для эффективного использования контекста агента - [ ] Где уместно, используются удобочитаемые идентификаторы - [ ] Сообщения об ошибках направляют агентов к корректному использованию ### Качество реализации - [ ] СФОКУСИРОВАННАЯ РЕАЛИЗАЦИЯ: реализованы наиболее важные и ценные инструменты - [ ] Все инструменты зарегистрированы через `registerTool` с полной конфигурацией - [ ] Все инструменты включают `title`, `description`, `inputSchema` и `annotations` - [ ] Аннотации заданы корректно (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) - [ ] Все инструменты используют схемы Zod для проверки входных данных во время выполнения с обязательным `.strict()` - [ ] Во всех схемах Zod есть надлежащие ограничения и описательные сообщения об ошибках - [ ] У всех инструментов есть исчерпывающие описания с явными типами входных/выходных данных - [ ] Описания включают примеры возвращаемых значений и полную документацию схем - [ ] Сообщения об ошибках ясны, подсказывают действия и обучают ### Качество TypeScript - [ ] Интерфейсы TypeScript определены для всех структур данных - [ ] Строгий TypeScript включён в tsconfig.json - [ ] Тип `any` не используется — вместо него используются `unknown` или корректные типы - [ ] У всех асинхронных функций явно заданы возвращаемые типы Promise - [ ] Обработка ошибок использует надлежащие защитники типов (например, `axios.isAxiosError`, `z.ZodError`) ### Расширенные возможности (где применимо) - [ ] Ресурсы зарегистрированы для подходящих конечных точек данных - [ ] Настроен подходящий транспорт (stdio или потоковый HTTP) - [ ] Реализованы уведомления для динамических возможностей сервера - [ ] Обеспечена типобезопасность с интерфейсами SDK ### Конфигурация проекта - [ ] Package.json включает все необходимые зависимости - [ ] Скрипт сборки создаёт работающий JavaScript в каталоге dist/ - [ ] Основная точка входа корректно настроена как dist/index.js - [ ] Имя сервера соответствует формату: `{service}-mcp-server` - [ ] tsconfig.json корректно настроен со строгим режимом ### Качество кода - [ ] Пагинация корректно реализована, где применимо - [ ] Большие ответы проверяются по константе CHARACTER_LIMIT и усекаются с ясными сообщениями - [ ] Для потенциально больших наборов результатов предоставлены возможности фильтрации - [ ] Все сетевые операции корректно обрабатывают тайм-ауты и ошибки соединения - [ ] Общая функциональность вынесена в переиспользуемые функции - [ ] Возвращаемые типы единообразны для сходных операций ### Тестирование и сборка - [ ] `npm run build` успешно завершается без ошибок - [ ] dist/index.js создан и является исполняемым - [ ] Сервер запускается: `node dist/index.js --help` - [ ] Все импорты корректно разрешаются - [ ] Пробные вызовы инструментов работают как ожидается