Главная / Статьи / Практические заметки: как работает MCP: подробный обзор с примерами кода

Практические заметки: как работает MCP: подробный обзор с примерами кода

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

3070 слов

В этом руководстве пошагово описывается путь от сырья до рабочей системы для проекта «Как работает MCP: углубленный анализ с примерами кода». Основное внимание уделяется выполнимым шагам, четким проверкам и коду, который можно просто добавить в репозиторий без необходимости догадываться о его назначении. На этапе обзора необходимо определить входные данные, ответственного за выполнение шага и критерии завершения перед внесением изменений в код. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии системы. Рядом с функциональными результатами следует записывать время выполнения и стоимость токенов или запросов. Отображение затрат с самого начала помогает избежать неожиданных расходов при переходе от демо-среды к общедоступным средам.

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {}
}
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "search_web",
        "description": "Search the web for a given query",
        "inputSchema": {
          "type": "object",
          "properties": {
            "query": { "type": "string" }
          },
          "required": ["query"]
        }
      }
    ]
  }
}
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "search_web",
    "arguments": {
      "query": "latest news on MCP protocol"
    }
  }
}
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Anthropic released MCP in Nov 2024 as an open standard..."
      }
    ]
  }
}

Что такое FastMCP?

При работе над этапом «Что такое FastMCP» сначала запишите контракт: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой чек-лист поможет сохранять честность при последующих изменениях кода. Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь код. Фиксируйте название инструмента, хэш аргументов, время задержки и результат каждого вызова. Без такой записи отладка циклов агента занимает часы.

from fastmcp import FastMCP

mcp = FastMCP("My Server")   # creates the server

@mcp.tool()                  # registers the function as an MCP tool
def add(a: float, b: float) -> float:
    """Add two numbers."""    # docstring → tool description sent to the LLM
    return a + b             # type hints → JSON Schema sent to the LLM

mcp.run()                    # starts the stdio message loop

Три примитива MCP

При работе над этапом «Три примитива MCP» сначала запишите условия работы: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой чек-лист поможет сохранять честность при последующих изменениях кода. Документируйте одновременно успешный сценарий работы и сценарий восстановления. Повторные попытки, проверки человеком и обработка неработоспособных сообщений являются частью продукта, а не элементами последующей доработки. Фиксируйте название инструмента, хеш аргументов, время задержки и результат каждого вызова. Без такой записи отладка агента занимает часы.

# servers/math_server.py

@mcp.tool()
def divide(a: float, b: float) -> float:
    """Divide a by b. Raises an error if b is zero."""
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b
# servers/math_server.py

@mcp.resource("math://constants")
def get_math_constants() -> str:
    """Common mathematical constants."""
    return f"π = {math.pi}\n e = {math.e}\n ..."

@mcp.resource("math://formulas/{category}")
def get_formulas(category: str) -> str:
    """Retrieve mathematical formulas by category (geometry | algebra | statistics)."""
    catalog = {
        "geometry": (
            "Geometry Formulas:\n"
            "  Circle area:            A = π × r²\n"
            "  Circle circumference:   C = 2π × r\n"
            "  Rectangle area:         A = length × width\n"
            "  Triangle area:          A = (base × height) / 2\n"
            "  Sphere volume:          V = (4/3) × π × r³\n"
        ),
        "algebra": (
            "Algebra Formulas:\n"
            "  Quadratic formula:      x = (−b ± √(b²−4ac)) / 2a\n"
            "  Difference of squares:  a²−b² = (a+b)(a−b)\n"
            "  Perfect square:         (a+b)² = a²+2ab+b²\n"
            "  Sum of arithmetic seq:  S = n(a₁+aₙ)/2\n"
        ),
        "statistics": (
            "Statistics Formulas:\n"
            "  Mean:       μ = Σx / n\n"
            "  Variance:   σ² = Σ(x−μ)² / n\n"
            "  Std Dev:    σ = √(Σ(x−μ)² / n)\n"
            "  Z-score:    z = (x−μ) / σ\n"
        ),
    }
    return catalog.get(
        category,
        f"Unknown category '{category}'. Available: geometry, algebra, statistics",
    )
# servers/math_server.py

@mcp.prompt()
def math_tutor(difficulty: str = "intermediate") -> str:
    return (
        f"You are an expert math tutor for {difficulty}-level students. "
        "Break every problem into numbered steps..."
    )

