Add an MCP server for the API, deployable next to the application
mcp/ exposes both APIs as MCP tools, one per operation in openapi.yaml, named after the Laravel route each lands on (public_projects_index, admin_pages_update, ...). Nothing is written by hand: the tools are derived from the document the target application serves, which the contract tests already hold to the truth. Two transports over one core: - stdio (`node mcp/server.mjs`), registered in .mcp.json, signing in with credentials from the environment and refreshing the token. - Streamable HTTP (`--http`), stateless, deployed as the landing-mcp service. It holds no credentials: each request brings its caller's Pireon Basic credentials or bearer token, so the API applies that person's permissions. Sessions live in memory keyed by a hash, refused credentials are remembered for a minute, and the spec is reloaded every minute so new endpoints appear without a redeploy. Left out on purpose: the token exchange, telemetry, and the contact and apply forms, which notify real people. The preview secret is never sent for anonymous callers, and is unset in the deployment by default. A CI job runs the Node tests, including the HTTP mode end to end against a fake application. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
96
README.md
96
README.md
@@ -1332,6 +1332,99 @@ Va **commiteado** —quien lo necesita debería poder leerlo sin ejecutar nada
|
||||
viajan como comentarios: la mitad de lo que sabe el `openapi.yaml` está en su
|
||||
prosa, y un tipo a secas no la conserva.
|
||||
|
||||
## Servidor MCP
|
||||
|
||||
[`mcp/`](mcp/) es un servidor MCP (Node ≥ 20) que da a Claude —o a cualquier
|
||||
cliente MCP— las dos APIs como herramientas: `public_projects_index`,
|
||||
`admin_pages_update`, `admin_roles_catalogue`… Una por operación documentada,
|
||||
con el nombre de la ruta de Laravel en la que acaba.
|
||||
|
||||
**No hay ninguna escrita a mano.** Lee el `openapi.yaml` que sirve la
|
||||
aplicación a la que apunta y las genera de ahí, así que describe la API con la
|
||||
que está hablando —local, staging o producción— y no la de este checkout. Por lo
|
||||
mismo que los tipos: el documento lo sujetan los tests de contrato, y una
|
||||
segunda descripción de la API escrita a mano no la sujetaría nada. La
|
||||
introducción del documento (las dos APIs, idiomas, campos traducibles) viaja
|
||||
como instrucciones del servidor.
|
||||
|
||||
### Desplegado (HTTP)
|
||||
|
||||
Va en el stack como un contenedor más, `landing-mcp`, que habla con
|
||||
`landing-app` por la red interna. En Dokploy, añádele un dominio en la pestaña
|
||||
«Domains» apuntando al puerto 3000; el endpoint es `https://<dominio>/mcp`
|
||||
(Streamable HTTP, sin estado).
|
||||
|
||||
**No tiene credenciales propias.** Cada petición trae las de quien la hace, y
|
||||
la API aplica los permisos de esa persona, igual que en el panel:
|
||||
|
||||
```bash
|
||||
claude mcp add --transport http landing https://mcp.example.org/mcp \
|
||||
--header "Authorization: Basic $(printf 'tu@email.com:contraseña' | base64)"
|
||||
```
|
||||
|
||||
- **`Basic`** con el email y la contraseña de Pireon: el servidor hace login la
|
||||
primera vez, guarda la sesión en memoria (indexada por un hash, nunca en
|
||||
disco ni en logs), refresca el token antes de que caduque y la olvida tras
|
||||
12 horas sin uso. Es lo recomendable, porque los tokens de Pireon caducan.
|
||||
- **`Bearer`** con un token de Pireon: funciona hasta que caduca. Se valida
|
||||
contra `GET /v1/me` y se revalida cada minuto, como hace la propia app.
|
||||
- **Sin cabecera**: solo la API pública, que ya es pública.
|
||||
`LANDING_MCP_REQUIRE_AUTH=true` lo cierra.
|
||||
|
||||
Unas credenciales rechazadas se recuerdan un minuto, sin volver a preguntar a
|
||||
la app. Por eso mismo `POST /v1/auth/token` limita **por cuenta y dirección**
|
||||
(6/min) y no solo por dirección (con un techo de 30/min por dirección): todos
|
||||
los logins del MCP salen del mismo contenedor, y un límite por IP haría que la
|
||||
contraseña mal escrita de uno —o los intentos de un extraño— dejaran fuera a
|
||||
todos los demás.
|
||||
|
||||
El documento se relee cada minuto, así que un endpoint nuevo desplegado en la
|
||||
app aparece como herramienta sin redesplegar el MCP; si la app está caída, se
|
||||
sigue usando el último que cargó.
|
||||
|
||||
`LANDING_MCP_PREVIEW_TOKEN` está vacío a propósito: con el valor de
|
||||
`SITE_PREVIEW_TOKEN`, cualquiera que pueda iniciar sesión ve todos los
|
||||
borradores por las herramientas públicas, sea cual sea su rol. Nunca se manda
|
||||
a quien llega sin credenciales.
|
||||
|
||||
### En local (stdio)
|
||||
|
||||
```bash
|
||||
cd mcp && npm install
|
||||
```
|
||||
|
||||
En este repo ya está registrado en [`.mcp.json`](.mcp.json), que lo lanza por
|
||||
stdio con la configuración del entorno. Aquí sí las credenciales van en
|
||||
variables, porque quien lo lanza es quien inicia sesión:
|
||||
|
||||
```
|
||||
LANDING_URL raíz de la aplicación (por defecto http://localhost:8000)
|
||||
LANDING_USERNAME credenciales de Pireon para la API de administración;
|
||||
LANDING_PASSWORD el token se pide y se refresca solo
|
||||
LANDING_TOKEN o un bearer token fijo en vez de credenciales
|
||||
LANDING_PREVIEW_TOKEN opcional: las herramientas públicas ven borradores
|
||||
LANDING_OPENAPI opcional: ruta o URL de la especificación
|
||||
```
|
||||
|
||||
`node mcp/server.mjs --http` arranca el modo HTTP en local (en `127.0.0.1:3000`
|
||||
salvo `LANDING_MCP_HOST`/`LANDING_MCP_PORT`), para probarlo sin Docker.
|
||||
|
||||
### En los dos modos
|
||||
|
||||
- **Sin credenciales solo aparece la mitad pública.** Ofrecer herramientas que
|
||||
solo pueden fallar le cuesta al modelo una llamada cada una para descubrirlo.
|
||||
- **Fuera a propósito**: `POST /v1/auth/token` (es cosa del cliente), la
|
||||
telemetría (la manda el frontend con su propio secreto) y los dos formularios
|
||||
del buzón — una llamada a `contact` o `apply` mete una fila en el buzón y
|
||||
avisa a gente real.
|
||||
- Las cabeceras no se exponen como parámetros: son secretos, y salen de la
|
||||
configuración, no del modelo.
|
||||
|
||||
`npm test` (en `mcp/`, y en CI) genera las herramientas desde el
|
||||
`openapi.yaml` de este repo —un cambio en el documento que haga chocar dos
|
||||
nombres rompe el build aquí, no la sesión de alguien— y prueba el modo HTTP de
|
||||
punta a punta contra una app simulada.
|
||||
|
||||
## Comandos
|
||||
|
||||
```bash
|
||||
@@ -1458,7 +1551,7 @@ párrafo aparte sin relación programática con él.
|
||||
## Estructura
|
||||
|
||||
```
|
||||
docker-compose.yml stack de producción (landing-app, worker, scheduler, postgres)
|
||||
docker-compose.yml stack de producción (landing-app, worker, scheduler, landing-mcp, postgres)
|
||||
docker-compose.test.yml la suite contra Postgres, desechable
|
||||
Dockerfile imagen única para los tres contenedores PHP
|
||||
docker/ Caddyfile, php.ini y entrypoint
|
||||
@@ -1491,6 +1584,7 @@ config/site.php frontend público, preview, caché, rate
|
||||
config/media.php disco, anchos y formatos de imagen
|
||||
config/git.php proveedores de git y etiquetas de issues
|
||||
public/openapi.yaml especificación de ambas APIs
|
||||
mcp/ servidor MCP (stdio y HTTP) generado desde la anterior
|
||||
```
|
||||
|
||||
## Despliegue con Docker
|
||||
|
||||
Reference in New Issue
Block a user