Протоколы и инструменты

MCP: основы и примитивы

Как устроен открытый стандарт Model Context Protocol

До появления открытых стандартов разработчики агентных систем создавали закрытые интерфейсы для каждого инструмента. Если агенту требовался доступ к базе данных PostgreSQL или репозиториям GitHub, приходилось писать отдельный адаптер под Claude Desktop, отдельный под Cursor, отдельный под VS Code Copilot и отдельный под внутренний фреймворк компании.

Каждая новая интеграция превращалась в проблему N × M: N агентных клиентов требовали M специфических коннекторов к внешним сервисам.

Model Context Protocol (MCP) решает эту проблему и стандартизирует взаимодействие между хост-приложением (клиентом) и поставщиками данных или действий (серверами). В статье про интерфейс инструментов мы разобрали фундаментальный цикл Describe → Decide → Execute → Observe, а в вызове функций - форматы интеграции инструментов в API провайдеров. MCP переносит этот же паттерн на уровень открытого сетевого и межпроцессного протокола.

Архитектура интеграций: N x M адаптеров против Hub-and-Spoke в MCP

Протокол создала компания Anthropic и передала в фонд Agentic AI Foundation под эгидой Linux Foundation. Сегодня MCP стал индустриальным стандартом подключения контекста и инструментов к языковым моделям.

Протокол сообщений: почему JSON-RPC 2.0

В основе MCP лежит легковесный протокол удаленного вызова процедур JSON-RPC 2.0. Выбор JSON-RPC вместо классического REST API обусловлен тремя архитектурными требованиями:

  • Двунаправленность: в REST запросы отправляет только клиент. В MCP сервер может присылать хосту уведомления об изменении данных и прогрессе прямо в потоке ответа.
  • Независимость от транспорта: единый JSON-формат сообщений одинаково работает через стандартные потоки ввода-вывода операционной системы (stdio) и через сетевые HTTP-соединения.
  • Готовые форматы сообщений: спецификация JSON-RPC задает строгие правила для запросов, ответов и односторонних уведомлений, избавляя от необходимости изобретать свои схемы обработки ошибок.

Структура сообщений

Протокол определяет три типа сообщений:

  • Запрос (Request): содержит уникальный id, имя вызываемого метода method и параметры params. Требует обязательного ответа с тем же id.
  • Ответ (Response): возвращает id исходного запроса и либо поле result при успехе, либо объект error с кодом ошибки и пояснением.
  • Уведомление (Notification): отправляется без поля id. Получатель обрабатывает событие, но не отправляет ответ.
// 1. Запрос клиента (Request) - вызов инструмента:
{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "query_database",
    "arguments": { "sql": "SELECT COUNT(*) FROM users;" },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28"
    }
  }
}
 
// 2. Успешный ответ сервера (Response с result):
{
  "jsonrpc": "2.0",
  "id": 42,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "1420"
      }
    ],
    "isError": false
  }
}
 
// 3. Ответ сервера при ошибке протокола (Response с error):
{
  "jsonrpc": "2.0",
  "id": 42,
  "error": {
    "code": -32602,
    "message": "Недопустимые аргументы: поле 'sql' обязательно"
  }
}
 
// 4. Уведомление сервера (Notification) в потоке ответа или subscriptions/listen:
{
  "jsonrpc": "2.0",
  "method": "notifications/tools/list_changed"
}

Спецификация резервирует диапазон кодов от -32020 до -32099 под протокольные ошибки MCP (например, HeaderMismatchError со значением -32020 или UnsupportedProtocolVersion со значением -32022). Для стандартных ошибок валидации аргументов используется код JSON-RPC -32602 (Invalid Params).

Жизненный цикл и согласование возможностей

Архитектура взаимодействия между клиентом и сервером прошла путь от сохранения состояния сессии к масштабируемой модели без состояния (stateless).

Современный стандарт: протокол без состояния (Stateless Core 2026-07-28)

В ревизии спецификации 2026-07-28 протокол MCP перешел на модель без состояния (SEP-2575, SEP-2567). Протокольные сессии и заголовок Mcp-Session-Id упразднили.

