@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 +4 -4
- package/dist/index.js +150 -41
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
[](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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
-
|
|
751
|
+
const url = buildUrl(getBaseUrl(subdomain), path, params);
|
|
752
|
+
return executeRequest(url, token);
|
|
750
753
|
};
|
|
751
754
|
const zendeskPost = (subdomain, token, path, body) => {
|
|
752
|
-
|
|
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
|
-
|
|
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
|
-
|
|
769
|
+
const url = buildUrl(getHelpCenterBaseUrl(subdomain), path, params);
|
|
770
|
+
return executeRequest(url, token);
|
|
765
771
|
};
|
|
766
772
|
const helpCenterPost = (subdomain, token, path, body) => {
|
|
767
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 = [];
|