@fruggr/zendesk-mcp-server 2.8.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/index.js +265 -48
- 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,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
|
-
|
|
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
|
};
|
|
778
786
|
const helpCenterDelete = (subdomain, token, path) => {
|
|
779
|
-
|
|
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,36 @@ 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
|
+
};
|
|
912
|
+
const formatFieldValue = (value) => Array.isArray(value) ? value.map(formatFieldValue).join(", ") : formatConditionValue(value);
|
|
913
|
+
const MACRO_VALUE_PREVIEW = 120;
|
|
914
|
+
const formatMacroActionValue = (value) => {
|
|
915
|
+
const oneLine = formatFieldValue(value).replace(/\s+/g, " ").trim();
|
|
916
|
+
return oneLine.length > MACRO_VALUE_PREVIEW ? `${oneLine.slice(0, MACRO_VALUE_PREVIEW)}…` : oneLine;
|
|
917
|
+
};
|
|
918
|
+
const formatMacroAction = (action) => ` - ${action.field} → ${formatMacroActionValue(action.value)}`;
|
|
919
|
+
const formatMacro = (macro) => {
|
|
920
|
+
const scope = macro.restriction?.type ? "restricted" : "shared";
|
|
921
|
+
const actions = macro.actions ?? [];
|
|
922
|
+
return [
|
|
923
|
+
`## ${macro.title} (id ${macro.id})`,
|
|
924
|
+
`- **${macro.active ? "active" : "inactive"}** | **Scope**: ${scope}`,
|
|
925
|
+
macro.description ? `- **Description**: ${macro.description}` : "",
|
|
926
|
+
actions.length > 0 ? "- **Actions**:" : "- **Actions**: none",
|
|
927
|
+
...actions.map(formatMacroAction)
|
|
928
|
+
].filter(Boolean).join("\n");
|
|
929
|
+
};
|
|
891
930
|
const minutesUntil = (iso) => {
|
|
892
931
|
const t = Date.parse(iso);
|
|
893
932
|
return Number.isNaN(t) ? null : Math.round((t - Date.now()) / 6e4);
|
|
@@ -967,7 +1006,8 @@ const formatLabel = (label) => `- **${label.name}** (${label.id})`;
|
|
|
967
1006
|
const formatUserSegment = (segment) => `- **${segment.name}** (${segment.id}) — ${segment.user_type}${segment.built_in ? " — Built-in" : ""}`;
|
|
968
1007
|
const formatAttachment = (attachment) => `- **${attachment.file_name}** (${attachment.id}) — ${attachment.content_type} — ${attachment.size} bytes`;
|
|
969
1008
|
const formatList = (items, formatter, meta) => {
|
|
970
|
-
|
|
1009
|
+
const text = [meta ? formatPagination(meta) : "", items.map(formatter).join("\n\n")].filter(Boolean).join("\n\n");
|
|
1010
|
+
return truncateIfNeeded(text);
|
|
971
1011
|
};
|
|
972
1012
|
//#endregion
|
|
973
1013
|
//#region src/utils/pagination.ts
|
|
@@ -988,6 +1028,11 @@ const extractPaginationMeta = (response, itemCount) => ({
|
|
|
988
1028
|
after_cursor: response.meta?.after_cursor ?? null,
|
|
989
1029
|
count: response.count ?? itemCount
|
|
990
1030
|
});
|
|
1031
|
+
const extractOffsetPaginationMeta = (response, itemCount, perPage, page) => response.count != null ? extractSearchPaginationMeta(response, perPage, page) : {
|
|
1032
|
+
count: itemCount,
|
|
1033
|
+
has_more: false,
|
|
1034
|
+
after_cursor: null
|
|
1035
|
+
};
|
|
991
1036
|
const extractSearchPaginationMeta = (response, perPage, page) => {
|
|
992
1037
|
const count = response.count ?? 0;
|
|
993
1038
|
const has_more = count > page * perPage;
|
|
@@ -1093,7 +1138,8 @@ const createTopologyProvider = (getToken, subdomain, onUnauthorized) => {
|
|
|
1093
1138
|
const now = Date.now();
|
|
1094
1139
|
if (cached && now - cached.at < 3e5) return cached.promise;
|
|
1095
1140
|
const promise = (async () => {
|
|
1096
|
-
|
|
1141
|
+
const token = await getToken();
|
|
1142
|
+
return formatTopology(await fetchTopology(subdomain, token));
|
|
1097
1143
|
})().catch((err) => {
|
|
1098
1144
|
cached = void 0;
|
|
1099
1145
|
if (onUnauthorized && err instanceof ZendeskApiError && err.status === 401) onUnauthorized();
|
|
@@ -1285,7 +1331,8 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1285
1331
|
handler: async (params) => {
|
|
1286
1332
|
const { article_id, locale } = params;
|
|
1287
1333
|
const token = await getToken();
|
|
1288
|
-
const
|
|
1334
|
+
const path = locale ? `/${locale}/articles/${article_id}` : `/articles/${article_id}`;
|
|
1335
|
+
const { article } = await helpCenterGet(subdomain, token, path);
|
|
1289
1336
|
const { translations } = await helpCenterGet(subdomain, token, `/articles/${article_id}/translations`);
|
|
1290
1337
|
return { content: [{
|
|
1291
1338
|
type: "text",
|
|
@@ -1312,7 +1359,9 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1312
1359
|
},
|
|
1313
1360
|
handler: async (params) => {
|
|
1314
1361
|
const { locale, page_size, cursor } = params;
|
|
1315
|
-
const
|
|
1362
|
+
const token = await getToken();
|
|
1363
|
+
const path = locale ? `/${locale}/categories` : "/categories";
|
|
1364
|
+
const response = await helpCenterGet(subdomain, token, path, buildCursorParams(page_size, cursor));
|
|
1316
1365
|
const categories = response.categories ?? [];
|
|
1317
1366
|
return { content: [{
|
|
1318
1367
|
type: "text",
|
|
@@ -1340,7 +1389,9 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1340
1389
|
},
|
|
1341
1390
|
handler: async (params) => {
|
|
1342
1391
|
const { category_id, locale, page_size, cursor } = params;
|
|
1343
|
-
const
|
|
1392
|
+
const token = await getToken();
|
|
1393
|
+
const path = category_id && locale ? `/${locale}/categories/${category_id}/sections` : category_id ? `/categories/${category_id}/sections` : locale ? `/${locale}/sections` : "/sections";
|
|
1394
|
+
const response = await helpCenterGet(subdomain, token, path, buildCursorParams(page_size, cursor));
|
|
1344
1395
|
const sections = response.sections ?? [];
|
|
1345
1396
|
return { content: [{
|
|
1346
1397
|
type: "text",
|
|
@@ -1377,7 +1428,8 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1377
1428
|
handler: async (params) => {
|
|
1378
1429
|
const { section_id, locale, page_size, cursor, sort_by, sort_order, include_translations } = params;
|
|
1379
1430
|
const token = await getToken();
|
|
1380
|
-
const
|
|
1431
|
+
const path = section_id && locale ? `/${locale}/sections/${section_id}/articles` : section_id ? `/sections/${section_id}/articles` : locale ? `/${locale}/articles` : "/articles";
|
|
1432
|
+
const response = await helpCenterGet(subdomain, token, path, {
|
|
1381
1433
|
...buildCursorParams(page_size, cursor),
|
|
1382
1434
|
sort_by,
|
|
1383
1435
|
sort_order
|
|
@@ -1414,7 +1466,8 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1414
1466
|
},
|
|
1415
1467
|
handler: async (params) => {
|
|
1416
1468
|
const { article_id } = params;
|
|
1417
|
-
const
|
|
1469
|
+
const token = await getToken();
|
|
1470
|
+
const { translations } = await helpCenterGet(subdomain, token, `/articles/${article_id}/translations`);
|
|
1418
1471
|
return { content: [{
|
|
1419
1472
|
type: "text",
|
|
1420
1473
|
text: formatList(translations, formatTranslationSummary)
|
|
@@ -1442,7 +1495,8 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1442
1495
|
},
|
|
1443
1496
|
handler: async (params) => {
|
|
1444
1497
|
const { article_id, locale, title, body, draft } = params;
|
|
1445
|
-
const
|
|
1498
|
+
const token = await getToken();
|
|
1499
|
+
const { translation } = await helpCenterPost(subdomain, token, `/articles/${article_id}/translations`, { translation: {
|
|
1446
1500
|
locale,
|
|
1447
1501
|
title,
|
|
1448
1502
|
body,
|
|
@@ -1475,7 +1529,8 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1475
1529
|
},
|
|
1476
1530
|
handler: async (params) => {
|
|
1477
1531
|
const { article_id, locale, ...updates } = params;
|
|
1478
|
-
const
|
|
1532
|
+
const token = await getToken();
|
|
1533
|
+
const { translation } = await helpCenterPut(subdomain, token, `/articles/${article_id}/translations/${locale}`, { translation: updates });
|
|
1479
1534
|
return { content: [{
|
|
1480
1535
|
type: "text",
|
|
1481
1536
|
text: `Translation updated for article #${article_id} in "${locale}".\n\n${formatTranslation(translation)}`
|
|
@@ -1496,9 +1551,10 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1496
1551
|
openWorldHint: true
|
|
1497
1552
|
},
|
|
1498
1553
|
handler: async () => {
|
|
1554
|
+
const token = await getToken();
|
|
1499
1555
|
return { content: [{
|
|
1500
1556
|
type: "text",
|
|
1501
|
-
text: formatList((await zendeskGet(subdomain,
|
|
1557
|
+
text: formatList((await zendeskGet(subdomain, token, "/guide/permission_groups")).permission_groups ?? [], formatPermissionGroup)
|
|
1502
1558
|
}] };
|
|
1503
1559
|
}
|
|
1504
1560
|
},
|
|
@@ -1529,7 +1585,8 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1529
1585
|
},
|
|
1530
1586
|
handler: async (params) => {
|
|
1531
1587
|
const { section_id, ...articleData } = params;
|
|
1532
|
-
const
|
|
1588
|
+
const token = await getToken();
|
|
1589
|
+
const { article } = await helpCenterPost(subdomain, token, `/sections/${section_id}/articles`, { article: articleData });
|
|
1533
1590
|
return { content: [{
|
|
1534
1591
|
type: "text",
|
|
1535
1592
|
text: `Article #${article.id} created.\n\n${formatArticle(article)}`
|
|
@@ -1562,7 +1619,8 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1562
1619
|
},
|
|
1563
1620
|
handler: async (params) => {
|
|
1564
1621
|
const { article_id, ...updates } = params;
|
|
1565
|
-
const
|
|
1622
|
+
const token = await getToken();
|
|
1623
|
+
const { article } = await helpCenterPut(subdomain, token, `/articles/${article_id}`, { article: updates });
|
|
1566
1624
|
return { content: [{
|
|
1567
1625
|
type: "text",
|
|
1568
1626
|
text: `Article #${article.id} updated.\n\n${formatArticle(article)}`
|
|
@@ -1588,7 +1646,8 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1588
1646
|
handler: async (params) => {
|
|
1589
1647
|
const { article_id, confirm } = params;
|
|
1590
1648
|
if (confirm !== true) throw new Error("Archiving is guarded: pass confirm: true to archive (soft-delete) this article. No changes were made.");
|
|
1591
|
-
|
|
1649
|
+
const token = await getToken();
|
|
1650
|
+
await helpCenterDelete(subdomain, token, `/articles/${article_id}`);
|
|
1592
1651
|
return { content: [{
|
|
1593
1652
|
type: "text",
|
|
1594
1653
|
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 +1705,8 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1646
1705
|
},
|
|
1647
1706
|
handler: async (params) => {
|
|
1648
1707
|
const { name } = params;
|
|
1649
|
-
const
|
|
1708
|
+
const token = await getToken();
|
|
1709
|
+
const { content_tag } = await zendeskPost(subdomain, token, "/guide/content_tags", { content_tag: { name } });
|
|
1650
1710
|
return { content: [{
|
|
1651
1711
|
type: "text",
|
|
1652
1712
|
text: `Content tag created.\n\n${formatContentTag(content_tag)}`
|
|
@@ -1667,9 +1727,10 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1667
1727
|
openWorldHint: true
|
|
1668
1728
|
},
|
|
1669
1729
|
handler: async () => {
|
|
1730
|
+
const token = await getToken();
|
|
1670
1731
|
return { content: [{
|
|
1671
1732
|
type: "text",
|
|
1672
|
-
text: formatList((await helpCenterGet(subdomain,
|
|
1733
|
+
text: formatList((await helpCenterGet(subdomain, token, "/articles/labels")).labels ?? [], formatLabel)
|
|
1673
1734
|
}] };
|
|
1674
1735
|
}
|
|
1675
1736
|
},
|
|
@@ -1687,9 +1748,10 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1687
1748
|
openWorldHint: true
|
|
1688
1749
|
},
|
|
1689
1750
|
handler: async () => {
|
|
1751
|
+
const token = await getToken();
|
|
1690
1752
|
return { content: [{
|
|
1691
1753
|
type: "text",
|
|
1692
|
-
text: formatList((await helpCenterGet(subdomain,
|
|
1754
|
+
text: formatList((await helpCenterGet(subdomain, token, "/user_segments")).user_segments ?? [], formatUserSegment)
|
|
1693
1755
|
}] };
|
|
1694
1756
|
}
|
|
1695
1757
|
},
|
|
@@ -1708,7 +1770,8 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1708
1770
|
},
|
|
1709
1771
|
handler: async (params) => {
|
|
1710
1772
|
const { article_id } = params;
|
|
1711
|
-
const
|
|
1773
|
+
const token = await getToken();
|
|
1774
|
+
const attachments = (await helpCenterGet(subdomain, token, `/articles/${article_id}/attachments`)).article_attachments ?? [];
|
|
1712
1775
|
if (attachments.length === 0) return { content: [{
|
|
1713
1776
|
type: "text",
|
|
1714
1777
|
text: `No attachments found on article #${article_id}.`
|
|
@@ -1780,7 +1843,8 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1780
1843
|
},
|
|
1781
1844
|
handler: async (params) => {
|
|
1782
1845
|
const { article_id, locale, section_index, format } = params;
|
|
1783
|
-
const
|
|
1846
|
+
const token = await getToken();
|
|
1847
|
+
const { translation } = await helpCenterGet(subdomain, token, `/articles/${article_id}/translations/${locale}`);
|
|
1784
1848
|
const sections = parseSections(translation.body);
|
|
1785
1849
|
const section = sections[section_index];
|
|
1786
1850
|
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 +2016,8 @@ const createSearchTools = (ctx) => {
|
|
|
1952
2016
|
},
|
|
1953
2017
|
handler: async (params) => {
|
|
1954
2018
|
const { query, per_page, page } = params;
|
|
1955
|
-
const
|
|
2019
|
+
const token = await getToken();
|
|
2020
|
+
const response = await zendeskGet(subdomain, token, "/search", {
|
|
1956
2021
|
query,
|
|
1957
2022
|
...buildOffsetParams(per_page, page)
|
|
1958
2023
|
});
|
|
@@ -2049,6 +2114,71 @@ const fetchTicketSla = async (subdomain, token, ticket) => {
|
|
|
2049
2114
|
return;
|
|
2050
2115
|
}
|
|
2051
2116
|
};
|
|
2117
|
+
const DIFF_SKIP_KEYS = /* @__PURE__ */ new Set([
|
|
2118
|
+
"comment",
|
|
2119
|
+
"fields",
|
|
2120
|
+
"custom_fields",
|
|
2121
|
+
"id",
|
|
2122
|
+
"url",
|
|
2123
|
+
"created_at",
|
|
2124
|
+
"updated_at",
|
|
2125
|
+
"generated_timestamp",
|
|
2126
|
+
"encoded_id"
|
|
2127
|
+
]);
|
|
2128
|
+
const valuesEqual = (a, b) => a === b || JSON.stringify(a) === JSON.stringify(b);
|
|
2129
|
+
const shownValue = (v) => {
|
|
2130
|
+
const s = formatFieldValue(v);
|
|
2131
|
+
return s === "" ? "(empty)" : s;
|
|
2132
|
+
};
|
|
2133
|
+
const diffLine = (label, before, after) => {
|
|
2134
|
+
const b = shownValue(before);
|
|
2135
|
+
const a = shownValue(after);
|
|
2136
|
+
return b === a ? null : `- **${label}**: ${b} → ${a}`;
|
|
2137
|
+
};
|
|
2138
|
+
const formatTagDiff = (before, after) => {
|
|
2139
|
+
const b = new Set(Array.isArray(before) ? before.map(String) : []);
|
|
2140
|
+
const a = new Set(Array.isArray(after) ? after.map(String) : []);
|
|
2141
|
+
const added = [...a].filter((t) => !b.has(t)).map((t) => `+${t}`);
|
|
2142
|
+
const removed = [...b].filter((t) => !a.has(t)).map((t) => `-${t}`);
|
|
2143
|
+
return added.length + removed.length === 0 ? null : `- **tags**: ${[...added, ...removed].join(", ")}`;
|
|
2144
|
+
};
|
|
2145
|
+
const formatMacroPreviewDiff = (ticketId, macroId, before, result) => {
|
|
2146
|
+
const after = result?.ticket ?? {};
|
|
2147
|
+
const beforeObj = before ?? {};
|
|
2148
|
+
const comment = after.comment ?? result?.comment;
|
|
2149
|
+
const changes = [];
|
|
2150
|
+
for (const [key, afterVal] of Object.entries(after)) {
|
|
2151
|
+
if (DIFF_SKIP_KEYS.has(key)) continue;
|
|
2152
|
+
const beforeVal = beforeObj[key];
|
|
2153
|
+
if (valuesEqual(beforeVal, afterVal)) continue;
|
|
2154
|
+
if (key === "tags") {
|
|
2155
|
+
const tagLine = formatTagDiff(beforeVal, afterVal);
|
|
2156
|
+
if (tagLine) changes.push(tagLine);
|
|
2157
|
+
continue;
|
|
2158
|
+
}
|
|
2159
|
+
if (afterVal !== null && typeof afterVal === "object" && !Array.isArray(afterVal)) continue;
|
|
2160
|
+
const line = diffLine(key, beforeVal, afterVal);
|
|
2161
|
+
if (line) changes.push(line);
|
|
2162
|
+
}
|
|
2163
|
+
const afterFields = [after.fields ?? after.custom_fields ?? []].flat();
|
|
2164
|
+
const beforeById = new Map((before?.custom_fields ?? []).map((f) => [f.id, f.value]));
|
|
2165
|
+
for (const f of afterFields) {
|
|
2166
|
+
const line = diffLine(`custom field ${f.id}`, beforeById.get(f.id), f.value);
|
|
2167
|
+
if (line) changes.push(line);
|
|
2168
|
+
}
|
|
2169
|
+
const lines = [
|
|
2170
|
+
`# Macro #${macroId} preview on ticket #${ticketId} (diff — nothing saved yet)`,
|
|
2171
|
+
"",
|
|
2172
|
+
"## Field changes",
|
|
2173
|
+
...changes.length > 0 ? changes : ["- none"]
|
|
2174
|
+
];
|
|
2175
|
+
if (comment?.body) {
|
|
2176
|
+
const visibility = comment.public === false ? "internal note" : "public comment";
|
|
2177
|
+
lines.push("", `## Reply (${visibility})`, "", comment.body);
|
|
2178
|
+
} else lines.push("", "## Reply", "- none");
|
|
2179
|
+
lines.push("", "## To apply these changes", "Nothing has been committed. Persist the field changes with `update_ticket` (or `manage_tags` for incremental tag edits), and post the reply with `add_public_comment` (public) or `add_private_note` (internal). Edit the reply text first if needed.");
|
|
2180
|
+
return lines.join("\n");
|
|
2181
|
+
};
|
|
2052
2182
|
const createTicketTools = (ctx) => {
|
|
2053
2183
|
const { subdomain, getToken } = ctx;
|
|
2054
2184
|
const attachmentSchema = z.object({
|
|
@@ -2156,7 +2286,8 @@ const createTicketTools = (ctx) => {
|
|
|
2156
2286
|
},
|
|
2157
2287
|
handler: async (params) => {
|
|
2158
2288
|
const { query, per_page, page } = params;
|
|
2159
|
-
const
|
|
2289
|
+
const token = await getToken();
|
|
2290
|
+
const response = await zendeskGet(subdomain, token, "/search", {
|
|
2160
2291
|
query: `type:ticket ${query}`,
|
|
2161
2292
|
include: "tickets(slas)",
|
|
2162
2293
|
...buildOffsetParams(per_page, page)
|
|
@@ -2173,7 +2304,7 @@ const createTicketTools = (ctx) => {
|
|
|
2173
2304
|
namespace: "tickets",
|
|
2174
2305
|
readOnly: false,
|
|
2175
2306
|
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.",
|
|
2307
|
+
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
2308
|
inputSchema: z.object({
|
|
2178
2309
|
subject: z.string().min(1).describe("Ticket subject — the short summary line shown in ticket lists and search results."),
|
|
2179
2310
|
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 +2326,7 @@ const createTicketTools = (ctx) => {
|
|
|
2195
2326
|
custom_fields: z.array(z.object({
|
|
2196
2327
|
id: z.number().int(),
|
|
2197
2328
|
value: z.unknown()
|
|
2198
|
-
})).optional().describe("Custom field values as { id, value } pairs (field ids come from your Zendesk admin settings).")
|
|
2329
|
+
})).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
2330
|
}),
|
|
2200
2331
|
annotations: {
|
|
2201
2332
|
readOnlyHint: false,
|
|
@@ -2205,7 +2336,8 @@ const createTicketTools = (ctx) => {
|
|
|
2205
2336
|
},
|
|
2206
2337
|
handler: async (params) => {
|
|
2207
2338
|
const { subject, description, ...rest } = params;
|
|
2208
|
-
const
|
|
2339
|
+
const token = await getToken();
|
|
2340
|
+
const { ticket } = await zendeskPost(subdomain, token, "/tickets", { ticket: {
|
|
2209
2341
|
subject,
|
|
2210
2342
|
comment: { body: description },
|
|
2211
2343
|
...rest
|
|
@@ -2251,7 +2383,7 @@ const createTicketTools = (ctx) => {
|
|
|
2251
2383
|
custom_fields: z.array(z.object({
|
|
2252
2384
|
id: z.number().int(),
|
|
2253
2385
|
value: z.unknown()
|
|
2254
|
-
})).optional().describe("Custom field values as { id, value } pairs (field ids come from your Zendesk admin settings).")
|
|
2386
|
+
})).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
2387
|
}),
|
|
2256
2388
|
annotations: {
|
|
2257
2389
|
readOnlyHint: false,
|
|
@@ -2261,7 +2393,8 @@ const createTicketTools = (ctx) => {
|
|
|
2261
2393
|
},
|
|
2262
2394
|
handler: async (params) => {
|
|
2263
2395
|
const { ticket_id, ...updates } = params;
|
|
2264
|
-
const
|
|
2396
|
+
const token = await getToken();
|
|
2397
|
+
const { ticket } = await zendeskPut(subdomain, token, `/tickets/${ticket_id}`, { ticket: updates });
|
|
2265
2398
|
return { content: [{
|
|
2266
2399
|
type: "text",
|
|
2267
2400
|
text: `Ticket #${ticket.id} updated.\n\n${formatTicket(ticket)}`
|
|
@@ -2350,7 +2483,8 @@ const createTicketTools = (ctx) => {
|
|
|
2350
2483
|
},
|
|
2351
2484
|
handler: async (params) => {
|
|
2352
2485
|
const { page_size, cursor } = params;
|
|
2353
|
-
const
|
|
2486
|
+
const token = await getToken();
|
|
2487
|
+
const response = await zendeskGet(subdomain, token, "/tickets", buildCursorParams(page_size, cursor));
|
|
2354
2488
|
const tickets = response.tickets ?? [];
|
|
2355
2489
|
return { content: [{
|
|
2356
2490
|
type: "text",
|
|
@@ -2373,7 +2507,8 @@ const createTicketTools = (ctx) => {
|
|
|
2373
2507
|
},
|
|
2374
2508
|
handler: async (params) => {
|
|
2375
2509
|
const { problem_id } = params;
|
|
2376
|
-
const
|
|
2510
|
+
const token = await getToken();
|
|
2511
|
+
const incidents = (await zendeskGet(subdomain, token, `/tickets/${problem_id}/incidents`)).tickets ?? [];
|
|
2377
2512
|
return { content: [{
|
|
2378
2513
|
type: "text",
|
|
2379
2514
|
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}.`)
|
|
@@ -2444,11 +2579,87 @@ const createTicketTools = (ctx) => {
|
|
|
2444
2579
|
const policies = response.sla_policies ?? [];
|
|
2445
2580
|
return { content: [{
|
|
2446
2581
|
type: "text",
|
|
2447
|
-
text: formatList(policies, formatSlaPolicy,
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2582
|
+
text: formatList(policies, formatSlaPolicy, extractOffsetPaginationMeta(response, policies.length, per_page, page))
|
|
2583
|
+
}] };
|
|
2584
|
+
}
|
|
2585
|
+
},
|
|
2586
|
+
{
|
|
2587
|
+
name: "list_ticket_fields",
|
|
2588
|
+
namespace: "tickets",
|
|
2589
|
+
readOnly: true,
|
|
2590
|
+
title: "List Ticket Fields",
|
|
2591
|
+
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.",
|
|
2592
|
+
inputSchema: z.object({
|
|
2593
|
+
page_size: z.number().int().min(1).max(100).default(100).describe("Ticket field definitions per page (1-100, default 100)."),
|
|
2594
|
+
cursor: z.string().optional().describe("Pagination cursor from a previous response; omit for the first page.")
|
|
2595
|
+
}),
|
|
2596
|
+
annotations: {
|
|
2597
|
+
readOnlyHint: true,
|
|
2598
|
+
destructiveHint: false,
|
|
2599
|
+
idempotentHint: true,
|
|
2600
|
+
openWorldHint: true
|
|
2601
|
+
},
|
|
2602
|
+
handler: async (params) => {
|
|
2603
|
+
const { page_size, cursor } = params;
|
|
2604
|
+
const token = await getToken();
|
|
2605
|
+
const response = await zendeskGet(subdomain, token, "/ticket_fields", buildCursorParams(page_size, cursor));
|
|
2606
|
+
const fields = response.ticket_fields ?? [];
|
|
2607
|
+
return { content: [{
|
|
2608
|
+
type: "text",
|
|
2609
|
+
text: formatList(fields, formatTicketField, extractPaginationMeta(response, fields.length))
|
|
2610
|
+
}] };
|
|
2611
|
+
}
|
|
2612
|
+
},
|
|
2613
|
+
{
|
|
2614
|
+
name: "list_macros",
|
|
2615
|
+
namespace: "tickets",
|
|
2616
|
+
readOnly: true,
|
|
2617
|
+
title: "List Zendesk Macros",
|
|
2618
|
+
description: "List the active macros available to the authenticated user. A macro bundles a canned reply and/or a set of field changes (status, priority, assignee, group, tags, custom fields) an agent applies to a ticket in one gesture; this returns each macro id, title, description, availability scope, and its ordered list of actions, offset-paginated. Results are scoped by per-user OAuth to what the current user can see, so no shared admin key is needed. Pass a macro id from here to preview_macro_diff to preview its effect on a specific ticket.",
|
|
2619
|
+
inputSchema: z.object({
|
|
2620
|
+
per_page: z.number().int().min(1).max(100).default(100).describe(PER_PAGE_DESC),
|
|
2621
|
+
page: z.number().int().min(1).default(1).describe(PAGE_DESC)
|
|
2622
|
+
}),
|
|
2623
|
+
annotations: {
|
|
2624
|
+
readOnlyHint: true,
|
|
2625
|
+
destructiveHint: false,
|
|
2626
|
+
idempotentHint: true,
|
|
2627
|
+
openWorldHint: true
|
|
2628
|
+
},
|
|
2629
|
+
handler: async (params) => {
|
|
2630
|
+
const { per_page, page } = params;
|
|
2631
|
+
const token = await getToken();
|
|
2632
|
+
const response = await zendeskGet(subdomain, token, "/macros/active", buildOffsetParams(per_page, page));
|
|
2633
|
+
const macros = response.macros ?? [];
|
|
2634
|
+
return { content: [{
|
|
2635
|
+
type: "text",
|
|
2636
|
+
text: formatList(macros, formatMacro, extractOffsetPaginationMeta(response, macros.length, per_page, page))
|
|
2637
|
+
}] };
|
|
2638
|
+
}
|
|
2639
|
+
},
|
|
2640
|
+
{
|
|
2641
|
+
name: "preview_macro_diff",
|
|
2642
|
+
namespace: "tickets",
|
|
2643
|
+
readOnly: false,
|
|
2644
|
+
title: "Preview a Macro Diff on a Ticket",
|
|
2645
|
+
description: "Preview the exact changes a macro would make to a specific ticket, as a before → after diff, WITHOUT saving anything. Orchestrates two reads — the ticket's current state and Zendesk's macro-apply preview (which returns the whole resulting ticket) — and returns only the fields the macro actually changes (status, priority, assignee, group, tags, custom fields) plus the canned reply with its public/internal flag; unchanged and identity fields are omitted. Nothing is committed: to apply it, follow up with update_ticket for the field changes and add_public_comment or add_private_note for the reply. This deliberate two-step keeps the mutation explicit and reviewable rather than hidden. Find macro ids via list_macros and the ticket id via search_tickets or list_tickets.",
|
|
2646
|
+
inputSchema: z.object({
|
|
2647
|
+
ticket_id: z.number().int().describe("Ticket ID — the numeric id of the ticket to preview the macro against. Obtain it from search_tickets or list_tickets."),
|
|
2648
|
+
macro_id: z.number().int().describe("Macro ID — the numeric id of the macro to preview. Obtain it from list_macros.")
|
|
2649
|
+
}),
|
|
2650
|
+
annotations: {
|
|
2651
|
+
readOnlyHint: false,
|
|
2652
|
+
destructiveHint: false,
|
|
2653
|
+
idempotentHint: true,
|
|
2654
|
+
openWorldHint: true
|
|
2655
|
+
},
|
|
2656
|
+
handler: async (params) => {
|
|
2657
|
+
const { ticket_id, macro_id } = params;
|
|
2658
|
+
const token = await getToken();
|
|
2659
|
+
const [{ ticket: before }, { result }] = await Promise.all([zendeskGet(subdomain, token, `/tickets/${ticket_id}`), zendeskGet(subdomain, token, `/tickets/${ticket_id}/macros/${macro_id}/apply`)]);
|
|
2660
|
+
return { content: [{
|
|
2661
|
+
type: "text",
|
|
2662
|
+
text: truncateIfNeeded(formatMacroPreviewDiff(ticket_id, macro_id, before, result))
|
|
2452
2663
|
}] };
|
|
2453
2664
|
}
|
|
2454
2665
|
}
|
|
@@ -2473,7 +2684,8 @@ const createUserTools = (ctx) => {
|
|
|
2473
2684
|
openWorldHint: true
|
|
2474
2685
|
},
|
|
2475
2686
|
handler: async () => {
|
|
2476
|
-
const
|
|
2687
|
+
const token = await getToken();
|
|
2688
|
+
const { user } = await zendeskGet(subdomain, token, "/users/me");
|
|
2477
2689
|
return { content: [{
|
|
2478
2690
|
type: "text",
|
|
2479
2691
|
text: formatUser(user)
|
|
@@ -2499,7 +2711,8 @@ const createUserTools = (ctx) => {
|
|
|
2499
2711
|
},
|
|
2500
2712
|
handler: async (params) => {
|
|
2501
2713
|
const { query, per_page, page } = params;
|
|
2502
|
-
const
|
|
2714
|
+
const token = await getToken();
|
|
2715
|
+
const response = await zendeskGet(subdomain, token, "/search", {
|
|
2503
2716
|
query: `type:user ${query}`,
|
|
2504
2717
|
...buildOffsetParams(per_page, page)
|
|
2505
2718
|
});
|
|
@@ -2524,7 +2737,8 @@ const createUserTools = (ctx) => {
|
|
|
2524
2737
|
},
|
|
2525
2738
|
handler: async (params) => {
|
|
2526
2739
|
const { user_id } = params;
|
|
2527
|
-
const
|
|
2740
|
+
const token = await getToken();
|
|
2741
|
+
const { user } = await zendeskGet(subdomain, token, `/users/${user_id}`);
|
|
2528
2742
|
return { content: [{
|
|
2529
2743
|
type: "text",
|
|
2530
2744
|
text: formatUser(user)
|
|
@@ -2546,7 +2760,8 @@ const createUserTools = (ctx) => {
|
|
|
2546
2760
|
},
|
|
2547
2761
|
handler: async (params) => {
|
|
2548
2762
|
const { organization_id } = params;
|
|
2549
|
-
const
|
|
2763
|
+
const token = await getToken();
|
|
2764
|
+
const { organization } = await zendeskGet(subdomain, token, `/organizations/${organization_id}`);
|
|
2550
2765
|
return { content: [{
|
|
2551
2766
|
type: "text",
|
|
2552
2767
|
text: formatOrganization(organization)
|
|
@@ -2571,7 +2786,8 @@ const createUserTools = (ctx) => {
|
|
|
2571
2786
|
},
|
|
2572
2787
|
handler: async (params) => {
|
|
2573
2788
|
const { page_size, cursor } = params;
|
|
2574
|
-
const
|
|
2789
|
+
const token = await getToken();
|
|
2790
|
+
const response = await zendeskGet(subdomain, token, "/organizations", buildCursorParams(page_size, cursor));
|
|
2575
2791
|
const organizations = response.organizations ?? [];
|
|
2576
2792
|
return { content: [{
|
|
2577
2793
|
type: "text",
|
|
@@ -2899,7 +3115,8 @@ const sendJsonRpcError = (res, status, code, message, headers = {}) => {
|
|
|
2899
3115
|
}));
|
|
2900
3116
|
};
|
|
2901
3117
|
const sendUnauthorized = (res, resource) => {
|
|
2902
|
-
|
|
3118
|
+
const wwwAuthenticate = `Bearer resource_metadata="${resource}/.well-known/oauth-protected-resource", error="invalid_token", error_description="${MISSING_BEARER_MESSAGE}"`;
|
|
3119
|
+
sendJsonRpcError(res, 401, -32e3, MISSING_BEARER_MESSAGE, { "WWW-Authenticate": wwwAuthenticate });
|
|
2903
3120
|
};
|
|
2904
3121
|
const readJsonBody = (req, maxBodyBytes) => new Promise((resolve) => {
|
|
2905
3122
|
const chunks = [];
|