Проблема: инструменты без стандарта

До MCP каждый разработчик решал задачу «дать LLM доступ к инструменту» по-своему. Нет стандарта — значит нет переиспользования. Ситуация выглядела примерно так:

Claude API (function calling)     OpenAI API (tools)      LangChain agents
       ↓                                ↓                       ↓
  tool_1_claude.py              tool_1_openai.py          tool_1_langchain.py
  tool_2_claude.py              tool_2_openai.py          tool_2_langchain.py
  tool_3_claude.py              tool_3_openai.py          tool_3_langchain.py

  Три версии одного и того же инструмента для трёх фреймворков.
  Инструмент в Claude Desktop? Четвёртая версия.
  В VS Code Copilot? Пятая.

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

Anthropic выпустил MCP в ноябре 2024 года как ответ на эту проблему. Спецификация открытая, не привязана к Claude и сейчас поддерживается также OpenAI, Google и десятками сторонних инструментов.

MCP — это не Claude-specific. Протокол открытый (лицензия MIT), спецификация на GitHub. MCP-серверы работают с любым MCP-совместимым клиентом: Claude Desktop, VS Code, Cursor, Zed, и кастомными приложениями.

Аналогия: USB-C для AI-инструментов

До USB-C у каждого устройства был свой разъём: у принтера один, у телефона другой, у ноутбука третий. Производители кабелей дублировали работу, пользователи собирали коллекцию адаптеров. USB-C стандартизировал разъём — теперь один кабель работает везде.

БЕЗ СТАНДАРТА                      С MCP (USB-C для AI)
─────────────────────────────      ─────────────────────────────
Claude Desktop ←→ адаптер A        Claude Desktop  ┐
VS Code        ←→ адаптер B        VS Code         ├──→  MCP Server
Custom App     ←→ адаптер C        Custom App      ┘     (один раз)
                                   (любой клиент)

Три реализации одного инструмента  Одна реализация — работает везде

MCP — это USB-C для инструментов: один стандарт подключения, работающий в любом MCP-совместимом хосте. Написал MCP-сервер для своей базы данных один раз — он работает в Claude Desktop, в твоём кастомном агенте, и в любом другом инструменте, который поддерживает MCP.

Архитектура: три роли — Host, Client, Server

MCP-экосистема строится вокруг трёх чётко разграниченных ролей. Понимание этих ролей критично — без этого легко запутаться в том, кто с кем общается и по какому протоколу.

MCP Host
host

Приложение, которое содержит пользовательский интерфейс и встраивает MCP-клиент. Именно хост принимает запросы пользователя и решает, какие серверы подключить.

  • Claude Desktop
  • VS Code с расширением
  • Cursor IDE
  • Твой кастомный чат-агент
MCP Client
client

Библиотека внутри хоста, которая говорит на языке MCP-протокола. Клиент устанавливает соединение с серверами, отправляет запросы и возвращает результаты хосту. Обычно встроен в хост.

  • mcp Python SDK (client mode)
  • TypeScript MCP SDK
  • Встроен в Claude Desktop
  • Anthropic SDK с MCP-поддержкой
MCP Server
server

Отдельный процесс (или сервис), который предоставляет инструменты, ресурсы или шаблоны промптов через MCP-протокол. Это то, что пишет разработчик инструментов.

  • Файловая система
  • PostgreSQL-коннектор
  • GitHub API
  • Твой собственный сервер

Важная деталь: один хост может подключаться к нескольким серверам одновременно. Claude Desktop может работать с сервером файловой системы, сервером базы данных и кастомным сервером одновременно. При этом каждый сервер — изолированный процесс со своими правами доступа.

