Перейти к содержимому
MCP на практике: как построить инструменты для AI-агентов
6 мин чтения

MCP на практике: как построить инструменты для AI-агентов

MCPModel Context ProtocolAI agentsTypeScriptPythondeveloper toolsopen standardClaudeagent infrastructure

USB для AI-агентов: зачем нужен MCP

Представьте, что каждый принтер требует собственного уникального кабеля. Именно в такой ситуации находилась индустрия AI-агентов до появления MCP. Каждый агент реализовывал собственную интеграцию с каждым инструментом. N агентов × M инструментов = N×M уникальных интеграций. Это не масштабируется.

Model Context Protocol (MCP) — это открытый стандарт, созданный Anthropic и переданный в Linux Foundation, который решает эту проблему. MCP определяет единый протокол взаимодействия между AI-агентами (клиентами) и внешними инструментами (серверами). Один стандарт — и любой агент может работать с любым инструментом.

Цифры говорят сами за себя: 97 миллионов загрузок SDK в месяц, более 10 000 активных серверов, и первоклассная поддержка со стороны ChatGPT, Claude, Cursor, Gemini, Microsoft Copilot и VS Code. MCP — один из самых быстрорастущих открытых стандартов в истории AI.

Архитектура MCP: три слоя

MCP построен на клиент-серверной архитектуре с тремя ключевыми компонентами:

1. Host (хост)

Приложение, в котором работает AI-агент: Claude Desktop, VS Code, Cursor, или ваше собственное приложение. Хост управляет жизненным циклом MCP-клиентов и обеспечивает безопасность.

2. Client (клиент)

MCP-клиент создаётся хостом и поддерживает соединение 1:1 с конкретным MCP-сервером. Клиент отвечает за обнаружение доступных инструментов, отправку запросов и обработку ответов.

3. Server (сервер)

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

  • Tools (инструменты) — функции, которые агент может вызывать: запросы к базе данных, отправка сообщений, работа с файлами
  • Resources (ресурсы) — данные, доступные для чтения: файлы, записи из базы данных, конфигурации
  • Prompts (промпты) — шаблоны промптов, которые сервер предлагает клиенту для типовых задач
┌─────────────────────────────────────────┐
│              Host (Claude, VS Code)      │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐ │
│  │Client A │  │Client B │  │Client C │ │
│  └────┬────┘  └────┬────┘  └────┬────┘ │
└───────┼────────────┼────────────┼───────┘
        │            │            │
   ┌────▼────┐  ┌────▼────┐  ┌───▼─────┐
   │ Server  │  │ Server  │  │ Server  │
   │ (DB)    │  │ (Slack) │  │ (Files) │
   └─────────┘  └─────────┘  └─────────┘

Транспорт: как клиент и сервер общаются

MCP поддерживает два основных транспортных механизма:

stdio (Standard I/O)

Клиент запускает серверный процесс и общается с ним через stdin/stdout. Идеально для локальных инструментов — Claude Desktop и VS Code используют именно этот режим. Минимальная задержка, нулевая конфигурация сети.

Streamable HTTP

Новый транспорт, заменивший SSE (Server-Sent Events). Поддерживает полноценную двустороннюю коммуникацию по HTTP. Подходит для облачных деплоев — AWS Lambda, Google Cloud Run, любая serverless инфраструктура. Работает через корпоративные прокси и файрволы.

// stdio — локальный сервер
{
  "mcpServers": {
    "weather": {
      "command": "node",
      "args": ["./weather-server/index.js"]
    }
  }
}

// Streamable HTTP — удалённый сервер
{
  "mcpServers": {
    "analytics": {
      "url": "https://analytics.example.com/mcp"
    }
  }
}

Пишем MCP-сервер на TypeScript

Создадим практический MCP-сервер — менеджер задач, который позволяет AI-агенту работать с to-do списком.

Шаг 1: Инициализация проекта

mkdir mcp-todo-server
cd mcp-todo-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --init

Шаг 2: Определяем сервер и инструменты

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// Хранилище задач (в продакшне — база данных)
interface Todo {
  id: string;
  title: string;
  done: boolean;
  createdAt: string;
}
const todos: Map<string, Todo> = new Map();

// Инициализируем MCP-сервер
const server = new McpServer({
  name: "todo-manager",
  version: "1.0.0",
});