Краткое описание сервера

При работе над этапом обзора сервера сначала запишите условия контракта: необходимые входные данные, сигнал успешного выполнения и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Предпочитайте небольшие, тестируемые модули большим скриптам. Когда какой-то шаг терпит неудачу, ошибка должна указывать на конкретную ответственность, а не на запутанную цепочку операций. Зафиксируйте название инструмента, хеш аргументов, время задержки и результат каждого вызова. Без такой информации отладка занимает часы.

С точки зрения клиента

При работе над этапом «От клиента» сначала запишите условия контракта: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой чек-лист поможет избежать ошибок при последующих изменениях кода. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Укажите названия элементов, определите критерии успешности и не допускайте молчаливого частичного выполнения задачи. Фиксируйте название инструмента, хеш аргументов, время задержки и результат каждого вызова. Без такой записи отладка занимает гораздо больше времени.

You type a query
      │
      ▼
main.py                    ← entry point, parses args, kicks off async loop
      │
      ▼
client/agent.py            ← spawns 3 MCP servers, builds the agent, invokes it
      │           │
      │           ▼
      │    utils/tracker.py ← fires on every LLM call, tool call, and result
      │
      ▼
LangGraph ReAct loop       ← think → call tool → observe → repeat
      │
      ▼
servers/{math,text,data}_server.py   ← each runs as an isolated subprocess

Шаг 1 — точка входа

При работе над первым шагом, этапом ввода, сначала запишите условия контракта: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой чек-лист помогает сохранять честность при последующих изменениях кода. Рядом с функциональными результатами записывайте время выполнения и стоимость токенов или запросов. Отслеживание затрат с самого начала предотвращает неожиданные счета при переходе от демо-среды к общедоступным средам. Для каждого вызова фиксируйте название инструмента, хэш аргументов, время задержки и результат. Без такой записи отладка агента занимает часы.

async def _run(queries: list) -> None:
    from client.agent import run_query   # imported here (late) to keep startup fast
    for i, q in enumerate(queries):
        await run_query(q)

def main() -> None:
    ...
    asyncio.run(_run(queries))
Generate 8 random numbers between 5 and 50 using seed=42,
then calculate their statistics, and tell me if there are any outliers.
python3 main.py --query "Generate 8 random numbers between 5 and 50 using seed=42, \
then calculate their statistics, and tell me if there are any outliers."
╭────────────────────────────── 🔌  MCP Example ───────────────────────────────╮
│ Multi-Server MCP Demo                                                        │
│                                                                              │
│ Three FastMCP servers, each exposing tools + resources + prompts:            │
│   ●  Math Server   — add, subtract, multiply, divide, power, sqrt,           │
│ percentage                                                                   │
│   ●  Text Server   — count_words, word_frequency, reverse, transform,        │
│ extract_emails                                                               │
│   ●  Data Server   — generate_numbers, calculate_statistics, find_outliers,  │
│ normalise                                                                    │
│                                                                              │
│ Stack : FastMCP · LangChain · LangGraph · OpenAI · Rich                      │
│ Track : live Rich panels + JSONL log files under logs/                       │
╰──────────────────────────────────────────────────────────────────────────────╯

Шаг 2 — Создание агента

При работе над шагом 2 «Создание сцены» сначала запишите условия контракта: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь код. Фиксируйте название инструмента, хеш аргументов, время задержки и результат каждого вызова. Без такой записи отладка агента занимает часы.

log_file = str(LOGS_DIR / f"run_{int(time.time())}.jsonl")
tracker = MCPTracker(log_file=log_file)
╭──── 💬  USER QUERY ────╮
│ Generate 8 random...   │
╰────────────────────────╯
def _server_config() -> Dict[str, Any]:
    silent_env = {**os.environ, "FASTMCP_LOG_LEVEL": "ERROR"}
    return {
        "math_server": {
            "command": sys.executable,
            "args": ["servers/math_server.py"],
            "transport": "stdio",
            "env": silent_env,
        },
        "text_server": { ... },
        "data_server":  { ... },
    }