Самодостаточные запросы: каждый сетевой запрос содержит информацию о версии протокола, клиенте и его возможностях внутри объекта _meta:

{
  "jsonrpc": "2.0",
  "id": 101,
  "method": "tools/call",
  "params": {
    "name": "search_docs",
    "arguments": { "query": "oauth" },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": {
        "name": "CursorIDE",
        "version": "3.1.0"
      },
      "io.modelcontextprotocol/clientCapabilities": {
        "extensions": {
          "io.modelcontextprotocol/tasks": {}
        }
      }
    }
  }
}

Обнаружение возможностей (server/discover): сервер обязан поддерживать RPC-метод server/discover. Клиент может вызвать его перед началом работы, чтобы проверить версию протокола и список возможностей сервера:

// Запрос обнаружения возможностей:
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "server/discover",
  "params": {
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28"
    }
  }
}
 
// Ответ сервера с метаданными:
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2026-07-28",
    "serverInfo": {
      "name": "postgres-server",
      "version": "2.0.0"
    },
    "capabilities": {
      "tools": { "listChanged": true },
      "resources": { "listChanged": true },
      "prompts": { "listChanged": false }
    }
  }
}
  • Балансировка без привязки к сессии: любой входящий запрос можно направить на произвольный экземпляр сервера за round-robin балансировщиком. Синхронизировать кэш сессий между подами не требуется.
  • Явные дескрипторы состояния: если инструменту требуется сохранять контекст между несколькими шагами (например, курсор пагинации или открытую транзакцию базы данных), сервер генерирует строковый дескриптор и возвращает его в теле ответа. Модель передает этот дескриптор аргументом в следующий вызов инструмента.

Классическое рукопожатие (Legacy-режим 2025-11-25)

В ранних версиях протокола (ревизия 2025-11-25), которые еще встречаются в локальных клиентах и плагинах IDE, работа строилась вокруг сессии из трех последовательных фаз: рукопожатие (initialize), рабочий цикл (operation) и завершение (shutdown).

Legacy 2025-11-25: рукопожатие initialize, рабочий цикл и завершение сессии

Эволюция протокола: сессия vs stateless

Legacy-режим (2025-11-25): сессионное рукопожатие initialize -> notifications/initialized и заголовок Mcp-Session-Id. В распределенных системах это требует привязки сессий (sticky sessions) и создает единую точку отказа при перезапуске экземпляров.

Современный стандарт (2026-07-28): каждый запрос автономен и несет метаданные в _meta. Серверы масштабируются в бессерверных средах как обычные HTTP-микросервисы.

Три серверных примитива: Tools, Resources, Prompts

Спецификация MCP делит все серверные возможности на три независимых примитива. Каждый примитив предназначен для своего сценария и по-разному воспринимается моделью и пользователем.

ПримитивНазначениеИнициатор вызоваСемантикаПример эндпоинтов
ToolsАктивные действия, вычисления, мутации данныхМодель (автономно)Вызов функции с аргументами, возврат блоков результатаtools/list
tools/call
ResourcesПассивные данные, контекст для чтенияХост / ПользовательЧтение по URI, кэширование и подписки на измененияresources/list
resources/read
subscriptions/listen
PromptsШаблоны диалогов и готовые сценарии (SOP)Пользователь (интерактивно)Заполнение аргументов шаблона, возврат сообщений диалогаprompts/list
prompts/get

Инструменты (Tools): активные действия

Инструменты в MCP соответствуют концепции вызова функций. Каждый инструмент содержит имя, текстовое описание для модели и схему параметров в формате JSON Schema.

Результат вызова инструмента возвращается массивом типизированных блоков content: текстовые блоки (type: "text"), бинарные данные или изображения (type: "image").

Прогресс и отмена долгих вызовов

Выполнение инструмента может занимать десятки секунд: тяжелый SQL-запрос, сборка проекта или сканирование репозитория. Если пользователь остановил генерацию или хост достиг таймаута, сервер не должен продолжать тратить ресурсы.

В протоколе это решается двумя механизмами.

Уведомления о прогрессе: в запросе tools/call клиент может передать токен прогресса _meta: { "progressToken": "t-123" }. Во время работы сервер отправляет промежуточные уведомления notifications/progress прямо в потоке ответа:

