Skip to main content
Glama
dromerotrans

servicios-mcp

by dromerotrans
README.md
# servicios-mcp

Servidor MCP remoto (Streamable HTTP), de **SOLO CONSULTA** (nunca escribe
ni modifica nada en SAP ni en Cisco), que expone la API de SAP Business One
Service Layer, la API de contratos de servicio de Cisco (CCWR) y la API de
estado de órdenes de Cisco (CCW) usadas por el proyecto Hermes de Trans
Industrias Electrónicas.

Pensado para que un cliente externo (otro Claude, en otra cuenta) pueda
agregarlo como conector MCP remoto y consultar los mismos datos sin acceso
directo al clúster ni a las credenciales reales de SAP/Cisco.

## Tools expuestas

**Las once tools son exclusivamente de lectura — no existe en este código
ninguna tool que pueda crear, modificar o borrar nada en SAP, CCWR ni CCW.**

- `sap_query(entity, select, filter, orderby, top, skip)` — `GET` genérico
  contra una colección OData de SAP B1 Service Layer (`Orders`,
  `DeliveryNotes`, `PurchaseOrders`, `PurchaseDeliveryNotes`,
  `BusinessPartners`, `Items`, etc.).
- `sap_get_entity(entity, entry_id)` — `GET` completo de un documento por
  clave primaria (necesario para sub-colecciones anidadas como
  `SerialNumbers`, que SAP omite si la consulta usa `$select`).
- `ccwr_search(serial_numbers, contract_numbers, instance_numbers, subscription_ids, web_order_ids, so_mso_numbers, po_mpo_numbers, limit, offset)`
  — búsqueda de **contratos** de servicio de Cisco (soporte/warranty, API
  CCWR — legacy). 7 criterios de búsqueda válidos en total (los últimos 4
  no figuran en el PDF oficial de Cisco, confirmados contra el schema real).
- `ccw_order_status(order_search_key, order_search_value, page, page_size)`
  — estado de **órdenes** de compra/venta de Cisco (API CCW — Commerce
  GraphQL). Distinto de `ccwr_search`: contratos y órdenes son dos APIs de
  Cisco separadas, con credenciales propias cada una.
- `ccwr_suscripcion_orden(subscription_id)` — dado un Subscription ID de
  Cisco (ej. `Sub2330047`, tal cual aparece en el portal CCW Renewals),
  encadena `ccwr_search` (subscription_ids) + `ccw_order_status` en una
  sola llamada para traer la orden real asociada. Exige `ccwr` y `ccw`
  habilitados en el token.
- `ccw_sub_ids_por_orden(order_search_key, order_search_values, page_size)`
  — dada una lista de hasta 200 órdenes (SALES_ORDER_ID o WEB_ORDER_ID),
  devuelve **solo** los Subscription ID reales (formato `Sub`+número) de
  cada una, con la línea de la orden (`linea`, el número tal cual lo
  muestra el portal CCW, ej. `"1.1"`), el SKU y la descripción de esa
  línea — útil cuando una orden tiene más de un Sub ID en líneas
  distintas. Descarta explícitamente cualquier otro identificador que
  Cisco deje en el mismo campo (ej. `SR...`, confirmado que NO es un Sub
  ID real).
- `turecibo_resumen(refresh)` / `turecibo_activos(sector, cargo, refresh)` /
  `turecibo_bajas(desde, hasta, refresh)` / `turecibo_buscar(query, refresh)`
  — padrón de empleados de TuRecibo (Visma), **solo lectura**. El roster se
  arma barriendo `/users/search` con ~114 prefijos frecuentes (TuRecibo no
  expone un listado completo) — es una heurística incompleta por diseño, y
  cada respuesta incluye un campo `warning` aclarándolo. Activo/baja se
  decide por el campo `ingreso` (no por `fecha_baja`, que puede venir
  vacío). Cache en disco 2h (`TURECIBO_CACHE_FILE`, permisos `0600`).
- `turecibo_licencias(query, tipo, estado)` — licencias (vacaciones,
  mudanza, etc.) de **una** persona puntual (`query` obligatorio: CUIL,
  legajo o nombre — nunca lista licencias de toda la empresa). Lee siempre
  en vivo del endpoint de licencias, sin cache. Solo lectura.

**Regla no negociable, heredada de los skills `sap-service-layer`,
`ccwr-contract-admin` y `order-status` de Hermes:** el servidor nunca arma
un `POST`/`PATCH`/`DELETE` contra una entidad de datos de SAP, CCWR o CCW.
Las únicas llamadas POST del código son `Login`/`Logout` de sesión SAP y
los token endpoints OAuth2 de Cisco — todas de autenticación, ninguna de
datos.

## Autenticación

