@fruggr/zendesk-mcp-server 2.10.0 → 2.11.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
@@ -1000,6 +1000,11 @@ const formatTranslation = (translation) => [
1000
1000
  ].join("\n");
1001
1001
  const formatCategory = (category) => `- **${category.name}** (${category.id}) — ${category.description || "No description"}`;
1002
1002
  const formatSection = (section) => `- **${section.name}** (${section.id}) — Category: ${section.category_id} — ${section.description || "No description"}`;
1003
+ const formatView = (view, count) => {
1004
+ const countText = count ? ` — ${count.pretty} ticket(s)${count.fresh ? "" : " (count updating)"}` : "";
1005
+ const description = view.description ? ` — ${view.description}` : "";
1006
+ return `- **${view.title}** (id ${view.id})${countText}${description}`;
1007
+ };
1003
1008
  const formatPermissionGroup = (group) => `- **${group.name}** (${group.id})${group.built_in ? " — Built-in" : ""}`;
1004
1009
  const formatContentTag = (tag) => `- **${tag.name}** (${tag.id})`;
1005
1010
  const formatLabel = (label) => `- **${label.name}** (${label.id})`;
@@ -2114,6 +2119,59 @@ const fetchTicketSla = async (subdomain, token, ticket) => {
2114
2119
  return;
2115
2120
  }
2116
2121
  };
2122
+ const VIEW_COUNT_BATCH = 20;
2123
+ const chunk = (items, size) => {
2124
+ const groups = [];
2125
+ for (let i = 0; i < items.length; i += size) groups.push(items.slice(i, i + size));
2126
+ return groups;
2127
+ };
2128
+ const fetchViewCounts = async (subdomain, token, viewIds) => {
2129
+ const counts = /* @__PURE__ */ new Map();
2130
+ for (const group of chunk(viewIds, VIEW_COUNT_BATCH)) try {
2131
+ const { view_counts } = await zendeskGet(subdomain, token, "/views/count_many", { ids: group.join(",") });
2132
+ for (const c of view_counts ?? []) counts.set(c.view_id, c);
2133
+ } catch {}
2134
+ return counts;
2135
+ };
2136
+ const resolveViewId = async (subdomain, token, view) => {
2137
+ if (typeof view === "number") return { id: view };
2138
+ const target = view.trim().toLowerCase();
2139
+ const available = [];
2140
+ let cursor;
2141
+ do {
2142
+ const response = await zendeskGet(subdomain, token, "/views", {
2143
+ active: "true",
2144
+ ...buildCursorParams(100, cursor)
2145
+ });
2146
+ const views = response.views ?? [];
2147
+ const match = views.find((v) => v.title.trim().toLowerCase() === target);
2148
+ if (match) return { id: match.id };
2149
+ available.push(...views.map((v) => v.title));
2150
+ cursor = response.meta?.has_more ? response.meta.after_cursor ?? void 0 : void 0;
2151
+ } while (cursor);
2152
+ return { available };
2153
+ };
2154
+ const extractRowTicketId = (row) => {
2155
+ if (typeof row.ticket?.id === "number") return row.ticket.id;
2156
+ for (const key of ["id", "ticket_id"]) if (typeof row[key] === "number") return row[key];
2157
+ };
2158
+ const executeView = async (subdomain, token, viewId, opts) => {
2159
+ const params = buildCursorParams(opts.page_size, opts.cursor);
2160
+ if (opts.sort_by) params["sort_by"] = opts.sort_by;
2161
+ if (opts.sort_order) params["sort_order"] = opts.sort_order;
2162
+ const response = await zendeskGet(subdomain, token, `/views/${viewId}/execute`, params);
2163
+ const rows = response.rows ?? [];
2164
+ return {
2165
+ rows,
2166
+ meta: extractPaginationMeta(response, rows.length)
2167
+ };
2168
+ };
2169
+ const hydrateViewTickets = async (subdomain, token, ids) => {
2170
+ if (ids.length === 0) return [];
2171
+ const { tickets } = await zendeskGet(subdomain, token, "/tickets/show_many", { ids: ids.join(",") });
2172
+ const byId = new Map((tickets ?? []).map((t) => [t.id, t]));
2173
+ return ids.map((id) => byId.get(id)).filter((t) => t !== void 0);
2174
+ };
2117
2175
  const DIFF_SKIP_KEYS = /* @__PURE__ */ new Set([
2118
2176
  "comment",
2119
2177
  "fields",
@@ -2610,6 +2668,88 @@ const createTicketTools = (ctx) => {
2610
2668
  }] };
2611
2669
  }
2612
2670
  },
