@fruggr/zendesk-mcp-server 2.8.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,37 +748,44 @@ 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
  };
778
786
  const helpCenterDelete = (subdomain, token, path) => {
779
- return executeRequest(buildUrl(getHelpCenterBaseUrl(subdomain), path), token, { method: "DELETE" });
787
+ const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
788
+ return executeRequest(url, token, { method: "DELETE" });
780
789
  };
781
790
  const fetchZendeskBinary = async (subdomain, token, contentUrl) => {
782
791
  const expectedHost = `${subdomain}.zendesk.com`;
@@ -888,6 +897,18 @@ const formatSlaPolicy = (policy) => {
888
897
  ...targets
889
898
  ].filter(Boolean).join("\n");
890
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
+ };
891
912
  const minutesUntil = (iso) => {
892
913
  const t = Date.parse(iso);
893
914
  return Number.isNaN(t) ? null : Math.round((t - Date.now()) / 6e4);
@@ -967,7 +988,8 @@ const formatLabel = (label) => `- **${label.name}** (${label.id})`;
967
988
  const formatUserSegment = (segment) => `- **${segment.name}** (${segment.id}) — ${segment.user_type}${segment.built_in ? " — Built-in" : ""}`;
968
989
  const formatAttachment = (attachment) => `- **${attachment.file_name}** (${attachment.id}) — ${attachment.content_type} — ${attachment.size} bytes`;
969
990
  const formatList = (items, formatter, meta) => {
970
- 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);
971
993
  };
972
994
  //#endregion
973
995
  //#region src/utils/pagination.ts