`Authorization: Bearer <token>` en cada request. Los tokens se gestionan
desde el panel web `/admin` (ver abajo) — cada persona tiene su propio
token, independiente de las credenciales reales de SAP/Cisco, así que se
puede revocar el acceso de una persona sin tocar nada más ni redeployar.

Dos formas de usar ese mismo token:

- **Directo (Claude Code):** `--header "Authorization: Bearer <token>"`.
- **OAuth2 (Claude Desktop):** el servidor implementa su propio
  Authorization Server OAuth 2.1 (RFC 8414/9728, DCR RFC 7591, vía el
  soporte nativo del SDK `mcp.server.auth`) para que Claude Desktop —que
  solo sabe hablar OAuth, no un Bearer fijo— pueda agregar el conector
  pegando solo la URL. El flujo: Claude Desktop se autoregistra como
  cliente OAuth (`POST /register`), abre el navegador en `/oauth/login`,
  la persona pega ahí su token de siempre, y el `access_token` que se
  emite **es ese mismo token** (revocable desde `/admin`, sin lógica de
  expiración/refresh propia). No hay usuarios ni contraseñas nuevas.

## Panel `/admin` — gestión de IPs habilitadas

Además de los tokens, `/admin` permite agregar/quitar IPs y CIDRs de la
whitelist de la propia Route (`haproxy.router.openshift.io/ip_whitelist`)
sin correr `oc` a mano — el cambio se aplica al toque contra la Route real
(capa de red/HAProxy, no se movió el bloqueo a la aplicación). El pod
necesita permiso de Kubernetes (RBAC) para leer y editar SOLO esa Route
puntual (`get`/`patch`, acotado por `resourceNames` — no puede tocar
ningún otro objeto del namespace); ver `rbac-ip-whitelist.yaml` para el
`ServiceAccount`/`Role`/`RoleBinding` que hay que aplicar antes de que
esta sección del panel funcione. API REST: `GET /admin/api/ips`,
`POST /admin/api/ips` (`{"cidr": "...", "label": "..."}`),
`DELETE /admin/api/ips?cidr=...`.

## Panel `/admin` — gestión de tokens

`GET /admin` sirve una página HTML+JS (sin dependencias nuevas) protegida
con un login web propio (`GET`/`POST /admin/login`, cookie de sesión
`admin_session` httpOnly/Secure, TTL 12h, en memoria del proceso —
reemplazó al HTTP Basic Auth original el 2026-08-28; `POST /admin/logout`
hace logout real) validado contra `ADMIN_USER`/`ADMIN_PASSWORD`, para
generar y revocar tokens de acceso en caliente. Los tokens se persisten en
`TOKENS_FILE` (default `/data/tokens.json`, pensado para montarse sobre un
PVC) — en el primer arranque sobre un archivo inexistente, se siembran
automáticamente desde la variable de entorno legacy `MCP_ACCESS_TOKENS`
(`token1:etiqueta1,token2:etiqueta2,...`), que de ahí en más queda sin
efecto real. API REST detrás de la misma sesión de `/admin`:
`GET /admin/api/tokens` (listar), `POST /admin/api/tokens`
(`{"label": "...", "services": ["sap","ccwr","ccw"]}` → crea y devuelve el
token; `services` es opcional, si se omite habilita todos), `DELETE
/admin/api/tokens/{label}` (revoca).

### Permisos por servicio (qué tools puede usar cada token)

Cada token tiene una lista `services` (subconjunto de `sap`, `ccwr`, `ccw`)
que define a qué tools puede llamar — no es todo o nada. El mapeo es:

| Servicio | Tools que cubre |
|---|---|
| `sap` | `sap_query`, `sap_get_entity` |
| `ccwr` | `ccwr_search`, `ccwr_suscripcion_orden` (requiere también `ccw`) |
| `ccw` | `ccw_order_status`, `ccwr_suscripcion_orden` (requiere también `ccwr`) |
| `turecibo` | `turecibo_resumen`, `turecibo_activos`, `turecibo_bajas`, `turecibo_buscar`, `turecibo_licencias` |

Un token que intenta llamar una tool fuera de sus servicios habilitados
recibe un error (`{"error": "tu token no tiene habilitado el servicio..."}`)
sin llegar a pegarle a SAP/Cisco, y queda registrado en el audit log como
`service_forbidden`. Se elige al crear el token desde `/admin` (checkboxes)
y se puede cambiar después sin generar un token nuevo, con
`PATCH /admin/api/tokens/{label}/services` (`{"services": [...]}`) — mismo
endpoint que usa la tabla del panel. `GET /admin/api/services` devuelve el
catálogo (id + descripción) que usa el panel para dibujar los checkboxes.
Los tokens emitidos antes de este feature (sin `services` en el JSON) se
migran automáticamente al primer arranque con TODOS habilitados — mismo
comportamiento que ya tenían, no se corta nada retroactivamente.

