From 8db2bb6e39556e6640763d6611c6b1a90cd74104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Tailland?= Date: Sat, 29 Aug 2026 08:48:45 +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 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 --- kong/mcp/ai-mcp-proxy.yaml | 991 +++++++++++++++++++++++++++++-------- kong/mcp/tools-policy.yaml | 32 ++ 2 files changed, 812 insertions(+), 211 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 4175726..53b44fd 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,260 +15,822 @@ config: mode: conversion-listener include_consumer_groups: true tools: - - name: list-offices - description: Retrieves a paginated list of offices in the classic-models organization. - annotations: - title: List all offices - read_only_hint: true - idempotent_hint: true - destructive_hint: false - open_world_hint: false - # Verified against ReferenceDataPermission.check() — read_only, - # product_manager, support pass for SAFE_METHODS; customer has NO - # access at all (not in the allow-list, unlike every other domain - # built this session). Not guessed — this domain is internal staff - # data, the backend enforces that explicitly. - acl: - allow: - - admin - - read_only - - product_manager - - support - method: GET - scheme: http - host: ${REST_HOSTNAME} - path: /workforce/v1/offices/ - - name: get-office - description: Retrieves a single office by office code. - annotations: - title: Get an office - read_only_hint: true - idempotent_hint: true - destructive_hint: false - open_world_hint: false - acl: - allow: - - admin - - read_only - - product_manager - - support - method: GET - scheme: http - host: ${REST_HOSTNAME} - path: /workforce/v1/offices/{officecode}/ - parameters: - - name: officecode - in: path - required: true - description: The office's unique code. - schema: {} - - name: get-office-employees - description: Retrieves all employees working in a specific office. - annotations: - title: Get office employees - read_only_hint: true - idempotent_hint: true - destructive_hint: false - open_world_hint: false - acl: - allow: - - admin - - read_only - - product_manager - - support - method: GET - scheme: http - host: ${REST_HOSTNAME} - path: /workforce/v1/offices/{officecode}/employees/ - parameters: - - name: officecode - in: path - required: true - description: The office's unique code. - schema: {} - - name: create-office - description: Creates a new office in the organization. - annotations: - title: Create an office - read_only_hint: false - idempotent_hint: false - destructive_hint: false - open_world_hint: false - # Writes are admin-only under ReferenceDataPermission (check() - # returns False for non-admin on any non-SAFE method). - acl: - allow: - - admin - method: POST - scheme: http - host: ${REST_HOSTNAME} - path: /workforce/v1/offices/ - request_body: - required: true + - annotations: + title: List all offices + read_only_hint: true + idempotent_hint: true + destructive_hint: false + open_world_hint: false + description: Retrieve a paginated list of all company office locations worldwide. + method: GET + name: list-offices + 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: /workforce/v1/offices/ + acl: + allow: + - admin + - read_only + - product_manager + - 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: Company office locations + properties: + officecode: + type: string + maxLength: 10 + city: + type: string + maxLength: 50 + phone: + type: string + maxLength: 50 + addressline1: + type: string + maxLength: 50 + addressline2: + type: string + nullable: true + maxLength: 50 + state: + type: string + nullable: true + maxLength: 50 + country: + type: string + maxLength: 50 + postalcode: + type: string + maxLength: 15 + territory: + type: string + maxLength: 10 + required: + - addressline1 + - city + - country + - officecode + - phone + - postalcode + - territory + - annotations: + title: Get a specific office + read_only_hint: true + idempotent_hint: true + destructive_hint: false + open_world_hint: false + description: Retrieve detailed information about a specific office by its code. + method: GET + name: get-office + parameters: + - description: A unique value identifying this office. + in: path + name: officecode + required: true + schema: + type: string + path: /workforce/v1/offices/{officecode}/ + acl: + allow: + - admin + - read_only + - product_manager + - support + scheme: http + host: ${REST_HOSTNAME} + responses: + '200': + content: + application/json: + schema: + type: object + description: Company office locations properties: officecode: type: string + maxLength: 10 city: type: string + maxLength: 50 phone: type: string + maxLength: 50 addressline1: type: string + maxLength: 50 addressline2: type: string + nullable: true + maxLength: 50 state: type: string + nullable: true + maxLength: 50 country: type: string + maxLength: 50 postalcode: type: string + maxLength: 15 territory: type: string + maxLength: 10 required: - - officecode - - city - - phone - - addressline1 - - country - - postalcode - - territory - - name: list-employees - description: Retrieves a paginated list of employees in the classic-models organization. - annotations: - title: List all employees - read_only_hint: true - idempotent_hint: true - destructive_hint: false - open_world_hint: false - acl: - allow: - - admin - - read_only - - product_manager - - support - method: GET - scheme: http - host: ${REST_HOSTNAME} - path: /workforce/v1/employees/ - - name: get-employee - description: Retrieves a single employee by employee number. - annotations: - title: Get an employee - read_only_hint: true - idempotent_hint: true - destructive_hint: false - open_world_hint: false - acl: - allow: - - admin - - read_only - - product_manager - - support - method: GET - scheme: http - host: ${REST_HOSTNAME} - path: /workforce/v1/employees/{employeenumber}/ - parameters: - - name: employeenumber - in: path - required: true - description: The employee's unique number. - schema: {} - - name: get-employee-reports - description: Retrieves all employees reporting to a specific manager. - annotations: - title: Get employee reports - read_only_hint: true - idempotent_hint: true - destructive_hint: false - open_world_hint: false - acl: - allow: - - admin - - read_only - - product_manager - - support - method: GET - scheme: http - host: ${REST_HOSTNAME} - path: /workforce/v1/employees/{employeenumber}/reports/ - parameters: - - name: employeenumber - in: path - required: true - description: The manager's employee number. - schema: {} - - name: get-employee-customers - description: Retrieves all customers assigned to a specific sales representative. - annotations: - title: Get employee's customers - read_only_hint: true - idempotent_hint: true - destructive_hint: false - open_world_hint: false - acl: - allow: - - admin - - read_only - - product_manager - - support - method: GET - scheme: http - host: ${REST_HOSTNAME} - path: /workforce/v1/employees/{employeenumber}/customers/ - parameters: - - name: employeenumber - in: path - required: true - description: The sales rep's employee number. - schema: {} - - name: create-employee - description: Adds a new employee to the organization. - annotations: - title: Create an employee - 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: /workforce/v1/employees/ - request_body: - required: true + - addressline1 + - city + - country + - officecode + - phone + - postalcode + - territory + - annotations: + title: Get employees by office + read_only_hint: true + idempotent_hint: true + destructive_hint: false + open_world_hint: false + description: Retrieve all employees working in a specific office. This endpoint supports office management + and organizational charts. + method: GET + name: get-office-employees + parameters: + - description: The office code + in: path + name: officecode + required: true + schema: + type: string + - 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: /workforce/v1/offices/{officecode}/employees/ + acl: + allow: + - admin + - read_only + - product_manager + - 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: Employee information and hierarchy + properties: + employeenumber: + type: integer + maximum: 2147483647 + minimum: -2147483648 + lastname: + type: string + maxLength: 50 + firstname: + type: string + maxLength: 50 + extension: + type: string + maxLength: 10 + email: + type: string + maxLength: 100 + jobtitle: + type: string + maxLength: 50 + officecode: + type: string + reportsto: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true + required: + - email + - employeenumber + - extension + - firstname + - jobtitle + - lastname + - officecode + - annotations: + title: Create a new office + read_only_hint: false + idempotent_hint: false + destructive_hint: false + open_world_hint: false + description: Add a new office location to the company's network. + method: POST + name: create-office + path: /workforce/v1/offices/ + request_body: + content: + application/json: + schema: + properties: + addressline1: + type: string + addressline2: + type: string + city: + type: string + country: + type: string + officecode: + type: string + phone: + type: string + postalcode: + type: string + state: + type: string + territory: + type: string + required: + - addressline1 + - city + - country + - officecode + - phone + - postalcode + - territory + type: object + required: true + acl: + allow: + - admin + scheme: http + host: ${REST_HOSTNAME} + responses: + '201': + content: + application/json: + schema: + type: object + description: Company office locations + properties: + officecode: + type: string + maxLength: 10 + city: + type: string + maxLength: 50 + phone: + type: string + maxLength: 50 + addressline1: + type: string + maxLength: 50 + addressline2: + type: string + nullable: true + maxLength: 50 + state: + type: string + nullable: true + maxLength: 50 + country: + type: string + maxLength: 50 + postalcode: + type: string + maxLength: 15 + territory: + type: string + maxLength: 10 + required: + - addressline1 + - city + - country + - officecode + - phone + - postalcode + - territory + - annotations: + title: List all employees + read_only_hint: true + idempotent_hint: true + destructive_hint: false + open_world_hint: false + description: Retrieve a paginated list of all employees in the organization with their details. + method: GET + name: list-employees + 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: /workforce/v1/employees/ + acl: + allow: + - admin + - read_only + - product_manager + - 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: Employee information and hierarchy + properties: + employeenumber: + type: integer + maximum: 2147483647 + minimum: -2147483648 + lastname: + type: string + maxLength: 50 + firstname: + type: string + maxLength: 50 + extension: + type: string + maxLength: 10 + email: + type: string + maxLength: 100 + jobtitle: + type: string + maxLength: 50 + officecode: + type: string + reportsto: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true + required: + - email + - employeenumber + - extension + - firstname + - jobtitle + - lastname + - officecode + - annotations: + title: Get a specific employee + read_only_hint: true + idempotent_hint: true + destructive_hint: false + open_world_hint: false + description: Retrieve detailed information about a specific employee by their employee number. + method: GET + name: get-employee + parameters: + - description: A unique value identifying this employee. + in: path + name: employeenumber + required: true + schema: + type: integer + path: /workforce/v1/employees/{employeenumber}/ + acl: + allow: + - admin + - read_only + - product_manager + - support + scheme: http + host: ${REST_HOSTNAME} + responses: + '200': + content: + application/json: + schema: + type: object + description: Employee information and hierarchy properties: employeenumber: type: integer + maximum: 2147483647 + minimum: -2147483648 lastname: type: string + maxLength: 50 firstname: type: string + maxLength: 50 extension: type: string + maxLength: 10 email: type: string + maxLength: 100 + jobtitle: + type: string + maxLength: 50 officecode: type: string reportsto: type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true + required: + - email + - employeenumber + - extension + - firstname + - jobtitle + - lastname + - officecode + - annotations: + title: Get employees by manager + read_only_hint: true + idempotent_hint: true + destructive_hint: false + open_world_hint: false + description: Retrieve all employees reporting to a specific manager. This endpoint supports organizational + hierarchy and team management. + method: GET + name: get-employee-reports + parameters: + - description: The employee number of the manager + in: path + name: employeenumber + 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: /workforce/v1/employees/{employeenumber}/reports/ + acl: + allow: + - admin + - read_only + - product_manager + - 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: Employee information and hierarchy + properties: + employeenumber: + type: integer + maximum: 2147483647 + minimum: -2147483648 + lastname: + type: string + maxLength: 50 + firstname: + type: string + maxLength: 50 + extension: + type: string + maxLength: 10 + email: + type: string + maxLength: 100 + jobtitle: + type: string + maxLength: 50 + officecode: + type: string + reportsto: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true + required: + - email + - employeenumber + - extension + - firstname + - jobtitle + - lastname + - officecode + - annotations: + title: Get customers by sales rep + read_only_hint: true + idempotent_hint: true + destructive_hint: false + open_world_hint: false + description: Retrieve all customers assigned to a specific sales representative. This endpoint supports + sales rep performance tracking and customer management. + method: GET + name: get-employee-customers + parameters: + - description: The employee number of the sales representative + in: path + name: employeenumber + 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: /workforce/v1/employees/{employeenumber}/customers/ + acl: + allow: + - admin + - read_only + - product_manager + - 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: 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: Create a new employee + read_only_hint: false + idempotent_hint: false + destructive_hint: false + open_world_hint: false + description: Add a new employee to the organization with their job details and office assignment. + method: POST + name: create-employee + path: /workforce/v1/employees/ + request_body: + content: + application/json: + schema: + properties: + email: + type: string + employeenumber: + type: integer + extension: + type: string + firstname: + type: string + jobtitle: + type: string + lastname: + type: string + officecode: + type: string + reportsto: + type: integer + required: + - email + - employeenumber + - extension + - firstname + - jobtitle + - lastname + - officecode + type: object + required: true + acl: + allow: + - admin + scheme: http + host: ${REST_HOSTNAME} + responses: + '201': + content: + application/json: + schema: + type: object + description: Employee information and hierarchy + properties: + employeenumber: + type: integer + maximum: 2147483647 + minimum: -2147483648 + lastname: + type: string + maxLength: 50 + firstname: + type: string + maxLength: 50 + extension: + type: string + maxLength: 10 + email: + type: string + maxLength: 100 jobtitle: type: string + maxLength: 50 + officecode: + type: string + reportsto: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true required: - - employeenumber - - lastname - - firstname - - extension - - email - - officecode - - jobtitle + - email + - employeenumber + - extension + - firstname + - jobtitle + - lastname + - officecode diff --git a/kong/mcp/tools-policy.yaml b/kong/mcp/tools-policy.yaml new file mode 100644 index 0000000..fb7fcb3 --- /dev/null +++ b/kong/mcp/tools-policy.yaml @@ -0,0 +1,32 @@ +# 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: workforce-mcp + +tools: + list-offices: + allow: [admin, read_only, product_manager, support] + get-office: + allow: [admin, read_only, product_manager, support] + get-office-employees: + allow: [admin, read_only, product_manager, support] + create-office: + allow: [admin] + list-employees: + allow: [admin, read_only, product_manager, support] + get-employee: + allow: [admin, read_only, product_manager, support] + get-employee-reports: + allow: [admin, read_only, product_manager, support] + get-employee-customers: + allow: [admin, read_only, product_manager, support] + create-employee: + allow: [admin]