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:
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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: []
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user