100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
MCP HOST MCP PROTOCOL MCP SERVERS Пользователь "Что в файле config.json?" Claude (LLM) решает вызвать инструмент MCP Client формирует JSON-RPC запрос результат → в ответ STDIO TRANSPORT (локальный) stdin/stdout · subprocess · JSON-RPC 2.0 → request: {"jsonrpc":"2.0","method":"tools/call", "params":{"name":"read_file","arguments":{...}}} ← response: {"result":{"content":[{"text":"..."}]}} HTTP + SSE TRANSPORT (сетевой) POST /message · GET /sse · JSON-RPC 2.0 → POST /message (инициирует вызов) {"jsonrpc":"2.0","method":"tools/call",...} ← SSE stream (возвращает прогресс/результат) data: {"result":{"content":[...]}} 📁 Filesystem read/write/list файлов и директорий 🐘 PostgreSQL SQL-запросы, схемы, транзакции 🌐 Browser / Playwright navigate, screenshot, click, fill 🐙 GitHub repos, issues, PRs, commits ⚙️ Custom Server твой инструмент на Python / TS / Go stdio — для локальных процессов (subprocess)  ·  HTTP+SSE — для сетевых сервисов

Что предоставляет MCP-сервер: три типа возможностей

MCP-сервер — это не просто обёртка над функциями. Протокол определяет три отдельных типа возможностей (capabilities), каждый со своим предназначением и способом взаимодействия.

🔧
Tools (инструменты)

Вызываемые функции с побочными эффектами или вычислениями. LLM может вызвать инструмент, получить результат и использовать его в ответе. Аналог function calling, но через MCP.

read_file, run_query, send_email, create_issue
📄
Resources (ресурсы)

Данные только для чтения, доступные по URI. Не предназначены для вызовов LLM — это контент, который хост может вставить в контекст. Подобны файлам или API endpoint'ам.

file://project/README.md, db://schema/users
💬
Prompts (шаблоны)

Переиспользуемые шаблоны промптов, которые сервер предоставляет хосту. Хост показывает их пользователю как slash-команды или готовые запросы. Параметризованные.

/summarize-pr 123, /explain-error "stack trace"
Tools — самое используемое из трёх. В большинстве MCP-серверов реализуют только Tools, потому что именно они позволяют LLM действовать: читать файлы, делать запросы, вызывать API. Resources и Prompts — полезные дополнения, но необязательные для начала.

Транспорты: stdio vs HTTP+SSE

MCP-сервер может общаться с клиентом двумя способами. Выбор транспорта определяет, где работает сервер и как к нему подключаются.

stdio — Process Transport
Как работает
Хост запускает сервер как subprocess, общается через stdin/stdout
Где живёт
Локально, рядом с хостом
Изоляция
Отдельный процесс, своя файловая система
Задержка
Минимальная (IPC)
Когда
Локальные инструменты: файлы, локальная БД, CLI-утилиты
HTTP + SSE — Network Transport
Как работает
HTTP POST для запросов, Server-Sent Events для ответов
Где живёт
Отдельный сервер (localhost или remote)
Многопользов.
Несколько клиентов одновременно
Задержка
Сетевая (хотя и небольшая на localhost)
Когда
Публичные сервисы, многопользовательские среды, облачные API
Начинай со stdio. Для подавляющего большинства кастомных серверов stdio — правильный выбор. Проще в разработке, нет открытых портов, лучше с безопасностью. HTTP+SSE нужен, когда сервер должен быть доступен нескольким клиентам или размещён удалённо.

Протокол: как происходит взаимодействие

MCP строится на JSON-RPC 2.0 — простом и хорошо известном протоколе вызова удалённых процедур. Вся коммуникация — это JSON-объекты с полями jsonrpc, method, params (запрос) и result или error (ответ).

Жизненный цикл соединения состоит из трёх фаз:

Фаза 1: Инициализация (один раз при запуске)
C→S
initialize клиент сообщает свою версию протокола и возможности
S→C
initialize (response) сервер отвечает своей версией и списком capabilities
C→S
initialized клиент подтверждает: соединение установлено
Фаза 2: Discovery — что умеет сервер?
C→S
tools/list запрос списка доступных инструментов
S→C
tools/list (response) список инструментов с JSON Schema для каждого
Фаза 3: Вызов инструмента (повторяется при каждом использовании)
C→S
tools/call {"name": "read_file", "arguments": {"path": "/etc/hosts"}}
S→C
tools/call (response) {"content": [{"type": "text", "text": "127.0.0.1 localhost…"}]}

