Production-ready MCP server for SAP S/4HANA Sales Order Management (sandbox-first, offline-testable)
Four well-named tools with comprehensive descriptions and clear USE WHEN/DO NOT use guidance. All tools have complete input schemas with types and descriptions. Output schemas are documented in docstrings. Tool names follow verb_noun pattern (list_, get_, search_). Descriptions average ~250 chars, exceeding the 194-char baseline. Parameters include enums (status), ranges (top: 1-100), and defaults. Main gaps: no tool annotations (readOnlyHint/destructiveHint/idempotentHint), no error classification guidance in descriptions, and error handling returns generic ToolError objects without recovery hints. Pagination heuristic (hasMore = len(refs)==top) is fragile. No per-tool validation of created_since date format.
Fetch header data of a single SalesOrder by ID. Returns: { "salesOrder": str, "soldToParty": str, "soldToPartyName": str, "orderType": str, "netAmount": str, "taxAmount": str, "totalAmount": str, "currency": str, "status": str, "createdAt": str, "requestedDeliveryDate": str | null } Args: order_id: SAP SalesOrder ID (e.g. "1000001"). USE WHEN: user has a SalesOrder ID and asks about that specific order's header, status, customer, or amount. DO NOT use for: - Multiple orders / filtering → use `list_sales_orders`. - Item/line-item detail → use `get_sales_order_items`. - When you don't have the ID yet → `list_sales_orders` or `search_orders_by_customer` first. 404 (order not found) returns a `business` error — not retryable.
Fetch all line items (positions) of a SalesOrder. Returns: { "salesOrder": str, "itemCount": int, "items": [ {itemNumber, material, materialDescription, requestedQuantity, unit, netAmount, currency}, ... ] } Args: order_id: SAP SalesOrder ID (e.g. "1000001"). USE WHEN: user asks about "line items", "positions", "products", "quantities", or "materials" of a specific SalesOrder they've identified. DO NOT use if: - You don't have the SalesOrder ID → get it first via `get_sales_order` or `list_sales_orders`. - User wants order-level totals only → `get_sales_order` is enough. - User asks for items across multiple orders → out of scope; iterate `list_sales_orders` + this tool per order (be careful of rate limits). Empty item list is NOT an error (an order can have zero items). 404 on the order itself IS a business error.
List SalesOrders with optional filters. Returns a paginated result. Returns: { "count": int, "orders": [ {salesOrder, soldToParty, orderType, netAmount, currency, status, createdAt}, ... ], "pagination": {"top": int, "skip": int, "hasMore": bool} } Args: sold_to_party: SAP customer ID (e.g. "17100001"). Filters to that customer. order_type: SAP order type code (e.g. "OR" = standard, "RE" = returns). status: One of "open", "in_process", "completed", "blocked", "any" (default). created_since: ISO date "YYYY-MM-DD" — only orders created on/after this date. top: Max results per page (1-100, default 25). skip: Number of results to skip for pagination (default 0). USE WHEN: user asks for "all/multiple orders", "orders for customer X", "orders created since Y", or wants to browse/filter orders. DO NOT use for: - Single order by ID → use `get_sales_order` instead. - Line-item detail → use `get_sales_order_items` (needs order ID first). - Customer-lookup by name → out of scope for this server (this tool needs the customer ID, not the name). Empty result (no matching orders) is NOT an error — returns {"count": 0, ...}.
No tool annotations (readOnlyHint, destructiveHint, idempotentHint). All four tools are read-only but lack explicit annotation in schema. Agents cannot infer safety properties without annotations.
Error handling returns generic ToolError.to_dict() without recovery guidance. E.g., 404 on order_id returns business error but does not suggest 'Try list_sales_orders() to find the correct ID.' Agents cannot self-correct.
Pagination heuristic (hasMore = len(refs)==top) is fragile. If API returns fewer results than requested, hasMore will be false even if more results exist. Should use explicit next_cursor or total_count from API.
Inferred effective spec: <=2025-11-25.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 63 | <=2025-11-25 | v2 |
Convenience search: list a customer's orders, optionally filtered by status. Equivalent to `list_sales_orders(sold_to_party=customer_id, status=status, top=top)` but with a description tuned to the natural-language pattern "orders for customer X" / "customer Y's open/completed orders". Returns: { "customerId": str, "count": int, "orders": [ ...same shape as list_sales_orders... ] } Args: customer_id: SAP customer / SoldToParty ID (e.g. "17100001"). status: "open" | "in_process" | "completed" | "blocked" | "any" (default). top: Max results (1-100, default 25). USE WHEN: user asks "show me orders for customer X", "customer Y's open orders", "what has customer Z bought recently", or a natural-language query scoped to one specific customer ID. DO NOT use for: - Single order by ID → use `get_sales_order`. - Broad browsing without a specific customer → use `list_sales_orders`. - When customer is only known by name (not ID) → customer lookup is out of scope for this server; you need the SAP customer ID.
created_since parameter accepts ISO date string but no validation of format. Invalid dates like '2024-13-45' will pass schema validation and fail at runtime. Add regex pattern or explicit format constraint.
search_orders_by_customer is a convenience wrapper around list_sales_orders(sold_to_party=customer_id). Agents may conflate the two tools, wasting reasoning cycles. Consider consolidating or clearly documenting the distinction.