XNOVA
← Все статьи

СТАТЬЯ / engineering

Что такое MCP: как AI-приложения подключаются к инструментам и данным

Разбираем архитектуру MCP, обнаружение и вызов инструментов, версии протокола и небольшой пример на Python без LLM API.

Автор: XNova

В AI-приложении нужно находить заказы во внутренней системе, читать файлы проекта и создавать задачи в GitHub. Для каждой операции уже может существовать API, но приложению ещё предстоит объяснить модели, какие действия доступны, как передавать аргументы и кому разрешено их выполнять.

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

Model Context Protocol (MCP) определяет общий интерфейс между AI-приложением и сервисом, который предоставляет инструменты или данные. Через него клиент может узнать о доступных операциях, получить схемы их аргументов, вызвать нужную операцию и прочитать результат. Сервер при этом сам решает, как обратиться к GitHub, базе или локальному файлу.

Например, сервер заказов предоставляет get_order(order_id). Одно приложение использует этот инструмент для ответа покупателю, другое — для внутренней диагностики. Оба узнают его имя и входную схему через MCP. Реализация поиска заказа остаётся общей, но каждому приложению по-прежнему нужны собственные правила доступа и обработки ответа.

MCP стандартизирует протокольный обмен; он не предоставляет бизнес-логику, права доступа к заказам или универсальную защиту от небезопасных действий. Далее проследим один вызов от приложения до данных, посмотрим на формат сообщений и реализуем тот же сценарий на Python.

Разбор относится к спецификации MCP от 28 июля 2026 года. В ней существенно изменился начальный обмен сообщениями; отличия от редакций 2025 года обозначим отдельно.

Компоненты: Host, Client и Server

Границы MCP Host, Client, Server и локального источника заказов

Рассмотрим путь запроса «найдите заказ ORD-101».

  • MCP Host — приложение, в котором работает пользователь: например, редактор кода или AI-чат. Host управляет подключениями, взаимодействием с моделью, набором доступных инструментов и пользовательскими разрешениями.
  • MCP Client — компонент host, реализующий протокол. Для каждого MCP-сервера host создаёт отдельный клиент. Клиент отправляет запросы и получает ответы; один клиент связан с одним сервером, а host может работать сразу с несколькими серверами.
  • MCP Server — локальная программа или удалённый сервис. Он объявляет возможности, принимает запросы и выполняет конкретную логику: в нашем примере ищет заказ и вычисляет сумму.
  • Источник данных — orders.json, база данных или внешний HTTP API. MCP-сервер обращается к нему обычным способом; источник не обязан поддерживать MCP.

Граница протокола проходит между Client и Server. Стрелка Host → Client на схеме обозначает внутреннее управление приложением, а Server → orders.json — работу с данными. Это помогает понять, где искать ошибку: неправильный выбор инструмента происходит в host, несовместимые протокольные сообщения — на границе client/server, неверная сумма — в логике сервера.

Подключение организует клиент под управлением host. В локальном сценарии клиент запускает процесс сервера и обменивается сообщениями через stdin/stdout; в сетевом — обращается к MCP endpoint по HTTP. Сервер предоставляет описания своих возможностей, клиент запрашивает их через протокольные методы. Наличие инструмента в списке ещё не означает, что пользователю следует разрешить его вызов.

Host контролирует и объём передаваемого контекста: к серверу уходит конкретный запрос с аргументами, а не автоматически вся история разговора и данные других серверов. Сам сервер, конечно, имеет доступ к тем ресурсам и учётным данным, которые ему предоставлены. Поэтому изоляция зависит не только от протокола, но и от конфигурации приложения и прав процесса.

Эти отношения закреплены в официальной архитектуре MCP.

Что предоставляет сервер: Tools, Resources, Prompts

Сервер объявляет поддерживаемые возможности (capabilities). Для нашего примера достаточно tools, но MCP также описывает resources и prompts. Их удобнее различать по действию клиента:

ВозможностьЧто получает клиентПример в системе заказовМетоды
ToolВыполняемую операцию с аргументамиПосчитать сумму ORD-101tools/list, tools/call
ResourceСодержимое, доступное по URIСправку о статусах orders://status-guideresources/list, resources/read
PromptГотовый шаблон сообщений, возможно с параметрамиШаблон разбора спорного заказаprompts/list, prompts/get

