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:38 +02:00
parent f0259d8dd9
commit 63d2d90107
2 changed files with 330 additions and 70 deletions
+310 -70
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,95 +15,328 @@ config:
mode: conversion-listener mode: conversion-listener
include_consumer_groups: true include_consumer_groups: true
tools: tools:
- name: list-customers - annotations:
description: Retrieves a paginated list of customers in the classic-models CRM. title: List all customers
annotations: read_only_hint: true
title: List all customers idempotent_hint: true
read_only_hint: true destructive_hint: false
idempotent_hint: true open_world_hint: false
destructive_hint: false description: Retrieve a paginated list of all customers with their contact and credit information.
open_world_hint: false method: GET
acl: name: list-customers
allow: parameters:
- admin - description: A page number within the paginated result set.
- read_only in: query
- product_manager name: page
- customer required: false
- support schema:
method: GET type: integer
scheme: http - description: Number of results to return per page.
host: ${REST_HOSTNAME} in: query
path: /customers/v1/customers/ name: page_size
- name: get-customer required: false
description: Retrieves a single customer by customer number. schema:
annotations: type: integer
title: Get a customer path: /customers/v1/customers/
read_only_hint: true acl:
idempotent_hint: true allow:
destructive_hint: false - admin
open_world_hint: false - read_only
acl: - product_manager
allow: - customer
- admin - support
- read_only scheme: http
- product_manager host: ${REST_HOSTNAME}
- customer responses:
- support '200':
method: GET
scheme: http
host: ${REST_HOSTNAME}
path: /customers/v1/customers/{customernumber}/
parameters:
- name: customernumber
in: path
required: true
description: The customer's unique number.
schema: {}
- name: create-customer
description: Creates a new customer in the CRM.
annotations:
title: Create a customer
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: /customers/v1/customers/
request_body:
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: Customer information and contact details
properties:
customernumber:
type: integer
maximum: 2147483647
minimum: -2147483648
customername:
type: string
maxLength: 50
contactlastname:
type: string
maxLength: 50
contactfirstname:
type: string
maxLength: 50
phone:
type: string
maxLength: 50
addressline1:
type: string
maxLength: 50
addressline2:
type: string
nullable: true
maxLength: 50
city:
type: string
maxLength: 50
state:
type: string
nullable: true
maxLength: 50
postalcode:
type: string
nullable: true
maxLength: 15
country:
type: string
maxLength: 50
creditlimit:
type: string
format: decimal
pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
nullable: true
salesrepemployeenumber:
type: integer
maximum: 2147483647
minimum: -2147483648
nullable: true
required:
- addressline1
- city
- contactfirstname
- contactlastname
- country
- customername
- customernumber
- phone
- annotations:
title: Get a specific customer
read_only_hint: true
idempotent_hint: true
destructive_hint: false
open_world_hint: false
description: Retrieve detailed information about a specific customer by their customer number.
method: GET
name: get-customer
parameters:
- description: A unique value identifying this customer.
in: path
name: customernumber
required: true
schema:
type: integer
path: /customers/v1/customers/{customernumber}/
acl:
allow:
- admin
- read_only
- product_manager
- customer
- support
scheme: http
host: ${REST_HOSTNAME}
responses:
'200':
content:
application/json:
schema:
type: object
description: Customer information and contact details
properties:
customernumber:
type: integer
maximum: 2147483647
minimum: -2147483648
customername: customername:
type: string type: string
maxLength: 50
contactlastname: contactlastname:
type: string type: string
maxLength: 50
contactfirstname: contactfirstname:
type: string type: string
maxLength: 50
phone: phone:
type: string type: string
maxLength: 50
addressline1: addressline1:
type: string type: string
maxLength: 50
addressline2:
type: string
nullable: true
maxLength: 50
city: city:
type: string type: string
maxLength: 50
state:
type: string
nullable: true
maxLength: 50
postalcode:
type: string
nullable: true
maxLength: 15
country: country:
type: string type: string
salesrepemployeenumber: maxLength: 50
type: integer
creditlimit: creditlimit:
type: string type: string
format: decimal
pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
nullable: true
salesrepemployeenumber:
type: integer
maximum: 2147483647
minimum: -2147483648
nullable: true
required: required:
- customername - addressline1
- contactlastname - city
- contactfirstname - contactfirstname
- phone - contactlastname
- addressline1 - country
- city - customername
- country - customernumber
- phone
- annotations:
title: Create a new customer
read_only_hint: false
idempotent_hint: false
destructive_hint: false
open_world_hint: false
description: Add a new customer to the system with their contact details and credit limit.
method: POST
name: create-customer
path: /customers/v1/customers/
request_body:
content:
application/json:
schema:
properties:
addressline1:
type: string
addressline2:
type: string
city:
type: string
contactfirstname:
type: string
contactlastname:
type: string
country:
type: string
creditlimit:
type: string
customername:
type: string
customernumber:
type: integer
phone:
type: string
postalcode:
type: string
salesrepemployeenumber:
type: integer
state:
type: string
required:
- addressline1
- city
- contactfirstname
- contactlastname
- country
- customername
- customernumber
- phone
type: object
required: true
acl:
allow:
- admin
scheme: http
host: ${REST_HOSTNAME}
responses:
'201':
content:
application/json:
schema:
type: object
description: Customer information and contact details
properties:
customernumber:
type: integer
maximum: 2147483647
minimum: -2147483648
customername:
type: string
maxLength: 50
contactlastname:
type: string
maxLength: 50
contactfirstname:
type: string
maxLength: 50
phone:
type: string
maxLength: 50
addressline1:
type: string
maxLength: 50
addressline2:
type: string
nullable: true
maxLength: 50
city:
type: string
maxLength: 50
state:
type: string
nullable: true
maxLength: 50
postalcode:
type: string
nullable: true
maxLength: 15
country:
type: string
maxLength: 50
creditlimit:
type: string
format: decimal
pattern: ^-?\d{0,8}(?:\.\d{0,2})?$
nullable: true
salesrepemployeenumber:
type: integer
maximum: 2147483647
minimum: -2147483648
nullable: true
required:
- addressline1
- city
- contactfirstname
- contactlastname
- country
- customername
- customernumber
- phone
+20
View File
@@ -0,0 +1,20 @@
# 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: customers-mcp
tools:
list-customers:
allow: [admin, read_only, product_manager, customer, support]
get-customer:
allow: [admin, read_only, product_manager, customer, support]
create-customer:
allow: [admin]