← Углубления  ·  Университет

DD — Claude Agent SDK и MCP: руками

Deep-dive к Модулю 7 («Выбор ядра»). Урок 7.2–7.3 сказал ЧТО выбрано — Claude Agent SDK как нервная система будущей ОС. Этот модуль — про то, КАК она физически устроена и как её включать руками: объявить субагента, поставить hook-заслон, подключить внешний сервис как MCP, заставить агента выдавать не «примерно JSON», а гарантированно валидный JSON. Курс до сих пор описывал механику ядра словами — здесь она превращается в код, который можно вставить в свой репозиторий сегодня вечером.


0. Зачем отдельный модуль

Курс до этого момента объяснял, ПОЧЕМУ ядром выбран именно Claude Agent SDK (Модуль 7), и КАКИЕ архитектурные паттерны на нём строятся (Модуль 2). Оба разговора шли на уровне схем и слов — «субагент изолирует контекст», «hook — детерминированный гейт». Этот модуль намеренно спускается на уровень, где ты можешь скопировать код в файл и запустить его сегодня же вечером, потому что твоя цель — не сдать экзамен по теории, а построить систему.

Ты уже де-факто пользуешься Agent SDK каждый раз, когда запускаешь claude в терминале — Claude Code и есть тонкая CLI-обёртка вокруг того же самого SDK 🟢 [подтверждено официальной документацией: Agent SDK — «инфраструктурный слой Claude Code, выставленный как библиотека»]. Разница между «работать в терминале руками» и «строить на SDK свою систему» — как разница между тем, чтобы лично осматривать каждого пациента, и тем, чтобы спроектировать протокол приёмного отделения, по которому дежурят другие врачи без тебя, по чётким инструкциям, а не по наитию. Твой оркестратор orchestrator.mjs — это как раз попытка написать такой протокол поверх claude -p, но вручную, до того, как у SDK появились нативные примитивы под каждый кусок. Всё, что ниже, — словарь и рабочие примеры для того, чтобы этот протокол дописать осознанно, используя готовые кирпичи SDK, а не изобретать их заново.

Четыре темы модуля отвечают на четыре конкретных вопроса, которые встают, как только садишься писать код, а не читать про архитектуру: «как именно я объявляю специалиста, а не пишу его руками с нуля» (субагенты), «как я гарантирую, что определённое действие НИКОГДА не проскочит мимо проверки, даже если модель ошибётся» (hooks), «как я подключаю чужой сервис, не изобретая клей заново» (MCP) и «как я гарантирую, что ответ агента — это данные, а не текст, который НАДЕЮСЬ распарсить» (structured output). Это ровно четыре места, где раньше был либо ручной код, либо голая надежда на то, что модель «в целом справится».

Термин English Медицинская аналогия Где на твоём заводе
Субагент Subagent Узкий специалист-консультант, которому даётся одно направление Будущая формализация того, что делает orchestrator.mjs вручную через subprocess
Контекстная изоляция Context isolation Специалист не читает всю карту болезни, только направление Причина, почему субагент thumbnail-studio не раздувает контекст главного агента
Hook Hook (PreToolUse/PostToolUse/Stop/...) Спинальный рефлекс — срабатывает до «осознанного» решения auto-commit-push-deploy-hook, будущий денежный гейт над resource-governor
MCP Model Context Protocol Стандартный разъём капельницы (Luer lock) вместо самодельной трубки под каждый мешок Способ подключить channel-hq/memory-bank/resource-governor как инструменты агента без ручного HTTP-клея
Structured output Structured outputs / strict tool use Бланк направления на анализы со строгими обязательными полями вместо рецепта от руки Замена ручного try/except json.loads(...) вокруг ответа QC-критика
Permission mode plan / acceptEdits / bypassPermissions Степень допуска в палату — от «только смотреть карту» до «оперировать без спроса» Реализация матрицы риска READ/WRITE/DELETE/ТРАТА (Урок 4.1)

1. Субагенты на пальцах

По-человечески. Представь консилиум: ты (главный агент) не читаешь сам всю историю болезни на 400 страниц — ты вызываешь узкого специалиста, даёшь ему ОДНО направление («посмотри снимки, скажи, есть ли перелом»), и он возвращается с одним заключением. Ты не видишь, как именно рентгенолог листал снимки, сколько раз пересматривал — тебе нужен только вывод. Это и есть субагент: у него свой изолированный кабинет (контекст), своё оборудование (набор инструментов), и наружу выходит только итоговое заключение.

