MCP (Model Context Protocol) — открытый стандарт, разработанный Anthropic, который позволяет AI-ассистентам взаимодействовать с внешними системами. При этом, конечно, интеграция с такими системами создает дополнительные риски безопасности.

С помощью MCP возможно расширить возможности AI-ассистента для выполнения необходимых задач за счёт подключения инструментов и источников данных. Таким образом, ассистент получает возможность самостоятельно выполнять поставленные задачи. Системы искусственного интеллекта с такими возможностями называют агентными (или ИИ-агентами).

Агентные системы уже используются повсеместно:
▸ Агент с доступом к Git проводит ревью кода в Pull Request
▸ При наличии доступа к календарю система может сама предлагать время и назначать встречи на свободные слоты
▸ Многие агенты для программирования умеют работать с bash и файловой системой, что позволяет им почти автономно разрабатывать продукт: человек лишь вносит правки и может не писать код вообще

Основные архитектурные компоненты MCP:

▸ MCP Хост — непосредственно AI-приложение, которое управляет MCP клиентами и определяет, к каким MCP серверам и примитивам обращаться для выполнения задачи
▸ MCP Клиент — компонент, который подключается к MCP серверу и обеспечивает обмен сообщениями между хостом и сервером
▸ MCP Сервер — сервер-прослойка между MCP клиентом и внешними системами: предоставляет стандартизированный интерфейс для доступа к примитивам (инструментам, ресурсам и готовым промптам)

Примитивы (primitives) сервера MCP:
▸ Инструменты (tools) — исполняемые функции, которые может вызывать AI-приложение
▸ Ресурсы (resources) — источники данных/контекста
▸ Промпты (prompts) — заранее подготовленные шаблоны подсказок или инструкций (для типовых сценариев)

Архитектура MCP
Архитектура MCP

Взаимодействие MCP клиента и MCP сервера на уровне сети

Общение между клиентом и сервером сводится к обмену сообщениями в формате JSON-RPC 2.0, которые могут передаваться с помощью различных транспортов (например, HTTP или stdio). С их помощью клиент запрашивает доступные примитивы MCP сервера, а затем обращается к ним (например, вызывает инструменты) и получает ответы.

MCP определяет методы работы с примитивами:
▸ Получение списка доступных примитивов (discovery) — */list
▸ Получение конкретного ресурса/промпта (retrieval) — */get или */read
▸ Вызов инструмента (execution) — tools/call

Источник: Architecture overview - Model Context Protocol

Практическая реализация с помощью FastMCP

Для демонстрации работы протокола MCP поднимем сервер и напишем клиент с использованием Python-библиотеки FastMCP.

Файл server.py:

from datetime import datetime
from fastmcp import FastMCP

mcp = FastMCP("Demo MCP Server")

# Tools


@mcp.tool(name="get_current_time")
def get_server_time() -> str:
    """Return server time in ISO format"""

    return datetime.now().isoformat()


@mcp.tool
def add_two_integers(a: int, b: int) -> int:
    """Add two integers"""
    return a + b

# Resources


@mcp.resource("docs://about")
def about() -> str:
    """Basic info about this MCP server"""
    return "This is a demo MCP server built with FastMCP"

# Prompts


@mcp.prompt
def security_audit_prompt(code: str) -> str:
    """Prompt template for a quick security audit."""
    return f"""You are a security engineer. 
Perform a short security audit for the following code: {code}

Output format:
1) Vulnerable functions
2) How to fix the vulnerabilities
3) Recommendation on how to avoid such vulnerabilities
"""

if __name__ == "__main__":
    mcp.run(transport="http", host="127.0.0.1", port=8888, path="/mcp")

В FastMCP примитивы MCP-сервера задаются с помощью декораторов @mcp.tool, @mcp.resource и @mcp.prompt. Для каждого примитива можно явно указать имя (name) — именно по нему клиент и будет обращаться.

В данном примере сервер запускается на порту 8888, использует HTTP в качестве транспорта и принимает запросы от MCP клиента на эндпоинте /mcp.

Файл client.py:

import asyncio
from fastmcp import Client


async def main() -> None:
    async with Client("http://127.0.0.1:8888/mcp") as client:
        tools = await client.list_tools()
        print("TOOLS:", [t.name for t in tools])

        add_result = await client.call_tool("add_two_integers", {"a": 13, "b": 37})
        print("add_two_integers(13,37) =", add_result.data)

        resources = await client.list_resources()
        print("\nRESOURCES:", [r.uri for r in resources])

        security_content = await client.read_resource("docs://about")
        print("docs://about:", security_content[0].text)

        prompts = await client.list_prompts()
        print("\nPROMPTS:", [p.name for p in prompts])

        rendered = await client.get_prompt(
            "security_audit_prompt", {"code": "print('Hello world!')"}
        )
        print("\nRendered prompt message:")
        print(rendered.messages[0].content.text)


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