Tool подходит, когда требуется действие или вычисление. Сервер публикует имя, описание и inputSchema — JSON Schema аргументов. Например, клиент узнаёт, что get_order принимает обязательную строку order_id. Вызов передаёт значение ORD-101; поиск выполняется уже в коде сервера. Инструмент может менять данные, поэтому права и подтверждения зависят от характера операции.

Resource предоставляет данные, которые приложение может прочитать и добавить в контекст модели: справку, документ, содержимое файла. URI адресует материал, а resources/read получает его. Для адресов с параметрами спецификация предусматривает шаблоны ресурсов. В отличие от вызова calculate_order_total, чтение справки не просит сервер произвести этот расчёт.

Prompt описывает повторяемый сценарий общения: например, начальные сообщения для анализа расхождений в заказе. prompts/get возвращает шаблон с подставленными параметрами, после чего host может передать сообщения модели. Само получение prompt не запускает модель.

Представьте последовательность работы пользователя: сначала приложение читает resource со значениями статусов, затем вызывает tool для получения заказа, а для объяснения расхождения выбирает prompt. Это три разных интерфейса одного сервера — если сервер действительно их реализует. Наш Python-пример ниже реализует только два tools; показанные resource и prompt служат иллюстрацией возможностей спецификации, а не обещанием функций демонстрационного сервера.

Подробные требования: tools, resources и prompts.

Как клиент обнаруживает и вызывает инструмент

MCP передаёт сообщения в формате JSON-RPC 2.0. В запросе есть jsonrpc: "2.0", уникальный для незавершённых запросов id и метод method. Ответ возвращает тот же id и поле result либо error. Это обмен между MCP-клиентом и сервером; обращение пользователя к AI-модели происходит вне этой границы.

Последовательность обнаружения и вызова инструмента

Проследим запрос стоимости заказа.

  1. Версия и возможности. В редакции 2026-07-28 сервер обязан поддерживать метод server/discover, а клиент может вызвать его заранее, чтобы узнать поддерживаемые версии и capabilities. Если клиент сразу послал другой метод с неподдерживаемой версией, сервер обязан вернуть UnsupportedProtocolVersionError (-32022) со списком поддерживаемых версий. Это другой этап, чем получение списка инструментов.
  2. Обнаружение tools. Клиент вызывает tools/list у сервера, объявившего capability tools. Он получает имена, описания и inputSchema — схемы аргументов. Например, для get_order схема требует строку order_id. Каталог может иметь несколько страниц и меняться в зависимости от авторизации запроса.
  3. Выбор и разрешение. Host соотносит пользовательскую задачу с описанием инструмента. Если выбор предлагает модель, host всё равно отвечает за проверку разрешений и подтверждение чувствительных действий.
  4. Вызов. Клиент отправляет tools/call с name: "get_order" и arguments: {"order_id": "ORD-101"}. Сервер проверяет аргументы, обращается к источнику данных и формирует результат.
  5. Ответ. Сервер возвращает данные в content и, если доступен структурированный результат, в structuredContent. Поле isError сообщает об ошибке выполнения инструмента. Host решает, что из результата передать модели или показать пользователю.

Так может выглядеть иллюстративный JSON-RPC-запрос по редакции 2026-07-28 в теле сообщения:

