@fruggr/zendesk-mcp-server 3.5.3 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/THIRD-PARTY-NOTICES.md +2 -2
- package/dist/{http-DLa2DU80.js → http-D-Xy9BWD.js} +452 -392
- package/dist/index.js +157 -97
- package/dist/sbom.cdx.json +10 -10
- package/package.json +10 -8
package/dist/index.js
CHANGED
|
@@ -7630,6 +7630,14 @@ const isStartupError = (err) => err instanceof Error && err.name === "StartupErr
|
|
|
7630
7630
|
/** The errno code of a failed filesystem call, for the message. */
|
|
7631
7631
|
const errnoCode = (err) => err?.code ?? "unknown error";
|
|
7632
7632
|
//#endregion
|
|
7633
|
+
//#region src/utils/compare.ts
|
|
7634
|
+
/**
|
|
7635
|
+
* Orders strings by UTF-16 code unit, which is what a bare `.sort()` does.
|
|
7636
|
+
* Not `localeCompare`: an order that feeds a hash or a stored key must not move
|
|
7637
|
+
* with the runtime's locale data.
|
|
7638
|
+
*/
|
|
7639
|
+
const compareCodeUnits = (a, b) => Number(a > b) - Number(a < b);
|
|
7640
|
+
//#endregion
|
|
7633
7641
|
//#region src/utils/validation.ts
|
|
7634
7642
|
const formatPath = (path) => path.map(String).join(".");
|
|
7635
7643
|
const formatIssue$1 = (issue) => issue.path.length > 0 ? `${formatPath(issue.path)}: ${issue.message}` : issue.message;
|
|
@@ -7651,7 +7659,7 @@ const keysAt = (node, path) => {
|
|
|
7651
7659
|
const child = typeof head === "number" ? def.element : def.shape?.[String(head)];
|
|
7652
7660
|
return child && keysAt(child, rest);
|
|
7653
7661
|
};
|
|
7654
|
-
const keyList = (keys) => [...keys].sort().join(", ") || "(none)";
|
|
7662
|
+
const keyList = (keys) => [...keys].sort(compareCodeUnits).join(", ") || "(none)";
|
|
7655
7663
|
/** One sentence per object holding unknown keys, the top level first. */
|
|
7656
7664
|
const unknownKeysMessage = (schema, issues, topLevelKeys) => [...issues].sort((a, b) => a.path.length - b.path.length).map(({ path, keys }) => {
|
|
7657
7665
|
if (path.length === 0) return `Unknown parameter(s): ${keys.join(", ")}. Valid parameters: ${topLevelKeys}.`;
|
|
@@ -30076,7 +30084,7 @@ const createHelpCenterTools = (env) => {
|
|
|
30076
30084
|
namespace: "help_center",
|
|
30077
30085
|
readOnly: true,
|
|
30078
30086
|
title: "List Permission Groups",
|
|
30079
|
-
description: "List all Guide permission groups. Use this to find the permission_group_id required when creating articles.",
|
|
30087
|
+
description: "List all Guide permission groups, which control who can edit and publish an article, with each group's id, name and whether it is built-in. Use this to find the permission_group_id required when creating articles with create_article, or to change it with update_article; to control who can view an article instead, use list_user_segments. Zendesk restricts this list to Guide admins and Help Center managers: without that role the call fails, and you can reuse the permission_group_id of an existing article read with get_article.",
|
|
30080
30088
|
inputSchema: object({}),
|
|
30081
30089
|
annotations: {
|
|
30082
30090
|
readOnlyHint: true,
|
|
@@ -30141,7 +30149,7 @@ const createHelpCenterTools = (env) => {
|
|
|
30141
30149
|
namespace: "help_center",
|
|
30142
30150
|
readOnly: false,
|
|
30143
30151
|
title: "Update Help Center Article",
|
|
30144
|
-
description: "Update article metadata only (draft, promoted, labels as label_names, content tags as content_tag_ids, visibility as permission_group_id / user_segment_id, section_id, sort position, etc.) and return the updated article. Does NOT update content (title, body) — use update_article_translation for that.",
|
|
30152
|
+
description: "Update article metadata only (draft, promoted, labels as label_names, content tags as content_tag_ids, visibility as permission_group_id / user_segment_id, section_id, sort position, author, etc.) and return the updated article; fields you omit keep their value. Does NOT update content (title, body) — use update_article_translation for that, or update_article_section to edit one section. To put an article first or last in its section, prefer reorder_article: a plain position write is ambiguous while articles share position 0. To remove an article from the Help Center, use archive_article (a Guide admin can restore it); draft: true only unpublishes it.",
|
|
30145
30153
|
inputSchema: object({
|
|
30146
30154
|
article_id: number$1().int().describe("Article ID — the numeric id of the article to update. Obtain it from list_articles or search_articles."),
|
|
30147
30155
|
draft: boolean().optional().describe("Set true to unpublish the article (revert to draft) or false to publish it."),
|
|
@@ -30328,7 +30336,7 @@ const createHelpCenterTools = (env) => {
|
|
|
30328
30336
|
namespace: "help_center",
|
|
30329
30337
|
readOnly: true,
|
|
30330
30338
|
title: "List Article Labels",
|
|
30331
|
-
description: "List all article labels. Labels improve Help Center search ranking and are not visible to end users. Attach them to an article through the label_names parameter of create_article or update_article.",
|
|
30339
|
+
description: "List all article labels, with each label's id and name. Labels improve Help Center search ranking and are not visible to end users. Attach them to an article through the label_names parameter of create_article or update_article, reusing an existing label rather than coining a near-duplicate. Labels are not content tags: for the end-user-visible tags readers browse by, use list_content_tags.",
|
|
30332
30340
|
inputSchema: object({ ...brandIdPart }),
|
|
30333
30341
|
annotations: {
|
|
30334
30342
|
readOnlyHint: true,
|
|
@@ -30352,7 +30360,7 @@ const createHelpCenterTools = (env) => {
|
|
|
30352
30360
|
namespace: "help_center",
|
|
30353
30361
|
readOnly: true,
|
|
30354
30362
|
title: "List User Segments",
|
|
30355
|
-
description: "List all user segments
|
|
30363
|
+
description: "List all Help Center user segments, which control article visibility (who can view an article), with each segment's id, name, user type and whether it is built-in. Use the id as user_segment_id when creating or updating articles with create_article or update_article; to control who can edit an article instead, use list_permission_groups. Zendesk restricts this list to Guide admins and Help Center managers: without that role the call fails, and you can reuse the user_segment_id of an existing article from get_article, or omit it on create_article to make the article visible to everyone.",
|
|
30356
30364
|
inputSchema: object({}),
|
|
30357
30365
|
annotations: {
|
|
30358
30366
|
readOnlyHint: true,
|
|
@@ -30832,10 +30840,11 @@ const fetchAllPages = async ({ subdomain, token, tool, path, extract, params = {
|
|
|
30832
30840
|
const items = [];
|
|
30833
30841
|
let page = 1;
|
|
30834
30842
|
while (true) {
|
|
30835
|
-
const
|
|
30843
|
+
const query = {
|
|
30836
30844
|
...params,
|
|
30837
30845
|
...buildOffsetParams(100, page)
|
|
30838
|
-
}
|
|
30846
|
+
};
|
|
30847
|
+
const response = await withForbiddenGuidance(tool, `GET ${path}`, () => zendeskGet(subdomain, token, path, query), forbiddenHint);
|
|
30839
30848
|
items.push(...extract(response) ?? []);
|
|
30840
30849
|
onPage?.(response);
|
|
30841
30850
|
if (!response.next_page) return items;
|
|
@@ -31097,8 +31106,8 @@ const createRequestTools = (env) => {
|
|
|
31097
31106
|
body: string().min(1).describe("The request itself: what happened, what was expected, and any steps to reproduce. Becomes the first public comment on the ticket."),
|
|
31098
31107
|
form_id: number$1().int().optional().describe("Form id chosen from list_request_forms, deciding which questions apply. Omit to use the account default form."),
|
|
31099
31108
|
custom_fields: array(object({
|
|
31100
|
-
id: number$1().int(),
|
|
31101
|
-
value: unknown()
|
|
31109
|
+
id: number$1().int().describe("Numeric id of a question on the chosen form, from get_request_form."),
|
|
31110
|
+
value: unknown().describe("The answer, typed after the question: the option tag (a string, never a number) for a dropdown, an array of option tags for a multiselect, true or false for a checkbox, text otherwise.")
|
|
31102
31111
|
}).strict()).optional().describe("Answers to the form's own questions, as { id, value } pairs. Take both the ids and the accepted values from get_request_form; an id this form does not carry, or a value a dropdown does not offer, is refused here because Zendesk would drop it without an error. Option values are the tag strings, never numbers."),
|
|
31103
31112
|
attachments: attachmentsParam("Files to attach to the request, such as a screenshot or a log, with their content base64-encoded.")
|
|
31104
31113
|
}),
|
|
@@ -31273,70 +31282,6 @@ const createRequestTools = (env) => {
|
|
|
31273
31282
|
];
|
|
31274
31283
|
};
|
|
31275
31284
|
//#endregion
|
|
31276
|
-
//#region src/tools/search.ts
|
|
31277
|
-
const formatSearchResult = (result) => {
|
|
31278
|
-
const lines = [`## [${result["result_type"]}] #${result["id"]}`];
|
|
31279
|
-
if (result["subject"]) lines.push(`**Subject**: ${result["subject"]}`);
|
|
31280
|
-
if (result["name"]) lines.push(`**Name**: ${result["name"]}`);
|
|
31281
|
-
if (result["title"]) lines.push(`**Title**: ${result["title"]}`);
|
|
31282
|
-
if (result["email"]) lines.push(`**Email**: ${result["email"]}`);
|
|
31283
|
-
if (result["status"]) lines.push(`**Status**: ${result["status"]}`);
|
|
31284
|
-
if (result["description"]) lines.push(descriptionExcerpt(String(result["description"])));
|
|
31285
|
-
return lines.join("\n");
|
|
31286
|
-
};
|
|
31287
|
-
const createSearchTools = (env) => {
|
|
31288
|
-
const { subdomain } = env;
|
|
31289
|
-
return [{
|
|
31290
|
-
name: "search",
|
|
31291
|
-
namespace: "tickets",
|
|
31292
|
-
readOnly: true,
|
|
31293
|
-
title: "Zendesk Unified Search",
|
|
31294
|
-
description: "Search across tickets, users, and organizations. Supports filters like \"type:ticket status:open\", \"type:user role:agent\". Returns total count and paginated results (25 per page by default, up to 100); a ticket description is cut to a 200-character excerpt, which get_ticket returns in full. Organization results include name and ID only — use get_organization for full details (tags, domains, details).",
|
|
31295
|
-
inputSchema: object({
|
|
31296
|
-
query: string().min(1).describe("Zendesk search query. Supports type/status/role filters (e.g. \"type:ticket status:open\", \"type:user role:agent\") and free text; omit a type filter to search tickets, users and organizations at once."),
|
|
31297
|
-
per_page: number$1().int().min(1).max(100).default(25).describe(PER_PAGE_DESC),
|
|
31298
|
-
page: number$1().int().min(1).default(1).describe(PAGE_DESC)
|
|
31299
|
-
}),
|
|
31300
|
-
annotations: {
|
|
31301
|
-
readOnlyHint: true,
|
|
31302
|
-
destructiveHint: false,
|
|
31303
|
-
idempotentHint: true,
|
|
31304
|
-
openWorldHint: true
|
|
31305
|
-
},
|
|
31306
|
-
handler: async (params, { caller }) => {
|
|
31307
|
-
const { query, per_page, page } = params;
|
|
31308
|
-
const token = await caller.getToken();
|
|
31309
|
-
const response = await zendeskGet(subdomain, token, "/search", {
|
|
31310
|
-
query,
|
|
31311
|
-
...buildOffsetParams(per_page, page)
|
|
31312
|
-
});
|
|
31313
|
-
const results = response.results ?? [];
|
|
31314
|
-
const meta = extractSearchPaginationMeta(response, per_page, page);
|
|
31315
|
-
const header = (m) => `Total: ${m.count} | Page ${page} (${results.length} results)${m.has_more ? ` | Next page: ${m.next_page}` : ""}`;
|
|
31316
|
-
return { content: [{
|
|
31317
|
-
type: "text",
|
|
31318
|
-
text: formatFittingList(results, formatSearchResult, meta, offsetResume(page, per_page), header)
|
|
31319
|
-
}] };
|
|
31320
|
-
}
|
|
31321
|
-
}];
|
|
31322
|
-
};
|
|
31323
|
-
//#endregion
|
|
31324
|
-
//#region src/utils/elicitation.ts
|
|
31325
|
-
/**
|
|
31326
|
-
* Whether a client can answer a form elicitation. A bare `elicitation: {}`
|
|
31327
|
-
* predates the modes and counts as form support (spec rule the SDK follows).
|
|
31328
|
-
*/
|
|
31329
|
-
const supportsFormElicitation = (capabilities) => {
|
|
31330
|
-
const elicitation = capabilities?.elicitation;
|
|
31331
|
-
if (!elicitation) return false;
|
|
31332
|
-
return Object.keys(elicitation).length === 0 || "form" in elicitation;
|
|
31333
|
-
};
|
|
31334
|
-
const inputRequiredError = (result) => Object.assign(/* @__PURE__ */ new Error("The tool needs input from the user before it can answer."), {
|
|
31335
|
-
name: "InputRequiredError",
|
|
31336
|
-
inputRequired: result
|
|
31337
|
-
});
|
|
31338
|
-
const isInputRequiredError = (err) => err instanceof Error && err.name === "InputRequiredError" && "inputRequired" in err;
|
|
31339
|
-
//#endregion
|
|
31340
31285
|
//#region src/utils/sla-urgency.ts
|
|
31341
31286
|
/** Most urgent first: the order alerts are grouped and ranked in. */
|
|
31342
31287
|
const URGENCY_LEVELS = [
|
|
@@ -31543,6 +31488,95 @@ const fetchScope = async (scope, get, userId) => {
|
|
|
31543
31488
|
});
|
|
31544
31489
|
return mergeSnapshots(snapshots);
|
|
31545
31490
|
};
|
|
31491
|
+
//#endregion
|
|
31492
|
+
//#region src/utils/group-me.ts
|
|
31493
|
+
const GROUP_ME = /(^|\s)(-?)group:(?:me|"me")(?=\s|$)/gi;
|
|
31494
|
+
const NOT_A_VALUE = "group:me is not a Zendesk search value: \"me\" works only for user keywords (assignee:, requester:, submitter:), and Zendesk would silently match nothing.";
|
|
31495
|
+
/**
|
|
31496
|
+
* Reads `group:me` in a search query as the caller's group. With exactly one
|
|
31497
|
+
* group it becomes `group:<id>`, with a note to show; with none or several it
|
|
31498
|
+
* throws, naming the groups, since one offset-paginated search cannot cover
|
|
31499
|
+
* several groups. Without `group:me`, nothing is looked up.
|
|
31500
|
+
*/
|
|
31501
|
+
const resolveGroupMe = async (query, get, userId) => {
|
|
31502
|
+
if (query.match(GROUP_ME) === null) return { query };
|
|
31503
|
+
const groupIds = await readGroupIds(get, await userId());
|
|
31504
|
+
const [only, ...others] = groupIds;
|
|
31505
|
+
if (only === void 0) throw new Error(`${NOT_A_VALUE} You belong to no group, so there is no group queue to search: drop group:me, or name a group with group:<id>.`);
|
|
31506
|
+
if (others.length === 0) return {
|
|
31507
|
+
query: query.replace(GROUP_ME, (_, lead, negation) => `${lead}${negation}group:${only}`),
|
|
31508
|
+
note: `Note: group:me was read as group:${only}, your only group.`
|
|
31509
|
+
};
|
|
31510
|
+
const advice = [...query.matchAll(GROUP_ME)].every(([, , negation]) => negation === "-") ? `exclude a group with -group:<id> (e.g. -group:${only})` : `search one with group:<id> (e.g. group:${only}), one call per group`;
|
|
31511
|
+
throw new Error(`${NOT_A_VALUE} You belong to groups ${groupIds.join(", ")}: ${advice}.`);
|
|
31512
|
+
};
|
|
31513
|
+
/** `resolveGroupMe` for a tool call: the caller comes from the SLA service when wired, else `/users/me`. */
|
|
31514
|
+
const resolveCallerGroupMe = (query, subdomain, token, sla) => resolveGroupMe(query, (path, params) => zendeskGet(subdomain, token, path, params), sla?.userId ?? (() => zendeskGet(subdomain, token, "/users/me").then(({ user }) => user.id)));
|
|
31515
|
+
//#endregion
|
|
31516
|
+
//#region src/tools/search.ts
|
|
31517
|
+
const formatSearchResult = (result) => {
|
|
31518
|
+
const lines = [`## [${result["result_type"]}] #${result["id"]}`];
|
|
31519
|
+
if (result["subject"]) lines.push(`**Subject**: ${result["subject"]}`);
|
|
31520
|
+
if (result["name"]) lines.push(`**Name**: ${result["name"]}`);
|
|
31521
|
+
if (result["title"]) lines.push(`**Title**: ${result["title"]}`);
|
|
31522
|
+
if (result["email"]) lines.push(`**Email**: ${result["email"]}`);
|
|
31523
|
+
if (result["status"]) lines.push(`**Status**: ${result["status"]}`);
|
|
31524
|
+
if (result["description"]) lines.push(descriptionExcerpt(String(result["description"])));
|
|
31525
|
+
return lines.join("\n");
|
|
31526
|
+
};
|
|
31527
|
+
const createSearchTools = (env) => {
|
|
31528
|
+
const { subdomain } = env;
|
|
31529
|
+
return [{
|
|
31530
|
+
name: "search",
|
|
31531
|
+
namespace: "tickets",
|
|
31532
|
+
readOnly: true,
|
|
31533
|
+
title: "Zendesk Unified Search",
|
|
31534
|
+
description: "Search across tickets, users, and organizations. Supports filters like \"type:ticket status:open\", \"type:user role:agent\". Returns total count and paginated results (25 per page by default, up to 100); a ticket description is cut to a 200-character excerpt, which get_ticket returns in full. Organization results include name and ID only — use get_organization for full details (tags, domains, details).",
|
|
31535
|
+
inputSchema: object({
|
|
31536
|
+
query: string().min(1).describe("Zendesk search query. Supports type/status/role filters (e.g. \"type:ticket status:open\", \"type:user role:agent\") and free text; omit a type filter to search tickets, users and organizations at once. \"me\" is a value of the user keywords only (assignee:, requester:, submitter:); group:me is read as your group when you belong to exactly one, else the call fails naming your groups, so use group:<id>."),
|
|
31537
|
+
per_page: number$1().int().min(1).max(100).default(25).describe(PER_PAGE_DESC),
|
|
31538
|
+
page: number$1().int().min(1).default(1).describe(PAGE_DESC)
|
|
31539
|
+
}),
|
|
31540
|
+
annotations: {
|
|
31541
|
+
readOnlyHint: true,
|
|
31542
|
+
destructiveHint: false,
|
|
31543
|
+
idempotentHint: true,
|
|
31544
|
+
openWorldHint: true
|
|
31545
|
+
},
|
|
31546
|
+
handler: async (params, { caller }) => {
|
|
31547
|
+
const { query, per_page, page } = params;
|
|
31548
|
+
const token = await caller.getToken();
|
|
31549
|
+
const resolved = await resolveCallerGroupMe(query, subdomain, token, caller.sla);
|
|
31550
|
+
const response = await zendeskGet(subdomain, token, "/search", {
|
|
31551
|
+
query: resolved.query,
|
|
31552
|
+
...buildOffsetParams(per_page, page)
|
|
31553
|
+
});
|
|
31554
|
+
const results = response.results ?? [];
|
|
31555
|
+
const meta = extractSearchPaginationMeta(response, per_page, page);
|
|
31556
|
+
const header = (m) => [resolved.note, `Total: ${m.count} | Page ${page} (${results.length} results)${m.has_more ? ` | Next page: ${m.next_page}` : ""}`].filter(Boolean).join("\n");
|
|
31557
|
+
return { content: [{
|
|
31558
|
+
type: "text",
|
|
31559
|
+
text: formatFittingList(results, formatSearchResult, meta, offsetResume(page, per_page), header)
|
|
31560
|
+
}] };
|
|
31561
|
+
}
|
|
31562
|
+
}];
|
|
31563
|
+
};
|
|
31564
|
+
//#endregion
|
|
31565
|
+
//#region src/utils/elicitation.ts
|
|
31566
|
+
/**
|
|
31567
|
+
* Whether a client can answer a form elicitation. A bare `elicitation: {}`
|
|
31568
|
+
* predates the modes and counts as form support (spec rule the SDK follows).
|
|
31569
|
+
*/
|
|
31570
|
+
const supportsFormElicitation = (capabilities) => {
|
|
31571
|
+
const elicitation = capabilities?.elicitation;
|
|
31572
|
+
if (!elicitation) return false;
|
|
31573
|
+
return Object.keys(elicitation).length === 0 || "form" in elicitation;
|
|
31574
|
+
};
|
|
31575
|
+
const inputRequiredError = (result) => Object.assign(/* @__PURE__ */ new Error("The tool needs input from the user before it can answer."), {
|
|
31576
|
+
name: "InputRequiredError",
|
|
31577
|
+
inputRequired: result
|
|
31578
|
+
});
|
|
31579
|
+
const isInputRequiredError = (err) => err instanceof Error && err.name === "InputRequiredError" && "inputRequired" in err;
|
|
31546
31580
|
/** Where the thresholds in force come from, as the report states it. */
|
|
31547
31581
|
const thresholdsSource = (sla, prefs, overridden) => {
|
|
31548
31582
|
if (overridden) return "overridden for this call";
|
|
@@ -31761,6 +31795,7 @@ const fetchAllTicketComments = async (subdomain, token, ticketId) => {
|
|
|
31761
31795
|
const all = [];
|
|
31762
31796
|
let cursor;
|
|
31763
31797
|
let pages = 0;
|
|
31798
|
+
let truncated = false;
|
|
31764
31799
|
while (pages < MAX_COMMENT_PAGES) {
|
|
31765
31800
|
const response = await zendeskGet(subdomain, token, `/tickets/${ticketId}/comments`, {
|
|
31766
31801
|
...buildCursorParams(100, cursor),
|
|
@@ -31770,9 +31805,14 @@ const fetchAllTicketComments = async (subdomain, token, ticketId) => {
|
|
|
31770
31805
|
pages += 1;
|
|
31771
31806
|
if (!response.meta?.has_more || !response.meta?.after_cursor) break;
|
|
31772
31807
|
cursor = response.meta.after_cursor;
|
|
31808
|
+
truncated = pages === MAX_COMMENT_PAGES;
|
|
31773
31809
|
}
|
|
31774
|
-
return
|
|
31810
|
+
return {
|
|
31811
|
+
comments: all,
|
|
31812
|
+
truncated
|
|
31813
|
+
};
|
|
31775
31814
|
};
|
|
31815
|
+
const COMMENT_CAP_NOTICE = `Comment scan stopped at its page cap (${MAX_COMMENT_PAGES} pages, COMMENT_MAX_PAGES): attachments on any later comments are not listed. Pass attachment_ids taken from list_ticket_comments to fetch them.`;
|
|
31776
31816
|
const commentPageMeta = (response, itemCount) => extractPaginationMeta({
|
|
31777
31817
|
...response.meta && { meta: response.meta },
|
|
31778
31818
|
...response.count !== void 0 && { count: response.count }
|
|
@@ -32074,6 +32114,10 @@ const createTicketTools = (env) => {
|
|
|
32074
32114
|
const ids = array(number$1().int().positive());
|
|
32075
32115
|
return (max === void 0 ? ids : ids.max(max)).optional().describe(description);
|
|
32076
32116
|
};
|
|
32117
|
+
const customFieldEntry = object({
|
|
32118
|
+
id: number$1().int().describe("Numeric id of the ticket field to set, as listed by list_ticket_fields."),
|
|
32119
|
+
value: unknown().describe("Value typed after the field: the option tag (a string) for a dropdown, an array of option tags for a multiselect, true or false for a checkbox, text or a number for a text or numeric field. For any other type (a date, a lookup), follow the type list_ticket_fields reports; it also lists each dropdown's accepted tags.")
|
|
32120
|
+
}).strict();
|
|
32077
32121
|
return [
|
|
32078
32122
|
{
|
|
32079
32123
|
name: "get_ticket",
|
|
@@ -32211,10 +32255,10 @@ const createTicketTools = (env) => {
|
|
|
32211
32255
|
namespace: "tickets",
|
|
32212
32256
|
readOnly: true,
|
|
32213
32257
|
title: "Get Zendesk Ticket Attachments",
|
|
32214
|
-
description: "
|
|
32258
|
+
description: "Download the files attached to a ticket's comments so you can look at them: images come back embedded inline, other files as text references (file name, id, content type, size, download URL). Use it to see an attached image, or a file's name, size and download URL (only images come back with their content); to learn which files a comment carries, list_ticket_comments already shows their ids. Pass those ids as attachment_ids to fetch only them, or omit it to collect the attachments of the ticket's comments, read up to a page cap (10 pages of 100 comments by default); past it the response says later comments were not scanned, and attachment_ids fetches their files. An image over the per-image size limit or past the embedded-image cap comes back as a reference saying why, and once the response size budget is spent the remaining files are counted as omitted.",
|
|
32215
32259
|
inputSchema: object({
|
|
32216
32260
|
ticket_id: number$1().int().describe("Ticket ID — the numeric id of the ticket whose attachments to fetch. Obtain it from search_tickets or list_tickets."),
|
|
32217
|
-
attachment_ids: array(number$1().int()).optional().describe("Attachment IDs to fetch directly (e.g. extracted from a previous list_ticket_comments or get_ticket(include_comments=true) call). When provided, skips the comments fetch entirely. When omitted,
|
|
32261
|
+
attachment_ids: array(number$1().int()).optional().describe("Attachment IDs to fetch directly (e.g. extracted from a previous list_ticket_comments or get_ticket(include_comments=true) call). When provided, skips the comments fetch entirely. When omitted, the attachments of the ticket's comments are returned, read up to a page cap: on a very long thread the scan stops there, and the response says so.")
|
|
32218
32262
|
}),
|
|
32219
32263
|
annotations: {
|
|
32220
32264
|
readOnlyHint: true,
|
|
@@ -32225,16 +32269,31 @@ const createTicketTools = (env) => {
|
|
|
32225
32269
|
handler: async (params, { caller }) => {
|
|
32226
32270
|
const { ticket_id, attachment_ids } = params;
|
|
32227
32271
|
const token = await caller.getToken();
|
|
32228
|
-
const attachments = attachment_ids && attachment_ids.length > 0 ?
|
|
32272
|
+
const { attachments, truncated } = attachment_ids && attachment_ids.length > 0 ? {
|
|
32273
|
+
attachments: await fetchAttachmentsByIds(subdomain, token, attachment_ids),
|
|
32274
|
+
truncated: false
|
|
32275
|
+
} : await fetchAllTicketComments(subdomain, token, ticket_id).then((walk) => ({
|
|
32276
|
+
attachments: walk.comments.flatMap((c) => c.attachments ?? []),
|
|
32277
|
+
truncated: walk.truncated
|
|
32278
|
+
}));
|
|
32229
32279
|
if (attachments.length === 0) return { content: [{
|
|
32230
32280
|
type: "text",
|
|
32231
|
-
text: `No attachments found on ticket #${ticket_id}.`
|
|
32281
|
+
text: truncated ? `No attachments found on the first ${MAX_COMMENT_PAGES} pages of comments of ticket #${ticket_id}. ${COMMENT_CAP_NOTICE}` : `No attachments found on ticket #${ticket_id}.`
|
|
32232
32282
|
}] };
|
|
32233
32283
|
const header = {
|
|
32234
32284
|
type: "text",
|
|
32235
32285
|
text: `# Attachments for ticket #${ticket_id} (${attachments.length} total)`
|
|
32236
32286
|
};
|
|
32237
|
-
|
|
32287
|
+
const notice = truncated ? [{
|
|
32288
|
+
type: "text",
|
|
32289
|
+
text: COMMENT_CAP_NOTICE
|
|
32290
|
+
}] : [];
|
|
32291
|
+
const blocks = await collectAttachmentBlocks(subdomain, token, attachments, [header, ...notice].reduce((total, block) => total + blockCost(block), 1));
|
|
32292
|
+
return { content: [
|
|
32293
|
+
header,
|
|
32294
|
+
...notice,
|
|
32295
|
+
...blocks
|
|
32296
|
+
] };
|
|
32238
32297
|
}
|
|
32239
32298
|
},
|
|
32240
32299
|
{
|
|
@@ -32244,7 +32303,7 @@ const createTicketTools = (env) => {
|
|
|
32244
32303
|
title: "Search Zendesk Tickets",
|
|
32245
32304
|
description: `Search tickets using Zendesk query syntax, returning each result with its live SLA state (per-metric stage and breach countdown) when an SLA policy applies. Examples: "status:open assignee:me", "priority:urgent ticket_type:incident". Returns total count, so queue triage like "breaching today" works without a per-ticket fetch. When the operator turned SLA push on, each SLA block also carries an Urgency line (BREACHED, CRITICAL or WARNING) against the caller's alert thresholds. ${EXCERPT_NOTE}`,
|
|
32246
32305
|
inputSchema: object({
|
|
32247
|
-
query: string().min(1).describe("Zendesk ticket search query — field filters like \"status:open\", \"assignee:me\", \"priority:urgent ticket_type:incident\", combined with free text. A \"type:ticket\" scope is added automatically, so filter the ticket kind with ticket_type: (e.g. ticket_type:incident), never type: (which the API rejects here)."),
|
|
32306
|
+
query: string().min(1).describe("Zendesk ticket search query — field filters like \"status:open\", \"assignee:me\", \"priority:urgent ticket_type:incident\", combined with free text. A \"type:ticket\" scope is added automatically, so filter the ticket kind with ticket_type: (e.g. ticket_type:incident), never type: (which the API rejects here). \"me\" is a value of the user keywords only (assignee:, requester:, submitter:); group:me is read as your group when you belong to exactly one, else the call fails naming your groups, so use group:<id>."),
|
|
32248
32307
|
per_page: number$1().int().min(1).max(100).default(25).describe(PER_PAGE_DESC),
|
|
32249
32308
|
page: number$1().int().min(1).default(1).describe(PAGE_DESC)
|
|
32250
32309
|
}),
|
|
@@ -32257,15 +32316,22 @@ const createTicketTools = (env) => {
|
|
|
32257
32316
|
handler: async (params, { caller }) => {
|
|
32258
32317
|
const { query, per_page, page } = params;
|
|
32259
32318
|
const token = await caller.getToken();
|
|
32260
|
-
const
|
|
32261
|
-
query
|
|
32262
|
-
|
|
32263
|
-
|
|
32264
|
-
|
|
32319
|
+
const search = async () => {
|
|
32320
|
+
const resolved = await resolveCallerGroupMe(query, subdomain, token, caller.sla);
|
|
32321
|
+
return {
|
|
32322
|
+
resolved,
|
|
32323
|
+
response: await zendeskGet(subdomain, token, "/search", {
|
|
32324
|
+
query: `type:ticket ${resolved.query}`,
|
|
32325
|
+
include: "tickets(slas)",
|
|
32326
|
+
...buildOffsetParams(per_page, page)
|
|
32327
|
+
})
|
|
32328
|
+
};
|
|
32329
|
+
};
|
|
32330
|
+
const [{ resolved, response }, urgency] = await Promise.all([search(), urgencyRenderer(caller.sla)]);
|
|
32265
32331
|
const formatTicketWithSla = (ticket) => formatTicketSummary(ticket) + formatSlaBlock(ticket.slas, urgency(ticket.slas));
|
|
32266
32332
|
return { content: [{
|
|
32267
32333
|
type: "text",
|
|
32268
|
-
text: formatFittingList(response.results ?? [], formatTicketWithSla, extractSearchPaginationMeta(response, per_page, page), offsetResume(page, per_page))
|
|
32334
|
+
text: formatFittingList(response.results ?? [], formatTicketWithSla, extractSearchPaginationMeta(response, per_page, page), offsetResume(page, per_page), (meta) => [resolved.note, formatPagination(meta)].filter(Boolean).join("\n"))
|
|
32269
32335
|
}] };
|
|
32270
32336
|
}
|
|
32271
32337
|
},
|
|
@@ -32293,10 +32359,7 @@ const createTicketTools = (env) => {
|
|
|
32293
32359
|
assignee_id: number$1().int().optional().describe("User id of the agent to assign the ticket to."),
|
|
32294
32360
|
group_id: number$1().int().optional().describe("Id of the group to assign the ticket to."),
|
|
32295
32361
|
tags: array(string()).optional().describe("Tags to set on the new ticket. Each tag is a single lowercase token (join multi-word tags with an underscore). Use manage_tags later to add or remove individual tags."),
|
|
32296
|
-
custom_fields: array(
|
|
32297
|
-
id: number$1().int(),
|
|
32298
|
-
value: unknown()
|
|
32299
|
-
}).strict()).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.")
|
|
32362
|
+
custom_fields: array(customFieldEntry).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.")
|
|
32300
32363
|
}),
|
|
32301
32364
|
annotations: {
|
|
32302
32365
|
readOnlyHint: false,
|
|
@@ -32350,10 +32413,7 @@ const createTicketTools = (env) => {
|
|
|
32350
32413
|
group_id: number$1().int().optional().describe("Id of the group to assign the ticket to."),
|
|
32351
32414
|
subject: string().optional().describe("New subject line for the ticket; replaces the current subject when provided."),
|
|
32352
32415
|
tags: array(string()).optional().describe("Replaces the full tag set on the ticket. Use manage_tags for incremental add/remove."),
|
|
32353
|
-
custom_fields: array(
|
|
32354
|
-
id: number$1().int(),
|
|
32355
|
-
value: unknown()
|
|
32356
|
-
}).strict()).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."),
|
|
32416
|
+
custom_fields: array(customFieldEntry).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."),
|
|
32357
32417
|
followers: object({
|
|
32358
32418
|
add: subscriberIdList("User ids to start following the ticket. Omit to only remove."),
|
|
32359
32419
|
remove: subscriberIdList("User ids to stop following the ticket. Omit to only add.")
|
|
@@ -33007,7 +33067,7 @@ const createUserTools = (env) => {
|
|
|
33007
33067
|
namespace: "users",
|
|
33008
33068
|
readOnly: true,
|
|
33009
33069
|
title: "Search Zendesk Users",
|
|
33010
|
-
description: "Search for users by name, email, or other criteria using Zendesk search query syntax
|
|
33070
|
+
description: "Search for users by name, email, or other criteria using Zendesk search query syntax, returning each match's id, name, email, role, active flag and organization id, with the total count. Use it to turn a person into the numeric user id other tools take, such as the followers of update_ticket; a name can match several users, so confirm the right one. With an id already in hand, call get_user to read that one user, and for your own account get_current_user. Prefer it over `search` when you want users only: `search` mixes in tickets and organizations unless the query says type:user.",
|
|
33011
33071
|
inputSchema: object({
|
|
33012
33072
|
query: string().min(1).describe("Zendesk user search query — free text matched against name and email, and/or field filters like \"email:jane@acme.com\", \"role:agent\", \"organization_id:123\". A \"type:user\" scope is added automatically."),
|
|
33013
33073
|
per_page: number$1().int().min(1).max(100).default(100).describe(PER_PAGE_DESC),
|
|
@@ -35136,7 +35196,7 @@ const main = async () => {
|
|
|
35136
35196
|
logger,
|
|
35137
35197
|
cleanup: async () => (await starting?.catch(() => void 0))?.close()
|
|
35138
35198
|
});
|
|
35139
|
-
const { startHttpTransport } = await import("./http-
|
|
35199
|
+
const { startHttpTransport } = await import("./http-D-Xy9BWD.js");
|
|
35140
35200
|
starting = startHttpTransport(config, logger);
|
|
35141
35201
|
await starting;
|
|
35142
35202
|
};
|
|
@@ -35146,4 +35206,4 @@ main().catch((error) => {
|
|
|
35146
35206
|
process.exit(1);
|
|
35147
35207
|
});
|
|
35148
35208
|
//#endregion
|
|
35149
|
-
export {
|
|
35209
|
+
export { STARTUP_DOCS as A, WebStandardStreamableHTTPServerTransport as C, OAuthError as D, verifyBearerToken as E, errnoCode as M, OAuthErrorCode as O, DEFAULT_MAX_REQUEST_BODY_SIZE as S, isLegacyRequest as T, zendeskGet as _, toolEnv as a, DEFAULT_CALLBACK_PORT as b, clearToken as c, resolveTokenPath as d, saveToken as f, createAllTools as g, silentLogger as h, defaultServerFactory as i, createStartupError as j, compareCodeUnits as k, configDir as l, createRequestStateCodec as m, listenedUris as n, watchGrantAlerts as o, signInRequiredError as p, createServerCaches as r, startSlaRuntime as s, createListenFeed as t, loadToken as u, REQUEST_TIMEOUT_MS as v, createMcpHandler as w, getOAuthUrls as x, deadlineSignal as y };
|
package/dist/sbom.cdx.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.6",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:081eda00-f41f-539e-8e22-325ebea4a10a",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
7
|
"component": {
|
|
8
8
|
"type": "application",
|
|
9
|
-
"bom-ref": "pkg:npm/%40fruggr/zendesk-mcp-server@3.
|
|
9
|
+
"bom-ref": "pkg:npm/%40fruggr/zendesk-mcp-server@3.7.0",
|
|
10
10
|
"name": "@fruggr/zendesk-mcp-server",
|
|
11
|
-
"version": "3.
|
|
12
|
-
"purl": "pkg:npm/%40fruggr/zendesk-mcp-server@3.
|
|
11
|
+
"version": "3.7.0",
|
|
12
|
+
"purl": "pkg:npm/%40fruggr/zendesk-mcp-server@3.7.0"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"components": [
|
|
@@ -927,10 +927,10 @@
|
|
|
927
927
|
},
|
|
928
928
|
{
|
|
929
929
|
"type": "library",
|
|
930
|
-
"bom-ref": "pkg:npm/jose@6.2.
|
|
930
|
+
"bom-ref": "pkg:npm/jose@6.2.13",
|
|
931
931
|
"name": "jose",
|
|
932
|
-
"version": "6.2.
|
|
933
|
-
"purl": "pkg:npm/jose@6.2.
|
|
932
|
+
"version": "6.2.13",
|
|
933
|
+
"purl": "pkg:npm/jose@6.2.13",
|
|
934
934
|
"licenses": [
|
|
935
935
|
{
|
|
936
936
|
"expression": "MIT"
|
|
@@ -1539,10 +1539,10 @@
|
|
|
1539
1539
|
},
|
|
1540
1540
|
{
|
|
1541
1541
|
"type": "library",
|
|
1542
|
-
"bom-ref": "pkg:npm/oidc-provider@9.
|
|
1542
|
+
"bom-ref": "pkg:npm/oidc-provider@9.13.0",
|
|
1543
1543
|
"name": "oidc-provider",
|
|
1544
|
-
"version": "9.
|
|
1545
|
-
"purl": "pkg:npm/oidc-provider@9.
|
|
1544
|
+
"version": "9.13.0",
|
|
1545
|
+
"purl": "pkg:npm/oidc-provider@9.13.0",
|
|
1546
1546
|
"licenses": [
|
|
1547
1547
|
{
|
|
1548
1548
|
"expression": "MIT"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fruggr/zendesk-mcp-server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"mcpName": "io.github.fruggr/zendesk-mcp-server",
|
|
5
5
|
"description": "Deep Zendesk MCP server for your AI assistant: search, draft, update and translate Help Center articles and manage Support tickets end to end — comments, triage and image attachments.",
|
|
6
6
|
"type": "module",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"dev:http": "tsx src/index.ts --transport http --host 127.0.0.1 --port 3000 --public-url http://localhost:3000 --mode all --namespace tickets --namespace help_center --namespace users --namespace requests",
|
|
22
22
|
"prepare": "npm run build && (lefthook install || true)",
|
|
23
23
|
"prepublishOnly": "npm run build",
|
|
24
|
-
"check": "
|
|
25
|
-
"check:fix": "
|
|
24
|
+
"check": "biome check --error-on-warnings .",
|
|
25
|
+
"check:fix": "biome check --write .",
|
|
26
26
|
"test": "vitest run",
|
|
27
27
|
"test:watch": "vitest watch",
|
|
28
28
|
"test:coverage": "vitest run --coverage",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"test:smoke": "pnpm build && node scripts/smoke-test.mjs",
|
|
34
34
|
"test:bundle": "pnpm build && vitest run --config vitest.bundle.config.ts",
|
|
35
35
|
"bundle:size": "pnpm build && node scripts/bundle-size.mjs",
|
|
36
|
-
"mcp:live": "tsx scripts/mcp-live.ts"
|
|
36
|
+
"mcp:live": "tsx scripts/mcp-live.ts",
|
|
37
|
+
"tool-definitions:report": "tsx scripts/tool-definitions-report.ts"
|
|
37
38
|
},
|
|
38
39
|
"keywords": [
|
|
39
40
|
"mcp",
|
|
@@ -73,9 +74,9 @@
|
|
|
73
74
|
"engines": {
|
|
74
75
|
"node": ">=22"
|
|
75
76
|
},
|
|
76
|
-
"packageManager": "pnpm@12.
|
|
77
|
+
"packageManager": "pnpm@12.16.1+sha512.091d1dee7927575661b25dab3c2ae31cafc1c54d6b71ed6a6600cf0c1cb10434a447eae14015f1b23e2e6bedeed79df94a932e2164d2832280dce3fb32f656d1",
|
|
77
78
|
"devDependencies": {
|
|
78
|
-
"@biomejs/biome": "2.
|
|
79
|
+
"@biomejs/biome": "2.6.2",
|
|
79
80
|
"@modelcontextprotocol/client": "2.3.1",
|
|
80
81
|
"@modelcontextprotocol/node": "2.1.1",
|
|
81
82
|
"@modelcontextprotocol/server": "2.3.1",
|
|
@@ -97,10 +98,11 @@
|
|
|
97
98
|
"conventional-changelog-conventionalcommits": "^10.2.1",
|
|
98
99
|
"hast-util-to-html": "9.0.5",
|
|
99
100
|
"hast-util-to-mdast": "10.1.2",
|
|
100
|
-
"jose": "6.2.
|
|
101
|
+
"jose": "6.2.13",
|
|
101
102
|
"lefthook": "^2.2.2",
|
|
103
|
+
"mcp-tdqs": "0.2.0",
|
|
102
104
|
"msw": "^3.0.0",
|
|
103
|
-
"oidc-provider": "~9.
|
|
105
|
+
"oidc-provider": "~9.13.0",
|
|
104
106
|
"open": "11.0.4",
|
|
105
107
|
"rehype-parse": "9.0.1",
|
|
106
108
|
"rehype-raw": "7.0.0",
|