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 сервера на уровне сети
Общение между клиентом и сервером сводится к обмену сообщениями в формате 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
Рассмотрим подробнее, какие запросы отправляются в процессе работы нашего клиента:

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 / outputSchema — JSON 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
Однако в комбинации эти инструменты превращаются в канал эксфильтрации чувствительных данных.
▸ Риск утечки чувствительных данных во внешние системы
При использовании инструментов, ресурсов и промптов модель может передавать во внешние системы фрагменты чувствительного контекста (код, запросы с персональными данными, внутренние документы). Это особенно критично при наличии инструментов с сетевым доступом.