Пример реального JSON-обмена при вызове инструмента:

Запрос (Client → Server)
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "read_file",
    "arguments": {
      "path": "/home/user/project/config.json"
    }
  }
}
Ответ (Server → Client)
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n \"db_host\": \"localhost\",\n \"port\": 5432\n}"
      }
    ],
    "isError": false
  }
}

Обратите внимание: в поле content массив объектов с полем type. Тип может быть text, image или resource. Это позволяет инструментам возвращать не только текст, но и бинарные данные (скриншоты, файлы).

Первый MCP-сервер: читаем и пишем файлы

Напишем минимальный MCP-сервер на Python — два инструмента: read_file и write_file. Для этого нужен официальный Python SDK: pip install mcp.

"""
Минимальный MCP-сервер: работа с файлами.
Запуск: python file_server.py
"""
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
import asyncio


# Создаём сервер с именем — имя видит клиент при discovery
server = Server("file-tools")


# ── Объявляем инструменты ──

@server.list_tools()
async def list_tools() -> list[types.Tool]:
    """Отвечаем на tools/list — возвращаем описания инструментов."""
    return [
        types.Tool(
            name="read_file",
            description="Читает содержимое файла по указанному пути",
            inputSchema={
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "Абсолютный или относительный путь к файлу"
                    }
                },
                "required": ["path"]
            }
        ),
        types.Tool(
            name="write_file",
            description="Записывает текст в файл (перезаписывает если существует)",
            inputSchema={
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "Путь к файлу"
                    },
                    "content": {
                        "type": "string",
                        "description": "Текст для записи"
                    }
                },
                "required": ["path", "content"]
            }
        ),
    ]


# ── Обрабатываем вызовы ──

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
    """Отвечаем на tools/call — выполняем инструмент и возвращаем результат."""

    if name == "read_file":
        path = arguments["path"]
        try:
            with open(path, encoding="utf-8") as f:
                content = f.read()
            return [types.TextContent(type="text", text=content)]
        except FileNotFoundError:
            return [types.TextContent(type="text", text=f"Error: file not found: {path}")]
        except Exception as e:
            return [types.TextContent(type="text", text=f"Error: {e}")]

    elif name == "write_file":
        path = arguments["path"]
        content = arguments["content"]
        try:
            with open(path, "w", encoding="utf-8") as f:
                f.write(content)
            return [types.TextContent(type="text", text=f"OK: written {len(content)} chars to {path}")]
        except Exception as e:
            return [types.TextContent(type="text", text=f"Error: {e}")]

    else:
        return [types.TextContent(type="text", text=f"Unknown tool: {name}")]


# ── Запускаем сервер через stdio транспорт ──

async def main():
    async with stdio_server() as streams:
        await server.run(
            streams[0],   # stdin
            streams[1],   # stdout
            server.create_initialization_options()
        )

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

Это полный рабочий MCP-сервер. Запусти его, и любой MCP-совместимый клиент сможет обнаружить и использовать инструменты read_file и write_file.

Почему async? MCP SDK использует asyncio, потому что MCP-сервер может обрабатывать несколько запросов параллельно (особенно в HTTP+SSE режиме). Даже в stdio-режиме async позволяет не блокировать цикл при медленных I/O.

Подключение к MCP-серверу из Python

Теперь посмотрим на другую сторону: как из своего кода подключиться к MCP-серверу как клиент, получить список инструментов и вызвать один из них.

"""
MCP-клиент: подключаемся к file_server.py и вызываем инструменты.
"""
import asyncio
from mcp import ClientSession
from mcp.client.stdio import stdio_client
import mcp.types as types