{
  "jsonrpc": "2.0",
  "method": "notifications/progress",
  "params": {
    "progressToken": "t-123",
    "progress": 45,
    "total": 100
  }
}

Отмена выполнения: если пользователь нажал кнопку отмены или сработал таймаут клиента, хост отправляет уведомление notifications/cancelled с идентификатором исходного запроса:

{
  "jsonrpc": "2.0",
  "method": "notifications/cancelled",
  "params": {
    "requestId": 42,
    "reason": "Пользователь отменил генерацию"
  }
}

Сервер перехватывает уведомление, прерывает фоновую задачу, освобождает ресурсы и отменяет транзакцию.

Особенности отмены зависят от выбранного транспорта:

  • В stdio: закрытие потока ввода (stdin) или завершение родительского процесса передает признак EOF или сигнал SIGINT, останавливая подпроцесс.
  • В Streamable HTTP: обрыв соединения не считается отменой задачи, так как возможны кратковременные сбои сети. Чтобы явно остановить вызов, хост отправляет отдельный POST-запрос с уведомлением notifications/cancelled.

Ресурсы (Resources): контекст и данные

Ресурсы предоставляют данные только для чтения. Каждый ресурс адресуется уникальным URI:

  • локальные файлы: file:///workspace/project/config.json
  • запросы к базе данных: postgres://analytics-db/schema/orders
  • сущности приложения: notes://note-128
  • динамические выборки: metrics://cluster/realtime

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

Шаблоны URI для динамических данных (Resource Templates)

Если в базе данных 10 000 таблиц или в трекере миллион задач, сервер не может вернуть их одним статическим списком в resources/list - это переполнит память и контекст.

Для таких массивов данных сервер объявляет параметризованные шаблоны resourceTemplates (RFC 6570):

{
  "uriTemplate": "postgres://analytics/tables/{table_name}/schema",
  "name": "Схема таблицы",
  "mimeType": "text/x-sql"
}

Клиент понимает структуру URI, запрашивает конкретные ресурсы на лету (resources/read) и предлагает автодополнение имен сущностей в интерфейсе.

В ревизии 2026-07-28 точечные подписки resources/subscribe заменены потоком subscriptions/listen (SEP-2575). Клиент отправляет POST /mcp с методом subscriptions/listen и подписывается на типы событий: resourcesListChanged, toolsListChanged или promptsListChanged. При обновлении данных сервер отправляет структурированное событие с идентификатором subscriptionId.

Промпты (Prompts): управляемые шаблоны

Промпты в MCP - это готовые шаблоны запросов с параметрами. Они избавляют пользователя от необходимости каждый раз вручную писать длинные инструкции. В клиентских интерфейсах (Claude Desktop, Cursor) такие шаблоны отображаются как slash-команды (например, /code_review или /triage_incident).

Пользователь выбирает команду, вводит аргументы, а клиент запрашивает у сервера готовый текст через prompts/get и вставляет его в диалог с моделью.

Кэширование каталогов и оптимизация Prompt Cache

Чтобы агент знал, какие инструменты, ресурсы и промпты доступны на сервере, клиент запрашивает их каталоги методами tools/list, resources/list и prompts/list.

Если запрашивать эти списки заново перед каждым обращением к модели, возникнет лишняя сетевая задержка. Чтобы этого избежать, ответы сервера возвращают параметры кэширования:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [ ... ],
    "ttlMs": 300000,
    "cacheScope": "public"
  }
}
  • ttlMs - время жизни кэша в миллисекундах. В течение этого времени клиент использует локальную копию каталога и не опрашивает сервер повторно.
  • cacheScope - область видимости кэша: "public" разрешает кэшировать список на уровне общего шлюза для всех пользователей, а "private" требует сохранять кэш отдельно для каждого пользователя.
  • детерминированный порядок инструментов: сервер обязан возвращать инструменты в строго фиксированном порядке. Клиент передает описания инструментов в системный промпт языковой модели. Если порядок инструментов случаен, текст промпта меняется при каждом вызове, что сбрасывает кэш промптов (Prompt Cache) на стороне LLM-провайдера (например, Anthropic или OpenAI) и увеличивает задержку и стоимость запросов.