## Variables de entorno

| Variable | Requerida | Descripción |
|---|---|---|
| `SAP_SL_COMPANY_DB` | Sí | Base de datos de SAP B1 |
| `SAP_SL_USERNAME` | Sí | Usuario SAP B1 |
| `SAP_SL_PASSWORD` | Sí | Password SAP B1 |
| `SAP_BASE_URL` | No (default `https://sap.trans.com.ar:50000/b1s/v1`) | Base de la Service Layer |
| `CCWR_CLIENT_ID` | Sí (si se usa `ccwr_search`) | Client ID OAuth2 Cisco (contratos) |
| `CCWR_CLIENT_SECRET` | Sí (si se usa `ccwr_search`) | Client Secret OAuth2 Cisco (contratos) |
| `CCWR_TOKEN_URL` | No (default `https://id.cisco.com/oauth2/default/v1/token`) | Token endpoint CCWR |
| `CCWR_API_URL` | No (default `.../ccw/renewals/api/v1.0/search/lines`) | Endpoint de búsqueda CCWR |
| `CCW_CLIENT_ID` | Sí (si se usa `ccw_order_status`) | Client ID OAuth2 Cisco (órdenes) |
| `CCW_CLIENT_SECRET` | Sí (si se usa `ccw_order_status`) | Client Secret OAuth2 Cisco (órdenes) |
| `CCW_TOKEN_URL` | No (default `https://id.cisco.com/oauth2/default/v1/token`) | Token endpoint CCW |
| `CCW_API_URL` | No (default `https://capi.cisco.com/commerce/apis`) | Endpoint GraphQL CCW |
| `MCP_ACCESS_TOKENS` | No | Solo usado para sembrar `TOKENS_FILE` en el primer arranque, ver "Panel /admin" |
| `TOKENS_FILE` | No (default `/data/tokens.json`) | Store persistente de tokens, gestionado desde `/admin` |
| `ADMIN_USER` | Sí | Usuario del panel `/admin` |
| `ADMIN_PASSWORD` | Sí | Password del panel `/admin` |
| `PUBLIC_BASE_URL` | No (default `https://servicios-mcp.trans.com.ar`) | URL pública canónica, usada en la metadata OAuth2 (issuer, resource) |
| `OAUTH_CLIENTS_FILE` | No (default `/data/oauth_clients.json`) | Store persistente de clientes OAuth registrados dinámicamente (DCR) |
| `ROUTE_NAME` | No (default `servicios-mcp`) | Nombre de la Route que `/admin` edita para la whitelist de IP |
| `K8S_NAMESPACE` | No (autodetectado del ServiceAccount in-cluster; fallback `hermes`) | Namespace de esa Route |
| `IP_ALLOWLIST_FILE` | No (default `/data/ip_allowlist.json`) | Store persistente de IPs/CIDRs habilitadas, gestionado desde `/admin` |
| `TURECIBO_USER` | Sí (si se usa `turecibo_*`) | CUIL del usuario admin de TuRecibo |
| `TURECIBO_PASS` | Sí (si se usa `turecibo_*`) | Password de ese usuario |
| `TURECIBO_URL` | No (default `https://app.turecibo.com`) | Host de login/licencias |
| `TURECIBO_ADMIN_URL` | No (default `https://api.turecibo.com`) | Host del padrón (admin) |
| `TURECIBO_CACHE_FILE` | No (default `/data/turecibo_roster.json`) | Cache del roster (datos personales, `0600`) |
| `TURECIBO_CACHE_TTL` | No (default `7200`) | TTL del cache en segundos |
| `TURECIBO_WORKERS` | No (default `8`) | Threads del barrido paralelo de perfiles |
| `PORT` | No (default `8080`) | Puerto HTTP |

## Correr local

```bash
pip install -r requirements.txt
SAP_SL_COMPANY_DB=... SAP_SL_USERNAME=... SAP_SL_PASSWORD=... \
MCP_ACCESS_TOKENS="tok_ejemplo:mi-token" \
python3 src/server.py
```

Healthcheck sin auth: `GET /healthz`. Endpoint MCP: `POST /mcp`.

## Despliegue

Corre como Deployment propio en OKD (namespace `hermes`, componente
`servicios-mcp` — renombrado desde `hermes-mcp` el 2026-08-14), detrás de
una Route pública (`servicios-mcp.trans.com.ar`) con TLS real, mismo patrón
que el resto de los componentes de Hermes en este proyecto (`hermes-agent`,
`honcho-api`, `trilium`). Ver el changelog del proyecto Hermes (doc
interno, no en este repo) para el detalle de despliegue real.