Проблема: инструменты без стандарта
До 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 и десятками сторонних инструментов.
Аналогия: 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-клиент. Именно хост принимает запросы пользователя и решает, какие серверы подключить.
- Claude Desktop
- VS Code с расширением
- Cursor IDE
- Твой кастомный чат-агент
Библиотека внутри хоста, которая говорит на языке MCP-протокола. Клиент устанавливает соединение с серверами, отправляет запросы и возвращает результаты хосту. Обычно встроен в хост.
mcpPython SDK (client mode)- TypeScript MCP SDK
- Встроен в Claude Desktop
- Anthropic SDK с MCP-поддержкой
Отдельный процесс (или сервис), который предоставляет инструменты, ресурсы или шаблоны промптов через MCP-протокол. Это то, что пишет разработчик инструментов.
- Файловая система
- PostgreSQL-коннектор
- GitHub API
- Твой собственный сервер
Важная деталь: один хост может подключаться к нескольким серверам одновременно. Claude Desktop может работать с сервером файловой системы, сервером базы данных и кастомным сервером одновременно. При этом каждый сервер — изолированный процесс со своими правами доступа.
Что предоставляет MCP-сервер: три типа возможностей
MCP-сервер — это не просто обёртка над функциями. Протокол определяет три отдельных типа возможностей (capabilities), каждый со своим предназначением и способом взаимодействия.
Вызываемые функции с побочными эффектами или вычислениями. LLM может вызвать инструмент, получить результат и использовать его в ответе. Аналог function calling, но через MCP.
Данные только для чтения, доступные по URI. Не предназначены для вызовов LLM — это контент, который хост может вставить в контекст. Подобны файлам или API endpoint'ам.
Переиспользуемые шаблоны промптов, которые сервер предоставляет хосту. Хост показывает их пользователю как slash-команды или готовые запросы. Параметризованные.
Транспорты: stdio vs HTTP+SSE
MCP-сервер может общаться с клиентом двумя способами. Выбор транспорта определяет, где работает сервер и как к нему подключаются.
Протокол: как происходит взаимодействие
MCP строится на JSON-RPC 2.0 — простом и хорошо известном протоколе
вызова удалённых процедур. Вся коммуникация — это JSON-объекты
с полями jsonrpc, method, params (запрос)
и result или error (ответ).
Жизненный цикл соединения состоит из трёх фаз:
Пример реального JSON-обмена при вызове инструмента:
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "read_file",
"arguments": {
"path": "/home/user/project/config.json"
}
}
}
"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.
Подключение к 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()
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 |
| Обновление инструментов | Нужно менять код приложения | Обновляешь только сервер |
| Когда использовать | Простой агент, уникальные инструменты, прототип | Экосистема, командное использование, переиспользование |
Экосистема: готовые MCP-серверы
Одно из главных преимуществ MCP — уже существующая экосистема из сотен серверов. Не нужно писать коннектор к PostgreSQL с нуля — установи официальный сервер:
Реестр серверов: github.com/modelcontextprotocol/servers (официальные),
mcp.so и smithery.ai — агрегаторы сообщества.
Установка, как правило, через npm или pip и одна строка конфигурации в Claude Desktop.
Типичные ошибки
call_tool выбрасывает необработанное исключение,
клиент получает внутреннюю ошибку MCP, а не содержательное сообщение.
LLM не понимает, что пошло не так, и не может исправить вызов.
TextContent
с описанием ошибки + isError=True. LLM увидит текст ошибки
и сможет попробовать другой подход.
~/.ssh и /etc/passwd.
LLM может быть обманут prompt injection атакой.
fs-server, db-server, api-server.
Клиент подключает только нужные для задачи серверы.
5–7 инструментов на сервер — комфортный размер.
requests.get()) заблокирует event loop
и сделает сервер недоступным на время запроса.
httpx.AsyncClient вместо requests,
aiofiles для файлов, asyncpg для PostgreSQL.
Для легаси-кода используй asyncio.to_thread().
Шпаргалка
- Что такое: открытый протокол стандартизации 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())
Практика
calc_server.py с инструментами:
add(a, b), subtract(a, b), multiply(a, b), divide(a, b).
Для деления обработай деление на ноль — верни TextContent с описанием ошибки
и isError=True. Протестируй через MCP-клиент: вызови все четыре инструмента.
list_files(directory: str) → list[str].
Ограничь доступ к директориям: сервер должен принимать список allowed_dirs
при инициализации и отклонять запросы за его пределами.
Попробуй передать путь за пределы allowed_dirs — убедись, что получаешь ошибку, а не файлы.
mcp.server.sse.SseServerTransport и starlette или fastapi).
Запусти два экземпляра сервера на разных портах, создай клиент,
который подключается к обоим и вызывает инструменты с обоих серверов.
Убедись, что оба отвечают параллельно.