На интервью по AI System Design мы используем простое правило выбора примитивов:

  • Если действие меняет состояние системы или требует параметров от модели во время рассуждения - проектируем Tool.
  • Если нужно предоставить неизменяемый контекст, журнал или снимок таблицы по URI - проектируем Resource.
  • Если нужно упаковать повторяющийся сценарий работы с системой в кнопку или команду для пользователя - проектируем Prompt.

Рассмотрим реализацию сервера, объединяющего все три примитива:

PythonЕдиный сервер 2026-07-28: server/discover, Tools, Resources и Prompts
import json
from typing import Any


class DatabaseMcpServer:
    """Сервер MCP с реализацией трех серверных примитивов: Tools, Resources и Prompts."""

    def __init__(self) -> None:
        self.server_info = {"name": "enterprise-db-server", "version": "1.0.0"}
        self.capabilities = {
            "tools": {"listChanged": True},
            "resources": {"subscribe": True, "listChanged": True},
            "prompts": {"listChanged": False},
        }

    def dispatch(self, request: dict[str, Any]) -> dict[str, Any] | None:
        """Единый диспетчер JSON-RPC 2.0 сообщений."""
        req_id = request.get("id")
        method = request.get("method")
        params = request.get("params", {})

        # Уведомления (Notifications) не имеют id и не требуют ответа
        if req_id is None:
            if method == "notifications/initialized":
                return None  # Рукопожатие успешно завершено
            return None

        try:
            # 0. Фаза рукопожатия
            if method == "initialize":
                return {
                    "jsonrpc": "2.0",
                    "id": req_id,
                    "result": {
                        "protocolVersion": "2025-11-25",
                        "serverInfo": self.server_info,
                        "capabilities": self.capabilities,
                    },
                }

            # 1. Примитив Tools: исполняемые действия
            if method == "tools/list":
                return {
                    "jsonrpc": "2.0",
                    "id": req_id,
                    "result": {
                        "tools": [
                            {
                                "name": "execute_query",
                                "description": "Выполняет SQL-запрос только для чтения (SELECT).",
                                "inputSchema": {
                                    "type": "object",
                                    "properties": {"sql": {"type": "string"}},
                                    "required": ["sql"],
                                },
                            }
                        ]
                    },
                }

            if method == "tools/call":
                tool_name = params.get("name")
                if tool_name != "execute_query":
                    return {
                        "jsonrpc": "2.0",
                        "id": req_id,
                        "result": {
                            "content": [
                                {"type": "text", "text": f"Неизвестный инструмент: {tool_name}"}
                            ],
                            "isError": True,
                        },
                    }

                sql = params.get("arguments", {}).get("sql", "")
                rows = [{"id": 1, "status": "active"}]  # Результат выборки
                return {
                    "jsonrpc": "2.0",
                    "id": req_id,
                    "result": {
                        "content": [
                            {"type": "text", "text": json.dumps(rows, ensure_ascii=False)}
                        ],
                        "isError": False,
                    },
                }

            # 2. Примитив Resources: контекст и данные по URI
            if method == "resources/list":
                return {
                    "jsonrpc": "2.0",
                    "id": req_id,
                    "result": {
                        "resources": [
                            {
                                "uri": "postgres://analytics/schema/users",
                                "name": "DDL схемы таблицы users",
                                "mimeType": "text/x-sql",
                                "description": "Актуальная структура колонок и индексов",
                            }
                        ]
                    },
                }

            if method == "resources/read":
                uri = params.get("uri")
                if uri == "postgres://analytics/schema/users":
                    ddl = "CREATE TABLE users (id SERIAL PRIMARY KEY, status VARCHAR(32));"
                    return {
                        "jsonrpc": "2.0",
                        "id": req_id,
                        "result": {
                            "contents": [{"uri": uri, "mimeType": "text/x-sql", "text": ddl}]
                        },
                    }
                return {
                    "jsonrpc": "2.0",
                    "id": req_id,
                    "error": {"code": -32002, "message": f"Ресурс не найден: {uri}"},
                }

            # 3. Примитив Prompts: шаблоны взаимодействия
            if method == "prompts/list":
                return {
                    "jsonrpc": "2.0",
                    "id": req_id,
                    "result": {
                        "prompts": [
                            {
                                "name": "triage_incident",
                                "description": "Шаблон расследования сбоя по таблицам логов",
                                "arguments": [
                                    {"name": "incident_id", "required": True}
                                ],
                            }
                        ]
                    },
                }

            if method == "prompts/get":
                prompt_name = params.get("name")
                if prompt_name != "triage_incident":
                    return {
                        "jsonrpc": "2.0",
                        "id": req_id,
                        "error": {
                            "code": -32602,
                            "message": f"Неизвестный промпт: {prompt_name}",
                        },
                    }

                incident_id = params.get("arguments", {}).get("incident_id")
                return {
                    "jsonrpc": "2.0",
                    "id": req_id,
                    "result": {
                        "description": f"Анализ инцидента #{incident_id}",
                        "messages": [
                            {
                                "role": "user",
                                "content": {
                                    "type": "text",
                                    "text": f"Проверь последние ошибки в логах для инцидента {incident_id}.",
                                },
                            }
                        ],
                    },
                }

            return {
                "jsonrpc": "2.0",
                "id": req_id,
                "error": {"code": -32601, "message": f"Метод не найден: {method}"},
            }

        except Exception as err:
            return {
                "jsonrpc": "2.0",
                "id": req_id,
                "error": {"code": -32603, "message": str(err)},
            }