async def main():
    # Запускаем сервер как subprocess и подключаемся через stdio
    async with stdio_client(
        command="python",
        args=["file_server.py"]
    ) as (read_stream, write_stream):

        async with ClientSession(read_stream, write_stream) as session:

            # Инициализация (handshake)
            await session.initialize()

            # Discovery: получаем список инструментов
            tools_response = await session.list_tools()
            print("Доступные инструменты:")
            for tool in tools_response.tools:
                print(f"  {tool.name}: {tool.description}")

            # Вызываем инструмент read_file
            result = await session.call_tool(
                "read_file",
                arguments={"path": "/etc/hostname"}
            )

            print("\nРезультат read_file:")
            for item in result.content:
                print(item.text)


asyncio.run(main())

Использование MCP в Claude API

Anthropic SDK поддерживает MCP нативно — можно передать список MCP-серверов в вызов API, и Claude будет использовать их инструменты автоматически:

import anthropic

client = anthropic.Anthropic()

# Подключаем MCP-сервер через Anthropic SDK
# (SDK запускает сервер как subprocess и управляет соединением)
with client.beta.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Прочитай файл /etc/hostname и скажи его содержимое"}],
    mcp_servers=[
        {
            "type": "stdio",
            "command": "python",
            "args": ["file_server.py"],
        }
    ],
    betas=["mcp-client-2025-04-04"],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)

print()
MCP в Claude API сейчас в beta. Используй заголовок betas=["mcp-client-2025-04-04"]. API может измениться в стабильном релизе. Следи за docs.anthropic.com для актуального статуса.

MCP vs Function Calling: когда что выбирать

MCP и function calling решают одну задачу — дать LLM доступ к внешним функциям. Но у них принципиально разные модели: встроенная vs внешняя. Выбор зависит от задачи.

Аспект Function Calling MCP
Где живут инструменты В коде приложения, передаются в запрос Отдельный процесс/сервис
Переиспользование Только внутри одного приложения Работает в любом MCP-клиенте
Изоляция Нет: инструмент в том же процессе Да: отдельный процесс
Сложность Минимальная: один файл, JSON Schema Выше: сервер, транспорт, SDK
Discovery Нет: ты явно передаёшь список tools Есть: клиент запрашивает tools/list
Обновление инструментов Нужно менять код приложения Обновляешь только сервер
Когда использовать Простой агент, уникальные инструменты, прототип Экосистема, командное использование, переиспользование
Практическое правило. Начинай с function calling — проще, быстрее, меньше движущихся частей. Переходи на MCP когда: инструментом хотят пользоваться несколько агентов/приложений, или когда хочешь раздавать его команде без переписывания их кода.

Экосистема: готовые MCP-серверы

Одно из главных преимуществ MCP — уже существующая экосистема из сотен серверов. Не нужно писать коннектор к PostgreSQL с нуля — установи официальный сервер:

📁
Filesystem
Чтение/запись файлов. Официальный от Anthropic.
🐘
PostgreSQL
SQL-запросы, схема БД, read-only режим.
🌐
Playwright / Puppeteer
Управление браузером, скриншоты, web scraping.
🐙
GitHub
Repos, issues, PRs, commits, code search.
🔍
Brave Search
Web и локальный поиск через Brave API.
🗄️
SQLite
Локальная БД: запросы, схема, бизнес-данные.
📧
Gmail / Google Calendar
Чтение/отправка писем, управление событиями.
💬
Slack
Читать каналы, отправлять сообщения, поиск.
🗺️
Google Maps
Маршруты, геокодирование, поиск мест.

Реестр серверов: github.com/modelcontextprotocol/servers (официальные), mcp.so и smithery.ai — агрегаторы сообщества. Установка, как правило, через npm или pip и одна строка конфигурации в Claude Desktop.

Типичные ошибки

