Хосты, серверы, клиенты. Как устроены потоки данных и взаимодействие компонентов.
Понимание архитектуры MCP — ключ к созданию надёжных и масштабируемых серверов. В этой теме разберём протокол до винтика.
┌─────────────────────────────────────────────────────────────────┐
│ MCP Host │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ LLM Engine │ │
│ │ (Claude, GPT, ...) │ │
│ └──────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────▼────────────────────────────────┐ │
│ │ MCP Client │ │
│ │ ┌──────────────┬──────────────┬──────────────────────┐ │ │
│ │ │ Connection │ Message │ Session │ │ │
│ │ │ Manager │ Router │ Manager │ │ │
│ │ └──────────────┴──────────────┴──────────────────────┘ │ │
│ └──────────────────────────┬────────────────────────────────┘ │
└─────────────────────────────┼───────────────────────────────────┘
│ MCP Protocol
│ (JSON-RPC 2.0 over Transport)
┌─────────────────────────────▼───────────────────────────────────┐
│ MCP Server │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Protocol Layer │ │
│ │ (Request Handler, Response Builder) │ │
│ └──────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────▼────────────────────────────────┐ │
│ │ Capability Providers │ │
│ │ ┌──────────────┬──────────────┬──────────────────────┐ │ │
│ │ │ Resources │ Tools │ Prompts │ │ │
│ │ │ Provider │ Provider │ Provider │ │ │
│ │ └──────────────┴──────────────┴──────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────▼────────────────────────────────┐ │
│ │ Backend Integrations │ │
│ │ (File System, Database, External APIs, ...) │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
MCP использует JSON-RPC 2.0 — лёгкий протокол удалённого вызова процедур.
Запрос (Request):
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}Ответ (Response):
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "search",
"description": "Search knowledge base",
"inputSchema": {
"type": "object",
"properties": {
"query": {"type": "string"}
},
"required": ["query"]
}
}
]
}
}Ошибка (Error):
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32601,
"message": "Method not found",
"data": "Unknown method: tools/invalid"
}
}| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
jsonrpc | string | Да | Версия протокола, всегда "2.0" |
id | integer/string | Да* | Уникальный ID запроса (отсутствует для notifications) |
method | string | Да | Имя метода, например tools/list |
params | object/array | Нет | Параметры метода |
result | object | Да* | Результат выполнения (в ответе) |
error | object | Да* | Ошибка выполнения (в ответе при ошибке) |
* — обязательно для ответа, но не одновременно
1. Request (Запрос)
idtools/list, resources/read2. Notification (Уведомление)
id)notifications/initialized3. Response (Ответ)
result или errorid соответствует запросу┌─────────────┐ ┌─────────────┐
│ MCP Client │ │ MCP Server │
│ (Host) │ │ (Server) │
└──────┬──────┘ └──────┬──────┘
│ │
│ 1. initialize (capabilities) │
│────────────────────────────────────────>│
│ │
│ 2. initialize response │
│ (server capabilities) │
│<────────────────────────────────────────│
│ │
│ 3. notifications/initialized │
│────────────────────────────────────────>│
│ │
│ ◄─── Сессия активна, запросы ───► │
│ │
│ 4. tools/list, resources/read, ... │
│────────────────────────────────────────>│
│ │
│ 5. response with data │
│<────────────────────────────────────────│
│ │
│ 6. close connection │
│────────────────────────────────────────>│
│ │
Client устанавливает соединение через выбранный транспорт (stdio, SSE, WebSocket).
stdio пример:
# Запуск процесса сервера
python mcp_server.py
# stdin/stdout готовы к обмену сообщениямиSSE пример:
# Client подключается к SSE endpoint
GET http://localhost:8000/sse
# Сервер отправляет event с endpoint для сообщений
event: endpoint
data: http://localhost:8000/messages?session_id=abc123Client отправляет initialize запрос:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"roots": {
"listChanged": true
}
},
"clientInfo": {
"name": "Claude Desktop",
"version": "1.0.0"
}
}
}Server отвечает своими возможностями:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"resources": {
"subscribe": true,
"listChanged": true
},
"tools": {
"listChanged": true
},
"prompts": {
"listChanged": true
}
},
"serverInfo": {
"name": "my-mcp-server",
"version": "1.0.0"
}
}
}Client отправляет уведомление:
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}Важно: После этого момента сессия считается активной, можно отправлять запросы.
Client отправляет запросы в любом порядке:
// Запрос списка инструментов
{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}
// Запрос ресурса
{"jsonrpc": "2.0", "id": 3, "method": "resources/read", "params": {"uri": "file:///tmp/test.txt"}}
// Вызов инструмента
{"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "search", "arguments": {"query": "MCP"}}}Server отвечает:
// Ответ tools/list
{"jsonrpc": "2.0", "id": 2, "result": {"tools": [...]}}
// Ответ resources/read
{"jsonrpc": "2.0", "id": 3, "result": {"contents": [{"uri": "file:///tmp/test.txt", "text": "Hello"}]}}
// Ответ tools/call
{"jsonrpc": "2.0", "id": 4, "result": {"content": [{"type": "text", "text": "Search results..."}]}}Соединение закрывается одной из сторон:
Server сообщает Client о своих возможностях при инициализации.
{
"resources": {
"subscribe": true,
"listChanged": true
}
}| Поле | Значение |
|---|---|
subscribe | Сервер поддерживает подписку на изменения ресурсов |
listChanged | Сервер отправляет уведомления при изменении списка ресурсов |
{
"tools": {
"listChanged": true
}
}| Поле | Значение |
|---|---|
listChanged | Сервер отправляет уведомления при изменении списка инструментов |
{
"prompts": {
"listChanged": true
}
}| Поле | Значение |
|---|---|
listChanged | Сервер отправляет уведомления при изменении списка промптов |
Если сервер поддерживает listChanged, он может отправлять уведомления:
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}Client после получения уведомления должен запросить обновлённый список через tools/list.
| Метод | Описание |
|---|---|
resources/list | Получить список доступных ресурсов |
resources/read | Прочитать содержимое ресурса по URI |
resources/subscribe | Подписаться на изменения ресурса |
resources/unsubscribe | Отписаться от изменений ресурса |
Пример resources/list:
// Запрос
{"jsonrpc": "2.0", "id": 1, "method": "resources/list", "params": {}}
// Ответ
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resources": [
{
"uri": "file:///etc/hosts",
"name": "Hosts file",
"description": "System hosts file",
"mimeType": "text/plain"
}
]
}
}Пример resources/read:
// Запрос
{
"jsonrpc": "2.0",
"id": 2,
"method": "resources/read",
"params": {"uri": "file:///etc/hosts"}
}
// Ответ
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"contents": [
{
"uri": "file:///etc/hosts",
"mimeType": "text/plain",
"text": "127.0.0.1 localhost\n"
}
]
}
}| Метод | Описание |
|---|---|
tools/list | Получить список доступных инструментов |
tools/call | Вызвать инструмент с аргументами |
Пример tools/list:
// Запрос
{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}
// Ответ
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "search_database",
"description": "Search the knowledge base",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
},
"limit": {
"type": "integer",
"description": "Max results",
"default": 10
}
},
"required": ["query"]
}
}
]
}
}Пример tools/call:
// Запрос
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "search_database",
"arguments": {"query": "MCP protocol", "limit": 5}
}
}
// Ответ
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "Found 3 results:\n1. MCP Specification...\n2. MCP Python SDK...\n3. MCP Servers Catalog..."
}
],
"isError": false
}
}| Метод | Описание |
|---|---|
prompts/list | Получить список доступных промптов |
prompts/get | Получить промпт по имени с аргументами |
Пример prompts/list:
// Запрос
{"jsonrpc": "2.0", "id": 1, "method": "prompts/list", "params": {}}
// Ответ
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"prompts": [
{
"name": "code_review",
"description": "Request code review",
"arguments": [
{
"name": "code",
"description": "Code to review",
"required": true
},
{
"name": "language",
"description": "Programming language",
"required": false
}
]
}
]
}
}Пример prompts/get:
// Запрос
{
"jsonrpc": "2.0",
"id": 2,
"method": "prompts/get",
"params": {
"name": "code_review",
"arguments": {
"code": "def hello():\n print('Hello')",
"language": "python"
}
}
}
// Ответ
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"description": "Code review request",
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Please review this Python code:\n\ndef hello():\n print('Hello')\n\nCheck for best practices and potential issues."
}
}
]
}
}| Код | Значение |
|---|---|
-32700 | Parse error — невалидный JSON |
-32600 | Invalid Request — невалидная структура запроса |
-32601 | Method not found — метод не существует |
-32602 | Invalid params — невалидные параметры |
-32603 | Internal error — внутренняя ошибка сервера |
Server может возвращать собственные коды ошибок в диапазоне −32000 до −32099:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32001,
"message": "Resource not found",
"data": {
"uri": "file:///nonexistent.txt"
}
}
}Ресурс не найден:
{
"jsonrpc": "2.0",
"id": 5,
"error": {
"code": -32001,
"message": "Resource not found",
"data": {"uri": "file:///missing.txt"}
}
}Инструмент не найден:
{
"jsonrpc": "2.0",
"id": 6,
"error": {
"code": -32601,
"message": "Tool not found: invalid_tool"
}
}Невалидные параметры:
{
"jsonrpc": "2.0",
"id": 7,
"error": {
"code": -32602,
"message": "Invalid parameters",
"data": "Missing required field: query"
}
}Внутренняя ошибка сервера:
{
"jsonrpc": "2.0",
"id": 8,
"error": {
"code": -32603,
"message": "Internal error",
"data": "Database connection failed: timeout after 30s"
}
}Уведомления — это сообщения без id, не требующие ответа.
tools/list_changed:
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}resources/list_changed:
{
"jsonrpc": "2.0",
"method": "notifications/resources/list_changed"
}prompts/list_changed:
{
"jsonrpc": "2.0",
"method": "notifications/prompts/list_changed"
}resources/updated (при подписке):
{
"jsonrpc": "2.0",
"method": "notifications/resources/updated",
"params": {
"uri": "file:///logs/app.log"
}
}initialized:
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}roots/list_changed:
{
"jsonrpc": "2.0",
"method": "notifications/roots/list_changed"
}Архитектура:
┌─────────────┐ ┌─────────────┐
│ MCP Client │ │ MCP Server │
│ (Host) │ │ (Process) │
└──────┬──────┘ └──────┬──────┘
│ │
│ stdout (read) │
│<──────────────────────│
│ │
│ stdin (write) │
│──────────────────────>│
│ │
Пример запуска:
// Конфиг для Claude Desktop
{
"mcpServers": {
"my-server": {
"command": "python",
"args": ["/path/to/server.py"],
"env": {
"API_KEY": "secret"
}
}
}
}Преимущества:
Недостатки:
Архитектура:
┌─────────────┐ ┌─────────────┐
│ MCP Client │ │ MCP Server │
│ (Host) │ │ (FastAPI) │
└──────┬──────┘ └──────┬──────┘
│ │
│ GET /sse (SSE connection) │
│──────────────────────────────────────>│
│ │
│ event: endpoint │
│ data: /messages?session_id=abc │
│<──────────────────────────────────────│
│ │
│ POST /messages?session_id=abc │
│ (JSON-RPC requests) │
│──────────────────────────────────────>│
│ │
│ SSE events (JSON-RPC responses) │
│<──────────────────────────────────────│
│ │
Пример сервера на FastAPI:
from fastapi import FastAPI, Request
from sse_starlette.sse import EventSourceResponse
import asyncio
app = FastAPI()
# Хранилище сессий
sessions = {}
@app.get("/sse")
async def sse_endpoint(request: Request):
session_id = generate_session_id()
sessions[session_id] = asyncio.Queue()
async def event_generator():
while True:
if await request.is_disconnected():
break
message = await sessions[session_id].get()
yield {"data": message}
return EventSourceResponse(event_generator())
@app.post("/messages")
async def messages_endpoint(session_id: str, request: Request):
body = await request.json()
response = await process_mcp_request(body)
# Отправка ответа через SSE очередь
await sessions[session_id].put(json.dumps(response))
return {"status": "ok"}Преимущества:
Недостатки:
Архитектура:
┌─────────────┐ ┌─────────────┐
│ MCP Client │ │ MCP Server │
│ (Host) │ │ (WS) │
└──────┬──────┘ └──────┬──────┘
│ │
│ WebSocket Connect /ws │
│──────────────────────────────────────>│
│ │
│ <двусторонний обмен сообщениями> │
│<─────────────────────────────────────>│
│ │
Пример сервера:
import asyncio
import websockets
async def handler(websocket):
async for message in websocket:
request = json.loads(message)
response = await process_mcp_request(request)
await websocket.send(json.dumps(response))
async def main():
async with websockets.serve(handler, "localhost", 8765):
await asyncio.Future() # run forever
asyncio.run(main())Преимущества:
Недостатки:
MCP протокол stateless — каждый запрос независим. Но сессия может хранить состояние:
Что хранится в сессии:
Пример управления сессией:
class MCPSession:
def __init__(self, session_id: str):
self.session_id = session_id
self.subscriptions: set[str] = set() # Подписки на ресурсы
self.context: dict = {} # Контекст выполнения
def subscribe(self, uri: str):
self.subscriptions.add(uri)
def unsubscribe(self, uri: str):
self.subscriptions.discard(uri)
def cleanup(self):
# Отписка от всех ресурсов при завершении
self.subscriptions.clear()
self.context.clear()Создание сессии:
async def on_connect(transport):
session_id = generate_uuid()
session = MCPSession(session_id)
sessions[session_id] = session
return sessionЗавершение сессии:
async def on_disconnect(session):
await session.cleanup()
del sessions[session.id]❌ ПЛОХО: Вся логика в одном классе
class MCPServer:
async def handle_request(self, request):
# 500 строк кода с обработкой всего
✅ ХОРОШО: Разделение по ответственности
class MCPServer:
def __init__(self):
self.resources_handler = ResourcesHandler()
self.tools_handler = ToolsHandler()
self.prompts_handler = PromptsHandler()
async def handle_request(self, request):
method = request.method
if method.startswith("resources/"):
return await self.resources_handler.handle(request)
elif method.startswith("tools/"):
return await self.tools_handler.handle(request)
elif method.startswith("prompts/"):
return await self.prompts_handler.handle(request)
❌ ПЛОХО: Нет валидации
async def search(query: str):
return await db.search(query)
✅ ХОРОШО: Валидация схемы и бизнес-правил
async def search(query: str, limit: int = 10):
# Валидация схемы (JSON Schema)
if not isinstance(query, str):
raise ValueError("query must be a string")
# Бизнес-валидация
if len(query) < 2:
raise ValueError("query must be at least 2 characters")
if limit > 100:
raise ValueError("limit cannot exceed 100")
return await db.search(query, limit)❌ ПЛОХО: Проглатывание ошибок
async def get_resource(uri):
try:
return await fetch(uri)
except:
return None
✅ ХОРОШО: Явная обработка с информативными ошибками
async def get_resource(uri):
try:
return await fetch(uri)
except ResourceNotFoundError as e:
raise MCPError(
code=-32001,
message=f"Resource not found: {uri}",
data={"uri": uri}
)
except PermissionError as e:
raise MCPError(
code=-32003,
message=f"Access denied: {uri}"
)
except Exception as e:
logger.exception(f"Internal error fetching {uri}")
raise MCPError(
code=-32603,
message="Internal error",
data=str(e)
)❌ ПЛОХО: Нет логирования
async def handle_request(request):
response = await process(request)
return response
✅ ХОРОШО: Структурированное логирование
import logging
import time
logger = logging.getLogger(__name__)
async def handle_request(request):
start_time = time.time()
request_id = generate_uuid()
logger.info(
"MCP request started",
extra={
"request_id": request_id,
"method": request.method,
"params": sanitize_params(request.params)
}
)
try:
response = await process(request)
duration = time.time() - start_time
logger.info(
"MCP request completed",
extra={
"request_id": request_id,
"method": request.method,
"duration_ms": duration * 1000
}
)
return response
except Exception as e:
logger.exception(
"MCP request failed",
extra={
"request_id": request_id,
"method": request.method
}
)
raise| Концепция | Суть |
|---|---|
| JSON-RPC 2.0 | Формат сообщений MCP |
| Сессия | Инициализация → Работа → Завершение |
| Capabilities | Сервер сообщает о возможностях при инициализации |
| Resources API | resources/list, resources/read, resources/subscribe |
| Tools API | tools/list, tools/call |
| Prompts API | prompts/list, prompts/get |
| Уведомления | Сообщения без id, не требуют ответа |
| Транспорты | stdio (локально), SSE/WebSocket (сеть) |
Следующая тема: Установка и настройка — установка Python SDK, настройка окружения, подготовка инструментов разработки.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.