mcp-tacit
TACIT: Tracked Agent Capabilities In Types
Paper: Securing Agents With Tracked Capabilities (ACM) · arXiv:2603.00991 · 🏆 Premio al Mejor Artículo en CAIS 26
TACIT (Tracked Agent Capabilities In Types) es un arnés de seguridad para agentes de IA. En lugar de llamar a herramientas directamente, los agentes escriben código en Scala 3 con capture checking: un sistema de tipos que rastrea estáticamente las capacidades y garantiza que el código del agente no pueda falsificar derechos de acceso, no pueda realizar efectos más allá de su presupuesto y no pueda filtrar información de subcomputaciones puras. Proporciona una interfaz MCP, de modo que puede ser utilizado fácilmente por todos los agentes compatibles con MCP.

El framework tiene tres componentes principales:
Compilador de Scala 3. El código enviado por el agente se valida y se verifica con el capture checking habilitado en modo seguro, que impone un subconjunto de lenguaje seguro para capacidades.
REPL de Scala. Una instancia REPL local ejecuta el código compilado y gestiona el estado entre interacciones. Admite tanto la ejecución de una sola vez sin estado como sesiones con estado.
Biblioteca de seguridad de capacidades. Una API tipada que sirve como la única puerta de entrada a través de la cual el código del agente interactúa con el mundo real: sistema de archivos, ejecución de procesos, red y subagentes. La biblioteca es extensible: añade nuevas capacidades modificando solo el código de la biblioteca, sin cambiar el servidor MCP en sí.
Inicio Rápido
TACIT proporciona un servidor MCP estándar que se comunica mediante JSON-RPC sobre stdio. Funciona con cualquier agente compatible con MCP, incluidos Claude Code, OpenCode, GitHub Copilot y otros.
Requiere JDK 17+.
Instalación de TACIT
Elige una de las opciones de instalación a continuación. El envoltorio CLI tacit es la opción recomendada.
Opción 1: Instalar tacit (Recomendado)
tacit es un pequeño comando envoltorio para gestionar TACIT localmente. Usa tacit setup una vez para instalar el comando y obtener la última versión, tacit update para actualizar los JARs, tacit self update para actualizar el propio envoltorio, y tacit serve para lanzar el servidor MCP.
# Download the wrapper directly (no git clone required)
curl -fsSL https://raw.githubusercontent.com/lampepfl/tacit/refs/heads/main/tacit -o tacit
chmod +x tacit
# Install it and download the latest TACIT release
./tacit setupEsto instala el comando tacit en ~/.local/bin, asegura que ~/.local/bin esté en PATH, y descarga la última versión en ~/.cache/tacit/.
Comandos comunes:
# Refresh the cached release if a new version exists
tacit update
# Refresh the tacit wrapper itself
tacit self update
# Start the MCP server
tacit serve
# Remove the wrapper and cached release
tacit self uninstallPor defecto, tacit usa:
Recurso | Ruta por defecto |
MCP Server |
|
Library |
|
Opción 2: Descargar los JARs de la versión precompilada directamente
Si no quieres el envoltorio, usa el script de descarga de la versión.
# Download the script directly (no git clone required)
curl -fsSL https://raw.githubusercontent.com/lampepfl/tacit/refs/heads/main/download_release.sh -o download_release.sh
chmod +x download_release.sh
./download_release.shOpcional:
# Or use wget instead of curl
wget -q https://raw.githubusercontent.com/lampepfl/tacit/refs/heads/main/download_release.sh -O download_release.sh
chmod +x download_release.sh
# Download into a custom directory
./download_release.sh ./dist
./download_release.sh --pre-release ./distPor defecto, esto descarga:
JAR | Ruta por defecto |
MCP Server |
|
Library |
|
Tanto el envoltorio como el script verifican los JARs descargados contra los resúmenes SHA-256 publicados en los metadatos de la versión y se niegan a instalar un JAR cuyo resumen falte o no coincida. Las descargas se guardan en un directorio temporal y se mueven a su lugar solo después de la verificación, por lo que una descarga fallida nunca reemplaza un JAR previamente instalado.
Para compilar desde el árbol de fuentes actual, consulta la Opción 3 a continuación.
Opción 3: Compilar desde el código fuente
Requiere JDK 17+ y sbt 1.12+.
git clone https://github.com/lampepfl/tacit.git
cd tacit
./build.shOpcional:
# Build and copy JARs into a custom directory
./build.sh ./dist
# Show full sbt output while building
./build.sh --verboseEsto compila y copia dos JARs:
JAR | Ruta |
MCP Server |
|
Library |
|
Una vez que TACIT esté instalado mediante cualquiera de las opciones anteriores, configura tu agente para lanzar el servidor MCP.
Configura tu Agente
Añade TACIT como servidor MCP en la configuración de tu agente. Si instalaste el CLI tacit, simplemente usa tacit serve. Si instalaste TACIT manualmente, usa la forma explícita java -jar ... --library-jar ... en su lugar.
Añade a .mcp.json de tu proyecto (o ~/.claude.json para global).
Con tacit:
{
"mcpServers": {
"tacit": {
"command": "tacit",
"args": ["serve"]
}
}
}Con rutas JAR manuales:
{
"mcpServers": {
"tacit": {
"command": "java",
"args": [
"-jar", "/path/to/TACIT.jar",
"--library-jar", "/path/to/TACIT-library.jar"
]
}
}
}Añade a tu opencode.json.
Con tacit:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"tacit": {
"type": "local",
"enabled": true,
"command": ["tacit", "serve"]
}
}
}Con rutas JAR manuales:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"tacit": {
"type": "local",
"enabled": true,
"command": [
"java",
"-jar", "/path/to/TACIT.jar",
"--library-jar", "/path/to/TACIT-library.jar"
]
}
}
}Añade a tu .vscode/mcp.json.
Con tacit:
{
"servers": {
"tacit": {
"command": "tacit",
"args": ["serve"]
}
}
}Con rutas JAR manuales:
{
"servers": {
"tacit": {
"command": "java",
"args": [
"-jar", "/path/to/TACIT.jar",
"--library-jar", "/path/to/TACIT-library.jar"
]
}
}
}Tu agente ahora puede usar las herramientas de TACIT para ejecutar código Scala en un entorno aislado.
Recomendado: Deshabilitar las Herramientas Integradas
Para beneficiarse completamente de la seguridad basada en capacidades de TACIT, deshabilita las herramientas integradas de archivo, shell y red del agente para que todas las operaciones pasen por el REPL aislado.
Lanza con --disallowedTools para bloquear las herramientas integradas:
claude --disallowedTools "Bash,Read,Write,Edit,WebFetch"O añade a .claude/settings.json de tu proyecto:
{
"permissions": {
"disallowedTools": ["Bash", "Read", "Write", "Edit", "WebFetch"]
}
}Establece los permisos de las herramientas integradas a "deny" en opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"*": "ask",
"bash": "deny",
"read": "deny",
"edit": "deny",
"glob": "deny",
"grep": "deny",
"list": "deny",
"tacit*": "allow"
},
"mcp": {
"tacit": { "..." : "..." }
}
}En tu settings.json de VS Code, restringe las herramientas disponibles para Copilot:
{
"github.copilot.chat.agent.tools": {
"terminal": false,
"fs_read": false,
"fs_write": false
}
}Related MCP server: edict-lang
Configuración
El servidor se puede configurar mediante banderas CLI o un archivo de configuración JSON. Pasa las banderas directamente en los argumentos MCP de tu agente, o usa --config para apuntar a un archivo JSON.
La configuración se divide en configuración del servidor (transporte, grabación, sesiones) y configuración de la biblioteca (comportamiento del sandbox, capacidades). En el archivo de configuración JSON, los ajustes de la biblioteca se encuentran bajo la clave libraryConfig y se pasan directamente a la biblioteca para su procesamiento.
Banderas CLI
Banderas del servidor:
Bandera | Descripción |
| Requerido. Ruta al JAR de la biblioteca ( |
| Registra cada ejecución en disco |
| Suprime el banner de inicio y el registro de solicitudes/respuestas |
| Deshabilita las herramientas relacionadas con sesiones |
| Habilita/deshabilita |
| Tiempo de espera de reloj de pared para una sola evaluación REPL (por defecto: ninguno; ver Tiempo de Espera de Ejecución) |
| Archivo de configuración JSON (las banderas después de |
Banderas de la biblioteca (abreviatura de algunos campos de libraryConfig):
Bandera | Descripción |
| Bloquea una lista negra integrada de comandos inseguros (operaciones de archivo, shells, intérpretes, herramientas de red, ejecutores de comandos, ...) a través de exec; la coincidencia no distingue mayúsculas. Conveniente para experimentos rápidos; para despliegues reales prefiere |
| Patrones glob separados por comas de comandos ejecutables (p. ej. |
| Patrones glob separados por comas de hosts alcanzables (p. ej. |
| Límite exterior separado por comas en las raíces de |
| Patrones de rutas clasificadas separados por comas (estilo gitignore, ver más abajo) |
| URL base de la API LLM |
| Clave de API LLM |
| Nombre del modelo LLM |
Archivo de Configuración JSON
{
"recordPath": "/tmp/recordings",
"quiet": true,
"sessionEnabled": true,
"safeMode": true,
"executionTimeoutMs": 60000,
"libraryJarPath": "/path/to/TACIT-library.jar",
"libraryConfig": {
"commandPermissions": ["sbt", "scala", "javac", "java", "make"],
"networkPermissions": ["*.scala-lang.org", "github.com", "docs.oracle.com"],
"allowedRoots": ["/home/user/project", "/tmp"],
"classifiedPaths": [".ssh", ".env", ".env.*", "secrets"],
"secureOutput": "/tmp/secure.log",
"classifiedWrite": false,
"llm": {
"baseUrl": "https://api.example.com",
"apiKey": "sk-...",
"model": "gpt-..."
}
}
}commandPermissions (opcional). La lista blanca de exec: una lista de patrones glob (solo * es un comodín) que todo comando pasado a exec debe coincidir. Esto se superpone al conjunto por ámbito declarado por requestExecPermission(...); un comando debe estar en ambos para ejecutarse realmente. Cuando se establece, strictMode se ignora. En despliegues reales siempre debes configurar esta lista explícitamente.
strictMode (opcional, por defecto true). Un valor por defecto para experimentos rápidos que bloquea una lista negra integrada de comandos inseguros a través de exec: comandos de operaciones de archivo (cat, ls, rm, tar, chmod, ...), shells, intérpretes (python, node, perl, ...), herramientas de red (curl, wget, ssh, ...), ejecutores de comandos (xargs, nohup, env, ...), y similares. La coincidencia no distingue mayúsculas en el nombre base del comando. Conveniente cuando solo quieres probar cosas, pero demasiado grueso para uso real; prefiere commandPermissions.
networkPermissions (opcional). La lista blanca de red: una lista de patrones glob (solo * es un comodín) que todo host alcanzado a través de httpGet/httpPost/httpRequest debe coincidir. Al igual que commandPermissions, esto se superpone al conjunto por ámbito declarado por requestNetwork(...); un host debe estar en ambos. Cuando no se establece, solo se aplica la lista blanca por ámbito de requestNetwork.
allowedRoots (opcional). El límite exterior del sistema de archivos: una lista de rutas que confinan dónde puede operar requestFileSystem(root). Una raíz solicitada debe resolverse (símbolos incluidos) a una ruta igual o anidada bajo una de estas, o se deniega el acceso. Cuando no se establece, se usa por defecto el directorio de trabajo actual del servidor, por lo que el sandbox queda confinado a ese subárbol (cierre por fallo). Establézcalo explícitamente para ampliar o reubicar el límite. El enmascaramiento de rutas clasificadas sigue aplicándose dentro de cualquier raíz concedida.
Los enlaces simbólicos siempre se resuelven antes de la comprobación de contención, incluidos los colgantes (una escritura a través de un enlace colgante crea su destino, por lo que el destino es lo que se comprueba). Las entradas encontradas por children/walk (y por tanto por find/grepRecursive) que se resuelven fuera de la raíz concedida, como un enlace .venv/bin/python o node_modules/.bin, se omiten en lugar de seguirse o notificarse como errores.
secureOutput (opcional). Ruta a un archivo de solo añadidura que refleja cada llamada println/print/printf del aislamiento, pero con los valores Classified[_] desenvueltos. La salida principal del agente sigue mostrando la forma enmascarada (Classified(***)), por lo que solo quien pueda leer este archivo ve el contenido real. Los directorios padre se crean automáticamente, y un archivo de sumidero que aún no existe se crea con permisos solo para el propietario (rw-------) en sistemas POSIX (atómicamente, de modo que nunca existe con permisos más amplios). Un archivo existente se añade con sus permisos sin cambios. Cuando no se configura, la impresión se comporta con normalidad y no se escribe nada en el disco.
classifiedWrite (opcional, por defecto true; solo en configuración JSON). Cuando se establece en false, todas las escrituras a rutas clasificadas se deniegan: writeClassified(path, content), access(path).writeClassified(content) y mkdir() en una ruta clasificada.
Tenga en cuenta que, como classify puede envolver cualquier valor, dejar esto habilitado significa que un agente puede sobrescribir archivos clasificados (p. ej. .ssh/authorized_keys) con contenido arbitrario: el mecanismo Classified protege la confidencialidad, no la integridad. Establezca esto en false en despliegues donde los archivos clasificados deban ser de solo lectura para el agente.
Patrones de Rutas Clasificadas
Los patrones de rutas clasificadas siguen la sintaxis de estilo gitignore. Una ruta se clasifica si coincide con un patrón o es descendiente de una coincidencia.
Patrón | Coincide con | Ejemplo |
| Cualquier componente de ruta llamado |
|
| Cualquier componente que coincida con el glob |
|
| Relativo a la raíz del sistema de archivos, con comodín |
|
|
|
|
| Ruta absoluta (símbolos resueltos) |
|
Reglas:
Sin
/en el patrón: coincide con cualquier componente de ruta (coincidencia de nombre base)Patrón relativo con
/: anclado a la raíz del sistema de archivos; admite*,**,?,[…]Patrón absoluto: se compara con la ruta completa; el prefijo no glob se resuelve a través de enlaces simbólicos
/final se elimina (sin distinción de solo directorio)
Patrones clasificados por defecto (cuando classifiedPaths no está configurado): .ssh, .gnupg, .env, .env.*, .netrc, .npmrc, .pypirc, .docker, .kube, .aws, .azure, .gcloud.
Herramientas
Herramienta | Parámetros | Descripción |
|
| Ejecuta un fragmento de Scala en un REPL nuevo (sin estado) |
| - | Crea una sesión REPL persistente, devuelve |
|
| Ejecuta código en una sesión existente (con estado) |
| - | Lista los IDs de sesión activos |
|
| Elimina una sesión |
| - | Muestra la referencia completa de la API de capacidades |
Las sesiones están limitadas a 100 sesiones activas; create_repl_session devuelve un error al alcanzar el límite. La salida de ejecución está limitada a 10 MiB (el truncamiento se marca en el resultado), y exec captura como máximo 8 MiB de stdout y 8 MiB de stderr por invocación.
Ejemplo: Sesión con Estado
1. create_repl_session → session_id: "abc-123"
2. execute_in_session(code: "val x = 42") → x: Int = 42
3. execute_in_session(code: "x * 2") → val res0: Int = 84
4. delete_repl_session(session_id: "abc-123")Características de Seguridad
El sistema de tipos de TACIT proporciona tres garantías de seguridad que se mantienen independientemente de si el agente está desalineado, alucinando o bajo un ataque de inyección de prompts:
Propiedad | Qué significa |
Seguridad de capacidades | Las capacidades no pueden falsificarse ni olvidarse. El agente solo puede acceder a recursos a través de capacidades explícitamente concedidas. |
Completitud de capacidades | Las capacidades regulan todos los efectos relevantes para la seguridad. El agente interactúa con el mundo solo a través de sus capacidades concedidas. |
Pureza local | Cálculos específicos pueden imponerse como libres de efectos secundarios. Esto evita la fuga de información cuando los agentes procesan datos clasificados. |
API de Capacidades
La biblioteca expone tres métodos de solicitud de capacidades, cada uno limitando el acceso a un bloque. Las capacidades no pueden escapar de su bloque de alcance. Esto se impone en tiempo de compilación por el verificador de captura.
// File system: scoped to a root directory
requestFileSystem("/tmp/work") {
val f = access("data.txt")
f.write("hello")
val lines = f.readLines()
grep("data.txt", "hello")
find(".", "*.txt")
}
// Process execution: scoped to an allowlist of commands
requestExecPermission(Set("ls", "cat")) {
val result = exec("ls", List("-la"))
println(result.stdout)
}
// Network: scoped to an allowlist of hosts
requestNetwork(Set("api.example.com")) {
val body = httpGet("https://api.example.com/data")
httpPost("https://api.example.com/submit", """{"key":"value"}""")
// Arbitrary verbs with a status code:
val resp = httpRequest("DELETE", "https://api.example.com/item/42") // resp.status, resp.body
}Los métodos de red también toman headers: Map[String, String] y secretHeaders: Map[String, Classified[String]] simples. Un valor de secretHeaders (p. ej. un token Authorization leído mediante readClassified) se envía al host de la lista de permitidos pero nunca es observable para el código del agente, lo que permite que un agente se autentique en una API permitida con un secreto que no puede leer de otro modo. httpPostClassified completa el cuadro: envía un cuerpo Classified[String] y devuelve una respuesta Classified[String], de modo que los datos sensibles pueden viajar de ida y vuelta a través de un servicio externo mientras permanecen bajo el control de flujo de información (ver más abajo).
Control de Flujo de Información mediante Classified
Considere un agente de código típico trabajando en un directorio de proyecto. Algunos archivos son ordinarios (código fuente, configuraciones de compilación, README). Otros son sensibles: claves API en .env, credenciales en secrets/, documentos internos. El agente está impulsado por un LLM alojado en la nube (un servicio de terceros). Queremos que el agente use o procese los datos sensibles (resumir documentos internos, rotar claves, procesar informes) pero nunca los filtre al proveedor de la nube.
TACIT resuelve esto mediante el tipo Classified[T]. Los archivos bajo rutas clasificadas designadas (configuradas mediante --classified-paths con patrones de estilo gitignore, p. ej. .ssh, .env.*, secrets, **/keys) devuelven su contenido envuelto en Classified[String] en lugar de String simple. Si no se configura de otro modo, las rutas de secretos comunes (.ssh, .gnupg, .env, .env.*, etc.) se clasifican por defecto. El sistema de tipos impone acceso solo puro: Classified.map acepta solo funciones puras (T -> U), lo que significa sin efectos y sin capacidades capturadas. Puede transformar los datos, pero no puede enviarlos a ningún lugar. Cualquier intento de exfiltrar datos clasificados se rechaza en tiempo de compilación:
requestFileSystem("/project") {
val secret = readClassified("secrets/api-key.txt")
// Compile error: map captures the file capability, not a pure function
secret.map: s =>
access("exfil.txt").write(s) // error: capturing f is not allowed
s
// Compile error: print out the classified content to the cloud LLM
secret.map: s =>
println(s) // error: capturing IOCapability is not allowed
s
}Entonces, ¿cómo puede el agente hacer un trabajo útil con datos clasificados? A través de un diseño LLM dual: un LLM local confiable separado procesa el contenido clasificado. El framework proporciona una sobrecarga de chat que acepta Classified[String] y devuelve Classified[String]. El LLM confiable ve el contenido, pero el resultado permanece envuelto y nunca puede fluir de vuelta al modelo de nube no confiable.

requestFileSystem("/project") {
// OK: read classified content
val doc = readClassified("secrets/contract-v2.txt")
// OK: pure transformation
val upper = doc.map(_.trim)
// OK: send to trusted local LLM, result stays Classified
val summary = chat(doc.map(s => s"Summarize the following document:\n$s"))
// summary: Classified[String], content is still protected
// OK: write back to a classified file
writeClassified("secrets/summary.txt", summary)
}Además del LLM confiable y los archivos clasificados, un valor Classified también puede fluir hacia un host de red de la lista de permitida sin ser desclasificado, ya sea como un encabezado de solicitud secreto (p. ej. autenticación a una API permitida) o como un cuerpo POST clasificado cuya respuesta permanece envuelta:
requestNetwork(Set("api.example.com")) {
requestFileSystem("/project") {
val key = readClassified("secrets/api.key")
// OK: the token reaches the allowlisted host as a header, but is never
// observable to agent code (the value cannot be printed or inspected).
val me = httpGet("https://api.example.com/me",
secretHeaders = Map("Authorization" -> key.map("Bearer " + _)))
// OK: secret body in, Classified response out.
val payload = readClassified("secrets/report.json")
val reply = httpPostClassified("https://api.example.com/process", payload)
// reply: Classified[String]
}
}Modo Seguro
El código generado por el agente se compila bajo el modo seguro de Scala 3 (import language.experimental.safe), que impone un subconjunto de lenguaje seguro de capacidades:
Sin conversiones de tipo no verificadas ni coincidencias de patrones
Sin características del módulo
caps.unsafeSin anotaciones
@uncheckedSin reflexión en tiempo de ejecución
Compilar con verificación de captura y nulos explícitos habilitados, rastreando todos los efectos de mutación
Los objetos y funciones globales son accesibles solo si se implementan de forma segura
Estas restricciones evitan que los agentes "olviden" capacidades a través de castings inseguros, reflexión o agujeros del sistema de tipos. El código que no pasa la compilación nunca se ejecuta.
El modo seguro es una característica experimental aún en desarrollo activo. Por defecto, TACIT usa un validador de código estático que verifica patrones prohibidos para imponer el subconjunto de modo seguro. La bandera --safe-mode (o "safeMode": true en la configuración JSON) además importa language.experimental.safe en cada ejecución REPL, optando por la aplicación en el compilador de Scala 3.
Tiempo de Espera de Ejecución
--exec-timeout-ms <ms> (o "executionTimeoutMs" en la configuración JSON) limita el tiempo de reloj de pared de una sola evaluación REPL. El valor debe ser positivo; los valores cero o negativos se rechazan al inicio. En caso de tiempo de espera, el cliente recibe un error de prompt en lugar de colgarse, y para sesiones con estado la sesión conserva su estado anterior, por lo que la declaración abandonada no tiene efecto observable.
El vigilante ejecuta cada evaluación en un hilo de trabajo y es de mejor esfuerzo: el trabajo que responde a interrupciones (I/O bloqueante, sueños, la mayoría de las llamadas de biblioteca) se limita de manera confiable, pero un bucle de CPU puro que nunca verifica la interrupción sigue ejecutándose en segundo plano y continúa manteniendo el bloqueo de salida del REPL. La preempción dura requeriría aislamiento a nivel de proceso; este control es una protección de robustez, no un límite de sandbox. Cuando no se configura (el valor por defecto), las evaluaciones se ejecutan sin tiempo de espera.
Integración con LLM
Un LLM secundario está disponible a través del método chat, sin necesidad de alcance de capacidad. La seguridad proviene del sistema de tipos Classified: chat(String): String para datos regulares, chat(Classified[String]): Classified[String] para datos sensibles.
// Regular chat
val answer = chat("What is 2 + 2?")
// Classified chat: input and output stay wrapped
requestFileSystem("/secrets") {
val secret = readClassified("/secrets/key.txt")
val result = chat(secret.map(s => s"Summarize: $s"))
// result is Classified[String], cannot be printed or leaked
}Configúrelo mediante banderas CLI (--llm-base-url, --llm-api-key, --llm-model) o un archivo de configuración JSON (--config). Se admite cualquier API compatible con OpenAI.
Resultados Experimentales
Evaluamos TACIT en seguridad y expresividad (ver la sección 4 del documento para detalles completos).
Seguridad (RQ1). En modo clasificado (secretos envueltos en Classified[String]), tanto Claude Sonnet 4.6 como MiniMax M2.5 logran 100% de seguridad en los 131 ensayos. Cada inyección y tarea maliciosa es bloqueada por el sistema de tipos. La utilidad sigue siendo alta (99.2% para Sonnet, 90.0% para MiniMax).
Expresividad (RQ2). En τ2-bench y SWE-bench Lite, los agentes que usan el arnés de capacidades seguras de TACIT igualan o superan ligeramente los puntos de referencia estándar de llamada a herramientas en todos los modelos probados (gpt-oss-120b, MiniMax M2.5, DeepSeek V3.2), demostrando que escribir Scala con seguridad de tipos no degrada el rendimiento agéntico.
Extender la Biblioteca: Añadir su Propia API
La biblioteca (library/) define la API de capacidades que el código de usuario puede llamar dentro del REPL.
Para implementar permisos personalizados y control de acceso de grano fino, puede añadir nuevas capacidades (p. ej., acceso a bases de datos, colas de mensajes, gestión de servidores) modificando la biblioteca y reconstruyendo solo el JAR de la biblioteca.
Estructura de la Biblioteca
library/
├── Interface.scala # Public API trait (what user code sees)
├── impl/
│ ├── InterfaceImpl.scala # Wires everything together (exports Ops objects)
│ ├── BaseFileSystem.scala # Shared path validation and gitignore-style classified-path matching
│ ├── FileOps.scala # grep, grepRecursive, find
│ ├── ProcessOps.scala # exec, execOutput
│ ├── WebOps.scala # httpGet, httpPost, httpRequest, httpPostClassified
│ ├── LlmOps.scala # chat
│ ├── RealFileSystem.scala # FileSystem on real disk
│ ├── VirtualFileSystem.scala # In-memory FileSystem (for testing)
│ ├── ClassifiedImpl.scala # Classified[T] wrapper implementation
│ ├── ProcessPermissionImpl.scala # Concrete ProcessPermission
│ ├── NetworkImpl.scala # Concrete Network
│ ├── GlobMatcher.scala # Shared `*`-glob to regex utility
│ ├── LibraryConfig.scala # Library configuration with JSON parsing
│ └── LlmConfig.scala # LLM configuration case class
└── test/ # Library-level testsPaso a paso: Añadir una nueva API
Aquí hay un ejemplo de cómo añadir una capacidad hipotética requestDatabase.
1. Define los tipos y la capacidad en Interface.scala
// Add a result type
case class QueryResult(columns: List[String], rows: List[List[String]])
// Add a capability class. Note the `private[library]` constructor: capability
// classes must not be constructible or extendable by agent code.
class DatabasePermission private[library] (val connectionString: String) extends caps.SharedCapability
// Add methods to the Interface trait
trait Interface:
// ... existing methods ...
def requestDatabase[T](connectionString: String)(op: DatabasePermission^ ?=> T)(using IOCapability): T
def query(sql: String)(using DatabasePermission): QueryResultPuntos clave:
La clase de capacidad debe extender
caps.SharedCapability. Esto es lo que permite al comprobador de captura de Scala 3 evitar que la capacidad escape de su bloque con ámbito.El método
request*toma un bloqueopque recibe la capacidad como parámetro de contexto (?=>). La marca^significa que la capacidad es rastreada por el comprobador de captura.Los métodos de operación (como
query) toman la capacidad como parámetrousing, por lo que solo se pueden llamar dentro del bloquerequest*correspondiente.
2. Implementa las operaciones en impl/
Crea library/impl/DatabaseOps.scala:
package tacit.library
import language.experimental.captureChecking
object DatabaseOps:
def query(sql: String)(using perm: DatabasePermission): QueryResult =
// Your implementation here
// perm.connectionString has the connection info
???3. Conéctalo en InterfaceImpl
En library/impl/InterfaceImpl.scala, exporta tus nuevas operaciones e implementa el método request*:
abstract class InterfaceImpl private[library] (...) extends Interface:
export FileOps.*
export ProcessOps.*
export WebOps.*
export DatabaseOps.* // ← add this
// ... existing methods ...
def requestDatabase[T](connectionString: String)(op: DatabasePermission^ ?=> T)(using IOCapability): T =
val perm = new DatabasePermission(connectionString)
op(using perm)4. Bloquea el acceso directo en el validador (lado del servidor)
Si tu nueva API envuelve una biblioteca Java/Scala a la que los usuarios no deberían llamar directamente, añade patrones prohibidos a src/main/scala/executor/CodeValidator.scala:
ForbiddenPattern("db-jdbc", raw"java\.sql\b".r, "Direct JDBC access is forbidden; use requestDatabase"),
ForbiddenPattern("db-driver", raw"DriverManager".r, "DriverManager is forbidden; use requestDatabase"),Esto garantiza que el código del usuario pase por la API de capacidades en lugar de omitirla.
5. Añade dependencias (si es necesario)
Si tu nueva API requiere bibliotecas externas, añádelas al proyecto lib en build.sbt:
lazy val lib = project
.in(file("library"))
.settings(
// ... existing settings ...
libraryDependencies ++= Seq(
"com.openai" % "openai-java" % "4.38.0",
"org.postgresql" % "postgresql" % "42.7.3", // ← add your dep
),
)6. Reconstruye el JAR de la biblioteca
sbt "lib/assembly"No necesitas reconstruir el JAR del servidor a menos que hayas cambiado CodeValidator (paso 4) u otro código del lado del servidor. Solo apunta el servidor al nuevo JAR de la biblioteca:
java -jar server.jar --library-jar new-library.jar7. Prueba tu nueva API en el REPL de desarrollo
Para iterar rápidamente sin iniciar un agente, lanza el REPL de desarrollo, un prompt interactivo de Scala precargado con la API de capacidades y el mismo CodeValidator que usa el servidor MCP:
sbt devRepl # default config
sbt "devRepl --strict --config my.json" # with flagsCosas a tener en cuenta
Las capacidades deben extender
caps.SharedCapability. Esto es lo que hace que funcione la comprobación de captura. Sin ello, el compilador no puede rastrear el ámbito de la capacidad y los usuarios podrían filtrarla fuera del bloquerequest*.Las clases de capacidades y sus implementaciones están selladas. Todos los tipos de capacidades (
FileSystem,Network,ProcessPermission,IOCapability,Classified,FileEntry) y todas las clases de implementación (RealFileSystem,NetworkImpl,LlmOps, ...) tienen constructoresprivate[library], por lo que el código del agente no puede ni instanciarlas ni extenderlas: las capacidades solo pueden provenir de los ámbitosrequest*. Mantén esta invariante para cualquier capacidad que añadas: dale a la clase un constructorprivate[library]y haz que las implementaciones concretas también seanprivate[library].La interfaz en sí también está sellada. El constructor de
InterfaceImplesprivate[library], por lo que nada fuera de la biblioteca puede elegir el JSON de políticas. El servidor registra la configuración de la biblioteca una vez por sandbox (InterfaceImpl.configure, llamado a través del cargador de clases del REPL antes de que se ejecute cualquier código), y el preámbulo instancia elSandboxInterfacesin parámetros, cuya política es esa configuración registrada. El código del agente que extiendeSandboxInterfacesolo obtiene una interfaz idéntica y vinculada a la política, nunca una más amplia.La comprobación de captura es experimental. El proyecto usa
-language:experimental.captureChecking. El comportamiento del compilador puede cambiar entre versiones nightly de Scala 3. Si encuentras errores inesperados, comprueba si el problema es la comprobación de captura eliminando temporalmente la bandera.La biblioteca usa Scala 3 nightly. La compilación descarga automáticamente la última versión nightly de Scala 3. Esto significa que tu código debe ser compatible con Scala de última generación. Fija una versión específica en
build.sbt(val scala3Version = "3.x.y") si necesitas estabilidad.Interface.scalase incluye como recurso. El servidor copiaInterface.scalaen sus recursos en tiempo de compilación para que la herramientashow_interfacepueda mostrarlo. Si añades nuevas APIs, los usuarios las verán automáticamente a través deshow_interface, sin trabajo adicional.Los patrones prohibidos se aplican al código del usuario, no al código de la biblioteca. El validador en
CodeValidator.scalasolo comprueba el código enviado por el usuario. La biblioteca en sí puede usar librementejava.io,java.net,ProcessBuilder, etc. en su implementación. Pero si tu nueva API envuelve una API de Java, deberías añadir un patrón prohibido correspondiente para que los usuarios no puedan omitir tu envoltorio de capacidades.El JAR de la biblioteca es un JAR gordo.
sbt "lib/assembly"produce un JAR que incluye todas las dependencias de la biblioteca (p. ej.,openai-java). Si añades una dependencia, se incluirá automáticamente.El servidor depende de los tipos de la biblioteca en tiempo de compilación. El servidor depende del tipo de interfaz para ejecutar el REPL. Asegúrate de que tu cambio sea compatible con la interfaz esperada por el servidor.
Prueba tu API a nivel de biblioteca primero. El directorio
library/test/contiene pruebas a nivel de biblioteca que usan MUnit, ejecutadas conscala-cli test library --server=false(no forman parte desbt test;--server=falseevita un conflicto de ASM entre las versiones nightly actuales de Scala 3 y el servidor Bloop incluido con scala-cli). Prueba tus nuevas operaciones allí antes de hacer pruebas de integración a través del servidor MCP. ConsultaLibrarySuite.test.scalapara ver ejemplos.
Desarrollo
Requisitos:
JDK 17+
sbt 1.12+
sbt clean # Clean build artifacts
sbt compile # Compile
sbt test # Run the server test suites (src/test/scala)
sbt "testOnly *McpServerSuite" # Run a single server suite
scala-cli test library --server=false # Run the library test suites (library/test)
sbt assembly # Build both JARs (server + library)
sbt "lib/assembly" # Build library JAR only
sbt devRepl # Interactive REPL for testing the library
sbt testejecuta solo las suites del servidor. Las suites delibrary/test/se ejecutan por separado conscala-cli test library --server=false.
# Basic
java -jar target/scala-*/TACIT-assembly-*.jar \
--library-jar library/target/scala-*/TACIT-library.jar
# With logging
java -jar server.jar --library-jar library.jar --record ./log
# With JSON config
java -jar server.jar --library-jar library.jar --config config.jsonCitación
@inbook{10.1145/3786335.3813127,
author = {Odersky, Martin and Zhao, Yaoyu and Xu, Yichen and Bra\v{c}evac, Oliver and Pham, Cao Nguyen},
title = {Securing Agents With Tracked Capabilities},
year = {2026},
isbn = {9798400724152},
publisher = {Association for Computing Machinery},
address = {New York, NY, USA},
url = {https://doi.org/10.1145/3786335.3813127},
booktitle = {Proceedings of the ACM Conference on AI and Agentic Systems},
pages = {812–838},
numpages = {27}
}Licencia
Apache-2.0
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
- FlicenseNot gradedqualityDmaintenanceA unified MCP server providing observability, safety control, and behavior evolution for high-agency AI agents through tracing, replaying, and auditing. It features real-time firewall guardrails and ML-driven anomaly detection to monitor, block, or fork agent actions based on risk.7
- AlicenseCqualityBmaintenanceAgent-first programming language: agents produce JSON AST, the compiler validates, type-checks, effect-checks, verifies contracts via Z3/SMT, and compiles to WASM. 19 MCP tools for the full compile-and-execute loop.2212311MIT
- AlicenseNot gradedqualityCmaintenanceRuntime safety guardrails for AI coding agents. Checks file access, validates shell commands, and scores your repo's AI safety — all via MCP.58MIT
- FlicenseBqualityAmaintenanceA disciplined engineering harness for AI coding agents that provides ground-truth MCP tools and adversarial enforcement skills to transform generic LLMs into high-precision engineers.1001
Related MCP Connectors
An effect gate for AI agents: at-most-once side effects, spend limits, and signed receipts.
Security firewall for AI agents — scans MCP calls for injection, secrets, and risks.
MCP-native Trust Infrastructure for AI Agents. Persistent encrypted memory with Trust Quotient.
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/lampepfl/TACIT'
If you have feedback or need assistance with the MCP directory API, please join our Discord server