From 32d6014b426f227fd56136d7bf1ccd3701e80fd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Tailland?= Date: Sat, 29 Aug 2026 08:43:36 +0200 Subject: [PATCH] refactor(mcp): generate the tool definitions from the spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tools: array was hand-maintained and had drifted from openapi.yaml in two ways that only surfaced when an agent hit them: create-product omitted productcode and could never succeed, and list-products declared no parameters so it could not paginate — which let a model answer a product count that looked right and had not come from the tool's output. It is now 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 three tools as before, same ACLs. What changed: pagination is now callable, create-product's required list matches the API (quantityinstock included, which the hand-written repair had missed), each tool declares its response schema, and the request body is no longer tripled across three identical content types. Co-Authored-By: Claude Opus 5 --- kong/mcp/ai-mcp-proxy.yaml | 329 +++++++++++++++++++++++++++---------- kong/mcp/tools-policy.yaml | 20 +++ 2 files changed, 265 insertions(+), 84 deletions(-) create mode 100644 kong/mcp/tools-policy.yaml diff --git a/kong/mcp/ai-mcp-proxy.yaml b/kong/mcp/ai-mcp-proxy.yaml index 569d0e7..a214f9e 100644 --- a/kong/mcp/ai-mcp-proxy.yaml +++ b/kong/mcp/ai-mcp-proxy.yaml @@ -1,3 +1,10 @@ +# GENERATED — do not edit. Edit openapi.yaml or tools-policy.yaml, then: +# k8s-platform/tools/generate-mcp-tools.py +# +# 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 kind: KongPlugin metadata: @@ -8,90 +15,142 @@ config: mode: conversion-listener include_consumer_groups: true tools: - - name: list-products - description: Retrieves a paginated list of products in the classic-models catalog. - annotations: - title: List all products - 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: /catalog/v1/products/ - - name: get-product - description: Retrieves a single product by its product code. - annotations: - title: Get a product - 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: /catalog/v1/products/{productcode}/ - parameters: - - name: productcode - in: path - required: true - description: The product's unique code. - schema: {} - - name: create-product - description: Creates a new product in the catalog. - annotations: - title: Create a product - read_only_hint: false - idempotent_hint: false - destructive_hint: false - open_world_hint: false - acl: - allow: - - admin - - product_manager - method: POST - scheme: http - host: ${REST_HOSTNAME} - path: /catalog/v1/products/ - request_body: - required: true + - annotations: + title: List all products + read_only_hint: true + idempotent_hint: true + destructive_hint: false + open_world_hint: false + description: Retrieve a paginated list of all products in the catalog with their details. + method: GET + name: list-products + parameters: + - 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: /catalog/v1/products/ + acl: + allow: + - admin + - read_only + - product_manager + - customer + - support + scheme: http + host: ${REST_HOSTNAME} + responses: + '200': content: application/json: - # Mirrors ProductRequest in catalog-api/openapi.yaml. It has to: - # this schema is the only thing the caller sees, so anything it - # omits the caller cannot send. `productcode` was missing and is - # required by the API, which made this tool impossible to call - # successfully — every attempt came back - # {"productcode":["This field is required."]}, and a caller that - # tried to supply it anyway was rejected earlier still, here. 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: Product catalog information + properties: + productcode: + type: string + maxLength: 15 + productname: + type: string + maxLength: 70 + productscale: + type: string + maxLength: 10 + productvendor: + type: string + maxLength: 50 + productdescription: + type: string + quantityinstock: + type: integer + maximum: 32767 + minimum: -32768 + buyprice: + type: string + format: decimal + pattern: ^-?\d{0,8}(?:\.\d{0,2})?$ + msrp: + type: string + format: decimal + pattern: ^-?\d{0,8}(?:\.\d{0,2})?$ + productline: + type: string + required: + - buyprice + - msrp + - productcode + - productdescription + - productline + - productname + - productscale + - productvendor + - quantityinstock + - annotations: + title: Get a specific product + read_only_hint: true + idempotent_hint: true + destructive_hint: false + open_world_hint: false + description: Retrieve detailed information about a specific product by its code. + method: GET + name: get-product + parameters: + - description: A unique value identifying this product. + in: path + name: productcode + required: true + schema: + type: string + path: /catalog/v1/products/{productcode}/ + acl: + allow: + - admin + - read_only + - product_manager + - customer + - support + scheme: http + host: ${REST_HOSTNAME} + responses: + '200': + content: + application/json: + schema: + type: object + description: Product catalog information properties: productcode: type: string maxLength: 15 - description: Unique catalog identifier, e.g. S10_1949. Ask for it; do not invent one. productname: type: string maxLength: 70 - productline: - type: string - description: Must be an existing product line, as returned by list-productlines. productscale: type: string maxLength: 10 @@ -102,20 +161,122 @@ config: type: string quantityinstock: type: integer - minimum: -32768 maximum: 32767 + minimum: -32768 buyprice: type: string - description: Decimal as a string, at most two places, e.g. "50.00". + format: decimal + pattern: ^-?\d{0,8}(?:\.\d{0,2})?$ msrp: type: string - description: Decimal as a string, at most two places, e.g. "99.00". + format: decimal + pattern: ^-?\d{0,8}(?:\.\d{0,2})?$ + productline: + type: string required: - - productcode - - productname - - productline - - productscale - - productvendor - - productdescription - - buyprice - - msrp + - buyprice + - msrp + - productcode + - productdescription + - productline + - productname + - productscale + - productvendor + - quantityinstock + - annotations: + title: Create a new product + read_only_hint: false + idempotent_hint: false + destructive_hint: false + open_world_hint: false + description: Add a new product to the catalog with all required information. + method: POST + name: create-product + path: /catalog/v1/products/ + request_body: + content: + application/json: + schema: + properties: + buyprice: + type: string + msrp: + type: string + productcode: + type: string + productdescription: + type: string + productline: + type: string + productname: + type: string + productscale: + type: string + productvendor: + type: string + quantityinstock: + type: integer + required: + - buyprice + - msrp + - productcode + - productdescription + - productline + - productname + - productscale + - productvendor + - quantityinstock + type: object + required: true + acl: + allow: + - admin + - product_manager + scheme: http + host: ${REST_HOSTNAME} + responses: + '201': + content: + application/json: + schema: + type: object + description: Product catalog information + properties: + productcode: + type: string + maxLength: 15 + productname: + type: string + maxLength: 70 + productscale: + type: string + maxLength: 10 + productvendor: + type: string + maxLength: 50 + productdescription: + type: string + quantityinstock: + type: integer + maximum: 32767 + minimum: -32768 + buyprice: + type: string + format: decimal + pattern: ^-?\d{0,8}(?:\.\d{0,2})?$ + msrp: + type: string + format: decimal + pattern: ^-?\d{0,8}(?:\.\d{0,2})?$ + productline: + type: string + required: + - buyprice + - msrp + - productcode + - productdescription + - productline + - productname + - productscale + - productvendor + - quantityinstock diff --git a/kong/mcp/tools-policy.yaml b/kong/mcp/tools-policy.yaml new file mode 100644 index 0000000..5d17400 --- /dev/null +++ b/kong/mcp/tools-policy.yaml @@ -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: catalog-mcp + +tools: + list-products: + allow: [admin, read_only, product_manager, customer, support] + get-product: + allow: [admin, read_only, product_manager, customer, support] + create-product: + allow: [admin, product_manager]