1. Смешивать роли: server пытается вызвать client
MCP-сервер не знает о клиенте ничего кроме запросов, которые он получает. Попытка из кода сервера сделать обратный вызов или сохранить состояние между запросами нарушает модель — у каждого запроса нет гарантированного контекста.
Сервер — stateless (без состояния) по дизайну. Сохраняй нужное состояние в файл или БД, а не в переменные сервера (если только это не кэш соединений).
2. Возвращать ошибки как исключения Python
Если call_tool выбрасывает необработанное исключение, клиент получает внутреннюю ошибку MCP, а не содержательное сообщение. LLM не понимает, что пошло не так, и не может исправить вызов.
Оборачивай всё в try/except и возвращай TextContent с описанием ошибки + isError=True. LLM увидит текст ошибки и сможет попробовать другой подход.
3. Делать инструменты с широким доступом без ограничений
Сервер файловой системы без ограничений на директории даёт LLM доступ ко всей файловой системе, включая ~/.ssh и /etc/passwd. LLM может быть обманут prompt injection атакой.
Явно ограничивай allowed_paths при создании сервера. Принцип наименьших привилегий: давай доступ только к тому, что нужно для конкретной задачи.
4. Один огромный сервер вместо нескольких специализированных
Соблазн сделать «один сервер для всего» — файлы, БД, API, уведомления. Результат: 30+ инструментов, LLM путается в списке, тяжело тестировать, тяжело разграничивать права.
Разбивай по доменам: fs-server, db-server, api-server. Клиент подключает только нужные для задачи серверы. 5–7 инструментов на сервер — комфортный размер.
5. Забывать об async в handler'ах
MCP SDK полностью асинхронный. Синхронный блокирующий вызов (например, requests.get()) заблокирует event loop и сделает сервер недоступным на время запроса.
Используй async-альтернативы: httpx.AsyncClient вместо requests, aiofiles для файлов, asyncpg для PostgreSQL. Для легаси-кода используй asyncio.to_thread().

Шпаргалка

MCP — краткая выжимка
  • Что такое: открытый протокол стандартизации AI-инструментов (JSON-RPC 2.0)
  • Роли: Host (приложение) → Client (библиотека внутри) → Server (инструмент)
  • Capabilities: Tools (вызываемые функции), Resources (данные), Prompts (шаблоны)
  • Транспорты: stdio (локальный subprocess), HTTP+SSE (сетевой сервис)
  • Lifecycle: initialize → tools/list → tools/call (повторяется)
  • SDK: pip install mcp, декораторы @server.list_tools() и @server.call_tool()
  • vs function calling: FC — встроенный, просто; MCP — внешний, переиспользуемый
  • Когда MCP: несколько агентов используют одни инструменты, командное использование
  • Когда FC: прототип, уникальные инструменты, один агент
# Минимальный MCP-сервер за 20 строк
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
import asyncio

server = Server("my-server")

@server.list_tools()
async def list_tools():
    return [types.Tool(
        name="hello",
        description="Возвращает приветствие",
        inputSchema={"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}
    )]

@server.call_tool()
async def call_tool(name: str, arguments: dict):
    if name == "hello":
        return [types.TextContent(type="text", text=f"Привет, {arguments['name']}!")]

async def main():
    async with stdio_server() as (r, w):
        await server.run(r, w, server.create_initialization_options())

asyncio.run(main())

Практика

Задание 1. Напиши MCP-сервер calc_server.py с инструментами: add(a, b), subtract(a, b), multiply(a, b), divide(a, b). Для деления обработай деление на ноль — верни TextContent с описанием ошибки и isError=True. Протестируй через MCP-клиент: вызови все четыре инструмента.
Задание 2. Добавь в файловый сервер из урока инструмент list_files(directory: str) → list[str]. Ограничь доступ к директориям: сервер должен принимать список allowed_dirs при инициализации и отклонять запросы за его пределами. Попробуй передать путь за пределы allowed_dirs — убедись, что получаешь ошибку, а не файлы.
Задание 3 (продвинутый). Перепиши свой файловый MCP-сервер на HTTP+SSE транспорт (используй mcp.server.sse.SseServerTransport и starlette или fastapi). Запусти два экземпляра сервера на разных портах, создай клиент, который подключается к обоим и вызывает инструменты с обоих серверов. Убедись, что оба отвечают параллельно.