Как называется по-настоящему (English): subagent — обычный дочерний вызов query()/ClaudeSDKClient, который главный агент делает через встроенный инструмент Agent (в коде иногда виден как Task).

Как объявить. Субагент — это словарь agents={} в опциях, где у каждого — обязательные description (когда его вызывать) и prompt (кто он), и необязательные tools, model, maxTurns, skills, background, effort:

from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition

options = ClaudeAgentOptions(
    allowed_tools=["Read", "Grep", "Glob", "Agent"],
    agents={
        "qc-critic": AgentDefinition(
            description="Смотрит финальный ролик и решает publish/reject по чек-листу",
            prompt="Ты независимый QC-критик. Тебе НЕ показывают рассуждения генератора — только артефакт и чек-лист.",
            tools=["Read"],           # намеренно урезанный набор — ему не нужен Bash
            model="haiku",            # механическая проверка по чек-листу — дешёвая модель
            maxTurns=6,
        ),
    },
)

Это буквально формализация того, что у тебя уже есть в thumbnail-studio — только там роль критика зашита в отдельный сервис, а здесь она — строка конфига одного и того же SDK.

Что субагент реально наследует от родителя, а что нет 🟢 [дословно из официальной документации Anthropic, code.claude.com/docs/en/agent-sdk/subagents]:

Наследует НЕ наследует
Свой собственный system prompt Историю разговора родителя
CLAUDE.md проекта Результаты инструментов родителя
Определения инструментов (или подмножество из tools) System prompt родителя
MCP-конфигурацию (по умолчанию, если не переопределена; официально явно не прописано, помечаю жёлтым) 🟡 Preloaded-контент skills, если явно не указан в skills

Это и есть контекстная изоляция из Модуля 5 (токеномика) — не абстракция, а буквально то, что в контекстное окно субагента не попадает 400-страничная история родителя, только направление-промпт.