2671
+ {
2672
+ name: "list_views",
2673
+ namespace: "tickets",
2674
+ readOnly: true,
2675
+ title: "List Zendesk Views",
2676
+ description: "List the agent's active Zendesk views — the saved ticket queues (\"Unassigned tickets\", \"My open tickets\", \"Breaching today\") the agent sees in the Zendesk UI — each with its current ticket count so you can tell at a glance where the workload sits. Views are per-agent scoped, so per-user auth returns exactly the queues this agent can see, with no shared key. Counts come from Zendesk's cache and can lag by up to about an hour (shown as \"(count updating)\" while a fresh value is still being computed); pass a view's title or id to get_view_tickets to read the tickets inside it.",
2677
+ inputSchema: z.object({
2678
+ page_size: z.number().int().min(1).max(100).default(100).describe("Views per page (1-100, default 100)."),
2679
+ cursor: z.string().optional().describe("Pagination cursor from a previous response; omit for the first page.")
2680
+ }),
2681
+ annotations: {
2682
+ readOnlyHint: true,
2683
+ destructiveHint: false,
2684
+ idempotentHint: true,
2685
+ openWorldHint: true
2686
+ },
2687
+ handler: async (params) => {
2688
+ const { page_size, cursor } = params;
2689
+ const token = await getToken();
2690
+ const response = await zendeskGet(subdomain, token, "/views", {
2691
+ active: "true",
2692
+ ...buildCursorParams(page_size, cursor)
2693
+ });
2694
+ const views = response.views ?? [];
2695
+ const counts = await fetchViewCounts(subdomain, token, views.map((v) => v.id));
2696
+ return { content: [{
2697
+ type: "text",
2698
+ text: formatList(views, (view) => formatView(view, counts.get(view.id)), extractPaginationMeta(response, views.length))
2699
+ }] };
2700
+ }
2701
+ },
2702
+ {
2703
+ name: "get_view_tickets",
2704
+ namespace: "tickets",
2705
+ readOnly: true,
2706
+ title: "Get Tickets In A View",
2707
+ description: "Read the tickets inside a Zendesk view, in the view's own configured sort order — the same order the agent sees in the Zendesk UI — which is the natural way to work a named queue like \"Unassigned tickets\" or \"Breaching today\". Accepts the view by title or by numeric id (discover both with list_views); a title is matched case-insensitively against the agent's active views, and on no match the available titles are returned so you can retry in one step. Tickets come back with the same fields as list_tickets and are cursor-paginated; there is no live SLA block here (use search_tickets when you need per-ticket SLA state), and sort_by/sort_order override the view's order when you want a different cut.",
2708
+ inputSchema: z.object({
2709
+ view: z.union([z.string().min(1), z.number().int().positive()]).describe("The view to read: its exact title as shown in Zendesk (e.g. \"Unassigned tickets\") or its numeric id from list_views. A title is matched case-insensitively against your active views; on no match the tool returns the available titles instead of erroring, so you can retry with a correct one."),
2710
+ sort_by: z.string().optional().describe("Optional column to sort by, overriding the view's own sort. Must be one of the view's columns (e.g. \"status\", \"priority\", \"updated_at\", or a custom field id); \"subject\" and \"submitter\" are not sortable. Omit to keep the view's configured order."),
2711
+ sort_order: z.enum(["asc", "desc"]).optional().describe("Sort direction applied to sort_by: \"asc\" (oldest/lowest first) or \"desc\" (newest/highest first). Only meaningful together with sort_by; omit to keep the view's configured direction."),
2712
+ page_size: z.number().int().min(1).max(100).default(100).describe("Tickets per page (1-100, default 100)."),
2713
+ cursor: z.string().optional().describe("Pagination cursor from a previous response; omit for the first page.")
2714
+ }),
2715
+ annotations: {
2716
+ readOnlyHint: true,
2717
+ destructiveHint: false,
2718
+ idempotentHint: true,
2719
+ openWorldHint: true
2720
+ },
2721
+ handler: async (params) => {
2722
+ const { view, sort_by, sort_order, page_size, cursor } = params;
2723
+ const token = await getToken();
2724
+ const resolved = await resolveViewId(subdomain, token, view);
2725
+ if ("available" in resolved) return { content: [{
2726
+ type: "text",
2727
+ text: resolved.available.length > 0 ? `No active view matches "${view}". Available views: ${resolved.available.join(", ")}.` : `No active view matches "${view}", and no active views were found for this agent.`
2728
+ }] };
2729
+ let rows;
2730
+ let meta;
2731
+ try {
2732
+ ({rows, meta} = await executeView(subdomain, token, resolved.id, {
2733
+ sort_by,
2734
+ sort_order,
2735
+ page_size,
2736
+ cursor
2737
+ }));
2738
+ } catch (error) {
2739
+ if (error instanceof ZendeskApiError && error.status === 403) throw new Error(`Access denied to view ${resolved.id} (HTTP 403). Zendesk views can be restricted to specific groups, and this agent is not allowed to read this one. Call list_views to see the queues available to this agent.`, { cause: error });
2740
+ throw error;
2741
+ }
2742
+ const ids = rows.map(extractRowTicketId).filter((id) => typeof id === "number");
2743
+ const tickets = await hydrateViewTickets(subdomain, token, ids);
2744
+ return { content: [{
2745
+ type: "text",
2746
+ text: formatList(tickets, formatTicket, {
2747
+ ...meta,
2748
+ count: tickets.length
2749
+ })
2750
+ }] };
2751
+ }
2752
+ },
2613
2753
  {
2614
2754
  name: "list_macros",
2615
2755
  namespace: "tickets",