Orderdetail now carries requireddate and order_status, denormalised from the
order the line belongs to — filtering by product and status already answers
the question this used to need a second call per order for.
list_orders also gained a status filter, matching the backend capability that
list_order_details already exposes.
Regenerated with generate-mcp-tools.py.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The backend gained productcode, ordernumber and ordernumber__status on the
order-lines list; the spec and the MCP tools now carry them.
The pair that matters is productcode with ordernumber__status: the first says
what is committed on a reference, the second restricts it to orders still open.
Without the second the answer counts every line ever shipped.
Regenerated with k8s-platform/tools/generate-mcp-tools.py, not by hand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The proxy cache held every GET on the whole service for 60 seconds, and these
APIs accept writes. Kong's proxy cache has no invalidation on write: a POST
that changes a product passes through the same gateway, and the gateway goes on
serving the value it captured before it, for up to a minute, with nothing in the
response saying so.
It produced a real wrong answer. An agent woken by a stock event read the figure
from before that event and reported, in one sentence, "68 in stock, above the
threshold" and "it crossed the threshold, from 68".
The manifest is kept, unreferenced, carrying the reason — so reinstating it is a
decision rather than a rediscovery.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ACL answers 'may this identity call create-product'. Nothing answered 'may
it call it with these arguments' — write authorises writing, not this
particular record. And the read cap counts calls, so one call asking for a
hundred thousand rows cost the same as one asking for ten.
The OPA plugin sends the decoded JSON-RPC body, so the decision point sees the
tool name and its arguments. The policy lives once in k8s-platform/opa and every
domain asks the same server.
Enforced at the gateway rather than in an agent, because an agent that has been
talked into something is the case this exists for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A scope authorises a read and says nothing about volume. An agent reads a
thousand records in seconds where a person clicks ten, and until now nothing
noticed: the token is valid, the tool is permitted, the ACL passes. That gap
stopped being theoretical the day an autonomous run started reading the whole
catalog on a schedule.
identifier: credential keys the counter on 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. Verified on
catalog: pm-user and ro-user hold separate counters.
Per-route namespace, so a busy domain cannot exhaust another's.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The response schemas added yesterday made every paginated list tool fail. The
plugin's fields are documented as OpenAPI format and validated with a JSON
Schema validator, and `nullable: true` is not a JSON Schema keyword — so it is
ignored, `type: string` is enforced, and `previous: null` on the first page is
rejected as *null is not of type "string"*.
The failure lands after the backend has already done the work, surfaces to the
caller as a bare tool retry exhaustion, and appears nowhere in the gateway's own
logs. Found only because an orchestrating agent asked a question that paged.
The generator now rewrites `nullable: true` into `type: [T, "null"]` wherever it
inlines a schema, request bodies included. A 3.1 spec needs none of this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Produced by k8s-platform/tools/generate-mcp-tools.py: deck file openapi2mcp
does the conversion, and the wrapper re-injects the five things Kong's
generator cannot emit — per-tool ACLs, the MCP behavioural annotations, the
response schemas the plugin uses for structuredOutput, the real upstream host,
and JSON-only request bodies. Each is a documented gap; see
k8s-platform/MCP-TOOL-GENERATION.md.
tools-policy.yaml carries the ACLs and doubles as the allow-list: an operation
becomes a tool only if the policy names it. Kong would otherwise expose every
operation in the spec, DELETE included, and can express no authorization at all
from a bearer-token spec — silently, exit 0, no warning.
Same tools as before, same ACLs. What changed: query parameters are now
declared and callable, each tool carries its response schema so calls return
structured content instead of raw JSON, and request bodies are no longer
tripled across three identical content types.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
14 routes: Order (single key, full CRUD + order-details sub-resource)
and Orderdetail (composite key ordernumber+productcode, own
top-level resource, mirrors backend's own URL structure). MCP layer
built with the corrected single-route pattern from the start
(STATUS.md section 18) — 7 tools, list/get read-only for all 5
roles, create-order/create-orderdetail for admin+customer+support
(verified against OrderPermission/OrderScopedPermission.check() —
support gets an unconditional class-level pass, the status-only
restriction is object-level and doesn't apply to create).
Keycloak (local): sales-api-audience + mcp-sales-audience client
scopes, sales-api + mcp-sales resource clients, attached to the
broker and librechat respectively. Deploy key and broker secret
reused/mirrored from catalog-api and the live broker client, same
pattern as customers-api/payments-api.