Глубина вложенности. Субагент может сам порождать субагентов, но максимум 5 уровней — на пятом дальше спавнить нельзя 🟢 [дословно: «A subagent five levels below the main agent can't spawn further subagents», code.claude.com/docs/en/agent-sdk/subagents, версия v2.1.172+]. Это ровно тот «иммунный сигнал не каскадирует бесконтрольно» из глоссария курса — предохранитель от бесконечного дробления задачи на под-под-под-агентов, которое сожгло бы бюджет незаметно для тебя.

Как строить дешёвые волны (parallel fan-out). Время параллельного веера = время самого медленного участника, а не сумма — но СТОИМОСТЬ растёт с числом параллельных субагентов линейно (это токены, не время). Три рычага, чтобы волна была дешёвой, а не «монстром за $1000/мин»:

  1. Урезай tools под каждого — критику не нужен Bash, разведчику не нужен Write. Меньше инструментов в системном промпте субагента = меньше токенов на каждый его ход.
  2. Ставь model="haiku" там, где работа механическая (проверка по чек-листу, парсинг, сверка формата) — это прямое применение лестницы моделей из Модуля 5.
  3. Используй background=True там, где не нужен немедленный ответ — субагент запускается как non-blocking задача, и родитель может параллельно делать другое, не блокируясь на ожидании. Начиная с v2.1.198 субагенты по умолчанию и так работают в фоне 🟢 [заявлено в документации SDK].

Для твоего веера MVP (Модуль 2.3, консультант-фабрика) это прямая инструкция: N explorer-агентов с разными prompt (разные приоритеты — перф/DX/дёшево) как отдельные записи в agents={}, вызванные параллельно через один Agent-вызов на каждого, а не 10 последовательных.

Role Panels — тот же примитив, другая цель. Вместо N explorer-агентов, которые делают N РАЗНЫХ вариантов, можно объявить N агентов, которые смотрят на ОДИН и тот же артефакт с разных углов одновременно — это и есть паттерн Role Panel из Модуля 2.1: не «сгенерируй мне ещё вариант», а «оцени этот вариант как security-сканер, как проверка стиля, как оценка покрытия тестами» параллельно. Для завода это ровно QC-петля thumbnail-studio, только явно разложенная на отдельных субагентов вместо одного монолитного критика:

options = ClaudeAgentOptions(
    allowed_tools=["Read", "Agent"],
    agents={
        "hook-judge": AgentDefinition(description="Оценивает хук ролика", prompt="...", tools=["Read"]),
        "fact-judge": AgentDefinition(description="Факт-чек по сценарию", tools=["Read", "WebSearch"], prompt="..."),
        "copyright-judge": AgentDefinition(description="Проверка на copyright-риски", tools=["Read"], prompt="..."),
    },
)

Три судьи запускаются одним ходом главного агента, каждый в своём изолированном контексте — это и есть разнородная судейская панель Урока 2.3, а не три копии одного и того же промпта.

Живой якорь: открой channel-hq/core/orchestrator.mjs и найди место, где он спавнит дочерние процессы claude -p — сравни с agents={} выше: это тот же паттерн «направление + изолированный контекст», просто у тебя он написан вручную через subprocess, а не через нативный Agent-инструмент SDK.

Для любопытных. Субагента можно resume — продолжить его сессию с того же места, передав resume=session_id в опции нового query(). Это полезно для долгих исследований: субагент останавливается на промежуточном чекпоинте (например, дошёл до середины аудита 20 файлов), а следующий вызов возобновляет его РОВНО с этой точки, не пересобирая контекст с нуля. Есть и «динамическая конфигурация» — AgentDefinition можно собирать программно, функцией, а не статическим словарём: например, ты можешь на лету выбирать model="opus" для строгого судьи в турнире (Модуль 2.3) и model="sonnet" для мягкого — оба явно перечислены в agents={}, но их prompt и model генерируются кодом, а не прописаны вручную для каждого судьи. Обнаружить сам факт вызова субагента можно, слушая ToolUseBlock с именем Task/Agent в потоке сообщений — удобно для логирования в memory-bank, кто из субагентов когда отработал.

Ограничения, которые стоит знать заранее: субагент не может САМ запросить у пользователя подтверждение во время работы (делегирование годится для разведки/исполнения, но не для шагов, требующих твоего «да» посреди процесса — такие шаги должны оставаться на уровне родителя); нет отдельного wall-clock таймаута на субагента, только maxTurns — то есть субагент, который зациклился внутри одного хода (например, застрял в долгом Bash-вызове), не остановится по времени сам, только по числу ходов.

🔬 Что видит субагент, а что — нет

Жми по пунктам — узнай, наследует ли субагент это от родителя.

Выбери пункт выше.

2. Hooks: детерминированные гейты

По-человечески. Спинальный рефлекс: рука отдёргивается от горячего ДО того, как сигнал вообще доходит до мозга и превращается в решение. Hook — то же самое для агента: код срабатывает НЕ дожидаясь, пока модель «подумает и решит», а перехватывает событие механически, по жёсткому правилу.

Как называется по-настоящему: hooks — колбэки на события жизненного цикла: PreToolUse (до вызова инструмента, может заблокировать), PostToolUse, Stop, SessionStart/SessionEnd, SubagentStart/SubagentStop, PreCompact, PermissionRequest.

from claude_agent_sdk import ClaudeAgentOptions, HookMatcher

async def block_env_writes(input_data, tool_use_id, context):
    path = input_data["tool_input"].get("file_path", "")
    if path.endswith(".env"):
        return {"hookSpecificOutput": {
            "hookEventName": input_data["hook_event_name"],
            "permissionDecision": "deny",
            "permissionDecisionReason": "Ночной билдер не трогает боевые .env",
        }}
    return {}

options = ClaudeAgentOptions(
    hooks={"PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[block_env_writes])]}
)

Это НЕ промпт-инструкция «пожалуйста, не трогай .env» — это код, который физически возвращает deny до того, как инструмент вообще выполнился. Модель не может «передумать» или «забыть» правило — она может только попытаться, и попытка технически отклоняется. Именно так должен быть устроен твой денежный гейт (закон №1 из красной линии профиля): не как строчка в системном промпте, а как PreToolUse-hook над операцией траты, который физически блокирует вызов, если сумма выше порога, и требует явного подтверждения через Telegram — то есть код, а не обещание модели. Набросок того же гейта, но для траты, а не для файла:

DAILY_BUDGET_USD = 15.0

async def budget_gate(input_data, tool_use_id, context):
    if input_data["tool_name"] != "mcp__resource-governor__spend":
        return {}
    amount = input_data["tool_input"].get("amount_usd", 0)
    spent_today = await get_spent_today()  # читает счётчик из memory-bank
    if spent_today + amount > DAILY_BUDGET_USD:
        await notify_telegram(f"Требуется подтверждение: трата ${amount} превысит дневной лимит")
        return {"hookSpecificOutput": {
            "hookEventName": input_data["hook_event_name"],
            "permissionDecision": "ask",
            "permissionDecisionReason": "Дневной бюджет исчерпан, нужно ручное 'да'",
        }}
    return {}

Три вещи в этом наброске стоит подчеркнуть: во-первых, решение "ask", а не жёсткий "deny" — потому что закон профиля не «нельзя тратить вообще», а «нельзя тратить БЕЗ ПОДТВЕРЖДЕНИЯ», то есть гейт должен уметь пропускать после ручного «да», а не блокировать навсегда. Во-вторых, счётчик spent_today живёт вне контекста агента — во внешнем хранилище (memory-bank/резервуар resource-governor), а не в переменной внутри сессии, потому что сессия может оборваться, а бюджет должен быть виден следующей. В-третьих, это ровно один и тот же hook, независимо от того, какой субагент или какая волна вызвала spend — гейт стоит НАД механикой вызова, а не внутри каждого промпта.

Важная деталь про приоритет: deny-hook применяется ДО обычных permission rules, но explicit deny-правила в settings.json всё равно имеют финальный приоритет над allow — то есть hooks расширяют матрицу разрешений, а не заменяют её насмерть 🟢 [дословно из документации: «Deny rules ВСЕ РАВНО имеют приоритет»].

Живой якорь: твой Stop-хук auto-commit-push-deploy-hook (описан в DD-азбуке, §2) — уже живой пример hook в проде: он срабатывает на событие Stop (агент закончил сессию) и автоматически коммитит/пушит/деплоит без твоего участия. Открой его код и найди, на каком событии он висит — теперь у этого есть официальное имя.

Полный список событий, на которые можно повесить hook — не только PreToolUse/Stop: PostToolUse (после выполнения — логировать каждое изменение файла), PostToolUseFailure (инструмент упал с ошибкой), UserPromptSubmit (можно незаметно подмешать дополнительный контекст в промпт, например текущий баланс бюджета), SubagentStart/SubagentStop (отслеживать, какие субагенты когда отработали и что вернули — прямая замена ручного логирования в orchestrator.mjs), PreCompact (агент вот-вот сожмёт историю — последний шанс сохранить полный transcript на диск перед тем, как детали исчезнут), PermissionRequest (кастомная логика вместо стандартного диалога подтверждения). Практический приём для завода: SubagentStop-hook, который при каждом завершении QC-критика пишет результат прямо в memory-bank — это ровно то самое замыкание стадии LEARNING (Модуль 3.4), реализованное не отдельным сервисом, а одной функцией-колбэком.

Порядок разрешения действия, если несколько механизмов противоречат друг другу: hooks → deny-правила → ask-правила → permission_mode → allow-правила → callbacks в коде 🟢 [из документации SDK] — то есть блокирующий hook (exit code 2 в shell-варианте, либо permissionDecision: "deny" в колбэке) отрабатывает раньше, чем система вообще посмотрит на settings.json. Это важно для денежного гейта: даже если кто-то (ты сам по невнимательности, или сам агент в рассуждении) пропишет permissions.allow на трату — hook всё равно успевает сработать первым и остановить операцию.

💸 Мини-симулятор: budget_gate из примера выше

Дневной бюджет DAILY_BUDGET_USD = 15.0. Двигай ползунки — смотри, какое решение вернёт hook.


Структурный вывод + hook = связка, а не два разных инструмента

Самая сильная комбинация для ночной автономии — не structured output САМ ПО СЕБЕ и не hook сам по себе, а оба вместе на одном решении. Пример полного цикла публикации ролика: агент вызывает строгий инструмент decide_publish с гарантированной схемой (publish: bool, reasons: list[str], confidence: float) — то есть его решение физически не может прийти в виде расплывчатого текста; а PreToolUse-hook над самим вызовом реальной публикации (mcp__channel-hq__upload) читает именно это структурированное решение и решает, пропускать ли вызов дальше, требовать ли ручного подтверждения при низком confidence, или блокировать при publish: false. Модель отвечает за суждение (нужен ли контент), код — за то, что происходит с этим суждением дальше. Ни одна из двух частей не может подменить другую: если бы решение было текстом, hook не смог бы надёжно его распарсить в критический момент; если бы гейта не было, structured output гарантировал бы только ФОРМУ решения, но не то, что решение реально на что-то влияет.

3. Settings: где живут правила игры

Коротко, потому что это в основном конфигурация, не архитектура. Четыре уровня, от самого сильного к самому слабому: managed (для организаций, нельзя переопределить) → CLI-аргументыproject-local (.claude/settings.local.json, не в git) → project shared (.claude/settings.json, в git, командный) → user (~/.claude/settings.json, все проекты). Для соло-оператора реально нужны только два нижних: project — общие правила репозитория сервиса (например, «в channel-hq всегда permissions.deny: ["Bash(git push *)"], деплой только через hook»), user — твои личные привычки на все проекты сразу.

permission_mode — отдельная быстрая ручка автономии: default (спрашивать на каждый новый инструмент), acceptEdits (правки файлов без вопроса), plan (только читать, ничего не менять — режим разведки), dontAsk (авто-отказ всему, что не одобрено заранее — удобно для холодного, недоверенного субагента), bypassPermissions (без вопросов вообще, кроме явных deny). Это прямая техническая реализация «матрицы риска READ/WRITE/DELETE/ТРАТА» из Модуля 4.1 — READ-агенты живут в plan, WRITE — в acceptEdits с deny-листом на секреты, ТРАТА — всегда default+hook, никогда не bypassPermissions.

Пример правил в самом settings.json — то, что реально стоит прописать в репозиторий каждого сервиса завода:

{
  "permissions": {
    "allow": ["Bash(npm run build)", "Read(./.env)", "mcp__resource-governor__*"],
    "deny": ["Bash(git push *)", "Read(/root/**/.env)", "mcp__*"],
    "ask": ["Bash(rm *)", "Edit(/config/**)"]
  },
  "defaultMode": "acceptEdits"
}

Приоритет всегда один и тот же порядок, независимо от того, в каком месте файла что написано: deny побеждает всегда, потом ask, потом allow 🟢 [из документации SDK]. Практическое следствие: если ты по ошибке одновременно разрешил и запретил один и тот же паттерн — сработает запрет, а не разрешение, то есть система «падает в безопасную сторону» по умолчанию.

Ещё один нюанс, который экономит нервы: часть настроек применяется мгновенно (permissions, hooks, env), а часть требует рестарта сессии (model, outputStyle) — если поменял модель в settings.json и ничего не изменилось, дело не в баге, а в том, что нужен /model или новая сессия.

Наблюдаемость (для любопытных). SDK умеет экспортировать телеметрию по стандарту OpenTelemetry — включается двумя переменными окружения (CLAUDE_CODE_ENABLE_TELEMETRY=1, OTEL_EXPORTER_OTLP_ENDPOINT=...) и даёт по каждому ходу: какие инструменты вызывались, сколько заняли, где сессия застряла. Это ровно тот сырой поток данных, из которого собирается mobile-панель burn-rate из Модуля 8.3 — не нужно писать логирование каждого вызова вручную внутри каждого сервиса, достаточно один раз включить OTEL-экспорт и один раз написать коллектор, который эти события копит и превращает в цифры на дашборде.

🎚️ permission_mode: степень допуска

Жми по режиму — узнай, что он разрешает и для какой стадии риска (READ/WRITE/ТРАТА) он годится.

Выбери режим выше.

4. MCP — прямой ответ на боль №1

По-человечески. У капельницы десятки разных мешков с растворами — но подключаются они к вене через ОДИН и тот же стандартный разъём (Luer lock), а не через самодельную трубку под каждый мешок. MCP — это ровно такой разъём, только для инструментов ИИ: не важно, что за сервис на другом конце (база данных, GitHub, твой собственный channel-hq) — если он говорит на протоколе MCP, агент подключается к нему одним и тем же способом, без написания уникального клея под каждую пару сервисов.

Как называется по-настоящему (English): MCP — Model Context Protocol, открытый стандарт (изначально от Anthropic, ноябрь 2024, сейчас развивается сообществом), который описывает, как AI-агент обнаруживает и вызывает внешние инструменты и источники данных единым протоколом, а не проприетарной интеграцией под каждый сервис.

Это буквально твоя боль №1 из профиля — «я сам клей между инструментами». Сейчас, когда channel-hq должен дёрнуть memory-bank или resource-governor, это ручной или полу-ручной HTTP-вызов, написанный именно под эту пару сервисов. MCP-сервер поверх того же сервиса даёт агенту стандартный, самоописывающийся интерфейс — агент сам видит список доступных функций и их схемы, без того чтобы ты каждый раз объяснял в промпте «вот адрес, вот формат запроса».

Как подключить готовый MCP-сервер

Три типа транспорта:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/root/projects"]
    }
  }
}