client = MultiServerMCPClient(_server_config())
tools = await client.get_tools()
Connected to 3 servers (math_server, text_server, data_server) with 19 tools: add, subtract, multiply,
divide, power, square_root, calculate_percentage, count_words, word_frequency, reverse_text,
transform_case, find_and_replace, extract_emails, count_vowels_consonants, generate_numbers,
calculate_statistics, find_outliers, sort_values, normalize_values
model = ChatOpenAI(model=model_name, temperature=0)
agent = create_react_agent(model, tools)
START
  │
  ▼
[call_model]  ─── no tool call ──▶  END
      │
  tool call requested
      │
      ▼
[call_tools]
      │
      ▼
[call_model]  (loop again with tool result in context)
config = {"callbacks": [tracker]}
result = await agent.ainvoke({"messages": [("human", query)]}, config=config)

Шаг 3 — Наблюдение за всем в реальном времени

При работе над этапом «Наблюдение за всем» из шага 3 сначала запишите условия контракта: необходимые входные данные, сигнал успешного выполнения и действия при частичной неудаче. Такой список поможет сохранять честность при последующих изменениях кода. Документируйте одновременно успешный и восстановительный сценарии работы. Повторные попытки, проверки со стороны человека и обработка неработоспособных сообщений являются частью продукта, а не элементами последующей доработки. Зафиксируйте название инструмента, хеш аргументов, время задержки и результат каждого вызова. Без такой записи отладка агента будет занимать часы.

agent.ainvoke() called
      │
      ├─▶ on_chain_start()       "▶ AGENT STARTED" panel
      │
      ├─▶ on_chat_model_start()  "🤖 LLM CALL #1" panel  (timer starts)
      ├─▶ on_llm_end()           "LLM responded ⏱ 1.23s → will call: add, multiply"
      │
      ├─▶ on_tool_start()        "🔧 TOOL CALL #1" panel  (timer starts)
      ├─▶ on_tool_end()          "✓ TOOL RESULT ⏱ 0.01s" panel
      │
      ├─▶ on_tool_start()        (second tool, if any)
      ├─▶ on_tool_end()
      │
      ├─▶ on_chat_model_start()  "🤖 LLM CALL #2" (LLM synthesises final answer)
      ├─▶ on_llm_end()           no tools this time → loop ends
      │
      └─▶ agent.ainvoke() returns
TOOL_SERVER_MAP = {
    "add":        "math_server",
    "count_words": "text_server",
    "generate_numbers": "data_server",
    ...
}
SERVER_COLORS = {
    "math_server": "cyan",
    "text_server":  "green",
    "data_server":  "yellow",
}
── 🤖  LLM CALL  #1  model=gpt-5-mini   messages=1 ──╮
│  Role    Content preview                             │
│  Human   Generate 8 random numbers between 5 and 50… │
╰──────────────────────────────────────────────────────╯
  LLM responded  ⏱ 5.35s  → will call: generate_numbers
{
  "name": "generate_numbers",
  "arguments": {"count": 8, "min_val": 5, "max_val": 50, "seed": 42}
}
╭── 🔧  TOOL CALL  #1 ──────────────────╮
│ Tool  :  generate_numbers             │
│ Server:  data_server                  │
│ Args  :                               │
│ {'count': 8, 'min_val': 5,            │
│  'max_val': 50, 'seed': 42}           │
╰───────────────────────────────────────╯
{"method": "tools/call", "params": {"name": "generate_numbers",
  "arguments": {"count": 8, "min_val": 5, "max_val": 50, "seed": 42}}}
╭── ✓  TOOL RESULT  ⏱ 0.63s ────────────────────────────╮
│ [33.77, 6.13, 17.38, 15.04, 38.14, 35.45, 45.15, 8.91] │
╰──────────────────────────────────────────────────────────╯
random.seed(42)
return [round(random.uniform(5, 50), 2) for _ in range(8)]
# → [33.77, 6.13, 17.38, 15.04, 38.14, 35.45, 45.15, 8.91]
╭── 🤖  LLM CALL  #2  model=gpt-5-mini   messages=3 ──╮
│  Role    Content preview                             │
│  Human   Generate 8 random numbers…                 │
│  AI      (the tool-call decision)                    │
│  Tool    [33.77, 6.13, 17.38, ...]                   │
╰──────────────────────────────────────────────────────╯
Tool  : calculate_statistics
Server: data_server
Args  : {'numbers': [33.77, 6.13, 17.38, 15.04, 38.14, 35.45, 45.15, 8.91]}
Result: {"count":8, "min":6.13, "max":45.15, "mean":24.9962,
         "median":25.575, "std_dev":14.8181, "variance":219.5755,
         "range":39.02, "q1":15.04, "q3":38.14}
