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/мин»:
- Урезай
toolsпод каждого — критику не нуженBash, разведчику не нуженWrite. Меньше инструментов в системном промпте субагента = меньше токенов на каждый его ход. - Ставь
model="haiku"там, где работа механическая (проверка по чек-листу, парсинг, сверка формата) — это прямое применение лестницы моделей из Модуля 5. - Используй
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 всё равно успевает сработать первым и остановить операцию.
Структурный вывод + 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-экспорт и один раз написать коллектор, который эти события копит и превращает в цифры на дашборде.
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-сервер
Три типа транспорта:
- stdio — локальный процесс, для инструментов на том же хосте (
npxпакет запускается как дочерний процесс):
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/root/projects"]
}
}
}
Это файл .mcp.json в корне проекта — project-scoped, то есть если положить его в репозиторий сервиса, MCP-сервер поднимется автоматически при работе с этим проектом.
- HTTP/SSE — удалённый сервер по сети (свой или чужой облачный):
mcpServers: {
"remote-api": { type: "http", url: "https://api.example.com/mcp", headers: { Authorization: `Bearer ${TOKEN}` } }
}
- SDK MCP Servers — сервер, написанный прямо внутри твоего кода, in-process, без отдельного процесса вообще — самый дешёвый вариант для собственных внутренних инструментов.
Каждый инструмент 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-го сервера не удвоило стоимость каждого хода агента.
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]:
- Отказ модели (
stop_reason: "refusal") — если Claude отказывается выполнять запрос по соображениям безопасности, схема может не соблюдаться вообще (сообщение отказа важнее формы), а токены всё равно списываются. - Обрыв по лимиту (
stop_reason: "max_tokens") — ответ обрублен на середине, схема не гарантирована; лечится ростомmax_tokens, не повторным парсингом. - Регистр в enum — модель иногда возвращает
"Тема 3"вместо ожидаемого"тема 3"; сравнивай значения без учёта регистра, а не строгим равенством строк.
Лимиты сложности схемы, о которые реально можно споткнуться 🟢 [подтверждено по официальной документации]: до 20 строгих инструментов на один запрос, до 24 необязательных параметров суммарно, до 16 параметров с union-типами — если больше, API вернёт 400: "Schema is too complex for compilation". Для твоего critic-агента с длинным чек-листом это значит: не пытайся впихнуть все 15 критериев QC в один гигантский строгий инструмент — разбей на несколько субагентов с узкими схемами (см. §1) или сократи чек-лист до сути.
Практический вывод для твоего завода: там, где сейчас критик/QC-агент возвращает текст, который парсится регулярками или "на глаза" — это первый кандидат на перевод на strict: true/output_format. Это не косметика: это разница между «85-95% SLA критика с редкими сбоями парсинга» (Модуль 4.2) и «85-95% SLA плюс дополнительные ложные отказы из-за сломанного JSON», которые к самой модели никакого отношения не имеют.
Ещё три технических нюанса, о которые легко споткнуться на практике:
- Первый запрос с новой схемой чуть медленнее — модель компилирует «грамматику» под конкретную схему один раз, дальше 24 часа она закэширована и повторные запросы с той же структурой схемы быстрее 🟢 [из документации]. Если ты часто меняешь набор полей на лету (например, экспериментируешь с чек-листом QC каждый день), закладывай эту разовую задержку — но смена ТОЛЬКО описаний полей (не структуры) кэш не сбрасывает.
- Не всё в JSON Schema поддерживается напрямую —
minLength,maximum,pattern(regex) и другие ограничения-валидаторы официально не входят в constrained-режим; SDK автоматически убирает их из схемы, переносит смысл в текстовое описание поля («должно быть не меньше 100») и дополнительно валидирует ответ на своей стороне — то есть жёсткая численная валидация всё равно требует твоего кода поверх, а не одной только схемы. - Несовместимо с 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 как денежный гейт — единственный способ технически, а не на словах, удержать твою красную линию «тратить без подтверждения нельзя», когда система работает ночью и никто не читает её рассуждения в реальном времени.
Чек-лист внедрения
- [ ] Взять один реальный REST-эндпойнт завода (например,
resource-governor/slots) и обернуть его в MCP-сервер на FastMCP (~15 строк), проверить, что агент видит инструмент и вызывает его. - [ ] Добавить
.mcp.jsonв один сервисный репозиторий с этим сервером, прописатьallowedTools: ["mcp__resource-governor__*"]. - [ ] Написать один
PreToolUsehook, который блокируетWrite/Editпо пути, содержащему.env— тестовый прототип будущего денежного гейта. - [ ] Объявить один субагент (
agents={}) с урезаннымtoolsиmodel="haiku"для механической задачи (например, парсинг лога) — сравнить стоимость с тем же промптом на sonnet без ограничения инструментов. - [ ] Перевести один текущий "критик/QC"-промпт, чей вывод сейчас парсится вручную, на
output_format/pydantic-модель — убедиться, что.parsed_outputприходит безtry/except. - [ ] Проверить лимиты сложности схемы (20 строгих инструментов / 24 опциональных параметра) на своём самом длинном QC-чек-листе — при необходимости разбить на несколько узких инструментов.
- [ ] Проверить в
settings.jsonодного сервиса матрицуpermission_modeпо стадиям (READ →plan, WRITE →acceptEdits+ deny-лист на секреты, ТРАТА →default+ hook). - [ ] Собрать один Role Panel из 2-3 узких субагентов-судей на существующем QC-артефакте (например, thumbnail) и сравнить с текущим монолитным критиком по цене/качеству.
- [ ] Включить
CLAUDE_CODE_ENABLE_TELEMETRY=1на одном сервисе и посмотреть, что реально прилетает в OTEL-поток — это сырьё для будущего дашборда burn-rate. - [ ] Собрать связку structured-output-решение +
PreToolUse-hook хотя бы на одном реальном действии («публиковать/не публиковать») — не как отдельные демо, а как одну цепочку.
Куда это в курсе
Этот модуль — прикладное расширение Модуля 7 («Выбор ядра», Урок 7.2–7.3: почему выбран Claude Agent SDK) и обязательный практический слой для трёх других:
- Модуль 2 (Оркестрация и консилиум) — субагенты и параллельные волны отсюда — это техническая реализация паттернов Sequential/Hierarchical/Concurrent fan-out из Урока 2.2 и архитектуры judge panel из Урока 2.3.
- Модуль 4 (Иммунная система) — hooks как денежный гейт напрямую реализуют матрицу риска Урока 4.1 и sandbox-изоляцию Урока 4.4 (hook, блокирующий доступ к
.env, — конкретный код к абстрактному требованию того урока). - Модуль 5 (Токеномика) — «дешёвые волны» (урезанные
tools,model="haiku",background=True) — прямое применение пятого рычага экономии («лестница моделей») из Урока 5.2 к реальному синтаксису SDK. - Модуль 9.4 (Клей: одна ОС вместо трёх заводов) — MCP-обёртки над каждым сервисом завода — это буквально механизм, которым «AI Glue Agent» из этого урока перестаёт быть тобой лично.
Живой якорь всего модуля один: обернуть хотя бы один эндпойнт завода в MCP-сервер сегодня — это первый физический кирпич моста от «полуручной фабрики» к «нервной системе», о которой курс говорит с Модуля 0.
Порядок освоения, если весь модуль сразу — много: начни с §4 (MCP), потому что это единственный кусок, который сразу закрывает названную тобой боль №1 и даёт видимый результат за один вечер (один адаптер — один сервис становится доступен агенту). §2 (hooks) — второй приоритет, потому что без него денежный гейт остаётся обещанием в промпте, а не гарантией. §1 (субагенты) и §5 (structured output) — оптимизации поверх уже работающей системы: они снижают стоимость и повышают надёжность того, что уже подключено и защищено, но не заменяют первые два шага.