Это файл .mcp.json в корне проекта — project-scoped, то есть если положить его в репозиторий сервиса, MCP-сервер поднимется автоматически при работе с этим проектом.

mcpServers: {
  "remote-api": { type: "http", url: "https://api.example.com/mcp", headers: { Authorization: `Bearer ${TOKEN}` } }
}

Каждый инструмент MCP-сервера появляется у агента под именем mcp__<имя-сервера>__<имя-инструмента> — и требует явного разрешения в allowedTools, как любой другой инструмент (mcp__github__* — все инструменты от github, mcp__db__query — конкретно один). Это тот же слой permissions из §3, просто применённый к внешним сервисам — MCP не обходит матрицу риска, он в неё встраивается.

Готовые серверы: что реально доступно

Официальный репозиторий modelcontextprotocol/servers держит сейчас 7 эталонных серверов: Filesystem, Fetch (веб-контент), Git, Memory (граф знаний — концептуально близко к твоему memory-bank), Sequential Thinking, Time, Everything (демо). 🟢 [подтверждено по актуальному README репозитория]. Важная деталь, которую легко пропустить: одиннадцать серверов, которые раньше числились «официальными» — GitHub, GitLab, Google Drive, PostgreSQL, Redis, Slack, SQLite и другие — сейчас архивированы и переданы сообществу или сторонним мейнтейнерам 🟢 [подтверждено по README]. На практике это значит: перед тем как ставить «официальный GitHub MCP» в продакшн, проверь, кто сейчас реально поддерживает конкретный форк — экосистема двигается быстрее, чем любой курс успевает её описать.