// Инструмент: добавить задачу
server.tool(
  "add_todo",
  "Add a new todo item",
  { title: z.string().describe("The todo item title") },
  async ({ title }) => {
    const id = crypto.randomUUID();
    const todo: Todo = {
      id,
      title,
      done: false,
      createdAt: new Date().toISOString(),
    };
    todos.set(id, todo);
    return {
      content: [{ type: "text", text: \`Created todo: \${title} (id: \${id})\` }],
    };
  }
);

// Инструмент: получить все задачи
server.tool(
  "list_todos",
  "List all todo items",
  {},
  async () => {
    const items = Array.from(todos.values());
    if (items.length === 0) {
      return { content: [{ type: "text", text: "No todos yet." }] };
    }
    const list = items
      .map((t) => \`[\${t.done ? "✓" : " "}] \${t.title} (id: \${t.id})\`)
      .join("\\n");
    return { content: [{ type: "text", text: list }] };
  }
);

// Инструмент: отметить задачу как выполненную
server.tool(
  "complete_todo",
  "Mark a todo item as completed",
  { id: z.string().describe("The todo item ID to complete") },
  async ({ id }) => {
    const todo = todos.get(id);
    if (!todo) {
      return { content: [{ type: "text", text: "Todo not found." }] };
    }
    todo.done = true;
    return {
      content: [{ type: "text", text: \`Completed: \${todo.title}\` }],
    };
  }
);

// Запускаем через stdio
const transport = new StdioServerTransport();
await server.connect(transport);

Шаг 3: Конфигурация для Claude Desktop

{
  "mcpServers": {
    "todo": {
      "command": "node",
      "args": ["--loader", "ts-node/esm", "./src/index.ts"]
    }
  }
}

После этого Claude сможет создавать, просматривать и завершать задачи прямо в диалоге. AI-агент увидит доступные инструменты через MCP discovery и будет использовать их контекстно.

Пишем MCP-сервер на Python

Python-версия ещё проще благодаря FastMCP — высокоуровневому API, который использует type hints и docstrings для автоматической генерации описания инструментов.

from mcp.server.fastmcp import FastMCP
from datetime import datetime
import uuid

# Инициализируем сервер
mcp = FastMCP("todo-manager")

# Хранилище
todos: dict[str, dict] = {}


@mcp.tool()
async def add_todo(title: str) -> str:
    """Add a new todo item.

    Args:
        title: The todo item title
    """
    todo_id = str(uuid.uuid4())[:8]
    todos[todo_id] = {
        "title": title,
        "done": False,
        "created_at": datetime.now().isoformat(),
    }
    return f"Created todo: {title} (id: {todo_id})"


@mcp.tool()
async def list_todos() -> str:
    """List all todo items."""
    if not todos:
        return "No todos yet."
    lines = []
    for tid, t in todos.items():
        status = "✓" if t["done"] else " "
        lines.append(f"[{status}] {t['title']} (id: {tid})")
    return "\\n".join(lines)


@mcp.tool()
async def complete_todo(todo_id: str) -> str:
    """Mark a todo item as completed.

    Args:
        todo_id: The ID of the todo item to complete
    """
    if todo_id not in todos:
        return "Todo not found."
    todos[todo_id]["done"] = True
    return f"Completed: {todos[todo_id]['title']}"


if __name__ == "__main__":
    mcp.run(transport="stdio")

Запуск: uv run python todo_server.py или через конфигурацию Claude Desktop.

Resources и Prompts: не только инструменты

MCP — это не только tools. Два дополнительных типа возможностей делают протокол по-настоящему мощным.

Resources — предоставляем данные

Resources позволяют серверу экспонировать данные для чтения. Идеально для файлов, записей из базы данных, конфигураций.

// Статический ресурс
server.resource(
  "config",
  "config://app",
  async (uri) => ({
    contents: [{
      uri: uri.href,
      mimeType: "application/json",
      text: JSON.stringify(appConfig),
    }],
  })
);

// Динамический ресурс с шаблоном
server.resource(
  "user-profile",
  new ResourceTemplate("users://{userId}/profile", { list: undefined }),
  async (uri, { userId }) => ({
    contents: [{
      uri: uri.href,
      mimeType: "application/json",
      text: JSON.stringify(await getUser(userId)),
    }],
  })
);

Prompts — переиспользуемые шаблоны

Серверы могут предоставлять шаблоны промптов для типовых задач:

server.prompt(
  "review-code",
  { language: z.string(), code: z.string() },
  ({ language, code }) => ({
    messages: [{
      role: "user",
      content: {
        type: "text",
        text: \`Review this \${language} code for bugs and improvements:\\n\\n\${code}\