Model Context Protocol (MCP): создание сервера и клиента на Python — практическое руководство 2026

Model Context Protocol (MCP) — открытый стандарт от Anthropic для подключения LLM к внешним инструментам и данным. В этом руководстве разберём архитектуру MCP, напишем свой сервер на Python с tools, resources и prompts, реализуем клиента и подключим его к Claude Desktop, Cursor и собственному агенту.

MCP на Python: сервер и клиент 2026

Model Context Protocol (MCP) — открытый стандарт, который Anthropic представила в конце 2024-го. К началу 2026-го его поддерживают Claude Desktop, Cursor, Zed, Windsurf, Continue, Cline и, честно говоря, уже десятки других инструментов. Сам по себе MCP решает довольно нудную, но фундаментальную проблему: как стандартизировать подключение LLM-приложений к внешним системам — БД, файлам, API, инструментам — и не писать одни и те же интеграции под каждого клиента заново.

В этом руководстве разберём протокол изнутри, напишем рабочий MCP-сервер на Python с инструментами, ресурсами и промптами, а потом — реализуем собственного клиента и подключим всё это к Claude Desktop и кастомному агенту на Anthropic SDK. Поехали.

Что такое Model Context Protocol и зачем он вообще нужен

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

  1. Tools — функции, которые LLM может вызвать (например, search_database, send_email). По сути полный аналог function calling, только описанный декларативно через JSON Schema.
  2. Resources — данные, которые сервер предоставляет хосту (файлы, записи БД, документы). Хост сам решает, какие из них вложить в контекст.
  3. 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-серверы можно тремя способами:

  1. MCP Inspector (mcp dev server.py) — интерактивный UI, в котором видны все RPC-сообщения. Главный инструмент локальной разработки.
  2. Юнит-тесты через in-memory транспорт. SDK предоставляет create_connected_server_and_client_session() для тестов без запуска отдельного процесса.
  3. Интеграционные тесты с реальным 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.

История статьи (1)
  • — SEO meta refreshed (title and description updated)
Editorial Team
Об авторе Editorial Team

Our team of expert writers and editors.