Как подключить СВОЙ сервис как MCP

Это прямой рецепт для channel-hq / resource-governor / memory-bank. Не нужно переписывать сервис — нужна тонкая MCP-обёртка поверх уже существующего REST API. Самый быстрый путь на Python — библиотека FastMCP: одна функция → один декоратор → готовый MCP-инструмент:

from fastmcp import FastMCP
import httpx

mcp = FastMCP("resource-governor")

@mcp.tool()
async def get_slot_status(generator: str) -> dict:
    """Проверить, свободен ли слот генератора (photo/omni/moss)."""
    async with httpx.AsyncClient() as client:
        r = await client.get(f"http://resource-governor:4700/slots/{generator}")
        return r.json()

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

Это ~10 строк, оборачивающих уже работающий эндпойнт resource-governor. После этого агент видит get_slot_status как нативный инструмент с автоматически сгенерированной схемой из type hints — вместо того чтобы ты вручную вставлял в промпт «сначала сходи по такому-то URL, вот формат ответа». Тот же приём применим ко ВСЕМ ~16 сервисам завода: не переписывать их, а по одному тонкому MCP-адаптеру на сервис — и тогда любой будущий агент (opportunity-scout, строитель приложений, MVP-explorer) получает доступ к заводу через один и тот же стандартный разъём, а не через 16 разных самодельных клиентов.