Tool  : find_outliers
Server: data_server
Args  : {'numbers': [...], 'threshold': 2}
Result: {"outliers": [], "outlier_count": 0,
         "total_checked": 8, "mean": 24.9962, "std_dev": 14.8181}
╭── 🤖  LLM CALL  #4  messages=7 ──╮
│  Human / AI / Tool                │
│  AI    / Tool                     │
│  AI    / Tool                     │
╰───────────────────────────────────╯
  LLM responded  ⏱ 30.25s
╭── ✅  FINAL ANSWER ───────────────────────────────╮
│ Random numbers: [33.77, 6.13, 17.38, ...]        │
│ Mean: 24.9962 / Median: 25.575 / Std Dev: 14.8181│
│ Outliers: none (all |z| < 2)                     │
╰───────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────╮
│ LLM Calls   4                                       │
│ Tool Calls  3                                       │
│ Total Time  42.25s                                  │
│ Log File    logs/run_1776864980.jsonl               │
╰─────────────────────────────────────────────────────╯

Полная последовательность сообщений

При работе над этапом полной последовательности сообщений сначала запишите условия взаимодействия: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Предпочитайте небольшие, тестируемые модули большим скриптам. Если какой-то шаг не сработает, причина должна быть связана с конкретной функцией, а не с запутанной цепочкой операций. Записывайте название инструмента, хеш аргументов, время задержки и результат каждого вызова. Без такой информации отладка занимает много времени. При работе над этапом полной последовательности сообщений сначала запишите условия взаимодействия: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Регистрируйте время выполнения и стоимость токенов или запросов рядом с функциональными результатами. Отслеживание затрат с самого начала предотвращает неожиданные расходы при переходе с демо-среды в общедоступные среды.

1  HumanMessage   "Generate 8 random numbers…"
2  AIMessage      [tool_call: generate_numbers({count:8, min:5, max:50, seed:42})]
3  ToolMessage    [33.77, 6.13, 17.38, 15.04, 38.14, 35.45, 45.15, 8.91]
4  AIMessage      [tool_call: calculate_statistics({numbers:[...]})]
5  ToolMessage    {count:8, mean:24.9962, std_dev:14.8181, ...}
6  AIMessage      [tool_call: find_outliers({numbers:[...], threshold:2})]
7  ToolMessage    {outliers:[], outlier_count:0, ...}
8  AIMessage      "Here are the results. Random numbers: …"   ← final answer

Журнал в формате JSONL

Этап логирования в формате JSONL работает наилучшим образом, когда его рассматривают как измеримую структуру. Соберите один идеальный пример работы, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объёма обработки. Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь код. Обеспечьте доступ к инструментам с узкими схемами и чёткими метками о побочных эффектах. Хостам необходимо знать, какие вызовы изменяют состояние, прежде чем они автоматически одобрят их.

{"timestamp": "...", "event": "agent_start",  "data": {"runnable": "LangGraph", "run_id": "..."}}
{"timestamp": "...", "event": "llm_call",     "data": {"call_number": 1, "model": "gpt-5-mini", "messages": 1}}
{"timestamp": "...", "event": "llm_end",      "data": {"elapsed": "5.35s", "tool_calls_requested": ["generate_numbers"]}}
{"timestamp": "...", "event": "tool_start",   "data": {"call_number": 1, "tool": "generate_numbers", "server": "data_server", "args": "..."}}
{"timestamp": "...", "event": "tool_end",     "data": {"elapsed": "0.63s", "output_preview": "[33.77, 6.13, ...]"}}
{"timestamp": "...", "event": "llm_call",     "data": {"call_number": 2, "model": "gpt-5-mini", "messages": 3}}
...
{"timestamp": "...", "event": "run_summary",  "data": {"llm_calls": 4, "tool_calls": 3, "total_elapsed": "42.25s"}}

Источники

Этап «Источники» работает наилучшим образом, когда его рассматривают как измеримую поверхность. Соберите один идеальный пример работы, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объёма работ. Документируйте одновременно успешный и восстановительный пути выполнения. Повторные попытки, проверки человеком и обработка неработающих сообщений являются частью продукта, а не этапом последующей доработки. Обеспечьте доступ к инструментам с узкими схемами и чёткими метками побочных эффектов. Администраторам необходимо знать, какие вызовы изменяют состояние, прежде чем они автоматически одобрят их.

