synartesis-proxy
Synartesis
Una capa de deshacer para agentes de IA.
Un agente con acceso de escritura a un sistema real ejecuta veinte pasos, lee mal el paso siete y aplica el resto a los registros equivocados. Hoy tus opciones son revertirlo a mano desde la transcripción, restaurar una copia de seguridad y perder todos los cambios legítimos hechos en el mismo intervalo, o aceptar el daño.
Synartesis se sitúa entre tu cliente MCP y los servidores con los que habla. Registra cada llamada a una herramienta junto con el estado que esa llamada reemplazó, y puede devolver ese estado a su lugar. Lo que no se puede devolver, no permite que un agente lo haga sin supervisión.
No es una sandbox: el contenedor donde corre tu agente es desechable, pero la fila del CRM que actualizó por red no lo es. No es una herramienta de trazabilidad: un rastro te dice que update_customer se ejecutó cuarenta veces, no cuáles eran los valores antes.
Qué puede y qué no puede hacer
Cada herramienta recibe una de cuatro clasificaciones, que escribes en un manifiesto:
Clase | Significado | Ejemplo | Qué ocurre |
| No cambia nada |
| Se registra, se reenvía |
| El estado previo puede restaurarse |
| Se captura el estado antes de escribir; se restaura al deshacer |
| No se puede revertir, pero sí compensar |
| Una llamada distinta lo neutraliza |
| Ni lo uno ni lo otro |
| Se suspende hasta que un humano lo aprueba |
Una herramienta que tu manifiesto no menciona se trata como irreversible. Es deliberado: reenviar en silencio una llamada destructiva desconocida es el único fallo que de verdad hay que evitar.
Related MCP server: mcp-compensator
Requisitos
Herramienta | Versión | Compruébalo con |
Node | 22 o superior |
|
pnpm | 9 o superior |
|
Un toolchain de C | cualquiera |
|
pnpm viene con Node a través de corepack:
corepack enable pnpmEl toolchain de C se necesita una sola vez, para compilar los bindings nativos de SQLite. En macOS ejecuta xcode-select --install; en Debian o Ubuntu, apt install build-essential.
Instalación
curl -fsSL https://raw.githubusercontent.com/ArhaanDev24/Synartesis/main/install.sh | bashO desde un clon, si prefieres leerlo primero:
git clone https://github.com/ArhaanDev24/Synartesis.git && cd Synartesis && ./install.shEl script comprueba tu versión de Node, compila y enlaza synartesis y synartesis-proxy en el primer directorio escribible que ya esté en tu PATH. No edita ningún perfil de shell y no necesita sudo. Pasa --no-link para solo compilar.
synartesis --helpSi no se pudiera enlazar nada, nada se rompe: cada comando de Synartesis se explica por sí mismo en la forma que realmente se ejecuta en tu máquina.
Recorrido
Esto usa un CRM de juguete que viene con el repositorio, para que veas el bucle completo sin apuntar a datos reales. Ejecútalo desde un directorio vacío.
mkdir -p /tmp/synartesis-demo && cd /tmp/synartesis-demo1. Escribe una política
init arranca un servidor, le pregunta qué herramientas tiene y escribe un manifiesto. Sustituye SYNARTESIS por la ruta donde clonaste.
node SYNARTESIS/dist/cli.js init crm -- node SYNARTESIS/dist/toy-crm.js --state ./crm.jsonAbre synartesis.yaml. Toda herramienta que no se declara a sí misma como lectura empieza como irreversible con un TODO. Completar esos TODOs es tu trabajo. Una política terminada para este fixture viene en el repositorio, así que cópiala en lugar de escribirla a mano:
cp SYNARTESIS/manifests/toy-crm.yaml ./synartesis.yamlLuego edita la línea que dice dónde vive el servidor, para que apunte a tu clon y guarde sus datos en este directorio:
servers:
crm:
command: node
args: ["SYNARTESIS/dist/toy-crm.js", "--state", "./crm.json"]2. Apunta tu agente al proxy
Donde tu cliente MCP liste servidores, sustituye la entrada del servidor que quieres cubrir por el proxy. Para Claude Desktop o Claude Code, eso es un bloque mcpServers:
{
"mcpServers": {
"crm": {
"command": "node",
"args": ["SYNARTESIS/dist/proxy.js", "--manifest", "/tmp/synartesis-demo/synartesis.yaml"]
}
}
}El agente ve las mismas herramientas, con los mismos nombres y los mismos resultados. Ese es el punto: nada cambia para tu agente.
Para este recorrido no necesitas un agente real. Esto hace lo mismo:
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/nullMira el daño:
cat crm.jsonAda está en el plan equivocado con las notas equivocadas, y Grace ha desaparecido.
3. Mira qué hizo
node SYNARTESIS/dist/cli.js list --journal ./journal.dbnode SYNARTESIS/dist/cli.js show RUN_ID --journal ./journal.dbshow imprime cada llamada con su clase, su estado y la llamada exacta que la desharía, ya resuelta a valores literales.
4. Deshazlo
Mira antes de saltar:
node SYNARTESIS/dist/cli.js undo RUN_ID --dry-run --journal ./journal.dbLuego hazlo:
node SYNARTESIS/dist/cli.js undo RUN_ID --journal ./journal.dbcat crm.jsonGrace ha vuelto y Ada está en su plan original, con sus notas originales.
5. Mira cómo se niega
Deshacer no es un instrumento contundente. Si algo más cambió un registro después de que el agente lo tocara, escribir el valor antiguo encima destruiría ese trabajo, así que Synartesis se detiene y te muestra ambos valores.
Ejecuta el comando dañino del paso 2 otra vez. Eso crea una segunda ejecución, así que toma el id de ejecución de arriba en list, que está ordenado de más reciente a más antiguo. Luego edita el registro a mano:
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.dbSe detiene, imprime el estado esperado y el real, sale con código distinto de cero y no cambia nada.
Aprobar lo que no se puede deshacer
send_email está clasificada como irreversible, así que el agente no puede enviar un correo por su cuenta. La llamada se rechaza de inmediato con un id de acción y el comando que la aprobaría. El agente te lo cuenta, tú decides, y lo intenta otra vez.
No mantiene la llamada abierta mientras espera. Ese fue el primer diseño y no sobrevive al contacto con un cliente real: cualquier ventana útil para que una persona se dé cuenta, abra una terminal y decida es más larga de lo que un cliente esperará a que una herramienta responda, así que no se pueden reconciliar eligiendo un mejor tiempo de espera.
La aprobación tampoco ocurre en la terminal que usa el agente: el proxy habla MCP por stdin y stdout, así que no hay nada donde mostrar un aviso, y un cliente de escritorio no tiene terminal alguna. La petición va al diario, y tú la respondes desde cualquier sitio:
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.dbUna aprobación es de un solo uso y caduca a la hora, así que cubre el reintento para el que se concedió y no puede autorizar en silencio la misma llamada mañana. No está atada a una sesión concreta, porque la gente reinicia su cliente y una aprobación atrapada en una sesión muerta no sería una aprobación en absoluto.
Nada se aprueba por silencio. Una petición sin respuesta simplemente sigue sin respuesta, visible en synartesis gates hasta que alguien decide.
Al agente se le cuenta todo esto cuando se conecta, para que pueda explicarse en lugar de informar de un fallo opaco.
Servidores reales
Si prefieres seguir los pasos a leer sobre ello, hay una guía para ejecutar esto contra tus propios archivos, con el gate y la comprobación de deriva como las dos cosas que merece la pena probar a propósito.
Synartesis no tiene nada que ver con el correo electrónico en particular. Se apoya en el protocolo MCP, así que su tema es lo que los servidores que has conectado puedan hacer: tus archivos, tus repositorios, tu base de datos, tus tickets, la memoria de tu agente. Lo que puede deshacer depende por completo de lo que esos servidores expongan, y cada manifiesto siguiente dice claramente hasta dónde llega.
Manifiesto | Servidor | Estado que gobierna |
| archivos reales en disco | |
| el grafo de conocimiento que un agente guarda sobre ti | |
| el índice y el historial de un repositorio real | |
| issues, pull requests, contenidos de archivos | |
el fixture de este repositorio | el ejemplo trabajado de cada clase |
Todos excepto github.yaml se comprobaron contra el servidor realmente en ejecución. Dos demos recorren el bucle completo de verdad:
./demo/filesystem-demo.sh
./demo/memory-demo.shLa demo de filesystem sobrescribe un archivo y mueve otro, restaura ambos, luego muestra a deshacer negándose cuando un humano editó el archivo en medio, y al gate negándose a crear un directorio que este servidor no tiene forma de eliminar.
La demo de memory es la más afilada. El agente añade dos personas al grafo, una de las cuales ya estaba, y el servidor ignora silenciosamente el duplicado. Deshacer tiene por tanto que eliminar exactamente una: la inversa se construye a partir de lo que el servidor dijo que creó, no de lo que el agente pidió, así que la persona que ya estaba primero sobrevive al deshacer. La misma sesión intenta luego borrar una entidad y se detiene, porque borrar una entidad también borra todas las relaciones que la tocan y una sola llamada inversa no puede devolver ambas.
Dónde se agota cada uno
Los límites son la parte interesante, y son propiedades de los servidores más que de Synartesis.
filesystem:
move_filees reversible solo con sus argumentos, así que no se declara ninguna pre-lectura y no se puede comprobar la deriva para ella.create_directoryesirreversibleno porque los directorios sean valiosos, sino porque este servidor no expone forma alguna de eliminar uno.memory:
add_observationsydelete_observationsson opuestos exactos que no se ponen de acuerdo sobre cómo llamar al mismo campo. Una ruta puede leer un campo y no puede renombrarlo, así que esa inversa no se puede escribir en absoluto y la llamada se somete al gate.git: casi toda lectura que este servidor ofrece responde en prosa pensada para una persona, así que casi nada puede invertirse a partir de un estado capturado, por reversible que sea la operación git subyacente. Los commits pasan por el gate porque este servidor no expone reset, ni revert, ni forma de mover una rama.
Dos cosas que conviene saber si escribes las tuyas, ambas descubiertas ejecutando estas contra servidores vivos en lugar de leyendo documentación:
$result es el bloque estructurado, y no tiene por qué coincidir con el bloque de texto. El servidor de memory responde a create_entities con una lista desnuda en su bloque de texto y con {"entities": [...]} en structuredContent. Synartesis recorre el estructurado, porque ese es el contrato legible por máquina.
Y synartesis check demuestra que una herramienta existe, no que una ruta se resuelve. No puede: no se ha hecho ninguna llamada, así que no hay resultado que recorrer. Ejecuta la cosa una vez y lee synartesis show antes de confiar en una inversa.
Escribir un manifiesto
El manifiesto es el producto entero. Debería llevarte quince minutos para una API que conozcas.
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: alwaysHay exactamente tres cosas a las que un valor puede referirse:
Prefijo | Se refiere a | Disponible en |
| los argumentos que envió el agente |
|
| lo que capturó la pre-lectura |
|
| lo que devolvió la llamada directa |
|
Cualquier otra cosa es un literal. Una referencia puede estar sola, en cuyo caso el valor conserva su tipo, o dentro de una frase, en cuyo caso se sustituye como texto:
sha: "$result.content.sha" # the value itself
message: "Revert agent change to $.path" # text with the path substitutedEscribe $$ para un signo de dólar literal. No hay expresiones, condicionales ni funciones, y no las habrá: en el momento en que esto se convierte en un lenguaje deja de ser algo que puedas escribir en quince minutos.
Las rutas pueden indexar una lista con [0] y leer un campo de cada elemento con []:
labels: "$snapshot.labels[].name" # [{name: "bug"}, ...] becomes ["bug", ...]Eso cubre el caso común en que una API devuelve un campo más rico de lo que lo acepta, que es lo que GitHub hace con las etiquetas de issues. [] lee la misma clave de cada elemento y nada más: sigue siendo una ruta, no una transformación. Una referencia copia valores, no puede calcularlos, así que una API que necesite una forma genuinamente distinta es una de la que la inversa debería omitir ese campo, y decirlo.
Otras cosas que saber:
matchadmite*, que coincide dentro de un segmento:crm.send_*coincide concrm.send_emailpero no concrm.a.b. El patrón más específico gana independientemente del orden en que estén escritas las reglas.La inversa de un parche debería restaurar todos los campos, no reaplicar un parche. Si el mismo registro se edita dos veces en una ejecución, una inversa parcial deja atrás los campos que tocó la segunda edición.
gate: on_writees una heurística para herramientas como un ejecutor de SQL sin procesar, donde la destructividad no se puede leer del nombre de la herramienta. Todo lo que no pueda leer con confianza como una única sentencia de lectura se somete a la compuerta. Usagate: alwaysdondequiera que la certeza importe.Un manifiesto malformado impide que el proxy se inicie, indicando el archivo y la línea a corregir. Nunca se ejecutará con una política que no haya podido entender.
Comandos
Comando | Función |
| Inspecciona un servidor y redacta un manifiesto |
| Cada ejecución registrada |
| La línea de tiempo de una ejecución, con el deshacer para cada paso |
| Lo que está esperando una decisión |
| Permitir una llamada suspendida |
| Rechazar una |
| Invertir una ejecución, acción más reciente primero |
| Igual, pero reconstruye cada deshacer desde el manifiesto actual |
| Carga un manifiesto y verifícalo contra los servidores que nombra |
--manifest y --journal se encuentran en lugar de escribirse. Ambos se buscan desde el directorio actual hacia arriba, de la misma manera que una herramienta de control de versiones encuentra su raíz, de modo que dentro de un proyecto que tenga un synartesis.yaml, todos los comandos funcionan sin ninguna bandera. Un diario que aún no existe se coloca junto a la política, de modo que el proxy que lo crea y la CLI que lo lee coinciden sin que se les diga.
Otras banderas: --dry-run, --to <seq> y --replan en undo, --all en approve y deny, --json en list, show y gates.
Códigos de salida: 0 éxito, 1 detenido o rechazado, 2 uso o configuración incorrectos.
El proxy acepta --manifest, --journal, --gate-timeout <seconds> y --log-level. Registra JSON estructurado en stderr; stdout está reservado para el tráfico de protocolo.
Lo que no hace
No puede desenviar lo que ya se ha visto. Un correo que ha sido leído, un mensaje publicado, un archivo eliminado sin copia de seguridad. Por eso existe la compuerta.
Las acciones compensables no pueden verificarse en busca de desviaciones. No declaran ninguna pre-lectura, por lo que el deshacer las compensa y las marca como
[unverified]en su informe.El deshacer se detiene ante la incertidumbre y pasa por alto lo meramente permanente. La desviación, un resultado desconocido o una llamada de reversión fallida lo detienen, porque continuar más allá de eso podría destruir algo. Una acción que simplemente no se puede deshacer, como un correo enviado, se informa y se deja en su lugar mientras todo lo demás se revierte: ninguna cantidad de detención lo desenvía, y detenerse solo dejaría el resto también mal. De cualquier manera, la ejecución se marca como
partial.Una llamada interrumpida a mitad de vuelo se registra como desconocida, no como fallida. El deshacer se niega a pasar por encima, porque no se puede determinar si se aplicó.
Un deshacer es tan bueno como la política que lo registró. Las inversas se resuelven cuando ocurre la llamada, no cuando deshaces, por lo que un error en un manifiesto se incorpora a cada ejecución realizada bajo él.
undo --replanlas reconstruye a partir de un manifiesto corregido usando el estado ya capturado, que es la salida.
Viéndolo funcionar
Synartesis no es un demonio y no puede serlo. Un cliente MCP genera un servidor stdio por sí mismo y es dueño de su ciclo de vida, por lo que nada de larga duración podría sentarse en medio y ver esas llamadas. Lo que una persona quiere de un demonio suele ser la tranquilidad de que está ahí y haciendo algo, y eso necesita un lugar donde mirar en lugar de un proceso en segundo plano:
synartesis watchSe redibuja mientras el agente trabaja: qué se ha llamado, qué clase era cada llamada, y cualquier cosa que espera una decisión, con el comando para aprobarla. Ctrl-C lo detiene. Si se canaliza en lugar de ejecutarse en una terminal, imprime el estado una vez y sale.
Confianza
Un manifiesto nombra comandos y Synartesis los ejecuta. Trata uno que no hayas escrito de la misma manera que tratarías un script de shell de la misma fuente: léelo primero. Aquí no hay sandbox, y no se pretende que lo haya.
Desarrollo
pnpm testpnpm typecheck && pnpm lintCada push ejecuta esos en Linux y macOS en Node 22 y 24, además de la demo y el instalador.
Licencia
MIT. Ver 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