MCP — это JSON-RPC 2.0 поверх stdio, HTTP/SSE или Streamable HTTP, описывающий, как LLM-хост (например, Claude Desktop) общается с MCP-сервером (вашей интеграцией). Самая близкая аналогия — Language Server Protocol (LSP) для редакторов кода: один сервер работает со всеми совместимыми клиентами.
До MCP каждый разработчик AI-агента писал свои адаптеры под каждый источник данных: один код для Slack, другой — для PostgreSQL, третий — для Google Drive. С MCP вы пишете сервер один раз, и его может использовать любой совместимый хост. Звучит банально, но именно эта банальность и оказалась прорывом.
Архитектура: host, client, server
- Host — приложение, в котором живёт LLM (Claude Desktop, Cursor, ваш собственный агент).
- Client — компонент внутри хоста, который держит соединение 1:1 с одним сервером.
- Server — отдельный процесс, экспонирующий возможности: tools, resources, prompts.
Один хост может подключаться к множеству серверов параллельно, и каждый клиент-серверный канал изолирован — это, к слову, ключевая штука для безопасности.
Три типа примитивов MCP
- Tools — функции, которые LLM может вызвать (например,
search_database, send_email). По сути полный аналог function calling, только описанный декларативно через JSON Schema.
- Resources — данные, которые сервер предоставляет хосту (файлы, записи БД, документы). Хост сам решает, какие из них вложить в контекст.
- Prompts — шаблоны диалогов с параметрами, которые пользователь хоста может выбрать из меню (например,
review_pr, summarize_logs).
Именно этим тройным делением MCP и отличается от обычного function calling: модель сама вызывает tools, но resources и prompts управляются пользователем. Это часть встроенной модели безопасности — "human-in-the-loop по дизайну", как любят формулировать в Anthropic.
Установка MCP SDK и проверка окружения
Официальный Python SDK живёт в пакете mcp. Для разработки полезен mcp[cli] — он добавляет инспектор и горячую перезагрузку (без этого вы взвоете на третьей итерации, честное слово).
pip install "mcp[cli]>=1.4.0" anthropic
# или с uv (рекомендуется командой Anthropic):
uv add "mcp[cli]" anthropic
Проверим, что инспектор запускается:
mcp --version
# MCP CLI 1.4.x
Пишем MCP-сервер: первый рабочий пример
Начнём с минимального сервера, который умеет ровно одну вещь — складывать числа. На этом примере разберём структуру, а потом нарастим функциональность.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("calculator")
@mcp.tool()
def add(a: float, b: float) -> float:
"""Складывает два числа и возвращает результат."""
return a + b
if __name__ == "__main__":
mcp.run(transport="stdio")
Сохраните как server.py и запустите через инспектор:
mcp dev server.py
Откроется веб-интерфейс на http://localhost:5173, где инструмент можно протестировать вручную. Это главный инструмент отладки — лично я использую его перед каждым деплоем, и пару раз он спас меня от очень странных багов в production.
Как FastMCP превращает Python-функции в tools
FastMCP читает сигнатуру функции и docstring и автоматически собирает JSON Schema:
- Имя инструмента берётся из имени функции (или из аргумента
name= в декораторе).
- Описание — из docstring. Это то, что увидит LLM при выборе инструмента, так что пишите по сути, без воды.
- Параметры извлекаются из type hints. Поддерживаются примитивы,
list, dict, Pydantic-модели, Literal, Annotated с описаниями.
Полноценный MCP-сервер: tools, resources, prompts
Соберём пример поинтереснее — сервер для работы с локальной базой задач. Он будет уметь читать список задач (resource), добавлять и закрывать задачи (tools), а ещё предлагать шаблон еженедельного отчёта (prompt).
from datetime import datetime
from pathlib import Path
import json
from typing import Annotated, Literal
from mcp.server.fastmcp import FastMCP
from pydantic import Field
DB_PATH = Path("tasks.json")
mcp = FastMCP("task-manager")
def _load() -> list[dict]:
if not DB_PATH.exists():
return []
return json.loads(DB_PATH.read_text(encoding="utf-8"))
def _save(tasks: list[dict]) -> None:
DB_PATH.write_text(json.dumps(tasks, ensure_ascii=False, indent=2), encoding="utf-8")
# ---------- Tools ----------
@mcp.tool()
def add_task(
title: Annotated[str, Field(description="Краткое название задачи")],
priority: Literal["low", "medium", "high"] = "medium",
) -> dict:
"""Добавляет новую задачу в трекер."""
tasks = _load()
task = {
"id": len(tasks) + 1,
"title": title,
"priority": priority,
"status": "open",
"created_at": datetime.utcnow().isoformat() + "Z",
}
tasks.append(task)
_save(tasks)
return task
@mcp.tool()
def close_task(task_id: int) -> dict:
"""Закрывает задачу по её идентификатору."""
tasks = _load()
for t in tasks:
if t["id"] == task_id:
t["status"] = "closed"
t["closed_at"] = datetime.utcnow().isoformat() + "Z"
_save(tasks)
return t
raise ValueError(f"Задача {task_id} не найдена")
# ---------- Resources ----------
@mcp.resource("tasks://all")
def all_tasks() -> str:
"""Возвращает все задачи в формате JSON."""
return json.dumps(_load(), ensure_ascii=False, indent=2)
@mcp.resource("tasks://open/{priority}")
def open_by_priority(priority: str) -> str:
"""Открытые задачи указанного приоритета."""
filtered = [t for t in _load() if t["status"] == "open" and t["priority"] == priority]
return json.dumps(filtered, ensure_ascii=False, indent=2)
# ---------- Prompts ----------
@mcp.prompt()
def weekly_report(week_of: str) -> str:
"""Шаблон еженедельного отчёта по задачам."""
return (
f"Сформируй еженедельный отчёт за неделю с {week_of}. "
"Сначала прочитай ресурс tasks://all, затем сгруппируй задачи по "
"статусу и приоритету. Выдели топ-3 риска."
)
if __name__ == "__main__":
mcp.run(transport="stdio")
Этот сервер демонстрирует все три примитива сразу. Обратите внимание на параметризованный URI ресурса (tasks://open/{priority}) — MCP поддерживает URI templates по RFC 6570, и это удобнее, чем кажется на первый взгляд.
Подключаем сервер к Claude Desktop
Откройте файл конфигурации Claude Desktop:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json
- Windows:
%APPDATA%\Claude\claude_desktop_config.json
Добавьте запись о сервере:
{
"mcpServers": {
"task-manager": {
"command": "uv",
"args": [
"--directory",
"/абсолютный/путь/к/проекту",
"run",
"server.py"
]
}
}
}
Перезапустите Claude Desktop. В правом нижнем углу окна ввода появится иконка с числом подключённых инструментов. Всё, теперь Claude сможет вызывать add_task и close_task, читать ресурсы и предлагать prompt weekly_report в меню "/".
Пишем MCP-клиент на Python
Если вы строите своего агента, а не используете Claude Desktop, понадобится собственный клиент. Вот минимальный пример: он подключается к серверу через stdio и вызывает инструменты от лица Claude.
import asyncio
from contextlib import AsyncExitStack
from anthropic import Anthropic
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
class MCPAgent:
def __init__(self, model: str = "claude-opus-4-7"):
self.anthropic = Anthropic()
self.model = model
self.session: ClientSession | None = None
self.stack = AsyncExitStack()
async def connect(self, server_script: str) -> None:
params = StdioServerParameters(command="python", args=[server_script])
read, write = await self.stack.enter_async_context(stdio_client(params))
self.session = await self.stack.enter_async_context(ClientSession(read, write))
await self.session.initialize()
async def _mcp_tools_to_anthropic(self) -> list[dict]:
resp = await self.session.list_tools()
return [
{
"name": t.name,
"description": t.description or "",
"input_schema": t.inputSchema,
}
for t in resp.tools
]
async def chat(self, user_message: str) -> str:
tools = await self._mcp_tools_to_anthropic()
messages = [{"role": "user", "content": user_message}]
while True:
resp = self.anthropic.messages.create(
model=self.model,
max_tokens=2048,
tools=tools,
messages=messages,
)
messages.append({"role": "assistant", "content": resp.content})
if resp.stop_reason != "tool_use":
return "".join(
block.text for block in resp.content if block.type == "text"
)
tool_results = []
for block in resp.content:
if block.type != "tool_use":
continue
result = await self.session.call_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": [c.model_dump() for c in result.content],
})
messages.append({"role": "user", "content": tool_results})
async def close(self) -> None:
await self.stack.aclose()
async def main():
agent = MCPAgent()
await agent.connect("server.py")
try:
answer = await agent.chat(
"Добавь задачу 'Подготовить релиз 1.4' с высоким приоритетом, "
"потом покажи все открытые задачи."
)
print(answer)
finally:
await agent.close()
if __name__ == "__main__":
asyncio.run(main())
Ключевая идея здесь простая: MCP-сервер раздаёт описание инструментов, а агент конвертирует их в формат, который понимает конкретный LLM-провайдер (в нашем случае — Anthropic). Та же логика применима к OpenAI tools или Gemini function declarations.
Транспорты: stdio, SSE и Streamable HTTP
MCP поддерживает три транспорта:
- stdio — сервер запускается как дочерний процесс хоста, общение идёт через stdin/stdout. Идеально для локальных интеграций: минимум накладных расходов, и сетевого attack surface нет вообще.
- SSE (Server-Sent Events) — для удалённых серверов. Постепенно вытесняется Streamable HTTP, но всё ещё используется довольно широко.
- Streamable HTTP — новый рекомендуемый транспорт для удалённых серверов (стабилизирован в спецификации 2025-03-26). Поддерживает single endpoint, stateless и stateful режимы.
Для production-сценариев с авторизацией и масштабированием выбирайте Streamable HTTP — без вариантов. Пример запуска FastMCP по HTTP:
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
Безопасность MCP: что обязательно учесть
В 2025 году исследователи опубликовали несколько работ об уязвимостях MCP — от prompt injection через описания инструментов ("tool poisoning") до confused deputy в OAuth-флоу. К 2026-му спецификация заметно ужесточилась, но ответственность за безопасность по-прежнему делится между хостом и сервером. Так что — да, читать чек-листы придётся.
Чек-лист для сервера
- Никогда не доверяйте параметрам tool call как чистым данным — это вполне может быть инжектированная пользователем строка. Применяйте валидацию (Pydantic, JSON Schema strict mode).
- Не возвращайте секреты, ключи API или PII в текст ресурсов — LLM передаст их обратно в свой контекст и, потенциально, в логи провайдера.
- Для удалённых серверов используйте OAuth 2.1 с PKCE (как требует MCP spec 2025-06-18) и audience-binding токенов.
- Логируйте все вызовы tools с ID сессии — это критично для аудита инцидентов.
Чек-лист для хоста и пользователя
- Включайте режим подтверждения tool calls для незнакомых серверов. Claude Desktop с версии 0.7.x делает это по умолчанию для destructive-операций.
- Запускайте локальные MCP-серверы в песочнице (Docker, sandbox-exec на macOS), если они трогают файловую систему или сеть.
- Регулярно проверяйте, что описания инструментов не изменились между запусками — атака "rug pull" подменяет безопасный tool на вредоносный уже после первоначального одобрения.
Тестирование и отладка
Тестировать MCP-серверы можно тремя способами:
- MCP Inspector (
mcp dev server.py) — интерактивный UI, в котором видны все RPC-сообщения. Главный инструмент локальной разработки.
- Юнит-тесты через in-memory транспорт. SDK предоставляет
create_connected_server_and_client_session() для тестов без запуска отдельного процесса.
- Интеграционные тесты с реальным LLM. Полезны при выкатывании в production: проверяют, что модель действительно правильно использует инструменты (а не просто "вроде бы понимает").
Пример юнит-теста с pytest:
import pytest
from mcp.shared.memory import create_connected_server_and_client_session
from server import mcp
@pytest.mark.asyncio
async def test_add_task():
async with create_connected_server_and_client_session(mcp._mcp_server) as client:
result = await client.call_tool("add_task", {"title": "Тест", "priority": "high"})
assert result.content[0].text
assert '"status": "open"' in result.content[0].text
Когда MCP — правильный выбор, а когда — нет
MCP отлично подходит, когда:
- Вы хотите, чтобы вашу интеграцию использовали сразу несколько разных AI-инструментов (Claude, Cursor, кастомные агенты).
- У вас уже есть внутреннее API, и хочется "оживить" его для команды через Claude Desktop.
- Нужно отдать пользователю контроль над тем, какие данные попадают в контекст (в этом и есть смысл resources и prompts).
MCP не нужен, если:
- Вы строите монолитного агента под единственного провайдера и не планируете переиспользовать инструменты — обычный function calling будет проще.
- Вам нужны строгие гарантии латентности — stdio-транспорт добавляет накладные расходы на сериализацию, а Streamable HTTP — сетевой round-trip.
- Логика инструмента живёт прямо внутри агентного цикла и зависит от его внутреннего состояния.
Что нового в спецификации 2025–2026
- Streamable HTTP заменил отдельные endpoints SSE и POST одним эндпоинтом.
- Elicitation — сервер может запросить у пользователя структурированный ввод во время выполнения tool (например, "введите код подтверждения").
- Structured tool output — tools могут возвращать строго типизированные данные с JSON Schema, что заметно упрощает дальнейшую обработку.
- Resource templates с completion — клиенты могут запрашивать автодополнение значений параметров (приятный UX-апгрейд для меню).
- OAuth 2.1 с обязательным PKCE заменил неформальные схемы авторизации первой версии.
FAQ
Чем MCP отличается от function calling в OpenAI или Anthropic?
Function calling — это API внутри одного LLM-провайдера: модель возвращает JSON с именем функции и аргументами, а сам вызов делает ваш код. MCP — это стандарт коммуникации между процессами, не привязанный к конкретному LLM. Tools в MCP под капотом превращаются в function calling, но MCP добавляет ещё resources и prompts — и работает кросс-вендорно.
Можно ли использовать MCP без Claude?
Да, конечно. MCP — открытая спецификация. Серверы работают с любым клиентом, реализующим протокол: VS Code Copilot, Cursor, Continue, Cline, локальные модели через Ollama-обёртки. На стороне агента вы сами решаете, какой LLM-провайдер обрабатывает контекст.
Нужен ли MCP, если я уже использую LangChain или LlamaIndex?
Это разные слои. LangChain/LlamaIndex — фреймворки для построения агентов и RAG. MCP — стандарт для подключения внешних интеграций. Можно (и нужно) комбинировать: MCP-серверы как источник tools, LangGraph как оркестратор агентского цикла. К 2026 году в LangChain есть готовые адаптеры для MCP-серверов.
Как развернуть MCP-сервер в production?
Для команд внутри компании — используйте Streamable HTTP за reverse proxy (nginx, Caddy) с OAuth 2.1 и mTLS. Серверы делайте stateless, состояние держите во внешнем хранилище. Для публичных серверов обязательны rate limiting, audit logging и подписанные описания инструментов. Anthropic и Cloudflare, к слову, уже предлагают managed-хостинг MCP-серверов в виде Workers.
Что делать с tool poisoning и prompt injection?
Минимум — никогда не передавайте сырой пользовательский ввод как часть описания tool, валидируйте параметры строгими схемами и используйте guardrails-слой между LLM и вызовом инструмента. Для критичных операций — human-in-the-loop подтверждение, для удалённых серверов — мониторинг изменений в описаниях инструментов между сессиями.
Итог
Model Context Protocol превратился из эксперимента 2024 года в стандарт де-факто для подключения LLM-приложений к внешнему миру. Имея один MCP-сервер, вы получаете интеграцию со всеми крупными AI-инструментами разом. Python SDK с FastMCP делает разработку настолько простой, что прототип сервера пишется за час, а production-готовый — за день-два. Я сам, признаться, не ожидал, что путь от "хочу попробовать" до "оно работает в Claude Desktop" окажется настолько коротким.
Следующий шаг — попробуйте упаковать в MCP-сервер какую-нибудь реальную внутреннюю систему вашей команды (Jira, БД метрик, внутреннюю wiki), подключите её к Claude Desktop и измерьте, сколько рутинных вопросов теперь снимаются без переключения контекста. Именно такие приземлённые сценарии и оправдывают инвестицию в MCP.