Транспортный уровень: stdio против Streamable HTTP

Спецификация определяет два официальных транспорта для передачи JSON-RPC сообщений. Выбор транспорта зависит от архитектуры развертывания:

Транспортный уровень MCP: локальный stdio и Streamable HTTP без сессии

Локальный транспорт (stdio)

  • Механизм: хост-клиент запускает процесс сервера как дочерний подпроцесс (через fork/exec или subprocess.Popen) и общается с ним через стандартные потоки ввода-вывода (stdin/stdout).
  • Формат: поток строк JSON-RPC, разделенных символом перевода строки (\n).
  • Безопасность: авторизация не требуется, так как сервер выполняется локально внутри доверенного контура операционной системы пользователя.
  • Область применения: локальные плагины IDE, утилиты для работы с файловой системой, локальные базы данных SQLite.

Удаленный транспорт (Streamable HTTP)

Исторический транспорт HTTP + SSE (2024 года) требовал создания двух разрозненных эндпоинтов для одной сессии, что создавало проблемы при работе через CDN и балансировщики нагрузки. В актуальной спецификации стандартом стал Streamable HTTP - единый эндпоинт /mcp.

В ревизии 2026-07-28 сетевой транспорт Streamable HTTP оптимизирован для высоконагруженных корпоративных сред (SEP-2243, SEP-2575).

Обязательные маршрутные заголовки: каждый POST-запрос обязан содержать HTTP-заголовки Mcp-Method, Mcp-Name и MCP-Protocol-Version:

POST /mcp HTTP/1.1
Host: mcp.company.internal
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: query_database
Content-Type: application/json
 
{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "query_database",
    "arguments": { "sql": "SELECT 1;" },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28"
    }
  }
}

Благодаря этим заголовкам корпоративные шлюзы, API Gateway, WAF и балансировщики применяют политики безопасности, распределяют нагрузку и считают квоты без парсинга JSON-тела запроса.

  • Работа без состояния: POST /mcp возвращает синхронный JSON либо короткий поток фрагментов. Заголовок Mcp-Session-Id не требуется.
  • Уведомления через subscriptions/listen: клиент отправляет POST /mcp с методом subscriptions/listen и удерживает однонаправленный поток событий.
  • Обработка обрывов связи: в Streamable HTTP нет механизма досылки пропущенных событий (заголовок Last-Event-ID из SSE в спецификации больше не используется). Если соединение разорвалось, клиент просто повторяет запрос или заново открывает поток подписки.

