@fruggr/zendesk-mcp-server 2.9.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -909,6 +909,24 @@ const formatTicketField = (field) => {
909
909
  ...options.map((o) => ` - ${o.name} → ${o.value}`)
910
910
  ].filter(Boolean).join("\n");
911
911
  };
912
+ const formatFieldValue = (value) => Array.isArray(value) ? value.map(formatFieldValue).join(", ") : formatConditionValue(value);
913
+ const MACRO_VALUE_PREVIEW = 120;
914
+ const formatMacroActionValue = (value) => {
915
+ const oneLine = formatFieldValue(value).replace(/\s+/g, " ").trim();
916
+ return oneLine.length > MACRO_VALUE_PREVIEW ? `${oneLine.slice(0, MACRO_VALUE_PREVIEW)}…` : oneLine;
917
+ };
918
+ const formatMacroAction = (action) => ` - ${action.field} → ${formatMacroActionValue(action.value)}`;
919
+ const formatMacro = (macro) => {
920
+ const scope = macro.restriction?.type ? "restricted" : "shared";
921
+ const actions = macro.actions ?? [];
922
+ return [
923
+ `## ${macro.title} (id ${macro.id})`,
924
+ `- **${macro.active ? "active" : "inactive"}** | **Scope**: ${scope}`,
925
+ macro.description ? `- **Description**: ${macro.description}` : "",
926
+ actions.length > 0 ? "- **Actions**:" : "- **Actions**: none",
927
+ ...actions.map(formatMacroAction)
928
+ ].filter(Boolean).join("\n");
929
+ };
912
930
  const minutesUntil = (iso) => {
913
931
  const t = Date.parse(iso);
914
932
  return Number.isNaN(t) ? null : Math.round((t - Date.now()) / 6e4);
@@ -1010,6 +1028,11 @@ const extractPaginationMeta = (response, itemCount) => ({
1010
1028
  after_cursor: response.meta?.after_cursor ?? null,
1011
1029
  count: response.count ?? itemCount
1012
1030
  });
1031
+ const extractOffsetPaginationMeta = (response, itemCount, perPage, page) => response.count != null ? extractSearchPaginationMeta(response, perPage, page) : {
1032
+ count: itemCount,
1033
+ has_more: false,
1034
+ after_cursor: null
1035
+ };
1013
1036
  const extractSearchPaginationMeta = (response, perPage, page) => {
1014
1037
  const count = response.count ?? 0;
1015
1038
  const has_more = count > page * perPage;
@@ -2091,6 +2114,71 @@ const fetchTicketSla = async (subdomain, token, ticket) => {
2091
2114
  return;
2092
2115
  }
2093
2116
  };
2117
+ const DIFF_SKIP_KEYS = /* @__PURE__ */ new Set([
2118
+ "comment",
2119
+ "fields",
2120
+ "custom_fields",
2121
+ "id",
2122
+ "url",
2123
+ "created_at",
2124
+ "updated_at",
2125
+ "generated_timestamp",
2126
+ "encoded_id"
2127
+ ]);
2128
+ const valuesEqual = (a, b) => a === b || JSON.stringify(a) === JSON.stringify(b);
2129
+ const shownValue = (v) => {
2130
+ const s = formatFieldValue(v);
2131
+ return s === "" ? "(empty)" : s;
2132
+ };
2133
+ const diffLine = (label, before, after) => {
2134
+ const b = shownValue(before);
2135
+ const a = shownValue(after);
2136
+ return b === a ? null : `- **${label}**: ${b} → ${a}`;
2137
+ };
2138
+ const formatTagDiff = (before, after) => {
2139
+ const b = new Set(Array.isArray(before) ? before.map(String) : []);
2140
+ const a = new Set(Array.isArray(after) ? after.map(String) : []);
2141
+ const added = [...a].filter((t) => !b.has(t)).map((t) => `+${t}`);
2142
+ const removed = [...b].filter((t) => !a.has(t)).map((t) => `-${t}`);
2143
+ return added.length + removed.length === 0 ? null : `- **tags**: ${[...added, ...removed].join(", ")}`;
2144
+ };
2145
+ const formatMacroPreviewDiff = (ticketId, macroId, before, result) => {
2146
+ const after = result?.ticket ?? {};
2147
+ const beforeObj = before ?? {};
2148
+ const comment = after.comment ?? result?.comment;
2149
+ const changes = [];
2150
+ for (const [key, afterVal] of Object.entries(after)) {
2151
+ if (DIFF_SKIP_KEYS.has(key)) continue;
2152
+ const beforeVal = beforeObj[key];
2153
+ if (valuesEqual(beforeVal, afterVal)) continue;
2154
+ if (key === "tags") {
2155
+ const tagLine = formatTagDiff(beforeVal, afterVal);
2156
+ if (tagLine) changes.push(tagLine);
2157
+ continue;
2158
+ }
2159
+ if (afterVal !== null && typeof afterVal === "object" && !Array.isArray(afterVal)) continue;
2160
+ const line = diffLine(key, beforeVal, afterVal);
2161
+ if (line) changes.push(line);
2162
+ }
2163
+ const afterFields = [after.fields ?? after.custom_fields ?? []].flat();
2164
+ const beforeById = new Map((before?.custom_fields ?? []).map((f) => [f.id, f.value]));
2165
+ for (const f of afterFields) {
2166
+ const line = diffLine(`custom field ${f.id}`, beforeById.get(f.id), f.value);
2167
+ if (line) changes.push(line);
2168
+ }
2169
+ const lines = [
2170
+ `# Macro #${macroId} preview on ticket #${ticketId} (diff — nothing saved yet)`,
2171
+ "",
2172
+ "## Field changes",
2173
+ ...changes.length > 0 ? changes : ["- none"]
2174
+ ];
2175
+ if (comment?.body) {
2176
+ const visibility = comment.public === false ? "internal note" : "public comment";
2177
+ lines.push("", `## Reply (${visibility})`, "", comment.body);
2178
+ } else lines.push("", "## Reply", "- none");
2179
+ lines.push("", "## To apply these changes", "Nothing has been committed. Persist the field changes with `update_ticket` (or `manage_tags` for incremental tag edits), and post the reply with `add_public_comment` (public) or `add_private_note` (internal). Edit the reply text first if needed.");
2180
+ return lines.join("\n");
2181
+ };
2094
2182
  const createTicketTools = (ctx) => {
2095
2183
  const { subdomain, getToken } = ctx;
2096
2184
  const attachmentSchema = z.object({
@@ -2491,11 +2579,7 @@ const createTicketTools = (ctx) => {
2491
2579
  const policies = response.sla_policies ?? [];
2492
2580
  return { content: [{
2493
2581
  type: "text",
2494
- text: formatList(policies, formatSlaPolicy, response.count != null ? extractSearchPaginationMeta(response, per_page, page) : {
2495
- count: policies.length,
2496
- has_more: false,
2497
- after_cursor: null
2498
- })
2582
+ text: formatList(policies, formatSlaPolicy, extractOffsetPaginationMeta(response, policies.length, per_page, page))
2499
2583
  }] };
2500
2584
  }
2501
2585
  },
@@ -2525,6 +2609,59 @@ const createTicketTools = (ctx) => {
2525
2609
  text: formatList(fields, formatTicketField, extractPaginationMeta(response, fields.length))
2526
2610
  }] };
2527
2611
  }
