@fruggr/zendesk-mcp-server 2.7.0 → 2.9.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/README.md CHANGED
@@ -9,10 +9,10 @@
9
9
  [![semantic-release](https://img.shields.io/badge/semantic--release-e10079?logo=semantic-release&logoColor=white)](https://github.com/semantic-release/semantic-release)
10
10
 
11
11
  **Bring Zendesk deep into your AI assistant.** A
12
- [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for a
13
- two-way integration: find answers in the Help Center, **draft, update and
14
- translate** articles (keeping languages in sync), and **manage Support tickets**
15
- end to end — comments, triage and image attachments — all in plain language,
12
+ [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server: find
13
+ answers in the Help Center, **draft, update and translate** articles (keeping
14
+ languages in sync), and **manage Support tickets** end to end — comments, triage
15
+ and image attachments — all in plain language,
16
16
  **without switching apps**.
17
17
 
18
18
  Think of it as the [Zendesk agent for Microsoft 365 Copilot](https://support.zendesk.com/hc/en-us/articles/9958331458458-Using-the-Zendesk-agent-in-Microsoft-365-Copilot),
package/dist/index.js CHANGED
@@ -261,14 +261,15 @@ const startBrowserAuth = (config, logger = silentLogger) => {
261
261
  });
262
262
  const port = callbackServer.address().port;
263
263
  const redirectUri = `http://localhost:${port}/callback`;
264
- const authUrl = `${authorizeBase}?${new URLSearchParams({
264
+ const params = new URLSearchParams({
265
265
  response_type: "code",
266
266
  client_id: oauthClientId,
267
267
  redirect_uri: redirectUri,
268
268
  scope: "read write",
269
269
  code_challenge: codeChallenge,
270
270
  code_challenge_method: "S256"
271
- }).toString()}`;
271
+ });
272
+ const authUrl = `${authorizeBase}?${params.toString()}`;
272
273
  logger.debug("oauth_callback_listening", {
273
274
  port,
274
275
  redirectUri
@@ -533,7 +534,8 @@ const createTokenStore = (config, logger = silentLogger) => {
533
534
  if (refreshed) return refreshed;
534
535
  }
535
536
  if (!starting) starting = beginAuth();
536
- throw createAuthRequiredError(authorizeUrl ?? await starting);
537
+ const url = authorizeUrl ?? await starting;
538
+ throw createAuthRequiredError(url);
537
539
  };
538
540
  const invalidate = () => {
539
541
  if (token?.refreshToken) {
@@ -746,35 +748,45 @@ const executeRequest = async (url, token, options = {}) => {
746
748
  return response.json();
747
749
  };
748
750
  const zendeskGet = (subdomain, token, path, params) => {
749
- return executeRequest(buildUrl(getBaseUrl(subdomain), path, params), token);
751
+ const url = buildUrl(getBaseUrl(subdomain), path, params);
752
+ return executeRequest(url, token);
750
753
  };
751
754
  const zendeskPost = (subdomain, token, path, body) => {
752
- return executeRequest(buildUrl(getBaseUrl(subdomain), path), token, {
755
+ const url = buildUrl(getBaseUrl(subdomain), path);
756
+ return executeRequest(url, token, {
753
757
  method: "POST",
754
758
  body
755
759
  });
756
760
  };
757
761
  const zendeskPut = (subdomain, token, path, body) => {
758
- return executeRequest(buildUrl(getBaseUrl(subdomain), path), token, {
762
+ const url = buildUrl(getBaseUrl(subdomain), path);
763
+ return executeRequest(url, token, {
759
764
  method: "PUT",
760
765
  body
761
766
  });
762
767
  };
763
768
  const helpCenterGet = (subdomain, token, path, params) => {
764
- return executeRequest(buildUrl(getHelpCenterBaseUrl(subdomain), path, params), token);
769
+ const url = buildUrl(getHelpCenterBaseUrl(subdomain), path, params);
770
+ return executeRequest(url, token);
765
771
  };
766
772
  const helpCenterPost = (subdomain, token, path, body) => {
767
- return executeRequest(buildUrl(getHelpCenterBaseUrl(subdomain), path), token, {
773
+ const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
774
+ return executeRequest(url, token, {
768
775
  method: "POST",
769
776
  body
770
777
  });
771
778
  };
772
779
  const helpCenterPut = (subdomain, token, path, body) => {
773
- return executeRequest(buildUrl(getHelpCenterBaseUrl(subdomain), path), token, {
780
+ const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
781
+ return executeRequest(url, token, {
774
782
  method: "PUT",
775
783
  body
776
784
  });
777
785
  };
786
+ const helpCenterDelete = (subdomain, token, path) => {
787
+ const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
788
+ return executeRequest(url, token, { method: "DELETE" });
789
+ };
778
790
  const fetchZendeskBinary = async (subdomain, token, contentUrl) => {
779
791
  const expectedHost = `${subdomain}.zendesk.com`;
780
792
  const headers = {};
@@ -885,6 +897,18 @@ const formatSlaPolicy = (policy) => {
885
897
  ...targets
886
898
  ].filter(Boolean).join("\n");
887
899
  };
900
+ const formatTicketField = (field) => {
901
+ const flags = [field.active ? "active" : "inactive", field.required ? "required" : "optional"].join(", ");
902
+ const options = field.custom_field_options ?? field.system_field_options ?? [];
903
+ return [
904
+ `## ${field.title} (id ${field.id})`,
905
+ `- **Type**: ${field.type} | **${flags}**`,
906
+ field.description ? `- **Description**: ${field.description}` : "",
907
+ field.tag ? `- **Tag**: ${field.tag}` : "",
908
+ options.length > 0 ? "- **Options** (name → value):" : "",
909
+ ...options.map((o) => ` - ${o.name} → ${o.value}`)
910
+ ].filter(Boolean).join("\n");
911
+ };
888
912
  const minutesUntil = (iso) => {
889
913
  const t = Date.parse(iso);
890
914
  return Number.isNaN(t) ? null : Math.round((t - Date.now()) / 6e4);
@@ -964,7 +988,8 @@ const formatLabel = (label) => `- **${label.name}** (${label.id})`;
964
988
  const formatUserSegment = (segment) => `- **${segment.name}** (${segment.id}) — ${segment.user_type}${segment.built_in ? " — Built-in" : ""}`;
965
989
  const formatAttachment = (attachment) => `- **${attachment.file_name}** (${attachment.id}) — ${attachment.content_type} — ${attachment.size} bytes`;
966
990
  const formatList = (items, formatter, meta) => {
967
- return truncateIfNeeded([meta ? formatPagination(meta) : "", items.map(formatter).join("\n\n")].filter(Boolean).join("\n\n"));
991
+ const text = [meta ? formatPagination(meta) : "", items.map(formatter).join("\n\n")].filter(Boolean).join("\n\n");
992
+ return truncateIfNeeded(text);
968
993
  };
969
994
  //#endregion
970
995
  //#region src/utils/pagination.ts
@@ -1090,7 +1115,8 @@ const createTopologyProvider = (getToken, subdomain, onUnauthorized) => {
1090
1115
  const now = Date.now();
1091
1116
  if (cached && now - cached.at < 3e5) return cached.promise;
1092
1117
  const promise = (async () => {
1093
- return formatTopology(await fetchTopology(subdomain, await getToken()));
1118
+ const token = await getToken();
1119
+ return formatTopology(await fetchTopology(subdomain, token));
1094
1120
  })().catch((err) => {
1095
1121
  cached = void 0;
1096
1122
  if (onUnauthorized && err instanceof ZendeskApiError && err.status === 401) onUnauthorized();
@@ -1282,7 +1308,8 @@ const createHelpCenterTools = (ctx) => {
1282
1308
  handler: async (params) => {
1283
1309
  const { article_id, locale } = params;
1284
1310
  const token = await getToken();
1285
- const { article } = await helpCenterGet(subdomain, token, locale ? `/${locale}/articles/${article_id}` : `/articles/${article_id}`);
1311
+ const path = locale ? `/${locale}/articles/${article_id}` : `/articles/${article_id}`;
1312
+ const { article } = await helpCenterGet(subdomain, token, path);
1286
1313
  const { translations } = await helpCenterGet(subdomain, token, `/articles/${article_id}/translations`);
1287
1314
  return { content: [{
1288
1315
  type: "text",
@@ -1309,7 +1336,9 @@ const createHelpCenterTools = (ctx) => {
1309
1336
  },
1310
1337
  handler: async (params) => {
1311
1338
  const { locale, page_size, cursor } = params;
1312
- const response = await helpCenterGet(subdomain, await getToken(), locale ? `/${locale}/categories` : "/categories", buildCursorParams(page_size, cursor));
1339
+ const token = await getToken();
1340
+ const path = locale ? `/${locale}/categories` : "/categories";
1341
+ const response = await helpCenterGet(subdomain, token, path, buildCursorParams(page_size, cursor));
1313
1342
  const categories = response.categories ?? [];
1314
1343
  return { content: [{
1315
1344
  type: "text",
@@ -1337,7 +1366,9 @@ const createHelpCenterTools = (ctx) => {
1337
1366
  },
1338
1367
  handler: async (params) => {
1339
1368
  const { category_id, locale, page_size, cursor } = params;
1340
- const response = await helpCenterGet(subdomain, await getToken(), category_id && locale ? `/${locale}/categories/${category_id}/sections` : category_id ? `/categories/${category_id}/sections` : locale ? `/${locale}/sections` : "/sections", buildCursorParams(page_size, cursor));
1369
+ const token = await getToken();
1370
+ const path = category_id && locale ? `/${locale}/categories/${category_id}/sections` : category_id ? `/categories/${category_id}/sections` : locale ? `/${locale}/sections` : "/sections";
1371
+ const response = await helpCenterGet(subdomain, token, path, buildCursorParams(page_size, cursor));
1341
1372
  const sections = response.sections ?? [];
1342
1373
  return { content: [{
1343
1374
  type: "text",
@@ -1374,7 +1405,8 @@ const createHelpCenterTools = (ctx) => {
1374
1405
  handler: async (params) => {
1375
1406
  const { section_id, locale, page_size, cursor, sort_by, sort_order, include_translations } = params;
1376
1407
  const token = await getToken();
1377
- const response = await helpCenterGet(subdomain, token, section_id && locale ? `/${locale}/sections/${section_id}/articles` : section_id ? `/sections/${section_id}/articles` : locale ? `/${locale}/articles` : "/articles", {
1408
+ const path = section_id && locale ? `/${locale}/sections/${section_id}/articles` : section_id ? `/sections/${section_id}/articles` : locale ? `/${locale}/articles` : "/articles";
1409
+ const response = await helpCenterGet(subdomain, token, path, {
1378
1410
  ...buildCursorParams(page_size, cursor),
1379
1411
  sort_by,
1380
1412
  sort_order
@@ -1411,7 +1443,8 @@ const createHelpCenterTools = (ctx) => {
1411
1443
  },
1412
1444
  handler: async (params) => {
1413
1445
  const { article_id } = params;
1414
- const { translations } = await helpCenterGet(subdomain, await getToken(), `/articles/${article_id}/translations`);
1446
+ const token = await getToken();
1447
+ const { translations } = await helpCenterGet(subdomain, token, `/articles/${article_id}/translations`);
1415
1448
  return { content: [{
1416
1449
  type: "text",
1417
1450
  text: formatList(translations, formatTranslationSummary)
@@ -1439,7 +1472,8 @@ const createHelpCenterTools = (ctx) => {
1439
1472
  },
1440
1473
  handler: async (params) => {
1441
1474
  const { article_id, locale, title, body, draft } = params;
1442
- const { translation } = await helpCenterPost(subdomain, await getToken(), `/articles/${article_id}/translations`, { translation: {
1475
+ const token = await getToken();
1476
+ const { translation } = await helpCenterPost(subdomain, token, `/articles/${article_id}/translations`, { translation: {
1443
1477
  locale,
1444
1478
  title,
1445
1479
  body,
@@ -1472,7 +1506,8 @@ const createHelpCenterTools = (ctx) => {
1472
1506
  },
1473
1507
  handler: async (params) => {
1474
1508
  const { article_id, locale, ...updates } = params;
1475
- const { translation } = await helpCenterPut(subdomain, await getToken(), `/articles/${article_id}/translations/${locale}`, { translation: updates });
1509
+ const token = await getToken();
1510
+ const { translation } = await helpCenterPut(subdomain, token, `/articles/${article_id}/translations/${locale}`, { translation: updates });
1476
1511
  return { content: [{
1477
1512
  type: "text",
1478
1513
  text: `Translation updated for article #${article_id} in "${locale}".\n\n${formatTranslation(translation)}`
@@ -1493,9 +1528,10 @@ const createHelpCenterTools = (ctx) => {
1493
1528
  openWorldHint: true
1494
1529
  },
1495
1530
  handler: async () => {
1531
+ const token = await getToken();
1496
1532
  return { content: [{
1497
1533
  type: "text",
1498
- text: formatList((await zendeskGet(subdomain, await getToken(), "/guide/permission_groups")).permission_groups ?? [], formatPermissionGroup)
1534
+ text: formatList((await zendeskGet(subdomain, token, "/guide/permission_groups")).permission_groups ?? [], formatPermissionGroup)
1499
1535
  }] };
1500
1536
  }
1501
1537
  },
@@ -1526,7 +1562,8 @@ const createHelpCenterTools = (ctx) => {
1526
1562
  },
1527
1563
  handler: async (params) => {
1528
1564
  const { section_id, ...articleData } = params;
1529
- const { article } = await helpCenterPost(subdomain, await getToken(), `/sections/${section_id}/articles`, { article: articleData });
1565
+ const token = await getToken();
1566
+ const { article } = await helpCenterPost(subdomain, token, `/sections/${section_id}/articles`, { article: articleData });
1530
1567
  return { content: [{
1531
1568
  type: "text",
1532
1569
  text: `Article #${article.id} created.\n\n${formatArticle(article)}`
@@ -1559,13 +1596,41 @@ const createHelpCenterTools = (ctx) => {
1559
1596
  },
1560
1597
  handler: async (params) => {
1561
1598
  const { article_id, ...updates } = params;
1562
- const { article } = await helpCenterPut(subdomain, await getToken(), `/articles/${article_id}`, { article: updates });
1599
+ const token = await getToken();
1600
+ const { article } = await helpCenterPut(subdomain, token, `/articles/${article_id}`, { article: updates });
1563
1601
  return { content: [{
1564
1602
  type: "text",
1565
1603
  text: `Article #${article.id} updated.\n\n${formatArticle(article)}`
1566
1604
  }] };
1567
1605
  }
1568
1606
  },
1607
+ {
1608
+ name: "archive_article",
1609
+ namespace: "help_center",
1610
+ readOnly: false,
1611
+ title: "Archive Help Center Article",
1612
+ description: "Archive (soft-delete) a Help Center article: it is removed from the Help Center but can be restored from the Guide admin UI. Returns a confirmation message; the article and all its translations become invisible to end users. This is the only removal the Zendesk API offers — permanent deletion is not available via the API (do it from the Guide admin UI). To only hide an article temporarily while keeping it in the knowledge base, use update_article with draft: true (unpublish) instead. Guarded by a required confirm flag.",
1613
+ inputSchema: z.object({
1614
+ article_id: z.number().int().describe(ARTICLE_ID_DESC),
1615
+ confirm: z.boolean().describe("Explicit safety guard: must be set to true to archive the article. Any other value refuses the operation without calling Zendesk.")
1616
+ }),
1617
+ annotations: {
1618
+ readOnlyHint: false,
1619
+ destructiveHint: true,
1620
+ idempotentHint: true,
1621
+ openWorldHint: true
1622
+ },
1623
+ handler: async (params) => {
1624
+ const { article_id, confirm } = params;
1625
+ if (confirm !== true) throw new Error("Archiving is guarded: pass confirm: true to archive (soft-delete) this article. No changes were made.");
1626
+ const token = await getToken();
1627
+ await helpCenterDelete(subdomain, token, `/articles/${article_id}`);
1628
+ return { content: [{
1629
+ type: "text",
1630
+ text: `Article #${article_id} archived (soft-deleted). It is removed from the Help Center; restore it from the Guide admin UI if needed.`
1631
+ }] };
1632
+ }
1633
+ },
1569
1634
  {
1570
1635
  name: "list_content_tags",
1571
1636
  namespace: "help_center",
@@ -1617,7 +1682,8 @@ const createHelpCenterTools = (ctx) => {
1617
1682
  },
1618
1683
  handler: async (params) => {
1619
1684
  const { name } = params;
1620
- const { content_tag } = await zendeskPost(subdomain, await getToken(), "/guide/content_tags", { content_tag: { name } });
1685
+ const token = await getToken();
1686
+ const { content_tag } = await zendeskPost(subdomain, token, "/guide/content_tags", { content_tag: { name } });
1621
1687
  return { content: [{
1622
1688
  type: "text",
1623
1689
  text: `Content tag created.\n\n${formatContentTag(content_tag)}`
@@ -1638,9 +1704,10 @@ const createHelpCenterTools = (ctx) => {
1638
1704
  openWorldHint: true
1639
1705
  },
1640
1706
  handler: async () => {
1707
+ const token = await getToken();
1641
1708
  return { content: [{
1642
1709
  type: "text",
1643
- text: formatList((await helpCenterGet(subdomain, await getToken(), "/articles/labels")).labels ?? [], formatLabel)
1710
+ text: formatList((await helpCenterGet(subdomain, token, "/articles/labels")).labels ?? [], formatLabel)
1644
1711
  }] };
1645
1712
  }
1646
1713
  },
@@ -1658,9 +1725,10 @@ const createHelpCenterTools = (ctx) => {
1658
1725
  openWorldHint: true
1659
1726
  },
1660
1727
  handler: async () => {
1728
+ const token = await getToken();
1661
1729
  return { content: [{
1662
1730
  type: "text",
1663
- text: formatList((await helpCenterGet(subdomain, await getToken(), "/user_segments")).user_segments ?? [], formatUserSegment)
1731
+ text: formatList((await helpCenterGet(subdomain, token, "/user_segments")).user_segments ?? [], formatUserSegment)
1664
1732
  }] };
1665
1733
  }
1666
1734
  },
@@ -1679,7 +1747,8 @@ const createHelpCenterTools = (ctx) => {
1679
1747
  },
1680
1748
  handler: async (params) => {
1681
1749
  const { article_id } = params;
1682
- const attachments = (await helpCenterGet(subdomain, await getToken(), `/articles/${article_id}/attachments`)).article_attachments ?? [];
1750
+ const token = await getToken();
1751
+ const attachments = (await helpCenterGet(subdomain, token, `/articles/${article_id}/attachments`)).article_attachments ?? [];
1683
1752
  if (attachments.length === 0) return { content: [{
1684
1753
  type: "text",
1685
1754
  text: `No attachments found on article #${article_id}.`
@@ -1751,7 +1820,8 @@ const createHelpCenterTools = (ctx) => {
1751
1820
  },
1752
1821
  handler: async (params) => {
1753
1822
  const { article_id, locale, section_index, format } = params;
1754
- const { translation } = await helpCenterGet(subdomain, await getToken(), `/articles/${article_id}/translations/${locale}`);
1823
+ const token = await getToken();
1824
+ const { translation } = await helpCenterGet(subdomain, token, `/articles/${article_id}/translations/${locale}`);
1755
1825
  const sections = parseSections(translation.body);
1756
1826
  const section = sections[section_index];
1757
1827
  if (!section) throw new Error(`Section index ${section_index} not found. Article has ${sections.length} section(s) (0-${Math.max(0, sections.length - 1)}).`);
@@ -1923,7 +1993,8 @@ const createSearchTools = (ctx) => {
1923
1993
  },
1924
1994
  handler: async (params) => {
1925
1995
  const { query, per_page, page } = params;
1926
- const response = await zendeskGet(subdomain, await getToken(), "/search", {
1996
+ const token = await getToken();
1997
+ const response = await zendeskGet(subdomain, token, "/search", {
1927
1998
  query,
1928
1999
  ...buildOffsetParams(per_page, page)
1929
2000
  });
@@ -2127,7 +2198,8 @@ const createTicketTools = (ctx) => {
2127
2198
  },
2128
2199
  handler: async (params) => {
2129
2200
  const { query, per_page, page } = params;
2130
- const response = await zendeskGet(subdomain, await getToken(), "/search", {
2201
+ const token = await getToken();
2202
+ const response = await zendeskGet(subdomain, token, "/search", {
2131
2203
  query: `type:ticket ${query}`,
2132
2204
  include: "tickets(slas)",
2133
2205
  ...buildOffsetParams(per_page, page)
@@ -2144,7 +2216,7 @@ const createTicketTools = (ctx) => {
2144
2216
  namespace: "tickets",
2145
2217
  readOnly: false,
2146
2218
  title: "Create Zendesk Ticket",
2147
- description: "Create a new Zendesk support ticket with subject, description, and optional priority/type/assignee/tags. The description becomes the first public comment of the ticket, and the new ticket id is returned. After creation, use update_ticket to change status or assignee, add_public_comment or add_private_note to reply, and manage_tags to adjust tags. Look up valid assignee_id / group_id and custom field ids via search_users or your Zendesk admin settings.",
2219
+ description: "Create a new Zendesk support ticket with subject, description, and optional priority/type/assignee/tags. The description becomes the first public comment of the ticket, and the new ticket id is returned. After creation, use update_ticket to change status or assignee, add_public_comment or add_private_note to reply, and manage_tags to adjust tags. Look up valid assignee_id / group_id and custom field ids via search_users or your Zendesk admin settings. Discover custom field ids and their accepted option values with list_ticket_fields.",
2148
2220
  inputSchema: z.object({
2149
2221
  subject: z.string().min(1).describe("Ticket subject — the short summary line shown in ticket lists and search results."),
2150
2222
  description: z.string().min(1).describe("Ticket description — the body of the request. It becomes the ticket's first public comment (visible to the requester)."),
@@ -2166,7 +2238,7 @@ const createTicketTools = (ctx) => {
2166
2238
  custom_fields: z.array(z.object({
2167
2239
  id: z.number().int(),
2168
2240
  value: z.unknown()
2169
- })).optional().describe("Custom field values as { id, value } pairs (field ids come from your Zendesk admin settings).")
2241
+ })).optional().describe("Custom field values as { id, value } pairs (field ids come from your Zendesk admin settings). Call list_ticket_fields first to discover the numeric field ids and, for dropdown/multiselect fields, the exact option values Zendesk accepts.")
2170
2242
  }),
2171
2243
  annotations: {
2172
2244
  readOnlyHint: false,
@@ -2176,7 +2248,8 @@ const createTicketTools = (ctx) => {
2176
2248
  },
2177
2249
  handler: async (params) => {
2178
2250
  const { subject, description, ...rest } = params;
2179
- const { ticket } = await zendeskPost(subdomain, await getToken(), "/tickets", { ticket: {
2251
+ const token = await getToken();
2252
+ const { ticket } = await zendeskPost(subdomain, token, "/tickets", { ticket: {
2180
2253
  subject,
2181
2254
  comment: { body: description },
2182
2255
  ...rest
@@ -2222,7 +2295,7 @@ const createTicketTools = (ctx) => {
2222
2295
  custom_fields: z.array(z.object({
2223
2296
  id: z.number().int(),
2224
2297
  value: z.unknown()
2225
- })).optional().describe("Custom field values as { id, value } pairs (field ids come from your Zendesk admin settings).")
2298
+ })).optional().describe("Custom field values as { id, value } pairs (field ids come from your Zendesk admin settings). Call list_ticket_fields first to discover the numeric field ids and, for dropdown/multiselect fields, the exact option values Zendesk accepts.")
2226
2299
  }),
2227
2300
  annotations: {
2228
2301
  readOnlyHint: false,
@@ -2232,7 +2305,8 @@ const createTicketTools = (ctx) => {
2232
2305
  },
2233
2306
  handler: async (params) => {
2234
2307
  const { ticket_id, ...updates } = params;
2235
- const { ticket } = await zendeskPut(subdomain, await getToken(), `/tickets/${ticket_id}`, { ticket: updates });
2308
+ const token = await getToken();
2309
+ const { ticket } = await zendeskPut(subdomain, token, `/tickets/${ticket_id}`, { ticket: updates });
2236
2310
  return { content: [{
2237
2311
  type: "text",
2238
2312
  text: `Ticket #${ticket.id} updated.\n\n${formatTicket(ticket)}`
@@ -2321,7 +2395,8 @@ const createTicketTools = (ctx) => {
2321
2395
  },
2322
2396
  handler: async (params) => {
2323
2397
  const { page_size, cursor } = params;
2324
- const response = await zendeskGet(subdomain, await getToken(), "/tickets", buildCursorParams(page_size, cursor));
2398
+ const token = await getToken();
2399
+ const response = await zendeskGet(subdomain, token, "/tickets", buildCursorParams(page_size, cursor));
2325
2400
  const tickets = response.tickets ?? [];
2326
2401
  return { content: [{
2327
2402
  type: "text",
@@ -2344,7 +2419,8 @@ const createTicketTools = (ctx) => {
2344
2419
  },
2345
2420
  handler: async (params) => {
2346
2421
  const { problem_id } = params;
2347
- const incidents = (await zendeskGet(subdomain, await getToken(), `/tickets/${problem_id}/incidents`)).tickets ?? [];
2422
+ const token = await getToken();
2423
+ const incidents = (await zendeskGet(subdomain, token, `/tickets/${problem_id}/incidents`)).tickets ?? [];
2348
2424
  return { content: [{
2349
2425
  type: "text",
2350
2426
  text: truncateIfNeeded(incidents.length > 0 ? `# Incidents linked to problem #${problem_id}\n\n${incidents.map(formatTicket).join("\n\n")}` : `No incidents linked to problem #${problem_id}.`)
@@ -2422,6 +2498,33 @@ const createTicketTools = (ctx) => {
2422
2498
  })
2423
2499
  }] };
2424
2500
  }
2501
+ },
2502
+ {
2503
+ name: "list_ticket_fields",
2504
+ namespace: "tickets",
2505
+ readOnly: true,
2506
+ title: "List Ticket Fields",
2507
+ description: "List the ticket field definitions configured on this Zendesk (both system fields and custom fields), returning each field's id, type, whether it is active/required, and — for dropdown and multiselect fields — the valid option values. Use this to discover the numeric field ids and accepted option tags that create_ticket and update_ticket expect in their custom_fields argument, so a natural-language intent (\"set severity to High\") maps to the right id and a value Zendesk will accept instead of a blind guess. Read-only reference lookup; cursor-paginated in Zendesk's default field order.",
2508
+ inputSchema: z.object({
2509
+ page_size: z.number().int().min(1).max(100).default(100).describe("Ticket field definitions per page (1-100, default 100)."),
2510
+ cursor: z.string().optional().describe("Pagination cursor from a previous response; omit for the first page.")
2511
+ }),
2512
+ annotations: {
2513
+ readOnlyHint: true,
2514
+ destructiveHint: false,
2515
+ idempotentHint: true,
2516
+ openWorldHint: true
2517
+ },
2518
+ handler: async (params) => {
2519
+ const { page_size, cursor } = params;
2520
+ const token = await getToken();
2521
+ const response = await zendeskGet(subdomain, token, "/ticket_fields", buildCursorParams(page_size, cursor));
2522
+ const fields = response.ticket_fields ?? [];
2523
+ return { content: [{
2524
+ type: "text",
2525
+ text: formatList(fields, formatTicketField, extractPaginationMeta(response, fields.length))
2526
+ }] };
2527
+ }
2425
2528
  }
2426
2529
  ];
2427
2530
  };
@@ -2444,7 +2547,8 @@ const createUserTools = (ctx) => {
2444
2547
  openWorldHint: true
2445
2548
  },
2446
2549
  handler: async () => {
2447
- const { user } = await zendeskGet(subdomain, await getToken(), "/users/me");
2550
+ const token = await getToken();
2551
+ const { user } = await zendeskGet(subdomain, token, "/users/me");
2448
2552
  return { content: [{
2449
2553
  type: "text",
2450
2554
  text: formatUser(user)
@@ -2470,7 +2574,8 @@ const createUserTools = (ctx) => {
2470
2574
  },
2471
2575
  handler: async (params) => {
2472
2576
  const { query, per_page, page } = params;
2473
- const response = await zendeskGet(subdomain, await getToken(), "/search", {
2577
+ const token = await getToken();
2578
+ const response = await zendeskGet(subdomain, token, "/search", {
2474
2579
  query: `type:user ${query}`,
2475
2580
  ...buildOffsetParams(per_page, page)
2476
2581
  });
@@ -2495,7 +2600,8 @@ const createUserTools = (ctx) => {
2495
2600
  },
2496
2601
  handler: async (params) => {
2497
2602
  const { user_id } = params;
2498
- const { user } = await zendeskGet(subdomain, await getToken(), `/users/${user_id}`);
2603
+ const token = await getToken();
2604
+ const { user } = await zendeskGet(subdomain, token, `/users/${user_id}`);
2499
2605
  return { content: [{
2500
2606
  type: "text",
2501
2607
  text: formatUser(user)
@@ -2517,7 +2623,8 @@ const createUserTools = (ctx) => {
2517
2623
  },
2518
2624
  handler: async (params) => {
2519
2625
  const { organization_id } = params;
2520
- const { organization } = await zendeskGet(subdomain, await getToken(), `/organizations/${organization_id}`);
2626
+ const token = await getToken();
2627
+ const { organization } = await zendeskGet(subdomain, token, `/organizations/${organization_id}`);
2521
2628
  return { content: [{
2522
2629
  type: "text",
2523
2630
  text: formatOrganization(organization)
@@ -2542,7 +2649,8 @@ const createUserTools = (ctx) => {
2542
2649
  },
2543
2650
  handler: async (params) => {
2544
2651
  const { page_size, cursor } = params;
2545
- const response = await zendeskGet(subdomain, await getToken(), "/organizations", buildCursorParams(page_size, cursor));
2652
+ const token = await getToken();
2653
+ const response = await zendeskGet(subdomain, token, "/organizations", buildCursorParams(page_size, cursor));
2546
2654
  const organizations = response.organizations ?? [];
2547
2655
  return { content: [{
2548
2656
  type: "text",
@@ -2870,7 +2978,8 @@ const sendJsonRpcError = (res, status, code, message, headers = {}) => {
2870
2978
  }));
2871
2979
  };
2872
2980
  const sendUnauthorized = (res, resource) => {
2873
- sendJsonRpcError(res, 401, -32e3, MISSING_BEARER_MESSAGE, { "WWW-Authenticate": `Bearer resource_metadata="${resource}/.well-known/oauth-protected-resource", error="invalid_token", error_description="${MISSING_BEARER_MESSAGE}"` });
2981
+ const wwwAuthenticate = `Bearer resource_metadata="${resource}/.well-known/oauth-protected-resource", error="invalid_token", error_description="${MISSING_BEARER_MESSAGE}"`;
2982
+ sendJsonRpcError(res, 401, -32e3, MISSING_BEARER_MESSAGE, { "WWW-Authenticate": wwwAuthenticate });
2874
2983
  };
2875
2984
  const readJsonBody = (req, maxBodyBytes) => new Promise((resolve) => {
2876
2985
  const chunks = [];