diff --git a/dev/flux-kustomization.yaml b/dev/flux-kustomization.yaml index 7489dd5..526b7da 100644 --- a/dev/flux-kustomization.yaml +++ b/dev/flux-kustomization.yaml @@ -28,6 +28,11 @@ spec: BACKEND_AUDIENCE: '["classic-models-api"]' BACKEND_SCOPES: '[]' API_RATE_LIMIT_MINUTE: "60" + # Read volume for an MCP caller, per identity per minute. An agent + # reads a thousand records in seconds where a person clicks ten; this is + # the only control that notices. Generous enough that a conversational + # turn never meets it, tight enough that a loop does. + MCP_READS_PER_MINUTE: "60" # The URI path is the collector's routing key: Fluent Bit tags the # record `catalog` and turns that into the Loki label domain=catalog. AUDIT_LOG_ENDPOINT: http://audit-log-collector.observability.svc.cluster.local/catalog diff --git a/kong/kustomization.yaml b/kong/kustomization.yaml index afc03db..1d7e17a 100644 --- a/kong/kustomization.yaml +++ b/kong/kustomization.yaml @@ -17,6 +17,7 @@ resources: - mcp/consumergroups.yaml - mcp/ai-mcp-proxy.yaml - mcp/openid-connect.yaml + - mcp/rate-limiting.yaml - mcp/audit-log.yaml - mcp/audit-log-body-capture.yaml - mcp/audit-log-body-capture-bindings.yaml diff --git a/kong/mcp/pluginbindings.yaml b/kong/mcp/pluginbindings.yaml index 18fbbc3..a3d532f 100644 --- a/kong/mcp/pluginbindings.yaml +++ b/kong/mcp/pluginbindings.yaml @@ -51,3 +51,21 @@ spec: group: configuration.konghq.com kind: KongService name: ${NAME}-catalog-mcp +--- +apiVersion: configuration.konghq.com/v1alpha1 +kind: KongPluginBinding +metadata: + name: ${NAME}-catalog-mcp-rate-limiting-binding + namespace: ${NAMESPACE} +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: ${CONTROL_PLANE_REF_NAME} + pluginRef: + name: ${NAME}-catalog-mcp-rate-limiting + targets: + routeRef: + group: configuration.konghq.com + kind: KongRoute + name: ${NAME}-catalog-mcp diff --git a/kong/mcp/rate-limiting.yaml b/kong/mcp/rate-limiting.yaml new file mode 100644 index 0000000..e728498 --- /dev/null +++ b/kong/mcp/rate-limiting.yaml @@ -0,0 +1,37 @@ +# A read-volume cap per caller identity. +# +# A scope authorises a read and says nothing about volume. An agent reads a +# thousand records in seconds where a person clicks ten, and the difference is +# invisible to every control the estate had until now — the token is valid, the +# tool is permitted, the ACL passes. +# +# `identifier: credential` keys the counter on what the openid-connect plugin +# published as the caller's credential, which is the `sub` claim: the end user, +# whoever is asking on their behalf. So an agent looping under one person's +# identity spends that person's budget, and cannot spend everyone's. +apiVersion: configuration.konghq.com/v1 +kind: KongPlugin +metadata: + name: ${NAME}-catalog-mcp-rate-limiting + namespace: ${NAMESPACE} +plugin: rate-limiting-advanced +config: + identifier: credential + # Per-route counters. Sharing a namespace across domains would let a busy + # catalog exhaust the budget for payments. + namespace: ${NAME}-catalog-mcp + strategy: local + sync_rate: -1 + window_type: sliding + window_size: + - 60 + limit: + - ${MCP_READS_PER_MINUTE} + # Loud rather than silent: a caller that hits the cap is told, and the headers + # say when it resets. An agent that only sees a 429 with no Retry-After + # retries immediately and makes it worse. + hide_client_headers: false + error_code: 429 + error_message: >- + Read volume limit reached for this identity. This is a cap on how much can + be read per minute, not a permission problem.