Протоколы и инструменты
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 переносит этот же паттерн на уровень открытого сетевого и межпроцессного протокола.
Протокол создала компания 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).
Эволюция протокола: сессия 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/listtools/call |
| Resources | Пассивные данные, контекст для чтения | Хост / Пользователь | Чтение по URI, кэширование и подписки на изменения | resources/listresources/readsubscriptions/listen |
| Prompts | Шаблоны диалогов и готовые сценарии (SOP) | Пользователь (интерактивно) | Заполнение аргументов шаблона, возврат сообщений диалога | prompts/listprompts/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.
Рассмотрим реализацию сервера, объединяющего все три примитива:
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 сообщений. Выбор транспорта зависит от архитектуры развертывания:
Локальный транспорт (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):
Изоляция пространств имен (Namespace Prefixing)
Разные серверы часто объявляют инструменты с одинаковыми именами: например,
search, read или get_status.
Если клиент передаст их языковой модели без изменений, возникнет конфликт имен, и модель не сможет определить целевую систему. Архитектурное решение - автоматическое добавление префиксов пространств имен на этапе агрегации инструментов:
github__search_repositories
jira__search_issues
slack__search_messages
При поступлении вызова от модели клиент отсекает префикс и направляет запрос
tools/call с оригинальным именем search_issues в соответствующий
транспортный канал нужного сервера.
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.
- Гибридный подход: поиск по базе со сложными фильтрами объявляется как
Tool (
Обработка долгих операций и отмена (Long-running Tool Execution)
- Формулировка задачи: агент запускает сборку проекта или тяжелую аналитику через MCP-инструмент. Как отображать прогресс пользователю и защитить сервер от утечки ресурсов при отмене запроса?
- Пример ответа:
- Индикация прогресса: хост передает
progressTokenв_metaзапросаtools/call. Сервер периодически отправляетnotifications/progressс процентом выполнения, который хост транслирует в UI. - Мгновенная отмена: при нажатии кнопки отмены пользователем или сработке
таймаута хост отправляет
notifications/cancelledсrequestId. Сервер перехватывает уведомление и немедленно завершает фоновый процесс. - Переход к асинхронным задачам: если длительность операции превышает 30
секунд, синхронный инструмент переводится в асинхронную задачу расширения
Tasks (
io.modelcontextprotocol/tasks) с периодическим опросом состояния черезtasks/get.
- Индикация прогресса: хост передает