MCP: контракт между приложением и поставщиками контекста
Function calling и Model Context Protocol решают разные задачи.
- Function calling — интерфейс модели: приложение передаёт модели схемы функций, модель предлагает имя и аргументы, приложение решает, выполнять ли вызов.
- MCP — протокол интеграции: приложение обнаруживает возможности внешнего процесса или сервиса и вызывает их по JSON-RPC. MCP не обязывает использовать LLM и не передаёт серверу право исполнять любой предложенный моделью вызов.
Обычно host переводит найденные MCP tools в формат function calling конкретного провайдера. Затем он валидирует предложенные аргументы, применяет policy и только тогда вызывает MCP server.
Host, client, server
Host — пользовательское приложение: IDE, агент или desktop-клиент. Он управляет согласием пользователя, моделью, политиками и общим контекстом. MCP client — соединение внутри host с одним сервером. MCP server публикует ограниченный набор возможностей: локальная программа через stdio или удалённый сервис через HTTP.
Связь строится как host → MCP client ↔ MCP server. Один host держит несколько клиентов, обычно по одному на сервер. Поэтому падение Git-сервера не должно ронять файловый сервер или весь агент: отдельные соединения, тайм-ауты, лимиты конкурентности, circuit breaker и отмена запросов нужны на границе каждого клиента.
Сначала handshake, потом работа
В версии спецификации 2025-06-18 жизненный цикл начинается с initialize:
- client отправляет поддерживаемую версию протокола, свои capabilities и сведения о реализации;
- server отвечает согласованной версией, своими capabilities, сведениями о реализации и, при наличии,
instructions; - client отправляет notification
notifications/initialized; - только после этого стороны используют согласованные возможности;
- при завершении host закрывает транспорт; для локального процесса — сначала
stdin, затем процесс с разумным grace period.
Capabilities — не декоративное описание. Они включают только реально поддерживаемые функции и дополнительные режимы, например уведомление об изменении списка tools. Нельзя вызывать метод лишь потому, что он известен host: сначала проверить capability. Версию также нельзя молча считать своей — нужен результат negotiation или отказ при несовместимости.
Три серверные примитивы
- Tools — действия, которые может выбрать модель:
tools/list,tools/call. У tool есть имя, описание иinputSchema; результат остаётся недоверенным вводом. - Resources — адресуемые данные для контекста:
resources/list,resources/read, URI и MIME type. Это чтение контента, а не команда на действие. - Prompts — серверные шаблоны сообщений:
prompts/list,prompts/get. Их обычно явно выбирает пользователь, а не автономно запускает модель.
Не сводите всё к tools. Файл справки лучше отдать resource, повторяемый сценарий — prompt, изменение внешнего состояния — tool. Так видны разные права и точки подтверждения.
Транспорт: local и remote
Для локальной интеграции используйте stdio: host запускает server как дочерний процесс, JSON-RPC-сообщения идут через stdin/stdout, диагностические логи — через stderr. Любой print с отладкой в stdout ломает framing.
Для удалённой интеграции актуальный основной транспорт — Streamable HTTP. У сервера один MCP endpoint, client отправляет JSON-RPC по HTTP POST, а ответы получает как application/json либо поток text/event-stream; отдельный GET может открыть серверный SSE-поток. Session ID, если сервер его выдал, передаётся в следующих запросах. Проверяйте Origin, а локальные серверы привязывайте к loopback и защищайте аутентификацией. Старый транспорт HTTP+SSE с двумя endpoint нужен лишь для совместимости, не как выбор для новой интеграции.
Discovery выполняется в runtime
После handshake client запрашивает tools/list, resources/list и prompts/list только для объявленных capabilities, проходит пагинацию по cursor и обновляет каталог по соответствующим notifications/*/list_changed. Кэш должен принадлежать конкретным server identity, session и версии конфигурации.
Имена tools уникальны только внутри сервера. Во внутреннем каталоге host добавляет namespace, например github.create_issue и warehouse.create_issue, даже если оба сервера объявили create_issue. Перед tools/call namespace снимается. Не позволяйте поздно подключённому серверу затереть прежнее имя.
Минимальный маршрут вызова:
list → namespace → показать модели → получить предложение
→ проверить server/tool allowlist и JSON Schema
→ при необходимости спросить пользователя
→ tools/call с timeout → проверить и очистить результатПроверяйте сервер без модели
Модель делает интеграционный тест недетерминированным и скрывает ошибки адаптера. Contract test должен напрямую поднять client и проверить handshake, discovery, схему и вызов:
async def test_weather_server(mcp_client):
await mcp_client.initialize(protocol_version="2025-06-18")
tools = await mcp_client.list_tools()
weather = next(t for t in tools if t.name == "get_weather")
assert weather.inputSchema["required"] == ["city"]
result = await mcp_client.call_tool(
"get_weather", {"city": "Казань"}
)
assert result.isError is False
assert result.contentВ отдельные тесты вынесите несовместимую версию, отсутствующую capability, пагинацию, list_changed, невалидные аргументы, timeout, аварийное завершение процесса и недоступный HTTP server. После этого нужен один сквозной тест с моделью — он проверяет выбор tool, но не заменяет контрактный набор.
Правило: function calling отвечает за предложение модели, MCP — за обнаружение и вызов интеграции; host остаётся местом авторизации, валидации и изоляции отказов.