Сообщение от нашего основателя

Сообщение типа The A с нашей площадки работает наилучшим образом, когда рассматривается как измеримая поверхность. Соберите один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объема работ. Предпочитайте небольшие, тестируемые единицы кода вместо обширных скриптов. Когда какой-то шаг сбивается, причина сбоя должна указывать на конкретную ответственность, а не на запутанную цепочку операций. Используйте инструменты с узкими схемами и четкими метками о побочных эффектах. Администраторам необходимо знать, какие вызовы изменяют состояние системы, прежде чем они автоматически одобрят операцию. Сообщение типа The A с нашей площадки работает наилучшим образом, когда рассматривается как измеримая поверхность. Соберите один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объема работ. Записывайте время выполнения и стоимость токенов или запросов рядом с функциональными результатами. Отображение стоимости на раннем этапе предотвращает неожиданные счета при переходе от демо-среды к общедоступным средам.

Чек-лист операций

Этап проверочного списка операций работает наилучшим образом, когда рассматривается как измеримая основа. Соберите один идеальный пример выполнения, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объема работ.

Рассматривайте этот этап как контракт между входными данными и проверенными результатами. Дайте названия создаваемым элементам, определите критерии успеха и не соглашайтесь на молчаливое частичное выполнение задач.

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

При наличии бюджета добавляйте тест на базовую работоспособность, который проверяет критический путь в системе CI с использованием фикстчеров, а не реальных платных API.

Записывайте время выполнения и стоимость токенов или запросов рядом с функциональными результатами. Отображение стоимости заранее предотвращает неожиданные счета при переходе от демо-среды к общедоступным средам.

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

Перед переходом на новую стек-архитектуру заморозьте версии, сохраните эталонный отчет для критически важных этапов и уточните шаги отката. В совместных средах требуются ограничения по частоте запросов, проверки принадлежности и четко определенный ответственный за обновление секретов. Лучше выбирать простую надежность, чем креативные одноразовые демонстрации.

Примечание для c7efc4f69698: не храните ключи поставщика в репозитории, установите лимит токенов на сессию и сохраняйте отчеты рядом с фикстурами для оценки, чтобы последующие замены моделей оставались сопоставимыми.

Для этапа 0 записки по укреплению безопасности необходимо определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии системы. Необходимо задокументировать как успешный, так и аварийный сценарии работы. Повторные попытки, проверки человеком и обработка неработоспособных сообщений являются частью продукта, а не элементами последующей доработки.

Подробности укрепления безопасности 0/888: измеряйте время выполнения, класс ошибок и расход токенов для данной записки, затем принимайте решение о сохранении изменений на основе фиксированного набора критериев, а не на основе устных оценок.

При работе над первым этапом записки по укреплению безопасности сначала запишите контракт: необходимые входные данные, сигнал успешного выполнения и действия при частичной неудаче. Такой чек-лист поможет сохранять честность при последующих изменениях кода. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Дайте названия соответствующим элементам, определите критерии успешности и не допускайте молчаливого частичного выполнения задач.

Подробность укрепления безопасности 1/888: измерьте время выполнения, класс ошибки и расход токенов для данной записки, затем решите, следует ли сохранять изменение на основе фиксированного набора критериев, а не на основе единичных примеров.

Второй этап записки по укреплению безопасности работает наилучшим образом, когда его рассматривают как измеримую область. Соберите один эталонный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объема работ. Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, которое операторы могут проверять, не читая весь кодовый граф.

Подробности усиления безопасности 2/888: измерьте время выполнения, класс ошибки и расход токенов для этой записи, затем решите, следует ли сохранить изменения на основе фиксированного набора вопросов, а не на основе единичных примеров.

На третьем этапе работы над усилением безопасности определите входные данные, ответственного за шаг и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Предпочтительнее использовать небольшие, проверяемые единицы кода вместо обширных скриптов. При сбое шага он должен указывать на конкретную причину, а не на запутанную структуру обработки данных.

Подробности усиления безопасности 3/888: измерьте время выполнения, класс ошибки и расход токенов для этой записи, затем решите, следует ли сохранить изменения на основе фиксированного набора вопросов, а не на основе единичных примеров.