2612
+ },
2613
+ {
2614
+ name: "list_macros",
2615
+ namespace: "tickets",
2616
+ readOnly: true,
2617
+ title: "List Zendesk Macros",
2618
+ description: "List the active macros available to the authenticated user. A macro bundles a canned reply and/or a set of field changes (status, priority, assignee, group, tags, custom fields) an agent applies to a ticket in one gesture; this returns each macro id, title, description, availability scope, and its ordered list of actions, offset-paginated. Results are scoped by per-user OAuth to what the current user can see, so no shared admin key is needed. Pass a macro id from here to preview_macro_diff to preview its effect on a specific ticket.",
2619
+ inputSchema: z.object({
2620
+ per_page: z.number().int().min(1).max(100).default(100).describe(PER_PAGE_DESC),
2621
+ page: z.number().int().min(1).default(1).describe(PAGE_DESC)
2622
+ }),
2623
+ annotations: {
2624
+ readOnlyHint: true,
2625
+ destructiveHint: false,
2626
+ idempotentHint: true,
2627
+ openWorldHint: true
2628
+ },
2629
+ handler: async (params) => {
2630
+ const { per_page, page } = params;
2631
+ const token = await getToken();
2632
+ const response = await zendeskGet(subdomain, token, "/macros/active", buildOffsetParams(per_page, page));
2633
+ const macros = response.macros ?? [];
2634
+ return { content: [{
2635
+ type: "text",
2636
+ text: formatList(macros, formatMacro, extractOffsetPaginationMeta(response, macros.length, per_page, page))
2637
+ }] };
2638
+ }
2639
+ },
2640
+ {
2641
+ name: "preview_macro_diff",
2642
+ namespace: "tickets",
2643
+ readOnly: false,
2644
+ title: "Preview a Macro Diff on a Ticket",
2645
+ description: "Preview the exact changes a macro would make to a specific ticket, as a before → after diff, WITHOUT saving anything. Orchestrates two reads — the ticket's current state and Zendesk's macro-apply preview (which returns the whole resulting ticket) — and returns only the fields the macro actually changes (status, priority, assignee, group, tags, custom fields) plus the canned reply with its public/internal flag; unchanged and identity fields are omitted. Nothing is committed: to apply it, follow up with update_ticket for the field changes and add_public_comment or add_private_note for the reply. This deliberate two-step keeps the mutation explicit and reviewable rather than hidden. Find macro ids via list_macros and the ticket id via search_tickets or list_tickets.",
2646
+ inputSchema: z.object({
2647
+ ticket_id: z.number().int().describe("Ticket ID — the numeric id of the ticket to preview the macro against. Obtain it from search_tickets or list_tickets."),
2648
+ macro_id: z.number().int().describe("Macro ID — the numeric id of the macro to preview. Obtain it from list_macros.")
2649
+ }),
2650
+ annotations: {
2651
+ readOnlyHint: false,
2652
+ destructiveHint: false,
2653
+ idempotentHint: true,
2654
+ openWorldHint: true
2655
+ },
2656
+ handler: async (params) => {
2657
+ const { ticket_id, macro_id } = params;
2658
+ const token = await getToken();
2659
+ const [{ ticket: before }, { result }] = await Promise.all([zendeskGet(subdomain, token, `/tickets/${ticket_id}`), zendeskGet(subdomain, token, `/tickets/${ticket_id}/macros/${macro_id}/apply`)]);
2660
+ return { content: [{
2661
+ type: "text",
2662
+ text: truncateIfNeeded(formatMacroPreviewDiff(ticket_id, macro_id, before, result))
2663
+ }] };
2664
+ }
2528
2665
  }
2529
2666
  ];
2530
2667
  };