Build sales-api REST + MCP layers (Layer 3), full build in one pass

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.
This commit is contained in:
Stéphane Tailland
2026-08-23 17:02:17 +02:00
parent 88b2136903
commit de69f88e29
28 changed files with 1481 additions and 11 deletions
+223
View File
@@ -0,0 +1,223 @@
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: ${NAME}-sales-mcp-ai-mcp-proxy
namespace: ${NAMESPACE}
plugin: ai-mcp-proxy
config:
mode: conversion-listener
include_consumer_groups: true
tools:
- name: list-orders
description: Retrieves a paginated list of orders in the classic-models CRM.
annotations:
title: List all orders
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/
- 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:
- name: ordernumber
in: path
required: true
description: The order's unique number.
schema: {}
- name: get-order-orderdetails
description: Retrieves all line items for a specific order.
annotations:
title: Get order line items
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}/order-details/
parameters:
- 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:
application/json:
schema:
type: object
properties:
ordernumber:
type: integer
orderdate:
type: string
requireddate:
type: string
shippeddate:
type: string
status:
type: string
comments:
type: string
customernumber:
type: integer
required:
- ordernumber
- orderdate
- requireddate
- status
- customernumber
- name: list-orderdetails
description: Retrieves a paginated list of order line items in the classic-models CRM.
annotations:
title: List all order line items
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/
- 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:
- name: ordernumber
in: path
required: true
description: The order's unique number.
schema: {}
- name: productcode
in: path
required: true
description: The product's unique code.
schema: {}
- name: create-orderdetail
description: Adds a new line item to an order.
annotations:
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:
allow:
- admin
- customer
- support
method: POST
scheme: http
host: ${REST_HOSTNAME}
path: /sales/v1/orderdetails/
request_body:
required: true
content:
application/json:
schema:
type: object
properties:
ordernumber:
type: integer
productcode:
type: string
quantityordered:
type: integer
priceeach:
type: string
orderlinenumber:
type: integer
required:
- ordernumber
- productcode
- quantityordered
- priceeach
- orderlinenumber