synartesis-proxy
Synartesis
Слой отмены для AI-агентов.
Агент с доступом на запись к реальной системе выполняет двадцать шагов, неверно прочитывает шаг семь и применяет остальные к не тем записям. Сегодня у вас есть три варианта: откатывать всё вручную по журналу вызовов, развернуть резервную копию и потерять все легитимные изменения, сделанные в том же окне, либо смириться с ущербом.
Synartesis находится между вашим MCP-клиентом и серверами, с которыми тот разговаривает. Она записывает каждый вызов инструмента вместе с состоянием, которое этот вызов заменил, и может вернуть это состояние обратно. То, что вернуть нельзя, она не позволит агенту делать без присмотра.
Это не песочница: контейнер, в котором работает ваш агент, одноразовый, а строка CRM, которую он обновил по сети, — нет. Это и не инструмент трассировки: трассировка скажет вам, что update_customer выполнялся сорок раз, но не скажет, какими значения были до этого.
Что она может и чего не может
Каждый инструмент получает один из четырёх классов, и вы прописываете его в манифесте:
Класс | Значение | Есампл | Что происходит |
| Ничего не изменяет |
| Записывается и передаётся дальше |
| Прежнее состояние можно восстановить точно |
| Состояние фиксируется до записи; возвращается при отмене |
| Обратить нельзя, но можно компенсировать |
| Другой вызов нейтрализует его |
| Ни то ни другое |
| Приостанавливается до одобрения человеком |
Инструмент, которого нет в вашем манифесте, считается irreversible. Это намеренное решение: молча пропустить неизвестный разрушающий вызов — единственный сбой, которого стоит избегать прежде всего.
Related MCP server: mcp-compensator
Требования
Инструмент | Версия | Проверка командой |
Node | 22 или новее |
|
pnpm | 9 или новее |
|
Тулчейн C | любой |
|
pnpm поставляется с Node через corepack.
corepack enable pnpmТулчейн C нужен один раз, чтобы скомпилировать нативные привязки SQLite. На macOS выполните xcode-select --install; на Debian или Ubuntu — apt install build-essential.
Установка
curl -fsSL https://raw.githubusercontent.com/ArhaanDev24/Synartesis/main/install.sh | bashИли из клона, если хотите сначала прочитать текст:
git clone https://github.com/ArhaanDev24/Synartesis.git && cd Synartesis && ./install.shСкрипт проверяет версию Node, собирает проект и создаёт ссылки synartesis и synartesis-proxy в первой записываемой директории, которая уже находится в вашем PATH. Он не редактирует никакие настройки shell и не требует sudo. Передайте --no-link, чтобы только собрать.
synartesis --helpЕсли ни одной ссылки сделать не удалось, ничего не ломается: каждая команда, которую печатает Synartesis, приводится в том виде, в котором она реально получится на вашей машине.
Пошаговое руководство
Здесь используется игрушечная CRM, которая лежит в этом репозитории, так что весь цикл можно увидеть, не направляя ничего на реальные данные. Запустите это из чистой директории.
mkdir -p /tmp/synartesis-demo && cd /tmp/synartesis-demo1. Напишите политику
init запускает сервер, спрашивает у него, какие инструменты у него есть, и формирует манифест. Замените SYNARTESIS на путь, в который вы клонировали.
node SYNARTESIS/dist/cli.js init crm -- node SYNARTESIS/dist/toy-crm.js --state ./crm.jsonОткройте synartesis.yaml. Каждый инструмент, который не объявил себя read-only, по умолчанию становится irreversible с пометкой TODO. Разбор этих TODO — это и есть ваша работа. Готовый вариант политики для этой фикстуры лежит в репозитории, так что скопируйте его, а не набирайте заново:
cp SYNARTESIS/manifests/toy-crm.yaml ./synartesis.yamlЗатем поправьте единственную строку, где указано, где живёт сервер: она должна указывать на ваш клон и хранить данные в этой директории:
servers:
crm:
command: node
args: ["SYNARTESIS/dist/toy-crm.js", "--state", "./crm.json"]2. Направьте агента на прокси
Там, где ваш MCP-клиент перечисляет серверы, замените запись для сервера, который хотите перекрыть, на прокси. Для Claude Desktop или Claude Code это блок mcpServers:
{
"mcpServers": {
"crm": {
"command": "node",
"args": ["SYNARTESIS/dist/proxy.js", "--manifest", "/tmp/synartesis-demo/synartesis.yaml"]
}
}
}Агент видит те же инструменты, с теми же именами и теми же результатами. В этом и суть: ничего в агенте не меняется.
Для этого прохода вам не нужен настоящий агент. То же самое делает и это:
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"demo-agent","version":"0"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"update_customer","arguments":{"id":"c_001","plan":"free","notes":"wrong edit"}}}' '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"delete_customer","arguments":{"id":"c_002"}}}' | node SYNARTESIS/dist/proxy.js --manifest ./synartesis.yaml --journal ./journal.db > /dev/nullПосмотрите на ущерб:
cat crm.jsonАда оказалась на неверном тарифе и с неправильными заметками, а Грейс исчезла.
3. Посмотрите, что он сделал
node SYNARTESIS/dist/cli.js list --journal ./journal.dbnode SYNARTESIS/dist/cli.js show RUN_ID --journal ./journal.dbshow печатает каждый вызов с его классом, его статусом и точным вызовом, который бы его отменил, уже разрешённым до литеральных значений.
4. Отмените
Сначала посмотрите, а потом прыгайте:
node SYNARTESIS/dist/cli.js undo RUN_ID --dry-run --journal ./journal.dbЗатем выполните отмену:
node SYNARTESIS/dist/cli.js undo RUN_ID --journal ./journal.dbcat crm.jsonГрейс вернулась, а Ада снова на своём исходном тарифе и со своими исходными заметками.
5. Посмотрите, как он отказывается
Отмена — не тупой инструмент. Если что-то другое изменило запись после того, как к ней прикоснулся агент, запись старого значения обратно уничтожит эту работу, поэтому Synartesis останавливается и показывает вам оба значения.
Запустите повреждающую команду из шага 2 ещё раз. Это создаст второй запуск, поэтому возьмите идентификатор запуска из верхней части вывода list, которая упорядочена от самых новых к самым старым. Затем измените запись вручную:
node -e 'const f="./crm.json",s=JSON.parse(require("fs").readFileSync(f));s.customers.c_001.notes="a human wrote this";require("fs").writeFileSync(f,JSON.stringify(s,null,2))'node SYNARTESIS/dist/cli.js undo RUN_ID --journal ./journal.dbОн останавливается, печатает ожидаемое и фактическое состояние, завершается с ненулевым кодом и ничего не меняет.
Одобрение того, что нельзя отменить
send_email относится к классу irreversible, поэтому агент не может отправить его сам. Вызов сразу же отклоняется вместе с идентификатором действия и командой, которая бы его одобрила. Агент рассказывает вам, вы решаете, и он пробует снова.
Он не удерживает вызов открытым, пока ждёт ответ. Это был первый вариант дизайна, и он не выживает при контакте с настоящим клиентом: любое полезное окно, за которое человек может заметить, открыть терминал и решить, длиннее, чем клиент готов ждать результат инструмента, поэтому их нельзя свести, выбрав лучший таймаут.
Одобрение также не происходит не в терминале, которым пользуется агент: прокси общается по протоколу MCP через stdin и stdout, поэтому там нет ничего, где можно было бы запросить подтверждение, а у настольного клиента терминала нет вообще. Запрос уходит в журнал, и вы отвечаете на него откуда угодно:
node SYNARTESIS/dist/cli.js gates --journal ./journal.dbnode SYNARTESIS/dist/cli.js approve ACTION_ID --by your-name --journal ./journal.dbnode SYNARTESIS/dist/cli.js deny ACTION_ID --by your-name --reason "not this one" --journal ./journal.dbОдобрение — одноразовое и истекает через час, поэтому оно покрывает только ту повторную попытку, для которой было дано, и не может покапать разрешение на тот же вызов завтра. Оно не привязано к одной сессии, потому что люди перезапускают своим клиентом, а одобрение, застрявшее в мёртвой сессии, не было бы одобрением вовсе.
Ничто никогда не одобряется молчанием. Отвеченный запрос просто остаётся без ответа и остаётся видимым в ssynlesis gates, пока кто-нибудь не решит этот вопрос.
Всё это сообщается агенту при подключении, чтобы он мог объяснить ситуацию собеседнику, а не выдавать непонятную ошибку.
Реальные серверы
Если вам удобнее повторять шаги, а не читать описание, есть инструкция по запуску на собствен стране файлах; из двух мест там специально стоит проверить гейт и проверку расхождения.
Synartesis не имеет специфических отношений именно с почтой. Она работает поверх протокола MCP, поэтому её предмет — то, что умеют подключённые вами серверы: ваши файлы, ваши репозитории, ваша база данных, ваши заявки, собственная память вашего агента. То, что она может отменить, целиком зависит от того, что эти серверы открывают наружу, и каждый манифест ниже явно говорит, где эти возможности заканчиваются.
Манифест | Сервер | Состояние, которым он управляет |
| реальные файлы на диске | |
| граф знаний, который агент хранит в вас | |
| индекс и история настоящего репозитория | |
| issues, pull requests, содержимое файлов | |
фикстура в этом репозитории | проработанный пример каждого класса |
Каждый из этих манифестов, кроме github.yaml, был проверен против реально запущенного сервера. Две демонстрации проходят весь цикл по-настоящему:
./demo/filesystem-demo.sh
./demo/memory-demo.shДемонстрация с файловой системой перезаписывает файл и перемещает другой, восстанавливает оба обратно, затем показывает, как отмена отказывается, когда между делом файл отредактировал человек, и как гейт отказывается создавать директорию, которую этот сервер вообще не умеет удалять.
Демонстрация с памятью — более острая. Агент добавляет в граф двух людей, один из которых уже там был, и сервер тихо игнорирует дубликат. Поэтому отмене приходится удалить ровно одного из них: обратный вызов строится из того, что сервер сообщил о создании, а не из того, что запрашивал агент, — и человек, который оказался там первым, переживает отмену. В этой же сессии пробуют удалить сущность, и вызов блокируется, потому что удаление сущности одновременно удаляет и все связанные с ней отношения, а один обратный вызов не может вернуть обратно сразу оба.
Где заканчиваются возможности каждого из них
Ограничения — самая интересная часть, и они определяются возможностями серверов, а не функциями самого Synartesis.
filesystem: ий
move_fileобратим уже из одних своих аргументов, поэтому предварительное чтение не объявлено и проверку на расхождение для него выполнить нельзя.create_directoryобъявлена необратимой не потому, что каталог так дорог, а потому, что этот сервер не предоставляет способа его удалить.memory:
add_observationsиdelete_observations— точные противоположности, которые расходятся в том, как назвать одно и то же поле. Путь умеет прочитать поле, но не переименовать его, поэтому такой обратный вызов не может быть написан вообще, и вместо этого вызов проходит через гейт.git: чуть ли не каждое чтение, которое предоставляет этот сервер, — это человеко-ориентированная проза, поэтому из зафиксированного состояния почти ничего нельзя восстановить, как бы ни была обратима сама операция git. Коммиты проходят через гейт, потому что этот сервер не предоставляет ни
reset, ниrevert, ни перемещения ветки.
Две вещи, которые стоит знать, если вы пишете свои пути. Обе пришли из тестов против живых серверов, а не из документации.
$result — это структурированный блок, и он не обязан совпадать с текстовым. Сервер памяти отвечает на create_entities простым списком в текстовом блоке и {"entities": [...]} в structuredContent. Synartesis идёт по структурированному, потому что это контракт для машинного чтения.
И synartesis check доказывает, что инструмент существует, а не что путь разрешается. Она и не может: никакой вызов не был сделан, поэтому нет результата, по которому можно идти. Запустите это один раз для себя и прочитайте synartesis show, прежде чем полагаться на обратный вызов.
Составление манифеста
Манифест — это и есть весь продукт. Для API, который вы уже знаете, на него должно хватить пятнадцати минут.
version: 1
servers:
crm:
command: node
args: ["./crm-server.js"]
tools:
- match: "crm.get_customer"
class: readonly
# Read the record before overwriting it, then write that record back.
- match: "crm.update_customer"
class: reversible
snapshot:
tool: "crm.get_customer"
args:
id: "$.id"
inverse:
tool: "crm.update_customer"
args:
id: "$.id"
name: "$snapshot.name"
plan: "$snapshot.plan"
# Nothing to read beforehand; the id only exists once the call returns.
- match: "crm.create_customer"
class: compensable
inverse:
tool: "crm.delete_customer"
args:
id: "$result.id"
- match: "crm.send_*"
class: irreversible
gate: alwaysЗначение может ссылаться ровно на три вещи:
Префикс | Что это | Доступность |
`. | аргументы, отправил которые место агент |
|
| что захватило предварительное чтение |
|
| что вернул прямой вызов |
|
Всё остальное — литерал. Ссылка может стоять одна, и тогда значение сохраняет свой тип, либо оказываться внутри строки, и тогда она подставляется как текст:
sha: "$result.content.sha" # the value itself
message: "Revert agent change to $.path" # text with the path substitutedНапишите $$, чтобы получить литеральный знак доллара. Никаких выражений, условия или функции здесь нет и не будет: как только это превращается в язык, его перестаёшь мог написать за пятнадцать минут.
Пути умеют адресовать элемент списка через [0] и читать поле из каждого элемента через []:
labels: "$snapshot.labels[].name" # [{name: "bug"}, ...] becomes ["bug", ...]Это закрывает
matchподдерживает*, который соответствует одному сегменту:crm.send_*соответствуетcrm.send_email, но неcrm.a.b. Выигрывает наиболее специфичный шаблон независимо от порядка, в котором записаны правила.Обратная операция патча должна восстанавливать каждое поле, а не применять патч заново. Если одна и та же запись изменена дважды за один запуск, частичная обратная операция оставит поля, которых коснулась вторая правка.
gate: on_write— эвристика для инструментов вроде сырого SQL-раннера, где деструктивность нельзя определить по имени инструмента. Всё, что нельзя с уверенностью прочитать как одиночный оператор чтения, блокируется. Используйтеgate: alwaysвезде, где важна определённость.Некорректный манифест не даст прокси запуститься, указав файл и строку для исправления. Он никогда не будет работать с политикой, которую не смог понять.
Команды
Команда | Действие |
| Проанализировать сервер и составить черновик манифеста |
| Все записанные запуски |
| Хронология одного запуска с undo для каждого шага |
| Что ожидает решения |
| Разрешить приостановленный вызов |
| Отказать в нём |
| Обратить запуск, сначала самое новое действие |
| То же, но пересобрать каждый undo из текущего манифеста |
| Загрузить манифест и проверить его по указанным серверам |
--manifest и --journal находятся, а не вводятся вручную. Оба ищутся от
текущей директории вверх, как инструмент контроля версий ищет свой корень,
поэтому внутри проекта с synartesis.yaml каждая команда работает вообще без
флагов. Журнал, которого ещё нет, размещается рядом с политикой, так что прокси,
который его создаёт, и CLI, который его читает, согласуются без дополнительных
указаний.
Прочие флаги: --dry-run, --to <seq> и --replan для undo, --all для
approve и deny, --json для list, show и gates.
Коды выхода: 0 — успех, 1 — остановлено или отклонено, 2 — неверное
использование или конфигурация.
Прокси принимает --manifest, --journal, --gate-timeout <seconds> и
--log-level. Он пишет структурированный JSON в stderr; stdout зарезервирован
для протокольного трафика.
Чего он не делает
Нельзя отправить обратно то, что уже увидено. Прочитанное письмо, опубликованное сообщение, удалённый без резервной копии файл. Именно для этого существует gate.
Компенсируемые действия нельзя проверить на расхождение. Они не объявляют предварительного чтения, поэтому undo компенсирует их и помечает как
[unverified]в своём отчёте.Undo останавливается при неопределённости и перешагивает лишь через безвозвратное. Расхождение, неизвестный исход или неудачный обратный вызов останавливают его, потому что продолжение могло бы что-то уничтожить. Действие, которое просто нельзя отменить, например отправленное письмо, фиксируется в отчёте и остаётся на месте, пока всё остальное откатывается: никакая остановка не отправит его обратно, а остановка лишь оставила бы остальное тоже неверным. В любом случае запуск помечается как
partial.Вызов, прерванный на полпути, записывается как неизвестный, а не как неудавшийся. Undo отказывается проходить мимо него, потому что нельзя определить, применился ли он.
Undo настолько хорош, насколько хороша политика, которая его записала. Обратные операции вычисляются в момент вызова, а не при откате, поэтому ошибка в манифесте встраивается в каждый запуск, выполненный под ним.
undo --replanпересобирает их из исправленного манифеста, используя уже захваченное состояние, — это и есть выход.
Наблюдение за работой
Synartesis — не демон и не может им быть. MCP-клиент сам порождает stdio-сервер и владеет его жизненным циклом, поэтому ничто долгоживущее не могло бы расположиться между ними и видеть эти вызовы. То, что человек обычно хочет от демона, — это уверенность, что он на месте и что-то делает, а для этого нужно место, куда можно посмотреть, а не фоновый процесс:
synartesis watchОн перерисовывается по мере работы агента: что было вызвано, к какому классу относился каждый вызов и что ожидает решения, с командой для одобрения. Ctrl-C останавливает его. При запуске через конвейер, а не в терминале, он печатает состояние один раз и завершается.
Доверие
Манифест называет команды, и Synartesis их выполняет. Относитесь к манифесту, который вы не писали, так же, как к shell-скрипту из того же источника: сначала прочитайте. Здесь нет песочницы, и её и не предполагается.
Разработка
pnpm testpnpm typecheck && pnpm lintКаждый push прогоняет их на Linux и macOS на Node 22 и 24, плюс демо и установщик.
Лицензия
MIT. См. LICENSE.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceA policy-enforcing MCP gateway that intercepts all tool calls to downstream MCP servers, applying allow/deny/ask rules with human approval and audit logging for safe access to dangerous tools.23MIT
- AlicenseNot gradedqualityBmaintenanceMCP proxy that journals mutating tool calls and enables undo via compensation. It adds checkpoint, list_changes, undo_to, and explain_blast_radius meta-tools while forwarding all original downstream tools unchanged.MIT
- FlicenseNot gradedqualityBmaintenanceProvides a secure MCP boundary for AI agents, intercepting and validating tool calls, redacting secrets, and requiring human approval for sensitive actions with a tamper-evident audit trail.
- AlicenseAqualityAmaintenanceAn MCP proxy that enforces policy on every tool call, blocking or flagging actions before they reach downstream MCP servers.1249MIT
Related MCP Connectors
Runtime permission, approval, and audit layer for AI agent tool execution.
Hash-chained HMAC-signed audit log MCP for A2A (agent-to-agent) calls. Every tool-call, agent-ha...
Preflight, approve, and prove consequential agent actions with signed evidence and x402 tools.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/ArhaanDev24/Synartesis'
If you have feedback or need assistance with the MCP directory API, please join our Discord server