From 829dbe1a3f18910e902366ce1f4c490541fc5272 Mon Sep 17 00:00:00 2001 From: spencerm Date: Mon, 24 Aug 2026 13:06:48 -0600 Subject: [PATCH] Label ticket enums server-side so clients never guess the scale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A webhook consumer read priority 4 off a ticket and called it 'Low (4)' in a posted note — inverting Freshservice's scale (4=Urgent). Add *_label companions (priority, status, impact, urgency) to every ticket returned by get_ticket and list_tickets; status labels resolve through the live instance status list, so custom statuses label correctly too. Co-Authored-By: Claude Fable 5 --- mcp-server/server.py | 52 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/mcp-server/server.py b/mcp-server/server.py index 6cc857c..f867a0a 100644 --- a/mcp-server/server.py +++ b/mcp-server/server.py @@ -204,6 +204,33 @@ def _open_status_ids() -> list[int]: return [c["id"] for c in _status_choices() if c.get("id") is not None and not _is_terminal(c)] +# Impact/urgency are fixed 1-3 scales in Freshservice (priority is fixed 1-4). +IMPACT_URGENCY = {1: "Low", 2: "Medium", 3: "High"} + + +def _label_ticket(t: Any) -> Any: + """Add *_label fields next to a ticket's numeric enums (priority, status, + impact, urgency) so no consumer ever guesses the scale. A weak model once + read priority 4 and called it "Low (4)" — Urgent — in a customer-visible + note; labels in the data remove that entire error class.""" + if not isinstance(t, dict): + return t + p = t.get("priority") + if p in TICKET_PRIORITY: + t["priority_label"] = TICKET_PRIORITY[p] + for field in ("impact", "urgency"): + v = t.get(field) + if v in IMPACT_URGENCY: + t[f"{field}_label"] = IMPACT_URGENCY[v] + s = t.get("status") + if isinstance(s, int) and "status_name" not in t: + label = next((c.get("value") for c in _status_choices() if c.get("id") == s), + None) or TICKET_STATUS.get(s) + if label: + t["status_label"] = label + return t + + # --- Tickets --------------------------------------------------------------- @mcp.tool() def list_tickets(query: str | None = None, unresolved: bool = False, @@ -247,11 +274,16 @@ def list_tickets(query: str | None = None, unresolved: bool = False, if query: params = {"query": f'"{query}"', "workspace_id": workspace_id, "page": page, "per_page": min(per_page, 100)} - return _out(_request("GET", "/tickets/filter", params=params)) - params = {"updated_since": updated_since, "order_by": order_by, - "order_type": order_type, "workspace_id": workspace_id, - "page": page, "per_page": min(per_page, 100)} - return _out(_request("GET", "/tickets", params=params)) + res = _request("GET", "/tickets/filter", params=params) + else: + params = {"updated_since": updated_since, "order_by": order_by, + "order_type": order_type, "workspace_id": workspace_id, + "page": page, "per_page": min(per_page, 100)} + res = _request("GET", "/tickets", params=params) + if res.get("ok"): + for t in res["data"].get("tickets", []): + _label_ticket(t) + return _out(res) @mcp.tool() @@ -271,9 +303,15 @@ def list_ticket_statuses() -> str: @mcp.tool() def get_ticket(ticket_id: int, include: str | None = None) -> str: - """Get a ticket by id. `include` e.g. "conversations,requester,stats".""" + """Get a ticket by id. `include` e.g. "conversations,requester,stats". + Numeric enums come back with companion *_label fields (priority_label, + status_label, impact_label, urgency_label) — use those names, never guess + the scale (priority 4 is Urgent, not low).""" params = {"include": include} if include else None - return _out(_request("GET", f"/tickets/{ticket_id}", params=params)) + res = _request("GET", f"/tickets/{ticket_id}", params=params) + if res.get("ok"): + _label_ticket(res["data"].get("ticket")) + return _out(res) @mcp.tool()