MCP клиент обращается к серверу по адресу http://127.0.0.1:8888/mcp, а затем последовательно выполняет следующие операции:
▸ Запрашивает список примитивов необходимого типа */list
▸ Затем выполняет одно из действий: вызывает инструмент tools/call, читает ресурс resources/read или получает промпт prompts/get

Запускаем сервер:

python ./server.py

И запускаем клиент:

python ./client.py

Получаем вывод на клиенте:

TOOLS: ['get_current_time', 'add_two_integers']
add_two_integers(13,37) = 50

RESOURCES: [AnyUrl('docs://about')]
docs://about: This is a demo MCP server built with FastMCP

PROMPTS: ['security_audit_prompt']

Rendered prompt message:
You are a security engineer. 
Perform a short security audit for the following code: print('Hello world!')

Output format:
1) Vulnerable functions
2) How to fix the vulnerabilities
3) Recommendation on how to avoid such vulnerabilities

Сетевое взаимодействие MCP клиента и сервера (FastMCP)

Теперь рассмотрим, как выглядит общение MCP Client и Server по сети. Для анализа трафика можно использовать Wireshark либо следующую модификацию для серверной части. Эта модификация расширяет логирование uvicorn-сервера, позволяя выводить заголовки и тело HTTP-запросов и ответов.

Файл server_with_logs.py:

class ExtendedLogs:
    def __init__(self, app: ASGIApp, logger: logging.Logger):
        self.app = app
        self.log = logger

    async def __call__(self, scope: Scope, receive: Receive, send: Send):
        if scope["type"] != "http":
            return await self.app(scope, receive, send)

        method = scope.get("method", "?")
        path = scope.get("path", "")
        query = scope.get("query_string", b"").decode(errors="replace")
        url = path + (f"?{query}" if query else "")

        req_headers = {
            k.decode(): v.decode(errors="replace") for k, v in scope.get("headers", [])
        }

        req_body = bytearray()

        async def receive2():
            msg = await receive()
            if msg["type"] == "http.request":
                req_body.extend(msg.get("body", b""))
            return msg

        resp = {
            "status": None,
            "headers": {},
            "body": bytearray(),
        }

        async def send2(msg):
            if msg["type"] == "http.response.start":
                resp["status"] = msg.get("status")
                resp["headers"] = {
                    k.decode(): v.decode(errors="replace")
                    for k, v in msg.get("headers", [])
                }
            elif msg["type"] == "http.response.body":
                resp["body"].extend(msg.get("body", b""))
            await send(msg)

        await self.app(scope, receive2, send2)

        self.log.info(">>> REQUEST %s %s", method, url)
        self.log.info("req.headers=%s", req_headers)
        self.log.info("req.body=%s", req_body.decode(errors="replace"))

        self.log.info("<<< RESPONSE %s %s", resp["status"], url)
        self.log.info("resp.headers=%s", resp["headers"])
        self.log.info("resp.body=%s", resp["body"].decode(errors="replace"))


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, format="%(message)s")
    logger = logging.getLogger("mcp")
    app = mcp.http_app(path="/mcp")

    app = ExtendedLogs(app, logger)

    uvicorn.run(app, host="127.0.0.1", port=8888)

Для запуска сервера:

python ./server_with_logs.py

Описание JSON-RPC 2.0

Клиент отправляет JSON с названием метода и параметрами, а сервер возвращает результат при необходимости.

Основные параметры запроса:
jsonrpc — версия протокола JSON-RPC
method — имя метода, который нужно вызывать
params — список параметров
id — идентификатор запроса (в случае, если нужен ответ)

Параметры ответа:
jsonrpc — версия протокола JSON-RPC
result или error — в зависимости от результата выполнения метода
id — идентификатор соответствующего запроса

Общение по сети в FastMCP

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

Схема запросов FastMCP
Схема запросов FastMCP

1) Инициализация сессии (initialize)

Сначала клиент отправляет запрос на инициализацию, где указывает:
▸ версию протокола (protocolVersion)
▸ информацию о клиенте (clientInfo)
▸ поддерживаемые возможности (capabilities)

Запрос клиента:

