OpenAPI: el contrato de la API al alcance del agente

Cómo conectar la especificación OpenAPI de tu backend para que el agente descubra endpoints y schemas, y cómo se ejecutan las requests autenticadas, que van por otro camino.

SetupMCP

La regla: el MCP solo lee

El MCP de OpenAPI (@ivotoby/openapi-mcp-server) sirve para descubrir qué endpoints existen y qué piden y devuelven. En este repo no ejecuta requests autenticadas. Para eso hay dos piezas más, cada una con un solo trabajo:

Paso Herramienta Resultado
1. Leer el schema MCP de OpenAPI: list-api-endpoints, get-api-endpoint-schema Endpoints, parámetros, body y respuesta esperada
2. Obtener el token bun run api:login El token queda en .auth/tokens.env
3. Ejecutar curl con el token La respuesta real del entorno bajo prueba

El MCP no tiene un modo de solo lectura, así que la protección es otra: no recibe ninguna credencial. Si alguien intenta ejecutar con él, la API responde 401, y ese 401 es la señal de usar curl. Mantener el token fuera del MCP evita además dos problemas conocidos: un header fijo no se refresca cuando el token caduca, y choca con un parámetro Authorization declarado en el spec (Cannot override authentication header).

Fuente canónica

La IA lee .agents/skills/agentic-qa-core/references/api-testing-doctrine.md. Esta página es la guía para personas.

Configurar el MCP

El server openapi ya está declarado en .mcp.json, opencode.jsonc y .codex/config.toml, igual en los tres: arranca detrás del loader de .env, que lee de .env solo las dos variables de su --filter. Así queda en .mcp.json (las versiones están fijadas ahí):

"openapi": {
  "command": "bunx",
  "args": [
    "-p", "varlock@<pin>", "varlock", "run", "--no-redact-stdout",
    "--inject", "vars", "--filter", "API_BASE_URL,OPENAPI_SPEC_PATH", "--",
    "bunx", "-y", "@ivotoby/openapi-mcp-server", "--tools", "dynamic"
  ]
}

No tiene API_HEADERS ni token, y no debe tenerlos. Lo único que configuras son dos variables en .env:

Variable Qué es
API_BASE_URL La URL base del backend bajo prueba. La usa el agente en curl y es también la base del MCP.
OPENAPI_SPEC_PATH De dónde lee el MCP el schema: la URL completa del spec (http://localhost:3000/api/openapi, la ruta que sirve tu backend al levantarlo en local) o una ruta de archivo relativa a la raíz del repo (./api/openapi.json, que descarga bun run api:sync). Nunca la ruta del endpoint sola: el server solo descarga lo que empieza con http:// o https://, así que /api/openapi lo busca como archivo, no lo encuentra y se cierra al arrancar.

Como con cualquier MCP, las variables se leen al arrancar la sesión. Abre el agente como siempre (claude, opencode o codex, o su app de escritorio) y reinícialo si cambias alguna de las dos.

Para comprobar que funciona, pregunta al agente qué endpoints tiene la API. Debería llamar a list-api-endpoints y devolver la lista.

Desde una app de escritorio

Este MCP es el más delicado de todos al abrir el harness desde el Dock o el menú de inicio: si al arrancar le falta API_BASE_URL, OPENAPI_SPEC_PATH o el propio spec, se cierra antes del handshake y el harness solo muestra un server caído. DBHub, en cambio, lee .env por su cuenta y arranca igual, por eso uno puede fallar mientras el otro parece bien.

Claude Code, OpenCode y Codex
Los tres arrancan este MCP con varlock run --no-redact-stdout ... -- delante, así que lee .env aunque la app no tenga variables de entorno. Hace falta bun install una vez (el loader es la dependencia varlock) y un .env con valores en la raíz del proyecto. Si cambias .env, reinicia la app.
Codex Desktop
En un worktree que crea la app, .env y api/openapi.json llegan por el .worktreeinclude del repo, y la app corre bun run worktree:provision (dependencias incluidas) desde .codex/environments/environment.toml.
El spec
api/openapi.json está en .gitignore, así que un worktree nuevo no lo trae. bun run setup:doctor avisa si el archivo no existe o si la URL no responde, y bun run api:sync lo vuelve a crear.

Las tres herramientas

Con --tools dynamic el MCP no crea una herramienta por endpoint. Expone exactamente tres herramientas genéricas:

Herramienta Uso en este repo
list-api-endpoints Sí: listar los endpoints disponibles
get-api-endpoint-schema Sí: leer parámetros, body y respuesta de un endpoint
invoke-api-endpoint No. Existe porque el paquete la trae, pero las requests van por curl

Obtener el token

bun run api:login solo obtiene el token. Se autentica con las credenciales del entorno y rol que le pidas (las lee de .env con los nombres que declara config/variables.ts, por ejemplo STAGING_USER_EMAIL y STAGING_USER_PASSWORD; si faltan, falla nombrándolas) y guarda el resultado en disco:

bun run api:login                       # entorno activo (TEST_ENV), rol user
bun run api:login staging               # entorno explícito
bun run api:login staging --role admin  # otro rol
bun run api:login staging --profile W1  # juego de tokens aislado en .auth/profiles/W1/
bun run api:login --help                # opciones, rutas y variables que necesita
Archivo Qué guarda
.auth/tokens.env Una línea por rol y entorno, lista para source: export API_TOKEN_<ROLE>_<ENV>='...'
.auth/tokens.json Metadatos por <ROLE>_<ENV>: token, tipo, expiresIn y createdAt
.auth/api-state.json El estado que usa la fixture de API de Playwright. No lo toca el flujo de curl

La variable del token va en mayúsculas: API_TOKEN_ADMIN_STAGING, API_TOKEN_USER_LOCAL. Varios roles y entornos conviven en el mismo archivo. Todo .auth/ está en .gitignore, nada se escribe en .env y no hace falta reiniciar la sesión después del login.

Si tu backend se autentica de otra forma, el archivo que se adapta es scripts/api-login.project.ts. El resto de la CLI vive en scripts/lib/api-login-core.ts y llega con las actualizaciones del repo.

Ejecutar con curl

Cada llamada Bash del agente abre una shell nueva, así que el token no sobrevive de una llamada a otra. Se hace source del archivo en la misma llamada que el curl:

source .auth/tokens.env && \
curl -s -H "Authorization: Bearer $API_TOKEN_ADMIN_STAGING" \
  "$API_BASE_URL/products"

Para probar aislamiento entre roles (por ejemplo, que un usuario no vea datos de otro tenant) basta con usar variables distintas del mismo archivo: $API_TOKEN_ADMIN_STAGING frente a $API_TOKEN_USER_STAGING.

En Windows

source no existe en cmd ni en PowerShell, y PowerShell no lee el formato export VAR='...'. Fuera de una shell POSIX, toma el token de .auth/tokens.json y llama a curl.exe con su nombre completo: en Windows PowerShell 5.1, curl a secas es un alias de Invoke-WebRequest, no entiende -H y el error parece de autenticación. bun run api:login imprime el siguiente paso correcto para tu plataforma.

Cuando el token expira

Antes de reutilizar un token, mira si sigue vigente: createdAt más expiresIn en .auth/tokens.json.

jq -r '.ADMIN_STAGING | (.createdAt + " +" + (.expiresIn|tostring) + "s")' .auth/tokens.json

Si ya pasó, o una request devuelve 401, vuelve a ejecutar bun run api:login <env> --role <role> y repite. Un 401 por token caducado no es un bug.

Nunca edites .mcp.json para renovar un token

Tampoco opencode.jsonc ni .codex/config.toml. El MCP no tiene credenciales a propósito. Un JWT caducado se renueva con bun run api:login, que reescribe .auth/tokens.env, y con nada más. No pegues un token en un comando, un artefacto, un commit ni en el chat.

Schema de dev, entorno de staging

El schema que lee el MCP suele ser el de dev, la versión más reciente. El entorno que estás probando (staging, por ejemplo) puede ir por detrás, así que un endpoint o un campo que aparece en el schema quizá todavía no existe en ese servidor. Ante un 4xx inesperado, un campo que falta o una respuesta con otra forma, sospecha primero de esta diferencia y compruébalo contra el entorno real antes de reportar un bug.

Tipos para los tests automatizados

bun run api:sync descarga el spec (de una URL, de un repo de GitHub o de un archivo local) y genera los tipos TypeScript en api/openapi-types.ts. Las facades de api/schemas/ los reexportan para que los tests los importen.

bun run api:sync                                          # interactivo
bun run api:sync --url http://localhost:3000/api/openapi  # desde una URL
bun run api:sync --file ./path/to/spec.json               # desde un archivo local
bun run api:sync --config                                 # con la configuración guardada

Los tipos se comprueban al compilar, no en ejecución. Un tipo detecta un test que lee un campo que el contrato no tiene, pero no detecta una API que devuelve otra forma. La forma que te importa se valida con asserts explícitos en el test.

Qué herramienta para qué

Necesidad Herramienta
Explorar endpoints y schemas MCP de OpenAPI (solo lectura)
Requests exploratorias o manuales bun run api:login + curl
Colecciones y exploración visual Postman
Suite de tests automatizados y CI ApiBase de KATA

Los patrones de API testing con KATA están en .agents/skills/test-automation/references/api-patterns.md.

Problemas frecuentes

Síntoma Qué revisar
El MCP no puede cargar el spec Que OPENAPI_SPEC_PATH apunte a algo que exista. Si es una URL, ábrela en el navegador; si pide autenticación, descarga el spec con bun run api:sync y apunta al archivo local. Si empieza con / (por ejemplo /api/openapi), es la ruta del endpoint sola: antepón el origen de API_BASE_URL. Una ruta relativa se resuelve desde la carpeta donde arranca el server, así que abre el agente desde la raíz del repo. bun run setup:doctor revisa las tres cosas.
El server aparece caído en la app de escritorio y en la terminal funciona La app no tiene las variables. Revisa la sección Desde una app de escritorio y corre bun run setup:doctor.
Spec inválido Que sea JSON o YAML válido. Puedes validarlo en editor.swagger.io.
list-api-endpoints devuelve una lista vacía Que el spec declare operaciones reales y no solo schemas.
401 al usar invoke-api-endpoint Es lo esperado: el MCP no tiene credencial. Ejecuta con curl.
401 en curl Token caducado o de otro entorno. Revisa .auth/tokens.json y vuelve a ejecutar bun run api:login.
La variable del token llega vacía Que el source .auth/tokens.env esté en la misma llamada que el curl y que el nombre coincida con rol y entorno.