@@ -1093,7 +1115,8 @@ const createTopologyProvider = (getToken, subdomain, onUnauthorized) => {
1093
1115
  const now = Date.now();
1094
1116
  if (cached && now - cached.at < 3e5) return cached.promise;
1095
1117
  const promise = (async () => {
1096
- return formatTopology(await fetchTopology(subdomain, await getToken()));
1118
+ const token = await getToken();
1119
+ return formatTopology(await fetchTopology(subdomain, token));
1097
1120
  })().catch((err) => {
1098
1121
  cached = void 0;
1099
1122
  if (onUnauthorized && err instanceof ZendeskApiError && err.status === 401) onUnauthorized();
@@ -1285,7 +1308,8 @@ const createHelpCenterTools = (ctx) => {
1285
1308
  handler: async (params) => {
1286
1309
  const { article_id, locale } = params;
1287
1310
  const token = await getToken();
1288
- 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);
1289
1313
  const { translations } = await helpCenterGet(subdomain, token, `/articles/${article_id}/translations`);
1290
1314
  return { content: [{
1291
1315
  type: "text",
@@ -1312,7 +1336,9 @@ const createHelpCenterTools = (ctx) => {
1312
1336
  },
1313
1337
  handler: async (params) => {
1314
1338
  const { locale, page_size, cursor } = params;
1315
- 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));
1316
1342
  const categories = response.categories ?? [];
1317
1343
  return { content: [{
1318
1344
  type: "text",
@@ -1340,7 +1366,9 @@ const createHelpCenterTools = (ctx) => {
1340
1366
  },
1341
1367
  handler: async (params) => {
1342
1368
  const { category_id, locale, page_size, cursor } = params;
1343
- 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));
1344
1372
  const sections = response.sections ?? [];
1345
1373
  return { content: [{
1346
1374
  type: "text",
@@ -1377,7 +1405,8 @@ const createHelpCenterTools = (ctx) => {
1377
1405
  handler: async (params) => {
1378
1406
  const { section_id, locale, page_size, cursor, sort_by, sort_order, include_translations } = params;
1379
1407
  const token = await getToken();
1380
- 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, {
1381
1410
  ...buildCursorParams(page_size, cursor),
1382
1411
  sort_by,
1383
1412
  sort_order
@@ -1414,7 +1443,8 @@ const createHelpCenterTools = (ctx) => {
1414
1443
  },
1415
1444
  handler: async (params) => {
1416
1445
  const { article_id } = params;
1417
- 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`);
1418
1448
  return { content: [{
1419
1449
  type: "text",
1420
1450
  text: formatList(translations, formatTranslationSummary)
@@ -1442,7 +1472,8 @@ const createHelpCenterTools = (ctx) => {
1442
1472
  },
1443
1473
  handler: async (params) => {
1444
1474
  const { article_id, locale, title, body, draft } = params;
1445
- 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: {
1446
1477
  locale,
1447
1478
  title,
1448
1479
  body,
@@ -1475,7 +1506,8 @@ const createHelpCenterTools = (ctx) => {
1475
1506
  },
1476
1507
  handler: async (params) => {
1477
1508
  const { article_id, locale, ...updates } = params;
1478
- 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 });
1479
1511
  return { content: [{
1480
1512
  type: "text",
1481
1513
  text: `Translation updated for article #${article_id} in "${locale}".\n\n${formatTranslation(translation)}`
@@ -1496,9 +1528,10 @@ const createHelpCenterTools = (ctx) => {
1496
1528
  openWorldHint: true
1497
1529
  },
1498
1530
  handler: async () => {
1531
+ const token = await getToken();
1499
1532
  return { content: [{
1500
1533
  type: "text",
1501
- 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)
1502
1535
  }] };
1503
1536
  }
1504
1537
  },
@@ -1529,7 +1562,8 @@ const createHelpCenterTools = (ctx) => {
1529
1562
  },
1530
1563
  handler: async (params) => {
1531
1564
  const { section_id, ...articleData } = params;
1532
- 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 });
1533
1567
  return { content: [{
1534
1568
  type: "text",
1535
1569
  text: `Article #${article.id} created.\n\n${formatArticle(article)}`
@@ -1562,7 +1596,8 @@ const createHelpCenterTools = (ctx) => {
1562
1596
  },
1563
1597
  handler: async (params) => {
1564
1598
  const { article_id, ...updates } = params;
1565
- 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 });
1566
1601
  return { content: [{
1567
1602
  type: "text",
1568
1603
  text: `Article #${article.id} updated.\n\n${formatArticle(article)}`
@@ -1588,7 +1623,8 @@ const createHelpCenterTools = (ctx) => {
1588
1623
  handler: async (params) => {
1589
1624
  const { article_id, confirm } = params;
1590
1625
  if (confirm !== true) throw new Error("Archiving is guarded: pass confirm: true to archive (soft-delete) this article. No changes were made.");
1591
- await helpCenterDelete(subdomain, await getToken(), `/articles/${article_id}`);
1626
+ const token = await getToken();
1627
+ await helpCenterDelete(subdomain, token, `/articles/${article_id}`);
1592
1628
  return { content: [{
1593
1629
  type: "text",
1594
1630
  text: `Article #${article_id} archived (soft-deleted). It is removed from the Help Center; restore it from the Guide admin UI if needed.`
@@ -1646,7 +1682,8 @@ const createHelpCenterTools = (ctx) => {
1646
1682
  },
1647
1683
  handler: async (params) => {
1648
1684
  const { name } = params;
1649
- 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 } });
1650
1687
  return { content: [{
1651
1688
  type: "text",
1652
1689
  text: `Content tag created.\n\n${formatContentTag(content_tag)}`
@@ -1667,9 +1704,10 @@ const createHelpCenterTools = (ctx) => {
1667
1704
  openWorldHint: true
1668
1705
  },
1669
1706
  handler: async () => {
1707
+ const token = await getToken();
1670
1708
  return { content: [{
1671
1709
  type: "text",
1672
- text: formatList((await helpCenterGet(subdomain, await getToken(), "/articles/labels")).labels ?? [], formatLabel)
1710
+ text: formatList((await helpCenterGet(subdomain, token, "/articles/labels")).labels ?? [], formatLabel)
1673
1711
  }] };
1674
1712
  }
1675
1713
  },
@@ -1687,9 +1725,10 @@ const createHelpCenterTools = (ctx) => {
1687
1725
  openWorldHint: true
1688
1726
  },
1689
1727
  handler: async () => {
1728
+ const token = await getToken();
1690
1729
  return { content: [{
1691
1730
  type: "text",
1692
- text: formatList((await helpCenterGet(subdomain, await getToken(), "/user_segments")).user_segments ?? [], formatUserSegment)
1731
+ text: formatList((await helpCenterGet(subdomain, token, "/user_segments")).user_segments ?? [], formatUserSegment)
1693
1732
  }] };
1694
1733
  }
1695
1734
  },
@@ -1708,7 +1747,8 @@ const createHelpCenterTools = (ctx) => {
1708
1747
  },
1709
1748
  handler: async (params) => {
1710
1749
  const { article_id } = params;
1711
- 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 ?? [];
1712
1752
  if (attachments.length === 0) return { content: [{
1713
1753
  type: "text",
1714
1754
  text: `No attachments found on article #${article_id}.`
@@ -1780,7 +1820,8 @@ const createHelpCenterTools = (ctx) => {
1780
1820
  },
1781
1821
  handler: async (params) => {
1782
1822
  const { article_id, locale, section_index, format } = params;
1783
- 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}`);
1784
1825
  const sections = parseSections(translation.body);
1785
1826
  const section = sections[section_index];
1786
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)}).`);
@@ -1952,7 +1993,8 @@ const createSearchTools = (ctx) => {
1952
1993
  },
1953
1994
  handler: async (params) => {
1954
1995
  const { query, per_page, page } = params;
1955
- const response = await zendeskGet(subdomain, await getToken(), "/search", {
1996
+ const token = await getToken();
1997
+ const response = await zendeskGet(subdomain, token, "/search", {
1956
1998
  query,
1957
1999
  ...buildOffsetParams(per_page, page)
1958
2000
  });
@@ -2156,7 +2198,8 @@ const createTicketTools = (ctx) => {
2156
2198
  },
2157
2199
  handler: async (params) => {
2158
2200
  const { query, per_page, page } = params;
2159
- const response = await zendeskGet(subdomain, await getToken(), "/search", {
2201
+ const token = await getToken();
2202
+ const response = await zendeskGet(subdomain, token, "/search", {
2160
2203
  query: `type:ticket ${query}`,
2161
2204
  include: "tickets(slas)",
2162
2205
  ...buildOffsetParams(per_page, page)
@@ -2173,7 +2216,7 @@ const createTicketTools = (ctx) => {
2173
2216
  namespace: "tickets",
2174
2217
  readOnly: false,
2175
2218
  title: "Create Zendesk Ticket",
2176
- 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.",
2177
2220
  inputSchema: z.object({
2178
2221
  subject: z.string().min(1).describe("Ticket subject — the short summary line shown in ticket lists and search results."),
2179
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)."),
@@ -2195,7 +2238,7 @@ const createTicketTools = (ctx) => {
2195
2238
  custom_fields: z.array(z.object({
2196
2239
  id: z.number().int(),
2197
2240
  value: z.unknown()
2198
- })).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.")
2199
2242
  }),
2200
2243
  annotations: {
2201
2244
  readOnlyHint: false,
@@ -2205,7 +2248,8 @@ const createTicketTools = (ctx) => {
2205
2248
  },
2206
2249
  handler: async (params) => {
2207
2250
  const { subject, description, ...rest } = params;
2208
- const { ticket } = await zendeskPost(subdomain, await getToken(), "/tickets", { ticket: {
2251
+ const token = await getToken();
2252
+ const { ticket } = await zendeskPost(subdomain, token, "/tickets", { ticket: {
2209
2253
  subject,
2210
2254
  comment: { body: description },
2211
2255
  ...rest
@@ -2251,7 +2295,7 @@ const createTicketTools = (ctx) => {
2251
2295
  custom_fields: z.array(z.object({
2252
2296
  id: z.number().int(),
2253
2297
  value: z.unknown()
2254
- })).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.")
2255
2299
  }),
2256
2300
  annotations: {
2257
2301
  readOnlyHint: false,
@@ -2261,7 +2305,8 @@ const createTicketTools = (ctx) => {
2261
2305
  },
2262
2306
  handler: async (params) => {
2263
2307
  const { ticket_id, ...updates } = params;
2264
- 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 });
2265
2310
  return { content: [{
2266
2311
  type: "text",
2267
2312
  text: `Ticket #${ticket.id} updated.\n\n${formatTicket(ticket)}`
@@ -2350,7 +2395,8 @@ const createTicketTools = (ctx) => {
2350
2395
  },
2351
2396
  handler: async (params) => {
2352
2397
  const { page_size, cursor } = params;
2353
- 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));
2354
2400
  const tickets = response.tickets ?? [];
2355
2401
  return { content: [{
2356
2402
  type: "text",
@@ -2373,7 +2419,8 @@ const createTicketTools = (ctx) => {
2373
2419
  },
2374
2420
  handler: async (params) => {
2375
2421
  const { problem_id } = params;
2376
- 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 ?? [];
2377
2424
  return { content: [{
2378
2425
  type: "text",
2379
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}.`)
@@ -2451,6 +2498,33 @@ const createTicketTools = (ctx) => {
2451
2498
  })
2452
2499
  }] };
2453
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
+ }
2454
2528
  }
2455
2529
  ];
2456
2530
  };
@@ -2473,7 +2547,8 @@ const createUserTools = (ctx) => {
2473
2547
  openWorldHint: true
2474
2548
  },
2475
2549
  handler: async () => {
2476
- const { user } = await zendeskGet(subdomain, await getToken(), "/users/me");
2550
+ const token = await getToken();
2551
+ const { user } = await zendeskGet(subdomain, token, "/users/me");
2477
2552
  return { content: [{
2478
2553
  type: "text",
2479
2554
  text: formatUser(user)
@@ -2499,7 +2574,8 @@ const createUserTools = (ctx) => {
2499
2574
  },
2500
2575
  handler: async (params) => {
2501
2576
  const { query, per_page, page } = params;
2502
- const response = await zendeskGet(subdomain, await getToken(), "/search", {
2577
+ const token = await getToken();
2578
+ const response = await zendeskGet(subdomain, token, "/search", {
2503
2579
  query: `type:user ${query}`,
2504
2580
  ...buildOffsetParams(per_page, page)
2505
2581
  });
@@ -2524,7 +2600,8 @@ const createUserTools = (ctx) => {
2524
2600
  },
2525
2601
  handler: async (params) => {
2526
2602
  const { user_id } = params;
2527
- 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}`);
2528
2605
  return { content: [{
2529
2606
  type: "text",
2530
2607
  text: formatUser(user)
@@ -2546,7 +2623,8 @@ const createUserTools = (ctx) => {
2546
2623
  },
2547
2624
  handler: async (params) => {
2548
2625
  const { organization_id } = params;
2549
- 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}`);
2550
2628
  return { content: [{
2551
2629
  type: "text",
2552
2630
  text: formatOrganization(organization)
@@ -2571,7 +2649,8 @@ const createUserTools = (ctx) => {
2571
2649
  },
2572
2650
  handler: async (params) => {
2573
2651
  const { page_size, cursor } = params;
2574
- 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));
2575
2654
  const organizations = response.organizations ?? [];
2576
2655
  return { content: [{
2577
2656
  type: "text",
@@ -2899,7 +2978,8 @@ const sendJsonRpcError = (res, status, code, message, headers = {}) => {
2899
2978
  }));
2900
2979
  };
2901
2980
  const sendUnauthorized = (res, resource) => {
2902
- 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 });
2903
2983
  };
2904
2984
  const readJsonBody = (req, maxBodyBytes) => new Promise((resolve) => {
2905
2985
  const chunks = [];