Живой якорь: возьми ЛЮБОЙ один эндпойнт из 08-contracts.md (например, статус генерации у photo-генератора) и оберни его прямо сегодня в 15-строчный FastMCP-сервер — это самый маленький шаг, закрывающий боль №1 не в теории, а физически.

Аутентификация и что делать, когда MCP не подключился

Три способа передать секрет MCP-серверу, в зависимости от транспорта: переменные окружения для stdio-серверов (env: {GITHUB_TOKEN: ...}), HTTP-заголовки для удалённых (headers: {Authorization: "Bearer ..."}), либо полноценный OAuth2-поток для серверов, которые его поддерживают (токен получаешь заранее и передаёшь так же через заголовок). Для внутренних сервисов завода, живущих в docker-сети factory за Caddy, обычно достаточно первого варианта — тот же .env, который уже используется остальным заводом (§6 DD-азбуки), просто ещё один потребитель переменной.

Подключение MCP-сервера может не удаться — типичные причины: не хватает переменной окружения, пакет не установлен (для npx-серверов — нет доступа в интернет или Node.js не в PATH), неверная строка подключения к БД, сеть/файрвол блокирует URL. Проверяется это по init-сообщению в самом начале сессии — там приходит список серверов и их статус (connected / нет), и если какой-то не подключился, агент об этом узнаёт до того, как попробует вызвать его инструмент, а не после невнятной ошибки посреди задачи.

Когда MCP — оверкилл