При разработке удаленных MCP-серверов критически важно проверять заголовок Origin по белому списку разрешенных доменов. Без валидации Origin сервер на localhost уязвим к атакам через подмену DNS (DNS-Rebinding) и межсайтовые запросы из браузера пользователя.

Архитектура клиента: агрегация серверов и коллизии имен

В реальных агентных платформах хост подключается не к одному, а к десяткам независимых MCP-серверов одновременно: локальному серверу файловой системы, корпоративному серверу GitLab, серверам Jira, PostgreSQL и мониторинга.

Хост выполняет роль агрегатора (MCP Client Hub):

Агрегация серверов в MCP Client Hub и маршрутизация пространств имен

Изоляция пространств имен (Namespace Prefixing)

Разные серверы часто объявляют инструменты с одинаковыми именами: например, search, read или get_status.

Если клиент передаст их языковой модели без изменений, возникнет конфликт имен, и модель не сможет определить целевую систему. Архитектурное решение - автоматическое добавление префиксов пространств имен на этапе агрегации инструментов:

github__search_repositories
jira__search_issues
slack__search_messages

При поступлении вызова от модели клиент отсекает префикс и направляет запрос tools/call с оригинальным именем search_issues в соответствующий транспортный канал нужного сервера.

PythonКлиент-агрегатор: server/discover, префиксы имен и маршрутизация tools/call
from dataclasses import dataclass
from typing import Any, Callable


@dataclass
class ConnectedServer:
    name: str
    transport_send: Callable[[dict[str, Any]], dict[str, Any] | None]
    capabilities: dict[str, Any]
    tools: list[dict[str, Any]]


class McpClientHub:
    """Клиент-агрегатор: объединяет серверы, изолирует пространства имен и маршрутизирует вызовы."""

    def __init__(self) -> None:
        self.servers: dict[str, ConnectedServer] = {}
        # Единый объединенный реестр: 'server_prefix__tool_name' -> (server_name, original_tool_name)
        self.tool_routing_table: dict[str, tuple[str, str]] = {}
        self._request_id = 0

    def _next_id(self) -> int:
        self._request_id += 1
        return self._request_id

    def connect_server(
        self, server_name: str, transport_send: Callable[[dict[str, Any]], dict[str, Any] | None]
    ) -> None:
        """Рукопожатие initialize, подтверждение и регистрация инструментов с префиксами."""
        # 1. Шаг Handshake: отправка возможностей клиента
        init_response = transport_send({
            "jsonrpc": "2.0",
            "id": self._next_id(),
            "method": "initialize",
            "params": {
                "protocolVersion": "2025-11-25",
                "clientInfo": {"name": "agent-host", "version": "2.0.0"},
                "capabilities": {"roots": {"listChanged": True}, "sampling": {}},
            },
        })

        if not init_response or "error" in init_response:
            raise RuntimeError(f"Сбой инициализации сервера '{server_name}'")

        server_caps = init_response.get("result", {}).get("capabilities", {})

        # 2. Подтверждение готовности клиента (завершение рукопожатия)
        transport_send({
            "jsonrpc": "2.0",
            "method": "notifications/initialized",
        })

        # 3. Получение каталога инструментов
        tools_response = transport_send({
            "jsonrpc": "2.0",
            "id": self._next_id(),
            "method": "tools/list",
            "params": {},
        })
        raw_tools = tools_response.get("result", {}).get("tools", []) if tools_response else []

        # 4. Регистрация в реестре с изоляцией пространств имен (Namespace Prefixing)
        for tool in raw_tools:
            original_name = tool["name"]
            prefixed_name = f"{server_name}__{original_name}"
            self.tool_routing_table[prefixed_name] = (server_name, original_name)

        self.servers[server_name] = ConnectedServer(
            name=server_name,
            transport_send=transport_send,
            capabilities=server_caps,
            tools=raw_tools,
        )

    def get_tools_for_model(self) -> list[dict[str, Any]]:
        """Формирует объединенный список инструментов с префиксами для передачи в контекст LLM."""
        model_tools = []
        for server_name, server in self.servers.items():
            for tool in server.tools:
                prefixed_name = f"{server_name}__{tool['name']}"
                model_tools.append({
                    "name": prefixed_name,
                    "description": f"[{server_name}] {tool.get('description', '')}",
                    "inputSchema": tool.get("inputSchema", {}),
                })
        return model_tools

    def execute_tool(self, namespaced_tool_name: str, arguments: dict[str, Any]) -> str:
        """Маршрутизация вызова целевому серверу по таблице префиксов."""
        if namespaced_tool_name not in self.tool_routing_table:
            raise KeyError(f"Инструмент '{namespaced_tool_name}' не найден в реестре.")

        server_name, original_name = self.tool_routing_table[namespaced_tool_name]
        server = self.servers[server_name]

        # Отправка вызова оригинального инструмента целевому серверу
        call_response = server.transport_send({
            "jsonrpc": "2.0",
            "id": self._next_id(),
            "method": "tools/call",
            "params": {"name": original_name, "arguments": arguments},
        })

        if not call_response or "error" in call_response:
            err_msg = (
                call_response.get("error", {}).get("message", "Неизвестная ошибка")
                if call_response
                else "Нет ответа от сервера"
            )
            raise RuntimeError(f"Сбой протокола MCP: {err_msg}")

        result = call_response.get("result", {})
        contents = result.get("content", [])
        output_text = "\n".join(c.get("text", "") for c in contents if c.get("type") == "text")

        # Обработка флага ошибки выполнения инструмента (isError)
        if result.get("isError"):
            raise RuntimeError(f"Ошибка выполнения инструмента: {output_text}")

        return output_text