{"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"mcp","version":"0.1.0"}},"jsonrpc":"2.0","id":0}

Ответ сервера:

{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-11-25","capabilities":{"experimental":{},"prompts":{"listChanged":true},"resources":{"subscribe":false,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"Demo MCP Server","version":"2.14.2"}}}

В ответе сервер также устанавливает следующие хедеры:
mcp-session-id: b9447fa87f6a4625a9324c2da4f51895
mcp-protocol-version: 2025-11-25
Дальнейшее взаимодействие клиента и сервера происходит в рамках этой MCP-сессии — клиент будет указывать полученный mcp-session-id в последующих запросах.

2) Подтверждение инициализации (notification)

После успешного initialize клиент отправляет подтверждение notifications/initialized. Это служебное сообщение, которое сигнализирует о завершении инициализации сессии.

Так как это уведомление, то у запроса нет поля id, поэтому сервер не возвращает ответ.

Запрос клиента:

{"method":"notifications/initialized","jsonrpc":"2.0"}

3) Открытие канала между сервером и клиентом

Далее клиент выполняет долгоживущий GET-запрос к эндпоинту /mcp с заголовком mcp-session-id, в рамках которого соединение не закрывается сразу. Сервер устанавливает streaming-ответ и использует это соединение как канал для передачи сообщений клиенту. Данный механизм реализован через SSE (Server-Sent Events).

Для этого клиент отправляет заголовок Accept: text/event-stream.

4) Листинг и вызов примитивов

После инициализации клиент запрашивает у сервера список доступных сущностей. В зависимости от типа взаимодействия это могут быть инструменты, ресурсы или промпты.

Запрос на листинг инструментов:

{"method":"tools/list","jsonrpc":"2.0","id":1}

Ответ сервера:

{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"get_current_time", "...":"..."},{"name":"add_two_integers", "...":"..."}]}}

В ответ сервер возвращает массив tools, где каждый инструмент описан набором полей:
name — идентификатор, который используется при tools/call
description — описание инструмента
inputSchema / outputSchemaJSON Schema, описывающие формат входных аргументов и результата

Также в описании инструмента могут встречаться служебные поля:
_meta — внутренние метаданные FastMCP (например, теги/атрибуты инструмента)
x-fastmcp-wrap-result — флаг, что результат инструмента возвращается в объекте вида {"result": ...}

Далее клиент вызывает выбранный инструмент через метод tools/call, передавая:
name — имя инструмента
arguments — аргументы вызова (в соответствии с inputSchema)

Запрос на вызов инструмента:

{"method":"tools/call","params":{"name":"add_two_integers","arguments":{"a":13,"b":37},"_meta":{"progressToken":2}},"jsonrpc":"2.0","id":2}

Ответ сервера:

{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"50"}],"structuredContent":{"result":50},"isError":false}}

В ответе сервера можно выделить следующие поля:
structuredContent — результат в структурированном виде
content — результат в виде текста для отображения
isError — флаг ошибки выполнения инструмента

Аналогичным образом выполняется листинг и получение ресурсов (resources/*) и промптов (prompts/*).

5) Завершение сессии

После завершения работы клиент отправляет DELETE-запрос к эндпоинту /mcp, указывая mcp-session-id. Данный запрос завершает указанную MCP-сессию на стороне сервера.

Проблемы безопасности в архитектуре MCP

Проанализировав архитектуру протокола MCP, можно выделить следующие проблемы безопасности:

Отсутствие встроенной модели разграничения доступа
Протокол MCP описывает формат и порядок обмена сообщениями, однако не задает обязательных механизмов авторизации — в результате безопасность зависит от реализации MCP-сервера. Любой, у кого есть сетевой доступ к эндпоинту MCP, может вызывать примитивы.
Тем не менее, для FastMCP существует middleware Eunomia Authorization, добавляющий policy-based авторизацию.

Расширение границ доверия
MCP-сервер может предоставлять доступ к внешним системам, тем самым расширяя периметр безопасности.

Prompt Injection (в том числе через метаданные MCP tools).
Модель получает контекст не только из resources и prompts, но и из метаданных сервера (например, description у tools). При наличии вредоносных инструкций в этих данных модель может быть спровоцирована на нежелательные действия.

Tool abuse
В MCP нет встроенного механизма, который строго определяет, какие инструменты модель может выбирать и в каком порядке. В результате даже “безобидные” инструменты при совместном использовании могут давать опасный эффект. Например, по отдельности инструменты могут выглядеть безопасно:

1. Инструмент для чтения приватного Git-репозитория
2. Инструмент для отправки email
Однако в комбинации эти инструменты превращаются в канал эксфильтрации чувствительных данных.

Риск утечки чувствительных данных во внешние системы
При использовании инструментов, ресурсов и промптов модель может передавать во внешние системы фрагменты чувствительного контекста (код, запросы с персональными данными, внутренние документы). Это особенно критично при наличии инструментов с сетевым доступом.