Мок-API
MCP-серверы
Мок MCP-сервера Apify и собственный MCP-сервер APIStend — подключение, инструменты, чем мок отличается от боевого сервера.
На этой странице · 11
MCP (Model Context Protocol) — то, чем ИИ-агент пользуется вместо HTTP-клиента. В стенде два MCP-сервера, и путать их не нужно:
Первый нужен тому, кто пишет агента к Apify. Второй — тому, кто пишет агентом интеграцию с любым из четырёх сервисов и хочет, чтобы агент видел каталог методов, а не гадал по названиям.
Мок MCP-сервера Apify
Зачем он нужен, видно из цены боевого вызова: каждый запуск актора у Apify стоит денег и минуты ожидания. Агента же отлаживают прогоном по кругу — десятки запусков подряд, пока не сойдётся разбор ответа.
Подключение — замена одного адреса:
{
"mcpServers": {
"apify": {
"url": "https://apistend.ru/apify/mcp",
"headers": { "Authorization": "Bearer stend_sk_…" }
}
}
}
Как и у боевого сервера, набор инструментов сужается параметром tools —
именами через запятую либо категориями actors, docs, runs, storage:
https://apistend.ru/apify/mcp?tools=actors,storage
Что отвечает полностью
Форма ответа каждого инструмента — та, что объявлена в его outputSchema,
снятой с боевого сервера. Это не педантизм: официальный MCP SDK валидирует
structuredContent по схеме, которую сервер сам же отдал в tools/list, и при
расхождении отбрасывает ответ целиком — клиент получает «Structured content does
not match the tool's output schema», хотя данные в ответе есть. Соответствие
держит тест, прогоняющий каждый инструмент против его собственной схемы.
Идентификатор датасета клиент берёт оттуда же, откуда у боевого сервера, —
storages.datasets.default.id; отказ приходит текстом без structuredContent,
потому что формы для неудачи в схеме не объявлено.
Самое полезное здесь — call-actor. Вход проверяется по настоящей схеме
актора, поэтому агент, собравший вызов неверно, узнаёт об этом бесплатно и сразу,
а не после платного запуска.
Форму строки даёт то, что автор актора сам сказал о своём результате, — в таком порядке:
- схема полей датасета из последней сборки: значения берутся из
examples, которые в ней написал автор; - строки результата из readme, если схемы нет: кусок JSON под заголовком «Output» на странице актора. Такие строки отдаются как есть и не размножаются до запрошенного количества — три показанных автором строки это три строки;
- пустой массив, если автор не описал результат никак. Придумывать за него поля мок не станет: по несуществующему полю агент напишет разбор, который в бою развалится.
Схема датасета объявлена у 429 акторов снимка, примеры в readme — у 739, вместе форма выхода известна для 874 из 1 200.
Значения товарных полей — это рынок: чужие предложения того же предмета, что и карточка продавца. За скрапером маркетплейса приходят именно за конкурентами, а не за своими же товарами. Строка распознаётся как карточка товара, если рядом с названием стоят цена, артикул или рейтинг; тогда выдача начинает зависеть от входа:
queries,query,search,keywordsотбирают предмет: «коврик для йоги» находит рынок ковриков, «кофе в зёрнах» — рынок кофе;maxItems,resultsLimit,maxResults,limitзадают количество строк.
Что в этом рынке есть:
Цена в самом каталоге тоже привязана к предмету: коврик стоит как коврик, а не как ноутбук. Пока цена была случайной по всему прайсу, медиана по категории считалась по величинам, которые вместе не встречаются.
Строка, которая карточкой товара не выглядит — пост в соцсети, точка на карте, вакансия, — остаётся ровно такой, какой её показал автор.
Запуск, сделанный через call-actor, запоминается: следующий вызов
get-dataset-items по его datasetId вернёт те же строки, а get-actor-run
по его runId — тот же запуск. Именно так работает агент: запустил, потом забрал
результат отдельным вызовом. Реестр живёт в памяти процесса — запуска на самом
деле не было, и хранить его в базе значило бы выдавать образец за состояние.
Идентификаторы детерминированы от актора и входа, поэтому после перезапуска
повторный вызов даёт те же самые.
Число элементов уважает вход: resultsLimit, maxItems, maxResults и limit.
Агент, попросивший пять элементов и получивший три, решил бы, что данные
кончились.
Стоимость прогона
get-actor-run-list в снимок инструментов не попал — набор снимался на дату,
когда боевой сервер его по умолчанию не отдавал, — но у mcp.apify.com он есть,
и объявлен здесь дословно: имя, описание и схема входа взяты у боевого сервера.
Схему выхода он для этого инструмента не объявляет, поэтому её нет и тут:
придуманная нами, она стала бы законом для клиента, и SDK отбрасывал бы законный
ответ боевого сервера при первом же расхождении.
Нужен он ради одного поля. Фактическую стоимость прогона отдаёт только список
запусков: в карточке одного запуска usageTotalUsd нет ни у боевого Apify, ни
здесь. Клиент, который списывает деньги за запуск, без списка вынужден списывать
оценку вместо факта — и расхождение между ними вылезает уже на живых деньгах.
Записи идут в форме RunShort из OpenAPI Apify, и стоимость считается по
настоящему тарифу актора из снимка магазина:
Compute unit — это гигабайт-час: память запуска, умноженная на его длительность.
Цены отдельных событий (PAY_PER_EVENT) в снимке магазина нет, и выдумывать
чужой прайс мок не станет: такой прогон считается по вычислениям.
Что агент узнаёт о песочнице
Модель не догадается сама, что перед ней мок, — ей об этом говорят прямо, в двух местах.
instructions мока — дословный текст боевого mcp.apify.com плюс раздел
«APIStend sandbox (not Apify)» в конце. Он объясняет главное: настоящего запуска
не было, значения демонстрационные, форма — настоящая; выдача берётся из схемы
датасета или из примеров readme, а пустой список означает «автор не описал
результат», а не «по запросу ничего не нашлось». Боевая часть текста не
переписана: по ней модель выбирает и запускает акторов, и «близко к тексту»
она перестала бы быть тем же интерфейсом.
Каждый ответ call-actor несёт ту же мысль дважды: строкой в тексте, который
читает модель, и полем _apistend в структуре, которую разбирает код:
Подчёркивание в имени не случайно: у боевого Apify такого поля нет, и клиент, разбирающий его форму ответа, не примет служебное поле песочницы за своё.
Собственный MCP-сервер APIStend говорит то же самое своими словами: его
instructions предупреждают, что данные демонстрационные, а list_services
показывает у Apify адрес мока MCP рядом с префиксом REST.
Ключ и поток — как у боевого сервера
mcp.apify.com требует токен на любой запрос: 401 приходит и на GET, и на
POST, и на DELETE, ещё до разбора тела. Мок ведёт себя так же, только ключ
здесь свой — stend_sk_… песочницы. Форма отказа снята дословно: конверт
{"error":"invalid_token","error_description":"…"}, заголовок
WWW-Authenticate: Bearer realm="OAuth", error="invalid_token", а на GET —
человекочитаемый абзац вместо конверта.
GET открывает поток «сервер → клиент» и держит его: раз в двадцать пять
секунд уходит комментарий SSE, через десять минут молчания поток закрывается —
клиент переоткрывает его сам, это штатная часть протокола. Сообщений по своей
инициативе мок не шлёт: выдумывать события сервера значило бы кормить клиента
тем, чего не происходило.
Раньше на GET приходило 405 Method Not Allowed, и клиенты уходили в цикл
переподключения: за день это три с половиной тысячи запросов, из которых
полторы тысячи — один и тот же список инструментов по 80 КБ.
Вызовы видны в журнале
Обращение по MCP — такой же вызов песочницы, как запрос к шлюзу, и попадает
в журнал кабинета: в строке видно, что именно звал агент — /apify/mcp tools/call call-actor, — а в карточке лежат тело запроса и ответа. Мок mcp.apify.com
пишется под кодом сервиса apify, собственный сервер APIStend — под apistend,
так что в фильтре «Сервис» они разделены.
Записать вызов можно только в песочницу, а её называет ключ. Вызов без
Authorization: Bearer stend_sk_… выполняется, но в журнал не попадает —
приписать его некому. Чтобы это не выглядело пропажей, такой ответ несёт
заголовок X-APIStend-Log: skipped-no-sandbox-key.
Что честно отказывает
search-apify-docs, fetch-apify-docs, apify--rag-web-browser и
apify--web-fetch в песочнице не выполняются. Все четыре обращаются к живой
сети — к документации Apify или к произвольной странице. Песочница обязана
работать без интернета, а подсунуть агенту правдоподобный текст страницы,
которой он не видел, — ровно то, чего этот проект не делает.
report-problem принимает обращение и сообщает, что наружу оно не ушло.
Откуда взяты описания инструментов
Имена, описания, схемы входа и выхода, serverInfo и instructions сняты
дословно с mcp.apify.com и лежат в specs/apify/mcp-tools.json вместе с датой
снимка. Переписанные «близко к тексту», они перестали бы быть тем же интерфейсом.
Обновить: APIFY_TOKEN=… ./scripts/vendor-apify-mcp-tools.sh. Без токена
снимается только анонимный набор из четырёх инструментов — больше боевой сервер
без токена не отдаёт.
Собственный MCP-сервер APIStend
Даёт агенту то же, что кабинет даёт человеку.
Включается в настройках
Доступ выключен по умолчанию, и включает его пользователь: кабинет, раздел Настройки → Доступ для ИИ-агента (MCP). Там же лежат два готовых блока — конфигурация клиента и инструкция для агента, — оба копируются целиком.
Кнопка «Создать ключ и подставить» заводит ключ и вставляет его прямо в оба текста: копировать ключ отдельно и искать в нём место для вставки не нужно. Скопировать надо сразу — полностью ключ существует один момент, сразу после создания, дальше в базе остаётся только его хеш. После перезагрузки страницы в текстах снова будет заглушка.
Почему выключено по умолчанию. Агент, которому дали ключ, вызывает моки и читает журнал запросов песочницы, а в журнале лежат тела запросов, написанные человеком. Такое включают осознанно, а не обнаруживают включённым.
Каталог методов при этом открыт и до включения: list_services,
search_catalog и describe_method работают без ключа вовсе. В них нет ничего
личного, а агенту каталог нужен как раз до того, как у него появится доступ
к песочнице.
Пока доступ выключен, call_mock и recent_requests отвечают отказом
с указанием, где включить, — агент передаёт это человеку, вместо того чтобы
молча упереться в «доступ запрещён».
{
"mcpServers": {
"apistend": {
"url": "https://apistend.ru/mcp",
"headers": { "Authorization": "Bearer stend_sk_…" }
}
}
}
Инструкция для агента
Второй блок на том же экране — готовый текст для системного промпта агента или
для AGENTS.md проекта: когда пользоваться стендом, в каком порядке вызывать
инструменты, чему верить в ответе по полю responseSource и чего мок не делает.
Текст собирается на сервере из живого списка инструментов, а не хранится отдельной строкой. Инструкция, разошедшаяся с сервером, хуже отсутствующей: агент вызовет то, чего нет, и решит, что сломан сервер.
list_services и search_catalog работают без ключа — это открытый каталог.
call_mock и recent_requests требуют ключ песочницы и уважают его область
действия: ключ только на Ozon не вызовет мок Wildberries.
Ключ тот же самый, stend_sk_…, что и у мок-шлюза. Отдельного вида токена не
заводится намеренно: у ключа уже есть и песочница, и список разрешённых сервисов,
и отзыв.
Зачем агенту каталог
Живую документацию Ozon и Wildberries агент не откроет — обе закрыты антиботом.
Каталог APIStend уже содержит разобранные методы с примерами ответов, и describe_method
отдаёт их напрямую. Поле responseSource при этом говорит, чему верить:
example — значения из документации сервиса, schema — форма настоящая,
значения выдуманы, generic — только то, что клиент не упадёт на разборе.
Транспорт
Оба сервера говорят по Streamable HTTP, и поведение снято с живого
mcp.apify.com, а не выведено из спецификации:
Последний пункт стоит отметить. Клиент, переживший перезапуск сервера, у боевого Apify продолжает работать; мок, отвечающий ему 404, сломал бы ровно тот сценарий, ради которого мок и берут.
Заголовки Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-Id и
WWW-Authenticate перечислены в Access-Control-Expose-Headers: клиентом MCP
всё чаще оказывается расширение браузера, а не локальный процесс.