Add payments-mcp (Layer 3 MCP surface), single-route pattern from the start

list-payments/get-payment (all 5 roles) and create-payment (admin
only — matches PaymentPermission's actual write-access logic, not
guessed). One KongRoute for GET/POST/DELETE, one ai-mcp-proxy
binding — avoids the duplicate-tool-name bug found and fixed on
customers-mcp (STATUS.md section 18), built correctly here instead
of repeating the two-route mistake.

Keycloak (local): mcp-payments-audience client scope + mcp-payments
resource client, attached to librechat as an optional scope, no
dedicated protocol mappers on the client itself.
This commit is contained in:
Stéphane Tailland
2026-08-23 16:27:56 +02:00
parent bf5adadb24
commit 028a0be2a6
12 changed files with 391 additions and 18 deletions
+30 -18
View File
@@ -9,32 +9,44 @@ split. Structure mirrors `../customers-api`, not `../catalog-api` — see
has a latent bug; `customers-api`'s single-route pattern is the corrected
one to copy going forward).
**REST only, no MCP layer.** 6 routes over `Payment`, which uses a
**composite key** (`customernumber` + `checknumber`, not a single id like
`Customer`/`Product`) — both URI segments are captured and passed through
to the backend's own composite-key URL pattern.
Two exposure surfaces, both proxying to the real `classic-models-api`
backend:
- **REST** (`kong/rest/`) — 6 routes over `Payment`, which uses a
**composite key** (`customernumber` + `checknumber`, not a single id
like `Customer`/`Product`) — both URI segments are captured and passed
through to the backend's own composite-key URL pattern.
- **MCP** (`kong/mcp/`) — `list-payments`/`get-payment` (all 5 roles) and
`create-payment` (admin only — verified against `PaymentPermission`'s
actual logic in `authentication/permissions.py`, not guessed: writes are
admin-only in practice, `customer` can only write their own via the
REST API directly, not exposed as an MCP tool). **One `KongRoute` for
all three MCP HTTP methods**, one `ai-mcp-proxy` binding — see
`../STATUS.md` section 18 for why this matters (a second route +
binding for the same plugin creates a duplicate-tool-name bug, found
and fixed on `customers-mcp`, built correctly here from the start).
| Route | Method | Backend path |
|---|---|---|
| `/payments/v1/payments/` | GET, POST | `/classic-models/api/v1/payments/` |
| `/payments/v1/payments/{customernumber}/{checknumber}/` | GET, PUT, PATCH, DELETE | `/classic-models/api/v1/payments/{customernumber}/{checknumber}/` |
| `/payments-mcp` | GET, POST, DELETE | MCP streamable-http (tools call into the REST routes above) |
## Auth
`openid-connect`, `audience_required`/token-exchange target =
`classic-models-api` (`BACKEND_AUDIENCE`) — same second-hop-exchange
pattern as `customers-api`'s REST layer: a caller gets a
`payments-api`-audience token from `kong-token-exchange-broker` directly
(no MCP hop exists for this domain), and this layer exchanges it again to
`classic-models-api` before the backend ever sees it. IdP is the local
Keycloak (`https://keycloak.sttlab.local/realms/classic-models`) — see
`../STATUS.md` sections 15/16. **No `payments-api` Keycloak client or
`payments-api-audience` client scope exists yet** — unlike `customers-api`,
this hasn't been created on the local Keycloak (nor attached to
`kong-token-exchange-broker`'s default scopes) as of this repo's initial
build. Nothing can actually authenticate against these routes until that's
done — same missing piece as `customers-api` had before its own Keycloak
wiring step.
`openid-connect` on both surfaces, distinct audience each (`payments-api`
for REST, `mcp-payments` for MCP) with a real RFC 8693 exchange chain:
MCP → `payments-api` → `classic-models-api`, same pattern as
`catalog-api`/`customers-api`. IdP is the local Keycloak
(`https://keycloak.sttlab.local/realms/classic-models`) — see
`../STATUS.md` sections 15/16. `payments-api-audience` and
`mcp-payments-audience` client scopes + `payments-api`/`mcp-payments`
resource clients created on the local Keycloak, attached to
`kong-token-exchange-broker`'s default scopes (REST audience) and to
`librechat`'s optional scopes (MCP audience) respectively — no dedicated
protocol mappers on either new client itself, same practice as every
other domain built this session. `mcp-payments`'s OAuth callback added to
`librechat`'s `redirectUris`, and a `payments-mcp` server entry added to
LibreChat's `librechat.yaml` (`infra-librechat` repo).
## Other policies