Симметрично Уроку 2.4 («когда мультиагентность вредна») стоит держать в голове и обратный чек-лист: не каждый вызов внешнего API заслуживает MCP-обёртки. Если сервис вызывается из ОДНОГО конкретного места в коде, не меняется динамически по решению модели и не должен быть виден агенту как «инструмент на выбор» — обычный HTTP-вызов внутри твоего скрипта дешевле и прозрачнее (это то же самое правило mechanical-first — закон №3 — применённое к интеграциям, а не к аудиту). MCP оправдан именно там, где agentic-решение реально нужно: агент сам решает, вызывать инструмент или нет, в каком порядке, с какими параметрами. Обёртывать в MCP жёстко детерминированный шаг пайплайна (например, «после генерации ролика всегда вызови QC ровно один раз») — это лишний слой индирекции без выгоды: там нужен просто вызов функции в коде оркестратора, не инструмент, который агент «решает» вызвать.

Tool Search: когда MCP-серверов становится много

Если подключить сразу десяток MCP-серверов, определения ВСЕХ их инструментов (имя + схема + описание каждого параметра) по умолчанию попадали бы в системный промпт каждого хода — это прямой канал раздувания контекста из Модуля 5 («метаболизм системы»), только теперь источник не история диалога, а сама библиотека доступных инструментов. Tool search — механизм, который прячет полные определения инструментов и подгружает конкретную схему только в тот момент, когда агент реально решил вызвать конкретный инструмент; включён по умолчанию 🟢 [из документации SDK]. Для завода с ~16 сервисами, каждый из которых со временем обрастёт своим MCP-адаптером, это не опция «на будущее», а условие, чтобы подключение 16-го сервера не удвоило стоимость каждого хода агента.

🔌 Три типа разъёма MCP

Клик по узлу схемы — когда его выбирать.

stdio HTTP / SSE SDK in-process
Выбери разъём выше.

5. Structured output: строгий JSON вместо надежды

По-человечески. Обычный текстовый промпт — как рецепт, написанный от руки: врач может забыть указать дозировку, аптека может не разобрать почерк. Structured output — как стандартный бланк направления на анализы: поля жёстко определены заранее (фамилия, анализ, дата), бланк физически нельзя сдать незаполненным по обязательному полю — форма не даст.

Как называется по-настоящему: structured outputs — режим Claude API/SDK, который через ограниченное сэмплирование (constrained decoding) гарантирует, что ответ модели строго соответствует заданной JSON-схеме, а не «обычно похож на неё». Два независимых применения: output_config.format — весь ответ модели превращается в валидный JSON нужной формы; strict: true на конкретном инструменте — гарантирует, что имя и параметры вызова инструмента точно совпадают со схемой.

from pydantic import BaseModel

class VideoQCResult(BaseModel):
    publish: bool
    hook_score: int
    reasons: list[str]

response = client.messages.parse(
    model="claude-sonnet-5",
    messages=[{"role": "user", "content": "Оцени этот сценарий по чек-листу QC..."}],
    output_format=VideoQCResult,
)
result = response.parsed_output  # гарантированно валидный объект, не строка для парсинга

Это прямая замена ручного try/except json.loads(...) вокруг ответа модели — то, что раньше ломалось на «модель добавила комментарий перед JSON» или «забыла закрывающую скобку», теперь физически не может сломаться на уровне синтаксиса.

Что НЕ гарантируется — и это важно знать заранее 🟢 [дословно из официальной документации Anthropic по structured outputs]:

Лимиты сложности схемы, о которые реально можно споткнуться 🟢 [подтверждено по официальной документации]: до 20 строгих инструментов на один запрос, до 24 необязательных параметров суммарно, до 16 параметров с union-типами — если больше, API вернёт 400: "Schema is too complex for compilation". Для твоего critic-агента с длинным чек-листом это значит: не пытайся впихнуть все 15 критериев QC в один гигантский строгий инструмент — разбей на несколько субагентов с узкими схемами (см. §1) или сократи чек-лист до сути.

📏 Проверь лимиты сложности своей схемы

Двигай ползунки под свой QC-чек-лист — узнай, влезет ли он в один строгий инструмент.

Практический вывод для твоего завода: там, где сейчас критик/QC-агент возвращает текст, который парсится регулярками или "на глаза" — это первый кандидат на перевод на strict: true/output_format. Это не косметика: это разница между «85-95% SLA критика с редкими сбоями парсинга» (Модуль 4.2) и «85-95% SLA плюс дополнительные ложные отказы из-за сломанного JSON», которые к самой модели никакого отношения не имеют.