Как применять на интервью

На собеседованиях по AI System Design глубокое понимание MCP демонстрирует умение строить открытые, расширяемые и безопасные платформенные интеграции.

Проектирование платформы интеграций (Enterprise AI Hub)

  • Формулировка задачи: спроектировать архитектуру корпоративного ассистента, который должен подключаться к 20 различным внутренним сервисам компании.
  • Пример ответа:
    • Отказ от кастомных адаптеров: вместо написания 20 проприетарных плагинов каждый сервис упаковывается в независимый MCP-сервер.
    • Унифицированный клиент без состояния: агент взаимодействует со всеми сервисами через единый протокол JSON-RPC 2.0. Каждый запрос несет метаданные в _meta, что позволяет масштабировать шлюз горизонтально.
    • Маршрутизация по заголовкам: на шлюзе используются HTTP-заголовки Mcp-Method и Mcp-Name для проверки прав доступа (RBAC) и ограничения частоты вызовов без накладных расходов на парсинг JSON.
    • Масштабируемость экосистемы: добавление нового сервиса требует только регистрации его MCP-эндпоинта без изменения логики агента.

Выбор правильного примитива для базы знаний (Knowledge Base Architecture)

  • Формулировка задачи: как спроектировать подключение корпоративной wiki к агенту - через инструменты поиска или через ресурсы?
  • Пример ответа:
    • Гибридный подход: поиск по базе со сложными фильтрами объявляется как Tool (kb_search(query, tags)), возвращающий список URI подходящих документов.
    • Ресурсы для чтения: каждый документ статьи доступен как Resource по схеме kb://articles/{id}. Хост подгружает полный текст статьи в контекст только при обращении по конкретному URI.

Обработка долгих операций и отмена (Long-running Tool Execution)

  • Формулировка задачи: агент запускает сборку проекта или тяжелую аналитику через MCP-инструмент. Как отображать прогресс пользователю и защитить сервер от утечки ресурсов при отмене запроса?
  • Пример ответа:
    • Индикация прогресса: хост передает progressToken в _meta запроса tools/call. Сервер периодически отправляет notifications/progress с процентом выполнения, который хост транслирует в UI.
    • Мгновенная отмена: при нажатии кнопки отмены пользователем или сработке таймаута хост отправляет notifications/cancelled с requestId. Сервер перехватывает уведомление и немедленно завершает фоновый процесс.
    • Переход к асинхронным задачам: если длительность операции превышает 30 секунд, синхронный инструмент переводится в асинхронную задачу расширения Tasks (io.modelcontextprotocol/tasks) с периодическим опросом состояния через tasks/get.
Войдите чтобы отмечать прогресс