Sidecar MCP: convertí tus APIs REST y CLIs en tools MCP con un archivo YAML
Cada vez que un equipo quiere que un agente de IA use un sistema interno, aparece la misma tarea: escribir otro servidor MCP. Un proyecto nuevo, dependencias nuevas, otro pipeline de build, otra forma de manejar credenciales, otros logs. Para la quinta integración ya hay cinco servidores casi idénticos que solo difieren en qué endpoint llaman y cómo arman la respuesta, y cada uno tiene su propia interpretación de qué validar, qué loguear y cómo cortar una llamada que no responde.
En Underqubit nos pasó lo mismo construyendo integraciones para agentes, y la conclusión fue que la mayoría de esas tools no necesitan código: necesitan un contrato. Qué recibe la tool, a qué API o comando la traduce, cómo se autentica y qué devuelve. Por eso hoy publicamos Sidecar MCP como proyecto open-source bajo licencia Apache 2.0: un servidor MCP en Node.js y TypeScript que convierte APIs REST y CLIs existentes en tools declaradas en YAML o JSON. Agregar una tool es editar la configuración y reiniciar el proceso. Sin programar, sin recompilar.
Qué es Sidecar MCP
El nombre viene del patrón sidecar: un proceso que se ubica al lado de sistemas que ya existen y les agrega una capacidad sin modificarlos. Sidecar MCP no reemplaza tu API ni tu CLI; los expone al protocolo Model Context Protocol con un contrato explícito, para que cualquier cliente MCP —Claude, Cursor, un IDE o tus propios agentes— los pueda descubrir e invocar.
Lo que trae la versión 1.0:
- Dos tipos de ejecución: requests HTTP con Axios para APIs REST, y procesos locales con
spawn(sin shell) para CLIs. - Dos transportes: stdio para clientes que lanzan el proceso, y Streamable HTTP para un servicio compartido.
- Contratos estrictos: cada tool declara
inputSchemayoutputSchemaen JSON Schema 2020-12. Las entradas y salidas se validan con AJV en cada llamada. - Autenticación de servicio: perfiles
apiKey,bearer,basicyoauth2(client credentials con caché en memoria), con secretos que solo pueden venir de variables de entorno. - Transformación de respuestas: parsers
json,textyregex, más expresiones JSONata para dar forma al resultado antes de que llegue al modelo. - Grupos de tools: varias integraciones en un mismo proceso, como catálogo unificado, endpoints separados o ambos.
- Importación desde OpenAPI: scripts que generan la configuración a partir de especificaciones Swagger 2.0 u OpenAPI 3.x.
Inicio rápido
Requiere Node.js 24 o superior. El paquete se publica en npm como @underqubit/sidecar-mcp, y el binario que instala se llama underqubit-sidecar-mcp. Con un archivo de configuración propio, el flujo son dos comandos:
npx -y @underqubit/[email protected] validate --config /ruta/absoluta/config.yaml
npx -y @underqubit/[email protected] serve --config /ruta/absoluta/config.yaml --transport stdiovalidate hace exactamente las mismas comprobaciones que el arranque: esquema de la configuración, nombres duplicados, JSON Schemas, perfiles de auth inexistentes, expresiones regex y JSONata, y presencia de todas las variables de entorno referenciadas. No llama a ninguna API ni ejecuta ningún CLI, así que es seguro correrlo en CI antes de desplegar.
Para conectarlo a un cliente que use el formato mcpServers (Claude Desktop, Cursor y otros):
{
"mcpServers": {
"underqubit": {
"command": "npx",
"args": [
"-y",
"@underqubit/[email protected]",
"serve",
"--config",
"/ruta/absoluta/config.yaml"
],
"env": {
"API_BASE_URL": "https://api.example.com",
"API_TOKEN": "replace-me"
}
}
}
}Y en Claude Code, con un solo comando:
claude mcp add --transport stdio \
--env API_BASE_URL=https://api.example.com \
--env API_TOKEN=replace-me \
underqubit -- npx -y @underqubit/[email protected] serve --config /ruta/absoluta/config.yamlSi preferís un servicio compartido en lugar de un proceso por cliente, el mismo archivo se sirve por HTTP con --transport http. El endpoint predeterminado es http://127.0.0.1:3000/mcp.
Anatomía de una tool
Una tool es un bloque de configuración con seis piezas: nombre, descripción, esquema de entrada, esquema de salida, ejecución y procesamiento de la respuesta. Este ejemplo expone los últimos commits de un repositorio usando git, sin una línea de código:
# yaml-language-server: $schema=./node_modules/@underqubit/sidecar-mcp/schema/config.schema.json
version: 1
tools:
- name: git_recent_commits
description: Lists the most recent commits of the application repository with hash, author and subject.
inputSchema:
type: object
properties:
limit: { type: integer, minimum: 1, maximum: 50, description: Number of commits to return. }
required: [limit]
additionalProperties: false
outputSchema:
type: object
properties:
commits:
type: array
items:
type: object
properties:
hash: { type: string }
author: { type: string }
subject: { type: string }
required: [hash, author, subject]
additionalProperties: false
required: [commits]
additionalProperties: false
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
execution:
type: cli
command: git
args: [log, '--max-count=${input.limit}', '--pretty=format:%h|%an|%s']
cwd: /srv/repos/app
response:
parser:
type: regex
pattern: '^(?<hash>[0-9a-f]+)\|(?<author>[^|]+)\|(?<subject>.*)$'
flags: m
mode: all
transform: '{"commits": data}'Algunas cosas que vale la pena notar:
- Las plantillas son el único puente entre el modelo y la ejecución.
${input.limit}toma un argumento validado de la tool;${env.VARIABLE}toma una variable de entorno que se captura al iniciar y nunca se expone al modelo. No hay evaluación de JavaScript ni sustituciones de shell. spawncorre conshell: false. Cada elemento deargses un argumento literal: si el modelo manda5; rm -rf /, el esquema lo rechaza por no ser entero, y aunque fuera un string, no crearía un segundo comando.- El proceso hijo hereda un entorno mínimo (
PATH,LANG, directorios temporales y poco más). Cualquier otra variable se declara explícitamente enexecution.env. - El header
yaml-language-serverapunta al JSON Schema de la configuración, así que el editor autocompleta y valida mientras escribís.
Integrar una API REST
El mismo contrato sirve para HTTP. Este ejemplo consulta un repositorio en la API de GitHub con un token de servicio y le devuelve al modelo solo los campos que necesita:
version: 1
auth:
github:
type: bearer
token: '${env.GITHUB_TOKEN}'
tools:
- name: github_get_repo
description: Gets stars, open issues, default branch and last push date of a GitHub repository.
inputSchema:
type: object
properties:
owner: { type: string, minLength: 1 }
repo: { type: string, minLength: 1 }
required: [owner, repo]
additionalProperties: false
outputSchema:
type: object
properties:
fullName: { type: string }
stars: { type: integer }
openIssues: { type: integer }
defaultBranch: { type: string }
pushedAt: { type: string }
required: [fullName, stars, openIssues, defaultBranch, pushedAt]
additionalProperties: false
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
execution:
type: http
baseUrl: https://api.github.com
method: GET
path: /repos/{owner}/{repo}
pathParams: { owner: '${input.owner}', repo: '${input.repo}' }
headers:
Accept: application/vnd.github+json
X-GitHub-Api-Version: '2022-11-28'
auth: github
timeoutMs: 10000
response:
parser: { type: json }
transform: >-
{"fullName": data.full_name, "stars": data.stargazers_count,
"openIssues": data.open_issues_count, "defaultBranch": data.default_branch,
"pushedAt": data.pushed_at}Los parámetros de ruta se codifican como segmentos de URL y el origen no puede cambiar: un owner como ../../otra-api no logra salir de api.github.com. Tampoco hay redirecciones, reintentos ni proxies implícitos; si la API responde con un estado fuera de 200–299 (o de los successStatuses que declares), la tool devuelve un error y no un resultado a medias.
Autenticación sin secretos en el archivo
Los perfiles se declaran una vez bajo auth y cada tool HTTP elige uno por nombre. La regla es simple: los secretos no pueden escribirse como literales, solo como referencias a variables de entorno. Así el archivo de configuración se puede versionar sin riesgo.
| Tipo | Campos |
|---|---|
none |
Sin campos adicionales. |
apiKey |
in: header o query, name, value: '${env.API_KEY}' |
bearer |
token: '${env.API_TOKEN}' |
basic |
username y password desde variables de entorno |
oauth2 |
tokenUrl, clientId, clientSecret, scopes, tokenAuthMethod |
El perfil OAuth2 usa el flujo client_credentials, comparte una única solicitud de token entre llamadas concurrentes, lo cachea en memoria y lo renueva antes de que venza:
auth:
oauth_service:
type: oauth2
tokenUrl: '${env.OAUTH_TOKEN_URL}'
clientId: '${env.OAUTH_CLIENT_ID}'
clientSecret: '${env.OAUTH_CLIENT_SECRET}'
tokenAuthMethod: basic
scopes: [users.read]Para los CLIs, las credenciales viajan por env o stdin, usando la autenticación que cada programa ya soporta. El servidor no carga .env por su cuenta; si lo necesitás, Node lo hace explícitamente con node --env-file=.env.
Qué pasa en cada llamada
Todas las tools, sean HTTP o CLI, por stdio o por HTTP, recorren el mismo pipeline:
Cada etapa tiene nombre (input, parameters, auth, execution, parse, transform, output), y si algo falla la respuesta MCP llega con isError: true y la etapa correspondiente. Eso hace que diagnosticar sea directo: un error en output significa que la API cambió su forma o que el outputSchema está mal; un error en auth apunta al token, no a la API.
Las respuestas correctas incluyen structuredContent —el JSON ya validado contra outputSchema— y un bloque de texto con el mismo contenido para clientes que todavía no leen contenido estructurado. Nada se trunca ni se convierte de tipo en silencio: si el resultado no cumple el contrato, es un error.
Regex y JSONata corren dentro de workers que se pueden terminar, con entorno vacío, sin acceso a filesystem ni red, un presupuesto predeterminado de un segundo y 64 MiB de heap. Una expresión mal escrita o un payload diseñado para causar backtracking catastrófico no bloquea el proceso.
Un caso real: Jira Cloud
El repositorio incluye una integración completa con Jira Cloud: buscar issues con JQL, obtener un issue, listar comentarios y su historial de cambios. Es un buen ejemplo de cómo la transformación protege al modelo de respuestas enormes. La API de búsqueda de Jira devuelve decenas de campos anidados por issue; la tool le entrega al agente solo lo que necesita para razonar (la transformación está abreviada; la completa contempla campos nulos):
{
"execution": {
"type": "http",
"baseUrl": "${env.JIRA_BASE_URL}",
"method": "GET",
"path": "rest/api/3/search/jql",
"headers": { "Accept": "application/json" },
"auth": "jira",
"successStatuses": [200],
"query": {
"jql": "${input.jql}",
"maxResults": "${input.maxResults}",
"nextPageToken": "${input.nextPageToken}",
"fields": "summary,status,issuetype,priority,assignee,updated"
}
},
"response": {
"parser": { "type": "json" },
"transform": "{\"issues\": [data.issues.{\"key\": key, \"summary\": fields.summary, \"status\": fields.status.name, ...}], \"isLast\": data.isLast, \"nextPageToken\": data.nextPageToken}"
}
}La descripción de la tool explica al modelo cómo paginar (nextPageToken: null en la primera página, reutilizar el token devuelto hasta isLast: true), y el esquema de entrada limita maxResults entre 1 y 100. Las reglas viven en el contrato, no en un prompt que el modelo puede ignorar.
Varias integraciones en un proceso
Cuando el número de integraciones crece, no hace falta un proceso por cada una. Una composición referencia varios archivos de integración, cada uno con sus propios perfiles de autenticación, límites y variables, y los expone en un solo listener:
version: 1
kind: composition
server:
name: underqubit
transport: http
http: { host: 127.0.0.1, port: 3000 }
mode: both
groups:
- id: local
config: ./cli.yaml
- id: jira
config: ./jira.json
unifiedpublica un único catálogo (en/mcpo por stdio) con nombres prefijados:jira_get_issuedel grupojirase convierte enjira.jira_get_issue.separatepublica un endpoint por grupo (/mcp/local,/mcp/jira) con los nombres originales. Útil para dar a cada agente solo las tools que le corresponden.bothhace las dos cosas en el mismo puerto.
Un detalle importante: los grupos separan configuración, no permisos. El servidor HTTP no implementa autenticación entrante, así que si exponés endpoints por grupo para distintos consumidores, el control de acceso se resuelve en el proxy que tengas delante.
De OpenAPI a tools en un comando
Si la API ya tiene una especificación, no hace falta escribir las tools a mano. Los scripts de importación generan una tool HTTP por operación, con descripción, esquemas, parámetros, autenticación y transformación:
node scripts/openapi-to-config.mjs \
--input ./api.yaml \
--output ./tools.json \
--base-url https://api.example.com/v1 \
--methods GET,HEAD \
--prefix catalog
El script acepta Swagger 2.0 y OpenAPI 3.0, 3.1 y 3.2, filtra por método o por tag, y falla sin generar configuración parcial si encuentra una operación que no puede representar (a menos que le pidas --skip-unsupported). Una advertencia que tomamos en serio: por defecto incluye todos los métodos, también los de escritura. Por eso el ejemplo filtra GET,HEAD, y por eso el último paso del flujo es una revisión humana antes de conectar el resultado a un agente.
Seguridad por diseño
Hace poco repasamos los diez vectores de ataque más críticos en agentes de IA, y varios de ellos —abuso de herramientas, escalamiento de privilegios, exfiltración— se juegan justamente en la capa que conecta al agente con los sistemas reales. Sidecar MCP está diseñado para que esa capa sea aburrida y predecible:
- El modelo solo entrega argumentos declarados. Ejecutable, directorio de trabajo y origen HTTP no aceptan plantillas de entrada; el agente no puede cambiar a qué se conecta.
- Sin shell. Los CLIs se ejecutan con
spawny argumentos literales; no hay pipelines ni metacaracteres interpretados. - Secretos fuera del alcance del modelo. Solo desde variables de entorno, nunca en los logs ni en la entrada de JSONata, que recibe únicamente el dato parseado y metadata como el status HTTP o el código de salida.
- Logs sin contenido sensible. Se registra en JSON por stderr la tool, la duración, el resultado y la etapa de error; no los bodies, argumentos ni credenciales.
- Loopback por defecto. El transporte HTTP escucha en
127.0.0.1, validaHostyOrigin, y limita el tamaño de los requests. Para exponerlo hacia afuera se pone detrás de un proxy con TLS y autenticación. - Límites en todo. Timeouts por tool con deadline global, máximo de bytes de salida (que también cuenta stderr), cancelación que mata al grupo de procesos y escala de SIGTERM a SIGKILL.
Las anotaciones MCP (readOnlyHint, destructiveHint) describen la intención de cada tool para que el cliente pueda pedir confirmación, pero no son un mecanismo de autorización. El servidor ejecuta lo que la configuración permite con los permisos del usuario del proceso: el mínimo privilegio real empieza por qué tools declarás y con qué credenciales corre el proceso.
Lo que no hace (todavía)
La versión 1.0 tiene un alcance deliberadamente acotado. No incluye recarga en caliente de la configuración, OAuth por usuario, pipelines que encadenen varias ejecuciones, formatos CSV o XML, ni multipart y archivos binarios. Para esos casos, o para lógica de negocio que no entra en un contrato declarativo, un servidor MCP escrito a medida sigue siendo la herramienta correcta. Para todo lo demás —que en nuestra experiencia es la mayoría— alcanza con un archivo YAML.
Pensado también para agentes
El repositorio incluye una skill de uso (skills/sidecar-mcp/SKILL.md) para que un agente de código sepa cómo configurar una integración nueva, conectar un cliente o diagnosticar una tool que falla, siguiendo las mismas convenciones del proyecto. También trae un set de evaluación con preguntas de solo lectura sobre una muestra fija, para medir qué tan bien usa un agente las tools expuestas.
La documentación completa, en español e inglés, cubre configuración, autenticación, respuestas, grupos de tools, importación OpenAPI, arquitectura y operación.
Empezá a usarlo
- Código fuente: github.com/underqubit/sidecar-mcp
- Paquete npm: @underqubit/sidecar-mcp
- Licencia: Apache 2.0
Si tenés una API o un CLI que tu equipo usa todos los días, probá describirlo como una tool y conectarlo a tu agente. Issues, pull requests e integraciones de ejemplo son bienvenidos. Y si estás evaluando cómo llevar agentes de IA a sistemas críticos de tu organización con estos mismos principios de contrato, mínimo privilegio y observabilidad, hablemos.