@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 +142 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
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
|
};
|