From 028a0be2a6770dbfd2af2a84d19edeaf7a399f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Tailland?= Date: Sun, 23 Aug 2026 16:27:56 +0200 Subject: [PATCH] Add payments-mcp (Layer 3 MCP surface), single-route pattern from the start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- README.md | 48 ++++++---- dev/flux-kustomization.yaml | 1 + kong/kustomization.yaml | 9 ++ kong/mcp/ai-mcp-proxy.yaml | 94 +++++++++++++++++++ kong/mcp/audit-log-body-capture-bindings.yaml | 35 +++++++ kong/mcp/audit-log-body-capture.yaml | 27 ++++++ kong/mcp/audit-log.yaml | 17 ++++ kong/mcp/consumergroups.yaml | 59 ++++++++++++ kong/mcp/openid-connect.yaml | 27 ++++++ kong/mcp/pluginbindings.yaml | 53 +++++++++++ kong/mcp/routes.yaml | 22 +++++ kong/mcp/service.yaml | 17 ++++ 12 files changed, 391 insertions(+), 18 deletions(-) create mode 100644 kong/mcp/ai-mcp-proxy.yaml create mode 100644 kong/mcp/audit-log-body-capture-bindings.yaml create mode 100644 kong/mcp/audit-log-body-capture.yaml create mode 100644 kong/mcp/audit-log.yaml create mode 100644 kong/mcp/consumergroups.yaml create mode 100644 kong/mcp/openid-connect.yaml create mode 100644 kong/mcp/pluginbindings.yaml create mode 100644 kong/mcp/routes.yaml create mode 100644 kong/mcp/service.yaml diff --git a/README.md b/README.md index bd5d916..a43e9e9 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/dev/flux-kustomization.yaml b/dev/flux-kustomization.yaml index 8786ff3..3c37c8f 100644 --- a/dev/flux-kustomization.yaml +++ b/dev/flux-kustomization.yaml @@ -20,6 +20,7 @@ spec: CONTROL_PLANE_REF_NAME: payments-dev-bz6vs BACKEND_HOST: classic-models-api.classic-models.svc.cluster.local OIDC_ISSUER: https://keycloak.sttlab.local/realms/classic-models + API_AUDIENCE: '["payments-api"]' BACKEND_AUDIENCE: '["classic-models-api"]' BACKEND_SCOPES: '[]' API_RATE_LIMIT_MINUTE: "60" diff --git a/kong/kustomization.yaml b/kong/kustomization.yaml index 2fead1b..afc03db 100644 --- a/kong/kustomization.yaml +++ b/kong/kustomization.yaml @@ -12,3 +12,12 @@ resources: - rest/audit-log-body-capture.yaml - rest/audit-log-body-capture-bindings.yaml - rest/pluginbindings.yaml + - mcp/service.yaml + - mcp/routes.yaml + - mcp/consumergroups.yaml + - mcp/ai-mcp-proxy.yaml + - mcp/openid-connect.yaml + - mcp/audit-log.yaml + - mcp/audit-log-body-capture.yaml + - mcp/audit-log-body-capture-bindings.yaml + - mcp/pluginbindings.yaml diff --git a/kong/mcp/ai-mcp-proxy.yaml b/kong/mcp/ai-mcp-proxy.yaml new file mode 100644 index 0000000..b522d9d --- /dev/null +++ b/kong/mcp/ai-mcp-proxy.yaml @@ -0,0 +1,94 @@ +apiVersion: configuration.konghq.com/v1 +kind: KongPlugin +metadata: + name: ${NAME}-payments-mcp-ai-mcp-proxy + namespace: ${NAMESPACE} +plugin: ai-mcp-proxy +config: + mode: conversion-listener + include_consumer_groups: true + tools: + - name: list-payments + description: Retrieves a paginated list of payments in the classic-models CRM. + annotations: + title: List all payments + read_only_hint: true + idempotent_hint: true + destructive_hint: false + open_world_hint: false + acl: + allow: + - admin + - read_only + - product_manager + - customer + - support + method: GET + scheme: http + host: ${REST_HOSTNAME} + path: /payments/v1/payments/ + - name: get-payment + description: Retrieves a single payment by customer number and check number. + annotations: + title: Get a payment + read_only_hint: true + idempotent_hint: true + destructive_hint: false + open_world_hint: false + acl: + allow: + - admin + - read_only + - product_manager + - customer + - support + method: GET + scheme: http + host: ${REST_HOSTNAME} + path: /payments/v1/payments/{customernumber}/{checknumber}/ + parameters: + - name: customernumber + in: path + required: true + description: The customer's unique number. + schema: {} + - name: checknumber + in: path + required: true + description: The payment's check number. + schema: {} + - name: create-payment + description: Records a new payment in the CRM. + annotations: + title: Create a payment + read_only_hint: false + idempotent_hint: false + destructive_hint: false + open_world_hint: false + acl: + allow: + - admin + method: POST + scheme: http + host: ${REST_HOSTNAME} + path: /payments/v1/payments/ + request_body: + required: true + content: + application/json: + schema: + type: object + properties: + customernumber: + type: integer + checknumber: + type: string + paymentdate: + type: string + amount: + type: string + required: + - customernumber + - checknumber + - paymentdate + - amount diff --git a/kong/mcp/audit-log-body-capture-bindings.yaml b/kong/mcp/audit-log-body-capture-bindings.yaml new file mode 100644 index 0000000..54ee760 --- /dev/null +++ b/kong/mcp/audit-log-body-capture-bindings.yaml @@ -0,0 +1,35 @@ +apiVersion: configuration.konghq.com/v1alpha1 +kind: KongPluginBinding +metadata: + name: ${NAME}-payments-mcp-audit-capture-request-binding + namespace: ${NAMESPACE} +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: ${CONTROL_PLANE_REF_NAME} + pluginRef: + name: ${NAME}-payments-mcp-audit-capture-request + targets: + serviceRef: + group: configuration.konghq.com + kind: KongService + name: ${NAME}-payments-mcp +--- +apiVersion: configuration.konghq.com/v1alpha1 +kind: KongPluginBinding +metadata: + name: ${NAME}-payments-mcp-audit-capture-response-binding + namespace: ${NAMESPACE} +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: ${CONTROL_PLANE_REF_NAME} + pluginRef: + name: ${NAME}-payments-mcp-audit-capture-response + targets: + serviceRef: + group: configuration.konghq.com + kind: KongService + name: ${NAME}-payments-mcp diff --git a/kong/mcp/audit-log-body-capture.yaml b/kong/mcp/audit-log-body-capture.yaml new file mode 100644 index 0000000..601197d --- /dev/null +++ b/kong/mcp/audit-log-body-capture.yaml @@ -0,0 +1,27 @@ +# dev only: captures raw request/response bodies into kong.ctx.shared for +# audit-log.yaml's custom_fields_by_lua to read back in the log phase. +# Drop this file (and its bindings) to stop body capture without touching +# audit-log.yaml itself — it degrades gracefully to metadata-only logging. +apiVersion: configuration.konghq.com/v1 +kind: KongPlugin +metadata: + name: ${NAME}-payments-mcp-audit-capture-request + namespace: ${NAMESPACE} +plugin: pre-function +config: + access: + - "kong.ctx.shared.audit_request_body = kong.request.get_raw_body()" +--- +apiVersion: configuration.konghq.com/v1 +kind: KongPlugin +metadata: + name: ${NAME}-payments-mcp-audit-capture-response + namespace: ${NAMESPACE} +plugin: post-function +config: + body_filter: + - | + local chunk = ngx.arg[1] + if chunk then + kong.ctx.shared.audit_response_body = (kong.ctx.shared.audit_response_body or "") .. chunk + end diff --git a/kong/mcp/audit-log.yaml b/kong/mcp/audit-log.yaml new file mode 100644 index 0000000..643d223 --- /dev/null +++ b/kong/mcp/audit-log.yaml @@ -0,0 +1,17 @@ +apiVersion: configuration.konghq.com/v1 +kind: KongPlugin +metadata: + name: ${NAME}-payments-mcp-audit-log + namespace: ${NAMESPACE} +plugin: http-log +# dev only: the raw Authorization (JWT) header is captured in clear text, +# not acceptable as-is for a non-dev environment. request_body/response_body +# below read from kong.ctx.shared, populated by audit-log-body-capture.yaml +# — nil (and silently omitted) if that file isn't applied. +config: + http_endpoint: ${AUDIT_LOG_ENDPOINT} + method: POST + content_type: application/json + custom_fields_by_lua: + request_body: "return kong.ctx.shared.audit_request_body" + response_body: "return kong.ctx.shared.audit_response_body" diff --git a/kong/mcp/consumergroups.yaml b/kong/mcp/consumergroups.yaml new file mode 100644 index 0000000..b18fe72 --- /dev/null +++ b/kong/mcp/consumergroups.yaml @@ -0,0 +1,59 @@ +apiVersion: configuration.konghq.com/v1beta1 +kind: KongConsumerGroup +metadata: + name: ${NAME}-admin + namespace: ${NAMESPACE} +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: ${CONTROL_PLANE_REF_NAME} + name: admin +--- +apiVersion: configuration.konghq.com/v1beta1 +kind: KongConsumerGroup +metadata: + name: ${NAME}-read-only + namespace: ${NAMESPACE} +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: ${CONTROL_PLANE_REF_NAME} + name: read_only +--- +apiVersion: configuration.konghq.com/v1beta1 +kind: KongConsumerGroup +metadata: + name: ${NAME}-product-manager + namespace: ${NAMESPACE} +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: ${CONTROL_PLANE_REF_NAME} + name: product_manager +--- +apiVersion: configuration.konghq.com/v1beta1 +kind: KongConsumerGroup +metadata: + name: ${NAME}-customer + namespace: ${NAMESPACE} +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: ${CONTROL_PLANE_REF_NAME} + name: customer +--- +apiVersion: configuration.konghq.com/v1beta1 +kind: KongConsumerGroup +metadata: + name: ${NAME}-support + namespace: ${NAMESPACE} +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: ${CONTROL_PLANE_REF_NAME} + name: support diff --git a/kong/mcp/openid-connect.yaml b/kong/mcp/openid-connect.yaml new file mode 100644 index 0000000..76567cf --- /dev/null +++ b/kong/mcp/openid-connect.yaml @@ -0,0 +1,27 @@ +apiVersion: configuration.konghq.com/v1 +kind: KongPlugin +metadata: + name: ${NAME}-payments-mcp-openid-connect + namespace: ${NAMESPACE} +plugin: openid-connect +config: + issuer: ${OIDC_ISSUER} + audience_required: ${API_AUDIENCE} + auth_methods: + - bearer + display_errors: false + ssl_verify: true + consumer_groups_claim: + - realm_access + - roles + client_id: + - ${TOKEN_EXCHANGE_CLIENT_ID} + client_secret: + - ${TOKEN_EXCHANGE_CLIENT_SECRET} + token_exchange: + subject_token_issuers: + - issuer: ${OIDC_ISSUER} + conditions: {} + request: + audience: ${API_AUDIENCE} + scopes: [] diff --git a/kong/mcp/pluginbindings.yaml b/kong/mcp/pluginbindings.yaml new file mode 100644 index 0000000..1aeb4c4 --- /dev/null +++ b/kong/mcp/pluginbindings.yaml @@ -0,0 +1,53 @@ +apiVersion: configuration.konghq.com/v1alpha1 +kind: KongPluginBinding +metadata: + name: ${NAME}-payments-mcp-openid-connect-binding + namespace: ${NAMESPACE} +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: ${CONTROL_PLANE_REF_NAME} + pluginRef: + name: ${NAME}-payments-mcp-openid-connect + targets: + serviceRef: + group: configuration.konghq.com + kind: KongService + name: ${NAME}-payments-mcp +--- +apiVersion: configuration.konghq.com/v1alpha1 +kind: KongPluginBinding +metadata: + name: ${NAME}-payments-mcp-ai-mcp-proxy-binding + namespace: ${NAMESPACE} +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: ${CONTROL_PLANE_REF_NAME} + pluginRef: + name: ${NAME}-payments-mcp-ai-mcp-proxy + targets: + routeRef: + group: configuration.konghq.com + kind: KongRoute + name: ${NAME}-payments-mcp +--- +apiVersion: configuration.konghq.com/v1alpha1 +kind: KongPluginBinding +metadata: + name: ${NAME}-payments-mcp-audit-log-binding + namespace: ${NAMESPACE} +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: ${CONTROL_PLANE_REF_NAME} + pluginRef: + name: ${NAME}-payments-mcp-audit-log + targets: + serviceRef: + group: configuration.konghq.com + kind: KongService + name: ${NAME}-payments-mcp diff --git a/kong/mcp/routes.yaml b/kong/mcp/routes.yaml new file mode 100644 index 0000000..ca92379 --- /dev/null +++ b/kong/mcp/routes.yaml @@ -0,0 +1,22 @@ +apiVersion: configuration.konghq.com/v1alpha1 +kind: KongRoute +metadata: + name: ${NAME}-payments-mcp + namespace: ${NAMESPACE} +spec: + serviceRef: + type: namespacedRef + namespacedRef: + name: ${NAME}-payments-mcp + name: payments-mcp + hosts: + - ${REST_HOSTNAME} + methods: + - GET + - POST + - DELETE + paths: + - /payments-mcp + strip_path: false + tags: + - payments diff --git a/kong/mcp/service.yaml b/kong/mcp/service.yaml new file mode 100644 index 0000000..848f002 --- /dev/null +++ b/kong/mcp/service.yaml @@ -0,0 +1,17 @@ +apiVersion: configuration.konghq.com/v1alpha1 +kind: KongService +metadata: + name: ${NAME}-payments-mcp + namespace: ${NAMESPACE} +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: ${CONTROL_PLANE_REF_NAME} + name: payments-mcp + host: ${REST_HOSTNAME} + port: 80 + protocol: http + path: / + tags: + - payments