Ещё три технических нюанса, о которые легко споткнуться на практике:

  1. Первый запрос с новой схемой чуть медленнее — модель компилирует «грамматику» под конкретную схему один раз, дальше 24 часа она закэширована и повторные запросы с той же структурой схемы быстрее 🟢 [из документации]. Если ты часто меняешь набор полей на лету (например, экспериментируешь с чек-листом QC каждый день), закладывай эту разовую задержку — но смена ТОЛЬКО описаний полей (не структуры) кэш не сбрасывает.
  2. Не всё в JSON Schema поддерживается напрямуюminLength, maximum, pattern (regex) и другие ограничения-валидаторы официально не входят в constrained-режим; SDK автоматически убирает их из схемы, переносит смысл в текстовое описание поля («должно быть не меньше 100») и дополнительно валидирует ответ на своей стороне — то есть жёсткая численная валидация всё равно требует твоего кода поверх, а не одной только схемы.
  3. Несовместимо с Citations и с message prefilling — если критик одновременно должен и цитировать источник дословно, и отдавать строгий JSON, это две разные фичи API, которые в одном вызове не работают вместе; на практике разносится на два хода или два субагента.

Медицинская параллель для этих трёх пунктов: бланк направления один раз утверждается больничным комитетом (компиляция схемы) и потом штампуется быстро; бланк не может одновременно быть «свободной формой врачебной записи с цитатами» и «строгой формой с чекбоксами» — это разные документы, даже если оба про одного пациента.

Не путать: субагент, skill и MCP-сервер

Три термина курса легко смешать, потому что все три «расширяют» агента, но расширяют РАЗНОЕ:

Что это добавляет Аналогия Пример на заводе
Субагент (§1) Ещё одного ИСПОЛНИТЕЛЯ со своим контекстом Отдельный врач-консультант QC-критик как отдельный вызов
Skill (Модуль 1.4) Процедурное ЗНАНИЕ для существующего исполнителя («как делать X») Методичка/протокол лечения на полке faceless-script-skill, capcut-marker-builder
MCP-сервер (§4) Доступ к внешней СИСТЕМЕ/данным, которых у агента иначе нет Разъём к капельнице с чужим раствором Доступ к resource-governor, GitHub, Postgres

Практическое правило: если проблема «агент не знает, КАК делать задачу» — нужен skill; если проблема «эта задача требует отдельного контекста и, может, другой модели» — нужен субагент; если проблема «агенту физически не хватает канала к данным/сервису вовне» — нужен MCP-сервер. Часто нужны все три вместе: субагент-критик (§1), который использует skill «чек-лист QC» (Модуль 1.4) и достаёт метрики канала через MCP (§4) — три независимых оси, не альтернативы друг другу.


Где здесь деньги

Каждый час, потраченный на ручной HTTP-клей между сервисами завода, — это час твоей боли №1, которую MCP закрывает системно, а не разово: один адаптер на сервис вместо N уникальных интеграций под каждую пару «кто с кем говорит». Structured output убирает целый класс «глюков», которые выглядят как «ИИ ошибся», а на деле — сломанный парсинг текста, который ты потом вручную чинишь руками (боль №2 — контроль и дожимание ИИ). Субагенты с урезанными tools и model="haiku" — это прямая экономия в веере MVP консультант-фабрики: 10 explorer-агентов, дешёвых по конструкции, обходятся в разы дешевле, чем 10 одинаковых полновесных агентов «на всякий случай». А hooks как денежный гейт — единственный способ технически, а не на словах, удержать твою красную линию «тратить без подтверждения нельзя», когда система работает ночью и никто не читает её рассуждения в реальном времени.


Чек-лист внедрения

Куда это в курсе

Этот модуль — прикладное расширение Модуля 7 («Выбор ядра», Урок 7.2–7.3: почему выбран Claude Agent SDK) и обязательный практический слой для трёх других:

Живой якорь всего модуля один: обернуть хотя бы один эндпойнт завода в MCP-сервер сегодня — это первый физический кирпич моста от «полуручной фабрики» к «нервной системе», о которой курс говорит с Модуля 0.

Порядок освоения, если весь модуль сразу — много: начни с §4 (MCP), потому что это единственный кусок, который сразу закрывает названную тобой боль №1 и даёт видимый результат за один вечер (один адаптер — один сервис становится доступен агенту). §2 (hooks) — второй приоритет, потому что без него денежный гейт остаётся обещанием в промпте, а не гарантией. §1 (субагенты) и §5 (structured output) — оптимизации поверх уже работающей системы: они снижают стоимость и повышают надёжность того, что уже подключено и защищено, но не заменяют первые два шага.