← Volver al blog

Sidecar MCP: convertí tus APIs REST y CLIs en tools MCP con un archivo YAML

Por Javier Carrara27 de octubre de 2026

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.

Arquitectura de Sidecar MCP: los clientes MCP se conectan por stdio o Streamable HTTP; el runtime carga la configuración YAML o JSON y despacha cada llamada al adaptador HTTP hacia APIs REST o al adaptador CLI hacia programas locales

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 inputSchema y outputSchema en JSON Schema 2020-12. Las entradas y salidas se validan con AJV en cada llamada.
  • Autenticación de servicio: perfiles apiKey, bearer, basic y oauth2 (client credentials con caché en memoria), con secretos que solo pueden venir de variables de entorno.
  • Transformación de respuestas: parsers json, text y regex, 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 stdio

validate 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.yaml

Si 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.
  • spawn corre con shell: false. Cada elemento de args es un argumento literal: si el modelo manda 5; 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 en execution.env.
  • El header yaml-language-server apunta 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:

Pipeline de una llamada en Sidecar MCP: validar entrada, resolver parámetros y credenciales, ejecutar HTTP o CLI, parsear, transformar con JSONata, validar salida y responder MCP; cada etapa tiene un nombre que se reporta si falla

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

Tres modos de composición de grupos de tools: unified publica un catálogo único con nombres prefijados por grupo; separate publica un endpoint por grupo con los nombres originales; both expone ambos en el mismo puerto

  • unified publica un único catálogo (en /mcp o por stdio) con nombres prefijados: jira_get_issue del grupo jira se convierte en jira.jira_get_issue.
  • separate publica un endpoint por grupo (/mcp/local, /mcp/jira) con los nombres originales. Útil para dar a cada agente solo las tools que le corresponden.
  • both hace 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

Flujo de importación OpenAPI: una especificación Swagger 2.0 u OpenAPI 3.x pasa por el script openapi-to-config con filtros de métodos, tags y prefijo, genera un archivo de tools que se revisa, valida y sirve

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 spawn y 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, valida Host y Origin, 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

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.

mcpopen-sourceai-agentstypescriptopenapidevops