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.
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).
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.envaunque la app no tenga variables de entorno. Hace faltabun installuna vez (el loader es la dependenciavarlock) y un.envcon valores en la raíz del proyecto. Si cambias.env, reinicia la app. - Codex Desktop
-
En un worktree que crea la app,
.envyapi/openapi.jsonllegan por el.worktreeincludedel repo, y la app correbun run worktree:provision(dependencias incluidas) desde.codex/environments/environment.toml. - El spec
-
api/openapi.jsonestá en.gitignore, así que un worktree nuevo no lo trae.bun run setup:doctoravisa si el archivo no existe o si la URL no responde, ybun run api:synclo 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.
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.
.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.
|