Label ticket enums server-side so clients never guess the scale

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 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 13:06:48 -06:00
parent e0bf450e08
commit 829dbe1a3f

View File

@@ -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)] 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 --------------------------------------------------------------- # --- Tickets ---------------------------------------------------------------
@mcp.tool() @mcp.tool()
def list_tickets(query: str | None = None, unresolved: bool = False, 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: if query:
params = {"query": f'"{query}"', "workspace_id": workspace_id, params = {"query": f'"{query}"', "workspace_id": workspace_id,
"page": page, "per_page": min(per_page, 100)} "page": page, "per_page": min(per_page, 100)}
return _out(_request("GET", "/tickets/filter", params=params)) res = _request("GET", "/tickets/filter", params=params)
else:
params = {"updated_since": updated_since, "order_by": order_by, params = {"updated_since": updated_since, "order_by": order_by,
"order_type": order_type, "workspace_id": workspace_id, "order_type": order_type, "workspace_id": workspace_id,
"page": page, "per_page": min(per_page, 100)} "page": page, "per_page": min(per_page, 100)}
return _out(_request("GET", "/tickets", params=params)) 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() @mcp.tool()
@@ -271,9 +303,15 @@ def list_ticket_statuses() -> str:
@mcp.tool() @mcp.tool()
def get_ticket(ticket_id: int, include: str | None = None) -> str: 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 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() @mcp.tool()