{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "tools/call",
  "params": {
    "name": "get_order",
    "arguments": {"order_id": "ORD-101"},
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

Здесь показаны обязательные поля _meta: версия протокола и возможности клиента. Его идентификатор io.modelcontextprotocol/clientInfo имеет уровень SHOULD — его рекомендуется передавать, но отсутствие не делает запрос недопустимым. В Streamable HTTP потребуются ещё обязательные HTTP-заголовки, в том числе MCP-Protocol-Version, совпадающий с версией в теле. Сообщение выше — пример формы запроса, а не сохранённый дамп выполнения Python SDK.

Результат в современной редакции использует resultType: complete означает завершение, input_required — необходимость получить дополнительные сведения и повторить запрос по правилам multi round-trip requests. Для нашего read-only инструмента предполагается обычное завершение complete; сценарий дополнительного ввода он не реализует. В прежних редакциях поля resultType не было; совместимый клиент учитывает это при разборе ответов.

Что означает initialize в старых примерах

В версиях до 2025-11-25 включительно клиент начинал работу с initialize, получал согласованную версию и возможности, а затем отправлял notifications/initialized. В редакции 2026-07-28 обязательной инициализации и протокольной сессии больше нет: каждый запрос несёт необходимые метаданные. При поддержке обоих поколений SDK способен переключаться между этими режимами.

У официального Python SDK v2 высокоуровневый Client по умолчанию работает с mode="auto": сначала пробует server/discover, а при подключении к старому серверу переходит к initialize. Поэтому код из прежних примеров с ClientSession.initialize() нельзя автоматически переносить в новый высокоуровневый Client и считать обязательным шагом протокола.

Две категории ошибок

Протокольная ошибка JSON-RPC означает, что запрос не удалось корректно обработать на уровне протокола: например, неизвестно имя инструмента или неверна форма сообщения. Ответ содержит error с кодом и сообщением.

Ошибка выполнения инструмента означает, что операция не дала нужного результата: заказ не найден, внешний API отказал, нарушено бизнес-правило или обработчик отклонил аргументы. Тогда сервер может вернуть результат с isError: true, чтобы host мог объяснить причину и при необходимости предложить корректный повтор.

Здесь есть отличие спецификации от реализации: спецификация относит неизвестное имя tool к протокольным ошибкам, а высокоуровневый MCPServer официального Python SDK v2 обрабатывает его через ToolError и может вернуть клиенту обычный результат с is_error=True. Это описано в документации об ошибках Python SDK. Поэтому в тестах ориентируйтесь на фактический контракт используемой библиотеки; не считайте каждый is_error JSON-RPC-ошибкой.

Описание полей, resultType и ошибок дано в базовом протоколе и разделе Tools.

stdio и Streamable HTTP

MCP определяет сообщения независимо от канала доставки. Клиент может отправить одинаковый по смыслу tools/call локальному процессу или удалённому сервису; различаются запуск, обрамление сообщений и средства защиты.

stdio используется в нашем примере. Host запускает сервер дочерним процессом; клиент пишет JSON-RPC-сообщения в stdin и читает ответы из stdout. Каждое сообщение — одна строка UTF-8. Отладочные сообщения сервер пишет в stderr: произвольный print() в stdout нарушит протокольный поток. Открывать HTTP-порт не нужно, но сервер получает права запущенного процесса и доступ к предоставленным файлам.

Streamable HTTP используется для обращения к HTTP-службе. Клиент отправляет каждое MCP-сообщение отдельным POST на единый endpoint, например /mcp. Для запроса сервер возвращает либо JSON-ответ, либо SSE-поток, связанный именно с этим запросом. Клиент должен поддерживать обе формы ответа. Запрос требует Accept с application/json и text/event-stream, MCP-Protocol-Version и Mcp-Method. Для tools/call нужен также Mcp-Name. Версия и значения заголовков метода и имени должны совпадать с соответствующими полями в теле.

В редакции 2026-07-28 удалены прежние протокольные сессии с Mcp-Session-Id и отдельный GET-поток. Если клиенту нужны уведомления об изменениях, он может открыть subscriptions/listen; это отдельный долгоживущий запрос с потоком ответа. SSE остаётся способом передачи событий внутри Streamable HTTP, но не самостоятельным основным транспортом. Старый HTTP+SSE — транспорт прежних редакций для совместимости.

ВопросstdioStreamable HTTP
Где работает серверОбычно локально как дочерний процессЛокально или на удалённом HTTP-узле
Как доставляется сообщениеСтрока JSON-RPC по stdin/stdoutОтдельный POST, JSON-ответ или SSE-поток
Что защищатьПрава процесса, файлы, запуск командHTTP endpoint, авторизацию, сетевой доступ и TLS
Что означает tools/callВызов инструментаТот же вызов инструмента

При реализации Streamable HTTP сервер MUST проверять Origin, если заголовок присутствует; недопустимый Origin требует ответа 403. Для локального сервера спецификация SHOULD рекомендует привязку к 127.0.0.1. Аутентификацию, полномочия на операции и защиту данных нужно проектировать отдельно — наличие HTTPS не означает, что пользователь имеет право на конкретный tool.

Подробнее: транспорты, stdio и Streamable HTTP.

Практика: два инструмента для локальных заказов

Теперь реализуем цепочку со схемы: Python-клиент создаёт MCP-соединение, узнаёт имена tools, затем читает заказ и вычисляет его сумму. Сервер берёт данные из orders.json. Для запуска нужен Python 3.11+ и официальный SDK mcp==2.3.0; модель, внешний API и секреты не используются.

Ниже четыре файла. Сохраните их в одной папке или возьмите готовыми из репозитория с примером. Там же находится test_orders.py для проверки успешных и ошибочных вызовов. Пример позволяет проверить описанную логику независимо от AI-приложения. Версию пакета и API сверяйте с документацией Python SDK и клиентским API.

Зависимости, requirements.txt:

mcp==2.3.0
pydantic==2.13.5

Данные, orders.json:

[
  {"id":"ORD-101","items":[
    {"sku":"KB-01","quantity":2,"unit_price":120},
    {"sku":"MS-02","quantity":1,"unit_price":140}
  ]},
  {"id":"ORD-102","items":[
    {"sku":"MON-03","quantity":1,"unit_price":900}
  ]}
]

У заказа ORD-101 две позиции: 2 × 120 + 1 × 140 = 380. Это целочисленные условные единицы; здесь нет налогов, скидок, округления или платёжных операций.

MCP-сервер, server.py:

"""Local read-only orders MCP server (mcp==2.3.0)."""
import json
from pathlib import Path

from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from pydantic import BaseModel, Field


DATA = Path(__file__).with_name("orders.json")
mcp = MCPServer("LocalOrders")


class LineItem(BaseModel):
    sku: str
    quantity: int = Field(gt=0)
    unit_price: int = Field(ge=0)


class Order(BaseModel):
    id: str
    items: list[LineItem]


class Total(BaseModel):
    order_id: str
    total: int


def find_order(order_id: str) -> Order:
    """Only look up records in the fixed demonstration catalog."""
    if not order_id.startswith("ORD-") or len(order_id) > 32:
        raise ToolError("Invalid order ID")
    orders = json.loads(DATA.read_text(encoding="utf-8"))
    for record in orders:
        if record["id"] == order_id:
            return Order.model_validate(record)
    raise ToolError("Order not found")


@mcp.tool()
def get_order(order_id: str) -> Order:
    """Return an order from the local catalog."""
    return find_order(order_id)


@mcp.tool()
def calculate_order_total(order_id: str) -> Total:
    """Calculate the total of order line items."""
    order = find_order(order_id)
    return Total(
        order_id=order.id,
        total=sum(item.quantity * item.unit_price for item in order.items),
    )


if __name__ == "__main__":
    mcp.run()

Декораторы @mcp.tool() делают две Python-функции доступными как MCP-инструменты. Из аргумента order_id: str SDK формирует входную схему, а модели Pydantic Order и Total описывают результаты. Функция find_order отвечает за бизнес-логику: читает фиксированный JSON, проверяет формат ID и возвращает запись. Ошибка ToolError сообщает, почему операция не выполнена. Это поведение конкретного SDK и нашего обработчика; сама спецификация не предписывает названия Python-классов.

mcp.run() запускает stdio-сервер. Его не нужно запускать в другом терминале: следующий скрипт создаёт процесс автоматически.

MCP-клиент, client.py:

"""List and call both MCP tools using a local stdio process."""
import asyncio
import sys
from pathlib import Path

from mcp import Client, StdioServerParameters


SERVER = Path(__file__).with_name("server.py")


async def run() -> None:
    parameters = StdioServerParameters(command=sys.executable, args=[str(SERVER)])
    async with Client(parameters) as client:
        response = await client.list_tools()
        names = {tool.name for tool in response.tools}
        assert names == {"get_order", "calculate_order_total"}, names
        print("tools:", ", ".join(sorted(names)))

        order = await client.call_tool("get_order", {"order_id": "ORD-101"})
        assert not order.is_error, order.content
        assert order.structured_content is not None
        assert order.structured_content["id"] == "ORD-101"
        assert len(order.structured_content["items"]) == 2
        print("order: ORD-101, items: 2")

        total = await client.call_tool(
            "calculate_order_total", {"order_id": "ORD-101"}
        )
        assert not total.is_error, total.content
        assert total.structured_content is not None
        assert total.structured_content["total"] == 380
        print("total: 380")


if __name__ == "__main__":
    asyncio.run(run())

Клиент использует StdioServerParameters для запуска server.py, затем вызывает list_tools() и call_tool(). Здесь инструменты выбирает обычный Python-код, а не модель. Высокоуровневый Client в стандартном режиме auto согласует поддерживаемую редакцию протокола; для нашего современного сервера это 2026-07-28. structured_content — атрибут ответа Python SDK; на уровне JSON-RPC соответствующее поле называется structuredContent.

Запуск в каталоге с этими файлами:

python -m venv .venv
# Linux/macOS
source .venv/bin/activate
python -m pip install -r requirements.txt
python client.py

В Windows PowerShell для активации используйте .\\.venv\\Scripts\\Activate.ps1, в cmd — .venv\\Scripts\\activate.bat. Команды установки и запуска после активации те же.

Проверьте, что клиент обнаружил именно get_order и calculate_order_total, получил заказ ORD-101 с двумя позициями и проверил сумму 380. Проверки assert останавливают скрипт при расхождении. Это ожидаемое поведение по коду, а не приписанный материалу результат тестового запуска.

Чтобы увидеть прикладную ошибку, замените в клиентском вызове get_order идентификатор ORD-101 на ORD-999 и снова запустите скрипт. Сервер не найдёт запись, вернёт результат с is_error, а клиент остановится на assert not order.is_error. После эксперимента верните исходный ID. В проекте предусмотрен отдельный test_orders.py, который также проверяет оба заказа и отрицательные сценарии.

Если клиент не стартует, проверьте версию Python и установку пакетов в активном окружении. Если список tools отличается от ожидаемого — проверьте наличие обоих декораторов и путь к серверу. Для stdio особенно важно не печатать произвольный текст в stdout сервера.

В демонстрации нет разграничения доступа к реальным заказам. Она показывает, за что отвечает протокол и за что — код сервера, не более.

MCP, обычный API и LLM tool calling

Эти механизмы работают на разных границах. Путаница возникает, когда одно слово «инструмент» используют и для функции, которую видит модель, и для удалённого сервиса, который эту функцию выполняет.

МеханизмМежду кем действует контрактЧто он описывает
HTTP APIКлиент приложения ↔ конкретный backendМаршруты, методы, авторизацию и предметный формат данных
Tool calling LLM-провайдераПриложение ↔ модельКак предоставить модели описание функции и получить предложенные имя и аргументы
MCPMCP Client ↔ MCP ServerКак обнаружить инструменты и данные, передать аргументы, получить результат и согласовать протокольные возможности

Представьте заказ ORD-101. Обычный backend может возвращать его по GET /orders/ORD-101. В приложении с tool calling разработчик описывает модели функцию get_order(order_id), а затем сам обрабатывает предложенный вызов. Если подключить MCP, эта функция обнаруживается на MCP-сервере: приложение получает её описание через tools/list, вызывает tools/call, а сервер уже обращается к исходному API. API остаётся на месте — появляется стандартизованный способ подключить его к поддерживающим MCP приложениям.

Модель предлагает get_order("ORD-101")
        ↓ решение и разрешение в Host
MCP Client → tools/call → MCP Server → HTTP API / SQL / JSON
        ← результат вызова ← данные заказа

В демонстрационном Python-клиенте модели вообще нет: вызовы записаны в коде. Это подтверждает разделение функций. Tool calling не обязателен для MCP, а MCP не обязателен для tool calling: приложение может напрямую вызывать HTTP API после ответа модели.

Когда выбрать прямой API. Если ваш backend делает несколько заранее известных запросов к одной системе и нет потребности подключать их к другим AI-host, отдельный MCP-сервер добавит обслуживание, версионирование и ещё одну точку отказа.

Когда MCP оправдан. Если одна интеграция нужна нескольким AI-приложениям, клиентам требуется обнаруживать доступные операции, а доступ к источнику удобно вынести за общую серверную границу, MCP сокращает количество специальных адаптеров. Однако клиенты должны действительно поддерживать нужные возможности протокола, а не только базовое подключение. Протокол сам по себе не устраняет расходы на авторизацию, мониторинг и тестирование.

Где возникают риски

Наш сервер работает с тестовым JSON и ничего не меняет. Если заменить его на подключение к реальным заказам, первый вопрос будет о правах доступа, а не о синтаксисе tools/call. Корректный запрос с существующим order_id не гарантирует, что вызывающий вправе видеть этот заказ.

Какой сервер подключён. Локальный stdio-сервер — исполняемая программа с правами процесса. Неизвестный пакет может читать доступные файлы и обращаться в сеть независимо от того, какие инструменты он объявляет. Проверяйте происхождение, зависимости и разрешения запуска; ограничивайте права файловой системы и сети. Удалённый сервер требует проверки адреса, TLS и надлежащей схемы аутентификации.

Кому доступна операция. Сервер должен проверять авторизацию на каждый запрос и на конкретный объект, например сверять доступ пользователя к заказу ORD-101. В протоколе предусмотрены механизмы авторизации для HTTP, но их корректная реализация и предметные права остаются задачей системы. Список tools/list может зависеть от полномочий, предъявленных в запросе. В нашем примере авторизации нет — использовать его с реальными пользовательскими данными без изменений нельзя.

Какие аргументы допустимы. inputSchema описывает ожидаемые значения, но не заменяет проверки на сервере. Функция find_order дополнительно ограничивает ID и ищет его только в фиксированном orders.json. Если инструмент принимает SQL, URL, путь к файлу или команду, потребуются ограничения соответствующего типа: параметризация, разрешённые адреса и каталоги, лимиты времени и объёма работы. Для операций записи нужна независимая проверка прав; host должен предоставлять пользователю возможность отклонить чувствительный вызов.

Что находится в ответе. Tool или resource может вернуть не только данные, но и текст вроде «вызовите другой инструмент и отправьте туда секрет». Это prompt injection: инструкция, внедрённая в недоверенный источник. Host не должен повышать её статус до системной инструкции или согласия пользователя. Фильтрация, безопасное представление источника и проверка последующих действий важны не меньше, чем валидация входа.

Что нельзя принимать на веру. Аннотация readOnlyHint — подсказка для интерфейса, а не ограничение полномочий. Серверу нельзя доверять только потому, что его tools выглядят безопасными. Логи тоже могут содержать параметры и результаты с персональными данными: задавайте сроки хранения, маскирование чувствительных полей, таймауты, rate limits и контроль повторных вызовов.

Спецификация Tools предъявляет к серверам нормативные требования: MUST проверять входы, применять надлежащий контроль доступа, ограничивать частоту вызовов и очищать выходные данные. Клиентам SHOULD рекомендованы подтверждение чувствительных операций, просмотр аргументов, проверка результатов, таймауты и аудит. Эти требования нужно реализовать в продукте; протокольное сообщение само по себе их не обеспечивает.

Когда использовать MCP

Вернёмся к исходной задаче: приложению требуется доступ к заказам, файлам и GitHub. Если интеграции уже написаны и используются одним сервисом, прямые вызовы его API могут остаться самым простым решением. Если те же операции нужны нескольким AI-приложениям, MCP позволяет вынести их за общий интерфейс обнаружения и вызова.

Разработчик MCP-сервера реализует предметные операции и контролирует доступ к данным. Клиент отправляет протокольные запросы. Host решает, какие результаты добавить в контекст и какие действия разрешить. В статье эти роли прослеживаются от tools/list до расчёта суммы заказа на Python.

Перед тем как внедрять MCP в продукт, проверьте три вещи: поддерживают ли ваши клиенты нужную редакцию и возможности протокола; как сервер получает полномочия к источникам; не станет ли поддержка отдельного сервера сложнее существующей прямой интеграции. Успешный вызов инструмента подтверждает прохождение протокольного обмена, но корректность расчёта и право на доступ проверяются отдельно.

Источники