refactor(mcp): generate the tool definitions from the spec

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>
This commit is contained in:
Stéphane Tailland
2026-08-29 08:48:42 +02:00
parent 482cd2e81f
commit 5ffd0837e1
2 changed files with 557 additions and 178 deletions
+499 -148
View File
@@ -1,3 +1,10 @@
# GENERATED — do not edit. Edit openapi.yaml or tools-policy.yaml, then:
# k8s-platform/tools/generate-mcp-tools.py <this domain>
#
# Produced by `deck file openapi2mcp`, then post-processed to add the ACLs,
# the MCP behavioural annotations, the response schemas, the real upstream
# host and the JSON-only bodies that Kong's generator does not emit. Each of
# those is a documented gap — see k8s-platform/MCP-TOOL-GENERATION.md.
apiVersion: configuration.konghq.com/v1 apiVersion: configuration.konghq.com/v1
kind: KongPlugin kind: KongPlugin
metadata: metadata:
@@ -8,58 +15,29 @@ config:
mode: conversion-listener mode: conversion-listener
include_consumer_groups: true include_consumer_groups: true
tools: tools:
- name: list-orders - annotations:
description: Retrieves a paginated list of orders in the classic-models CRM.
annotations:
title: List all orders title: List all orders
read_only_hint: true read_only_hint: true
idempotent_hint: true idempotent_hint: true
destructive_hint: false destructive_hint: false
open_world_hint: false open_world_hint: false
acl: description: Retrieve a paginated list of all customer orders with their status and details.
allow:
- admin
- read_only
- product_manager
- customer
- support
method: GET method: GET
scheme: http name: list-orders
host: ${REST_HOSTNAME}
path: /sales/v1/orders/
- name: get-order
description: Retrieves a single order by order number.
annotations:
title: Get an order
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: /sales/v1/orders/{ordernumber}/
parameters: parameters:
- name: ordernumber - description: A page number within the paginated result set.
in: path in: query
required: true name: page
description: The order's unique number. required: false
schema: {} schema:
- name: get-order-orderdetails type: integer
description: Retrieves all line items for a specific order. - description: Number of results to return per page.
annotations: in: query
title: Get order line items name: page_size
read_only_hint: true required: false
idempotent_hint: true schema:
destructive_hint: false type: integer
open_world_hint: false path: /sales/v1/orders/
acl: acl:
allow: allow:
- admin - admin
@@ -67,157 +45,530 @@ config:
- product_manager - product_manager
- customer - customer
- support - support
method: GET
scheme: http scheme: http
host: ${REST_HOSTNAME} host: ${REST_HOSTNAME}
path: /sales/v1/orders/{ordernumber}/order-details/ responses:
parameters: '200':
- name: ordernumber
in: path
required: true
description: The order's unique number.
schema: {}
- name: create-order
description: Creates a new order in the CRM.
annotations:
title: Create an order
read_only_hint: false
idempotent_hint: false
destructive_hint: false
open_world_hint: false
# Verified against OrderPermission.check() (authentication/permissions.py):
# admin always passes; customer passes if get_customer_number(user) is
# not None; support passes unconditionally at this class-level check
# (object-level restricts support to status-only on *existing* orders,
# which doesn't apply to create — no object yet). read_only/
# product_manager only pass for SAFE_METHODS, excluded here.
acl:
allow:
- admin
- customer
- support
method: POST
scheme: http
host: ${REST_HOSTNAME}
path: /sales/v1/orders/
request_body:
required: true
content: content:
application/json: application/json:
schema: schema:
type: object type: object
properties:
count:
type: integer
example: 123
next:
type: string
nullable: true
format: uri
example: http://api.example.org/accounts/?page=4
previous:
type: string
nullable: true
format: uri
example: http://api.example.org/accounts/?page=2
results:
type: array
items:
type: object
description: Customer order information
properties: properties:
ordernumber: ordernumber:
type: integer type: integer
maximum: 2147483647
minimum: -2147483648
orderdate:
type: string
format: date
requireddate:
type: string
format: date
shippeddate:
type: string
format: date
nullable: true
status:
type: string
maxLength: 15
comments:
type: string
nullable: true
customernumber:
type: integer
maximum: 2147483647
minimum: -2147483648
required:
- customernumber
- orderdate
- ordernumber
- requireddate
- status
- annotations:
title: Get a specific order
read_only_hint: true
idempotent_hint: true
destructive_hint: false
open_world_hint: false
description: Retrieve detailed information about a specific order by its order number.
method: GET
name: get-order
parameters:
- description: A unique value identifying this order.
in: path
name: ordernumber
required: true
schema:
type: integer
path: /sales/v1/orders/{ordernumber}/
acl:
allow:
- admin
- read_only
- product_manager
- customer
- support
scheme: http
host: ${REST_HOSTNAME}
responses:
'200':
content:
application/json:
schema:
type: object
description: Customer order information
properties:
ordernumber:
type: integer
maximum: 2147483647
minimum: -2147483648
orderdate:
type: string
format: date
requireddate:
type: string
format: date
shippeddate:
type: string
format: date
nullable: true
status:
type: string
maxLength: 15
comments:
type: string
nullable: true
customernumber:
type: integer
maximum: 2147483647
minimum: -2147483648
required:
- customernumber
- orderdate
- ordernumber
- requireddate
- status
- annotations:
title: Get order details by order
read_only_hint: true
idempotent_hint: true
destructive_hint: false
open_world_hint: false
description: Retrieve all line items for a specific order. This endpoint supports order fulfillment
and invoice generation.
method: GET
name: get-order-order-details
parameters:
- description: The order number
in: path
name: ordernumber
required: true
schema:
type: integer
- description: A page number within the paginated result set.
in: query
name: page
required: false
schema:
type: integer
- description: Number of results to return per page.
in: query
name: page_size
required: false
schema:
type: integer
path: /sales/v1/orders/{ordernumber}/order-details/
acl:
allow:
- admin
- read_only
- product_manager
- customer
- support
scheme: http
host: ${REST_HOSTNAME}
responses:
'200':
content:
application/json:
schema:
type: object
properties:
count:
type: integer
example: 123
next:
type: string
nullable: true
format: uri
example: http://api.example.org/accounts/?page=4
previous:
type: string
nullable: true
format: uri
example: http://api.example.org/accounts/?page=2
results:
type: array
items:
type: object
description: Order line items with product details
properties:
id:
type: integer
readOnly: true
quantityordered:
type: integer
maximum: 2147483647
minimum: -2147483648
priceeach:
type: string
format: decimal
pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
orderlinenumber:
type: integer
maximum: 32767
minimum: -32768
ordernumber:
type: integer
maximum: 2147483647
minimum: -2147483648
productcode:
type: string
required:
- id
- orderlinenumber
- ordernumber
- priceeach
- productcode
- quantityordered
- annotations:
title: Create a new order
read_only_hint: false
idempotent_hint: false
destructive_hint: false
open_world_hint: false
description: Create a new customer order with order date, required date, and status information.
method: POST
name: create-order
path: /sales/v1/orders/
request_body:
content:
application/json:
schema:
properties:
comments:
type: string
customernumber:
type: integer
orderdate: orderdate:
type: string type: string
ordernumber:
type: integer
requireddate: requireddate:
type: string type: string
shippeddate: shippeddate:
type: string type: string
status: status:
type: string type: string
comments:
type: string
customernumber:
type: integer
required: required:
- ordernumber - customernumber
- orderdate - orderdate
- ordernumber
- requireddate - requireddate
- status - status
type: object
required: true
acl:
allow:
- admin
- customer
- support
scheme: http
host: ${REST_HOSTNAME}
responses:
'201':
content:
application/json:
schema:
type: object
description: Customer order information
properties:
ordernumber:
type: integer
maximum: 2147483647
minimum: -2147483648
orderdate:
type: string
format: date
requireddate:
type: string
format: date
shippeddate:
type: string
format: date
nullable: true
status:
type: string
maxLength: 15
comments:
type: string
nullable: true
customernumber:
type: integer
maximum: 2147483647
minimum: -2147483648
required:
- customernumber - customernumber
- name: list-orderdetails - orderdate
description: Retrieves a paginated list of order line items in the classic-models CRM. - ordernumber
annotations: - requireddate
title: List all order line items - status
- annotations:
title: List all order details
read_only_hint: true read_only_hint: true
idempotent_hint: true idempotent_hint: true
destructive_hint: false destructive_hint: false
open_world_hint: false open_world_hint: false
acl: description: Retrieve a paginated list of all order line items with product and quantity information.
allow:
- admin
- read_only
- product_manager
- customer
- support
method: GET method: GET
scheme: http name: list-order-details
host: ${REST_HOSTNAME}
path: /sales/v1/orderdetails/
- name: get-orderdetail
description: Retrieves a single order line item by order number and product code.
annotations:
title: Get an order line item
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: /sales/v1/orderdetails/{ordernumber}/{productcode}/
parameters: parameters:
- name: ordernumber - description: A page number within the paginated result set.
in: path in: query
required: true name: page
description: The order's unique number. required: false
schema: {} schema:
- name: productcode type: integer
in: path - description: Number of results to return per page.
required: true in: query
description: The product's unique code. name: page_size
schema: {} required: false
- name: create-orderdetail schema:
description: Adds a new line item to an order. type: integer
annotations: path: /sales/v1/orderdetails/
title: Create an order line item
read_only_hint: false
idempotent_hint: false
destructive_hint: false
open_world_hint: false
# Verified against OrderScopedPermission.check() (Orderdetail's base
# permission class) — same shape as create-order above: admin,
# customer (if get_customer_number is not None), support (class-level
# unconditional pass) allowed; read_only/product_manager excluded
# (SAFE_METHODS only).
acl: acl:
allow: allow:
- admin - admin
- read_only
- product_manager
- customer - customer
- support - support
method: POST
scheme: http scheme: http
host: ${REST_HOSTNAME} host: ${REST_HOSTNAME}
path: /sales/v1/orderdetails/ responses:
request_body: '200':
required: true
content: content:
application/json: application/json:
schema: schema:
type: object type: object
properties: properties:
count:
type: integer
example: 123
next:
type: string
nullable: true
format: uri
example: http://api.example.org/accounts/?page=4
previous:
type: string
nullable: true
format: uri
example: http://api.example.org/accounts/?page=2
results:
type: array
items:
type: object
description: Order line items with product details
properties:
id:
type: integer
readOnly: true
quantityordered:
type: integer
maximum: 2147483647
minimum: -2147483648
priceeach:
type: string
format: decimal
pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
orderlinenumber:
type: integer
maximum: 32767
minimum: -32768
ordernumber: ordernumber:
type: integer type: integer
maximum: 2147483647
minimum: -2147483648
productcode:
type: string
required:
- id
- orderlinenumber
- ordernumber
- priceeach
- productcode
- quantityordered
- annotations:
title: Get a specific order detail
read_only_hint: true
idempotent_hint: true
destructive_hint: false
open_world_hint: false
description: Retrieve a specific line item from an order using the composite key (orderNumber, productCode).
method: GET
name: get-order-detail
parameters:
- description: The order number
in: path
name: ordernumber
required: true
schema:
type: integer
- description: The product code
in: path
name: productcode
required: true
schema:
type: string
path: /sales/v1/orderdetails/{ordernumber}/{productcode}/
acl:
allow:
- admin
- read_only
- product_manager
- customer
- support
scheme: http
host: ${REST_HOSTNAME}
responses:
'200':
content:
application/json:
schema:
type: object
description: Order line items with product details
properties:
id:
type: integer
readOnly: true
quantityordered:
type: integer
maximum: 2147483647
minimum: -2147483648
priceeach:
type: string
format: decimal
pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
orderlinenumber:
type: integer
maximum: 32767
minimum: -32768
ordernumber:
type: integer
maximum: 2147483647
minimum: -2147483648
productcode:
type: string
required:
- id
- orderlinenumber
- ordernumber
- priceeach
- productcode
- quantityordered
- annotations:
title: Create a new order detail
read_only_hint: false
idempotent_hint: false
destructive_hint: false
open_world_hint: false
description: Add a new line item to an existing order with product code, quantity, and price.
method: POST
name: create-order-detail
path: /sales/v1/orderdetails/
request_body:
content:
application/json:
schema:
properties:
orderlinenumber:
type: integer
ordernumber:
type: integer
priceeach:
type: string
productcode: productcode:
type: string type: string
quantityordered: quantityordered:
type: integer type: integer
priceeach:
type: string
orderlinenumber:
type: integer
required: required:
- orderlinenumber
- ordernumber - ordernumber
- priceeach
- productcode - productcode
- quantityordered - quantityordered
- priceeach type: object
required: true
acl:
allow:
- admin
- customer
- support
scheme: http
host: ${REST_HOSTNAME}
responses:
'201':
content:
application/json:
schema:
type: object
description: Order line items with product details
properties:
id:
type: integer
readOnly: true
quantityordered:
type: integer
maximum: 2147483647
minimum: -2147483648
priceeach:
type: string
format: decimal
pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
orderlinenumber:
type: integer
maximum: 32767
minimum: -32768
ordernumber:
type: integer
maximum: 2147483647
minimum: -2147483648
productcode:
type: string
required:
- id
- orderlinenumber - orderlinenumber
- ordernumber
- priceeach
- productcode
- quantityordered
+28
View File
@@ -0,0 +1,28 @@
# Which operations of openapi.yaml become MCP tools, and who may call each.
#
# This file is the allow-list as well as the ACL: an operation that is not
# named here does not become a tool. Kong's generator exposes every operation
# it finds and can express no authorization at all from a bearer-token spec —
# see k8s-platform/MCP-TOOL-GENERATION.md, limits 1 and 5.
#
# `allow` lists consumer groups, which come from realm_access.roles in the end
# user's own token. Kong applies them at tools/list, so a caller never sees a
# tool it may not call.
mcp_name: sales-mcp
tools:
list-orders:
allow: [admin, read_only, product_manager, customer, support]
get-order:
allow: [admin, read_only, product_manager, customer, support]
get-order-order-details:
allow: [admin, read_only, product_manager, customer, support]
create-order:
allow: [admin, customer, support]
list-order-details:
allow: [admin, read_only, product_manager, customer, support]
get-order-detail:
allow: [admin, read_only, product_manager, customer, support]
create-order-detail:
allow: [admin, customer, support]