@fruggr/zendesk-mcp-server 2.5.0 → 2.6.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/index.js CHANGED
@@ -960,6 +960,8 @@ const formatList = (items, formatter, meta) => {
960
960
  };
961
961
  //#endregion
962
962
  //#region src/utils/pagination.ts
963
+ const PER_PAGE_DESC = "Number of results per page for offset pagination (1-100). Pair with `page` to walk large result sets; the response header reports the total count and whether more pages remain.";
964
+ const PAGE_DESC = "1-based page number for offset pagination. Increment it while keeping `per_page` fixed to fetch subsequent pages; page 1 is the first page.";
963
965
  const buildCursorParams = (pageSize, cursor) => {
964
966
  const params = { "page[size]": String(pageSize) };
965
967
  if (cursor) params["page[after]"] = cursor;
@@ -1207,6 +1209,7 @@ const markdownToHtml = (markdown) => {
1207
1209
  };
1208
1210
  //#endregion
1209
1211
  //#region src/tools/help-center.ts
1212
+ const ARTICLE_ID_DESC = "Article ID — the numeric id of the Help Center article. Obtain it from list_articles or search_articles.";
1210
1213
  const largeArticleHint = (body, sectionCount) => {
1211
1214
  if (body.length < 3e3 && sectionCount < 4) return null;
1212
1215
  return [
@@ -1226,10 +1229,10 @@ const createHelpCenterTools = (ctx) => {
1226
1229
  title: "Search Help Center Articles",
1227
1230
  description: "Full-text search across Help Center articles (metadata only, no body). Use get_article for full content. Supports locale filtering. Returns total count.",
1228
1231
  inputSchema: z.object({
1229
- query: z.string().min(1).describe("Search query"),
1232
+ query: z.string().min(1).describe("Full-text query matched against article titles and body. Plain keywords; combine with the locale filter to scope to one language."),
1230
1233
  locale: z.string().optional().describe("Filter by locale (e.g., \"en-us\", \"fr\")"),
1231
- per_page: z.number().int().min(1).max(100).default(100).describe("Results per page"),
1232
- page: z.number().int().min(1).default(1).describe("Page number")
1234
+ per_page: z.number().int().min(1).max(100).default(100).describe(PER_PAGE_DESC),
1235
+ page: z.number().int().min(1).default(1).describe(PAGE_DESC)
1233
1236
  }),
1234
1237
  annotations: {
1235
1238
  readOnlyHint: true,
@@ -1259,7 +1262,7 @@ const createHelpCenterTools = (ctx) => {
1259
1262
  title: "Get Help Center Article",
1260
1263
  description: "Retrieve an article by ID with full body content. For large articles, prefer get_article_outline + get_article_section to save tokens. Optionally specify locale for a translated version. Returns body (HTML), metadata, source_locale, and list of available translations.",
1261
1264
  inputSchema: z.object({
1262
- article_id: z.number().int().describe("Article ID"),
1265
+ article_id: z.number().int().describe(ARTICLE_ID_DESC),
1263
1266
  locale: z.string().optional().describe("Locale for translated version")
1264
1267
  }),
1265
1268
  annotations: {
@@ -1341,8 +1344,8 @@ const createHelpCenterTools = (ctx) => {
1341
1344
  title: "List Help Center Articles",
1342
1345
  description: "List articles (metadata only, no body). Use get_article for full content. Optionally filter by section ID and locale. Supports sort_by (\"title\", \"created_at\", \"updated_at\") and include_translations: true to show available translation locales per article. Note: include_translations must be re-sent on each paginated request.",
1343
1346
  inputSchema: z.object({
1344
- section_id: z.number().int().optional(),
1345
- locale: z.string().optional(),
1347
+ section_id: z.number().int().optional().describe("Restrict the listing to one section (numeric id from list_sections). Omit to list articles across all sections."),
1348
+ locale: z.string().optional().describe("Restrict to a single locale, e.g. \"en-us\" or \"fr\". Omit for the default locale."),
1346
1349
  page_size: z.number().int().min(1).max(100).default(100).describe("Articles per page (1-100, default 100)."),
1347
1350
  cursor: z.string().optional().describe("Pagination cursor from a previous response; omit for the first page."),
1348
1351
  sort_by: z.enum([
@@ -1350,8 +1353,8 @@ const createHelpCenterTools = (ctx) => {
1350
1353
  "updated_at",
1351
1354
  "position",
1352
1355
  "title"
1353
- ]).default("position").describe("Sort field"),
1354
- sort_order: z.enum(["asc", "desc"]).default("asc").describe("Sort direction"),
1356
+ ]).default("position").describe("Field to sort by; \"position\" (the default) is the manual order set in Guide."),
1357
+ sort_order: z.enum(["asc", "desc"]).default("asc").describe("Sort direction: ascending or descending."),
1355
1358
  include_translations: z.boolean().default(false).describe("Include available translation locales per article (causes 1 extra API call per article)")
1356
1359
  }),
1357
1360
  annotations: {
@@ -1391,7 +1394,7 @@ const createHelpCenterTools = (ctx) => {
1391
1394
  readOnly: true,
1392
1395
  title: "List Article Translations",
1393
1396
  description: "List all available translations for an article (metadata only, no body: locale, title, draft, updated_at). Use get_article with locale for full translated content.",
1394
- inputSchema: z.object({ article_id: z.number().int().describe("Article ID") }),
1397
+ inputSchema: z.object({ article_id: z.number().int().describe(ARTICLE_ID_DESC) }),
1395
1398
  annotations: {
1396
1399
  readOnlyHint: true,
1397
1400
  destructiveHint: false,
@@ -1447,11 +1450,11 @@ const createHelpCenterTools = (ctx) => {
1447
1450
  title: "Update Article Translation",
1448
1451
  description: "Update article content (title, body) in a specific locale. For targeted edits on one or a few sections, prefer update_article_section — this tool replaces the FULL body and re-sends the entire article on each write. Use the article's source_locale (from get_article) for the default language, or another locale for translations.",
1449
1452
  inputSchema: z.object({
1450
- article_id: z.number().int(),
1451
- locale: z.string(),
1452
- title: z.string().optional(),
1453
- body: z.string().optional(),
1454
- draft: z.boolean().optional()
1453
+ article_id: z.number().int().describe("Article ID — the numeric id of the article whose translation to update. Obtain it from list_articles or search_articles."),
1454
+ locale: z.string().describe("Locale of the translation to update, e.g. \"en-us\" or \"fr\". Use the source_locale (from get_article) to edit the default language."),
1455
+ title: z.string().optional().describe("New title for this locale. Omit to leave the current title unchanged."),
1456
+ body: z.string().optional().describe("New full body (HTML) for this locale. Replaces the entire body — for a single-section edit prefer update_article_section. Omit to leave the body unchanged."),
1457
+ draft: z.boolean().optional().describe("When true, keeps this translation as a draft; when false, publishes it.")
1455
1458
  }),
1456
1459
  annotations: {
1457
1460
  readOnlyHint: false,
@@ -1495,16 +1498,16 @@ const createHelpCenterTools = (ctx) => {
1495
1498
  title: "Create Help Center Article",
1496
1499
  description: "Create a new article in a section. The locale becomes the article's source_locale. Requires a permission_group_id (use list_permission_groups to find available IDs). To add content in other locales afterwards, use create_article_translation.",
1497
1500
  inputSchema: z.object({
1498
- section_id: z.number().int(),
1499
- title: z.string().min(1),
1500
- body: z.string().min(1).describe("Article body (HTML)"),
1501
+ section_id: z.number().int().describe("Section that will contain the article (numeric id from list_sections)."),
1502
+ title: z.string().min(1).describe("Title of the new article, in its source locale."),
1503
+ body: z.string().min(1).describe("Article body as HTML (this becomes the source-locale content)."),
1501
1504
  permission_group_id: z.number().int().describe("Permission group ID (use list_permission_groups to find it)"),
1502
1505
  user_segment_id: z.number().int().optional().describe("User segment ID for visibility (use list_user_segments to find it). Defaults to everyone."),
1503
1506
  author_id: z.number().int().optional().describe("Author user ID. Defaults to the authenticated user."),
1504
1507
  content_tag_ids: z.array(z.string()).optional().describe("Content tag IDs (use list_content_tags to find them)"),
1505
- locale: z.string().optional(),
1506
- draft: z.boolean().default(true),
1507
- promoted: z.boolean().default(false),
1508
+ locale: z.string().optional().describe("Source locale for the article, e.g. \"en-us\" or \"fr\". Defaults to the Help Center's default locale; becomes the article's source_locale."),
1509
+ draft: z.boolean().default(true).describe("When true (default), the article is created unpublished; set false to publish immediately."),
1510
+ promoted: z.boolean().default(false).describe("When true, marks the article as promoted (featured) in its section. Defaults to false."),
1508
1511
  label_names: z.array(z.string()).optional().describe("Label names for search ranking (use list_labels to see existing labels)")
1509
1512
  }),
1510
1513
  annotations: {
@@ -1529,15 +1532,15 @@ const createHelpCenterTools = (ctx) => {
1529
1532
  title: "Update Help Center Article",
1530
1533
  description: "Update article metadata only (draft, promoted, labels, tags, visibility, section, sort position, etc.). Does NOT update content (title, body) — use update_article_translation for that.",
1531
1534
  inputSchema: z.object({
1532
- article_id: z.number().int(),
1533
- draft: z.boolean().optional(),
1534
- promoted: z.boolean().optional(),
1535
- label_names: z.array(z.string()).optional().describe("Label names for search ranking"),
1536
- content_tag_ids: z.array(z.string()).optional().describe("Content tag IDs"),
1537
- user_segment_id: z.number().int().optional().describe("User segment ID for visibility"),
1538
- author_id: z.number().int().optional().describe("Author user ID"),
1539
- permission_group_id: z.number().int().optional().describe("Permission group ID"),
1540
- section_id: z.number().int().optional(),
1535
+ article_id: z.number().int().describe("Article ID — the numeric id of the article to update. Obtain it from list_articles or search_articles."),
1536
+ draft: z.boolean().optional().describe("Set true to unpublish the article (revert to draft) or false to publish it."),
1537
+ promoted: z.boolean().optional().describe("Set true to promote (feature) the article in its section, or false to unpromote it."),
1538
+ label_names: z.array(z.string()).optional().describe("Label names for search ranking (use list_labels to see existing labels)."),
1539
+ content_tag_ids: z.array(z.string()).optional().describe("Content tag ids to attach (use list_content_tags to find them)."),
1540
+ user_segment_id: z.number().int().optional().describe("User segment that controls who can see the article (id from list_user_segments)."),
1541
+ author_id: z.number().int().optional().describe("User id of the article author (from search_users)."),
1542
+ permission_group_id: z.number().int().optional().describe("Guide permission group controlling who can edit (id from list_permission_groups)."),
1543
+ section_id: z.number().int().optional().describe("Move the article to this section (numeric id from list_sections)."),
1541
1544
  position: z.number().int().min(0).optional().describe("Sort position within the section (manual ordering only; 0 = first/top). New articles default to position 0. To move an article to the END of its section, set this to one more than the highest current position: read the highest position P from list_articles with sort_by=\"position\", sort_order=\"desc\", then set position = P + 1.")
1542
1545
  }),
1543
1546
  annotations: {
@@ -1652,9 +1655,14 @@ const createHelpCenterTools = (ctx) => {
1652
1655
  },
1653
1656
  handler: async (params) => {
1654
1657
  const { article_id } = params;
1658
+ const attachments = (await helpCenterGet(subdomain, await getToken(), `/articles/${article_id}/attachments`)).article_attachments ?? [];
1659
+ if (attachments.length === 0) return { content: [{
1660
+ type: "text",
1661
+ text: `No attachments found on article #${article_id}.`
1662
+ }] };
1655
1663
  return { content: [{
1656
1664
  type: "text",
1657
- text: formatList((await helpCenterGet(subdomain, await getToken(), `/articles/${article_id}/attachments`)).article_attachments ?? [], formatAttachment)
1665
+ text: formatList(attachments, formatAttachment)
1658
1666
  }] };
1659
1667
  }
1660
1668
  },
@@ -1665,7 +1673,7 @@ const createHelpCenterTools = (ctx) => {
1665
1673
  title: "Get Article Outline",
1666
1674
  description: "Return a compact outline of an article (list of sections delimited by h1/h2/h3, with word counts) for the given locale (defaults to source_locale). Includes available translations with their outdated status. Use get_article_section to fetch a specific section.",
1667
1675
  inputSchema: z.object({
1668
- article_id: z.number().int().describe("Article ID"),
1676
+ article_id: z.number().int().describe(ARTICLE_ID_DESC),
1669
1677
  locale: z.string().optional().describe("Locale of the body to outline (defaults to article source_locale)")
1670
1678
  }),
1671
1679
  annotations: {
@@ -1706,7 +1714,7 @@ const createHelpCenterTools = (ctx) => {
1706
1714
  title: "Get Article Section",
1707
1715
  description: "Retrieve the content of a single section of an article in a given locale. Use get_article_outline first to discover section indexes. Default format=\"html\" for round-trip safety. Pass format=\"markdown\" only for human review — the Markdown representation is lossy on some structures (<pre> with <br>, tables with multi-<p> cells are kept as raw HTML to limit the damage, but do not round-trip markdown content back through update_article_section).",
1708
1716
  inputSchema: z.object({
1709
- article_id: z.number().int().describe("Article ID"),
1717
+ article_id: z.number().int().describe(ARTICLE_ID_DESC),
1710
1718
  locale: z.string().describe("Locale of the body (e.g., \"en-us\", \"fr\")"),
1711
1719
  section_index: z.number().int().min(0).describe("0-based index of the section (see get_article_outline)"),
1712
1720
  format: z.enum(["html", "markdown"]).default("html").describe("Output format. \"html\" (default) is round-trip safe. \"markdown\" is lossy on some HTML structures — use only for human review, not before update_article_section.")
@@ -1742,7 +1750,7 @@ const createHelpCenterTools = (ctx) => {
1742
1750
  title: "Update Article Section",
1743
1751
  description: "Replace the content of a single section of an article in a given locale, keeping the rest of the body intact. The server fetches the current body, replaces the targeted section, and PUTs the full reconstructed body via the Translations API. Default format=\"html\" for fidelity. Use format=\"markdown\" only when you control the input and know it does not rely on structures that round-trip poorly (code blocks with line breaks, tables with multi-paragraph cells). The section heading is preserved and is NOT part of the replaced content.",
1744
1752
  inputSchema: z.object({
1745
- article_id: z.number().int().describe("Article ID"),
1753
+ article_id: z.number().int().describe(ARTICLE_ID_DESC),
1746
1754
  locale: z.string().describe("Locale of the translation to update"),
1747
1755
  section_index: z.number().int().min(0).describe("0-based index of the section to replace (see get_article_outline)"),
1748
1756
  content: z.string().describe("New content for the section (heading excluded). HTML by default, Markdown if format=\"markdown\"."),
@@ -1776,8 +1784,8 @@ const createHelpCenterTools = (ctx) => {
1776
1784
  title: "Compare Article Translations",
1777
1785
  description: "Compare section structure between two locales of the same article, matched by index. Returns a compact table (one row per section) with status: \"ok\" (both present, source/target word count ratio within 25%), \"different\" (word count ratio diverges by more than 25% — size signal only, NOT a semantic divergence: two locales may legitimately differ in verbosity) or \"missing\" (section absent in target). Useful to spot structurally stale or missing sections; do not interpret \"different\" as an edit regression on its own.",
1778
1786
  inputSchema: z.object({
1779
- article_id: z.number().int().describe("Article ID"),
1780
- source_locale: z.string().describe("Source (reference) locale"),
1787
+ article_id: z.number().int().describe(ARTICLE_ID_DESC),
1788
+ source_locale: z.string().describe("Reference locale to diff against, e.g. \"en-us\". Usually the article source_locale (from get_article)."),
1781
1789
  target_locale: z.string().describe("Target locale to compare against source")
1782
1790
  }),
1783
1791
  annotations: {
@@ -1828,7 +1836,7 @@ const createHelpCenterTools = (ctx) => {
1828
1836
  title: "Create Article Attachment",
1829
1837
  description: "Upload an attachment to an article. Provide file content as base64-encoded string.",
1830
1838
  inputSchema: z.object({
1831
- article_id: z.number().int().describe("Article ID"),
1839
+ article_id: z.number().int().describe(ARTICLE_ID_DESC),
1832
1840
  file_name: z.string().min(1).describe("File name (e.g., \"screenshot.png\")"),
1833
1841
  file_base64: z.string().min(1).describe("File content encoded as base64"),
1834
1842
  content_type: z.string().default("application/octet-stream").describe("MIME type (e.g., \"image/png\", \"application/pdf\")")
@@ -1879,9 +1887,9 @@ const createSearchTools = (ctx) => {
1879
1887
  title: "Zendesk Unified Search",
1880
1888
  description: "Search across tickets, users, and organizations. Supports filters like \"type:ticket status:open\", \"type:user role:agent\". Returns total count and paginated results (100 per page). Organization results include name and ID only — use get_organization for full details (tags, domains, details).",
1881
1889
  inputSchema: z.object({
1882
- query: z.string().min(1).describe("Zendesk search query"),
1883
- per_page: z.number().int().min(1).max(100).default(100).describe("Results per page (max 100)"),
1884
- page: z.number().int().min(1).default(1).describe("Page number (1-based)")
1890
+ query: z.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."),
1891
+ per_page: z.number().int().min(1).max(100).default(100).describe(PER_PAGE_DESC),
1892
+ page: z.number().int().min(1).default(1).describe(PAGE_DESC)
1885
1893
  }),
1886
1894
  annotations: {
1887
1895
  readOnlyHint: true,
@@ -2012,8 +2020,8 @@ const createTicketTools = (ctx) => {
2012
2020
  title: "Get Zendesk Ticket",
2013
2021
  description: "Retrieve a Zendesk ticket by ID, including its live SLA state (per-metric stage and breach countdown) when an SLA policy applies, plus its comments if requested. Returns ticket details (subject, status, priority, assignee, tags, description) and optionally all comments/internal notes. The per-ticket Show endpoint exposes no SLA, so the SLA block is resolved via a scoped search and may be absent for a very high-volume requester or a just-updated ticket; SLA targets and policy conditions live in list_sla_policies.",
2014
2022
  inputSchema: z.object({
2015
- ticket_id: z.number().int().describe("Ticket ID"),
2016
- include_comments: z.boolean().default(false).describe("Include ticket comments")
2023
+ ticket_id: z.number().int().describe("Ticket ID — the numeric id of the ticket to fetch. Obtain it from search_tickets or list_tickets."),
2024
+ include_comments: z.boolean().default(false).describe("When true, appends the full public comment and internal note thread to the response. Defaults to false to keep the payload small; enable it when you need the conversation, not just the ticket fields.")
2017
2025
  }),
2018
2026
  annotations: {
2019
2027
  readOnlyHint: true,
@@ -2043,7 +2051,7 @@ const createTicketTools = (ctx) => {
2043
2051
  title: "Get Zendesk Ticket Attachments",
2044
2052
  description: "Retrieve ticket attachments. Images are embedded inline; other files are listed as text references.",
2045
2053
  inputSchema: z.object({
2046
- ticket_id: z.number().int().describe("Ticket ID"),
2054
+ ticket_id: z.number().int().describe("Ticket ID — the numeric id of the ticket whose attachments to fetch. Obtain it from search_tickets or list_tickets."),
2047
2055
  attachment_ids: z.array(z.number().int()).optional().describe("Attachment IDs to fetch directly (e.g. extracted from a previous get_ticket(include_comments=true) call). When provided, skips the comments fetch entirely. When omitted, all attachments of the ticket are returned.")
2048
2056
  }),
2049
2057
  annotations: {
@@ -2081,11 +2089,11 @@ const createTicketTools = (ctx) => {
2081
2089
  namespace: "tickets",
2082
2090
  readOnly: true,
2083
2091
  title: "Search Zendesk Tickets",
2084
- 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 type:incident\". Returns total count, so queue triage like \"breaching today\" works without a per-ticket fetch.",
2092
+ 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.",
2085
2093
  inputSchema: z.object({
2086
- query: z.string().min(1).describe("Zendesk search query string"),
2087
- per_page: z.number().int().min(1).max(100).default(100).describe("Results per page"),
2088
- page: z.number().int().min(1).default(1).describe("Page number")
2094
+ query: z.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)."),
2095
+ per_page: z.number().int().min(1).max(100).default(100).describe(PER_PAGE_DESC),
2096
+ page: z.number().int().min(1).default(1).describe(PAGE_DESC)
2089
2097
  }),
2090
2098
  annotations: {
2091
2099
  readOnlyHint: true,
@@ -2114,8 +2122,8 @@ const createTicketTools = (ctx) => {
2114
2122
  title: "Create Zendesk Ticket",
2115
2123
  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.",
2116
2124
  inputSchema: z.object({
2117
- subject: z.string().min(1).describe("Ticket subject"),
2118
- description: z.string().min(1).describe("Ticket description"),
2125
+ subject: z.string().min(1).describe("Ticket subject — the short summary line shown in ticket lists and search results."),
2126
+ 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)."),
2119
2127
  priority: z.enum([
2120
2128
  "urgent",
2121
2129
  "high",
@@ -2130,7 +2138,7 @@ const createTicketTools = (ctx) => {
2130
2138
  ]).optional().describe("Ticket type. One of problem, incident, question, task."),
2131
2139
  assignee_id: z.number().int().optional().describe("User id of the agent to assign the ticket to."),
2132
2140
  group_id: z.number().int().optional().describe("Id of the group to assign the ticket to."),
2133
- tags: z.array(z.string()).optional().describe("Tags to set on the ticket."),
2141
+ tags: z.array(z.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."),
2134
2142
  custom_fields: z.array(z.object({
2135
2143
  id: z.number().int(),
2136
2144
  value: z.unknown()
@@ -2162,7 +2170,7 @@ const createTicketTools = (ctx) => {
2162
2170
  title: "Update Zendesk Ticket",
2163
2171
  description: "Update an existing ticket (status, priority, type, assignee, group, subject, tags, custom fields). Only the fields you pass are changed, and the updated ticket is returned. Setting tags here replaces the whole tag set — use manage_tags to add or remove individual tags without overwriting the rest. This tool does not post replies: use add_public_comment or add_private_note for that. Find the ticket id via search_tickets or list_tickets.",
2164
2172
  inputSchema: z.object({
2165
- ticket_id: z.number().int().describe("Ticket ID"),
2173
+ ticket_id: z.number().int().describe("Ticket ID — the numeric id of the ticket to update. Obtain it from search_tickets or list_tickets."),
2166
2174
  status: z.enum([
2167
2175
  "new",
2168
2176
  "open",
@@ -2185,7 +2193,7 @@ const createTicketTools = (ctx) => {
2185
2193
  ]).optional().describe("Ticket type. One of problem, incident, question, task."),
2186
2194
  assignee_id: z.number().int().optional().describe("User id of the agent to assign the ticket to."),
2187
2195
  group_id: z.number().int().optional().describe("Id of the group to assign the ticket to."),
2188
- subject: z.string().optional().describe("New ticket subject line."),
2196
+ subject: z.string().optional().describe("New subject line for the ticket; replaces the current subject when provided."),
2189
2197
  tags: z.array(z.string()).optional().describe("Replaces the full tag set on the ticket. Use manage_tags for incremental add/remove."),
2190
2198
  custom_fields: z.array(z.object({
2191
2199
  id: z.number().int(),
@@ -2212,10 +2220,10 @@ const createTicketTools = (ctx) => {
2212
2220
  namespace: "tickets",
2213
2221
  readOnly: false,
2214
2222
  title: "Add Private Note",
2215
- description: "Add an internal note (not visible to requester) to a ticket, optionally with file attachments (uploaded via the Zendesk Uploads API and carried on the note).",
2223
+ description: "Add an internal note (not visible to requester) to a ticket, optionally with file attachments (uploaded via the Zendesk Uploads API and carried on the note). The note is appended to the ticket thread; use add_public_comment instead when the reply should be visible to the requester.",
2216
2224
  inputSchema: z.object({
2217
- ticket_id: z.number().int().describe("Ticket ID"),
2218
- body: z.string().min(1).describe("Note content"),
2225
+ ticket_id: z.number().int().describe("Ticket ID — the numeric id of the ticket to annotate. Obtain it from search_tickets or list_tickets."),
2226
+ body: z.string().min(1).describe("Note text (internal, agent-only). Plain text or HTML; not shown to the requester."),
2219
2227
  attachments: z.array(attachmentSchema).optional().describe("Files to attach to this note (base64-encoded content).")
2220
2228
  }),
2221
2229
  annotations: {
@@ -2244,10 +2252,10 @@ const createTicketTools = (ctx) => {
2244
2252
  namespace: "tickets",
2245
2253
  readOnly: false,
2246
2254
  title: "Add Public Comment",
2247
- description: "Add a public comment (visible to requester) to a ticket, optionally with file attachments (uploaded via the Zendesk Uploads API and carried on the comment).",
2255
+ description: "Add a public comment (visible to requester) to a ticket, optionally with file attachments (uploaded via the Zendesk Uploads API and carried on the comment). The comment is appended to the ticket thread and emails the requester; use add_private_note instead for an internal, agent-only note.",
2248
2256
  inputSchema: z.object({
2249
- ticket_id: z.number().int().describe("Ticket ID"),
2250
- body: z.string().min(1).describe("Comment content"),
2257
+ ticket_id: z.number().int().describe("Ticket ID — the numeric id of the ticket to reply on. Obtain it from search_tickets or list_tickets."),
2258
+ body: z.string().min(1).describe("Comment text sent to the requester. Plain text or HTML; visible in the ticket."),
2251
2259
  attachments: z.array(attachmentSchema).optional().describe("Files to attach to this comment (base64-encoded content).")
2252
2260
  }),
2253
2261
  annotations: {
@@ -2276,7 +2284,7 @@ const createTicketTools = (ctx) => {
2276
2284
  namespace: "tickets",
2277
2285
  readOnly: true,
2278
2286
  title: "List Zendesk Tickets",
2279
- description: "List tickets with cursor-based pagination, sorted by most recently updated. Page size is controlled by page_size (not per_page, which is the offset-based parameter used by search_tickets); paginate by passing the returned cursor.",
2287
+ description: "List tickets with cursor-based pagination, in Zendesk's default order (ascending ticket id), not by recency. Page size is controlled by page_size (not per_page, which is the offset-based parameter used by search_tickets); paginate by passing the returned cursor. To find tickets by recency or any other criterion, use search_tickets with a query.",
2280
2288
  inputSchema: z.object({
2281
2289
  page_size: z.number().int().min(1).max(100).default(100).describe("Tickets per page (1-100, default 100)."),
2282
2290
  cursor: z.string().optional().describe("Pagination cursor from a previous response; omit for the first page.")
@@ -2302,8 +2310,8 @@ const createTicketTools = (ctx) => {
2302
2310
  namespace: "tickets",
2303
2311
  readOnly: true,
2304
2312
  title: "Get Linked Incidents",
2305
- description: "Get all incident tickets linked to a problem ticket.",
2306
- inputSchema: z.object({ problem_id: z.number().int().describe("Problem ticket ID") }),
2313
+ description: "Get all incident tickets linked to a problem ticket. Returns the list of incidents that reference the given problem (Zendesk problem/incident relationship); useful to gauge a problem's blast radius before resolving it.",
2314
+ inputSchema: z.object({ problem_id: z.number().int().describe("Problem ticket ID — the numeric id of the ticket of type \"problem\" whose linked incidents to list. Obtain it from search_tickets or list_tickets.") }),
2307
2315
  annotations: {
2308
2316
  readOnlyHint: true,
2309
2317
  destructiveHint: false,
@@ -2324,11 +2332,11 @@ const createTicketTools = (ctx) => {
2324
2332
  namespace: "tickets",
2325
2333
  readOnly: false,
2326
2334
  title: "Manage Ticket Tags",
2327
- description: "Add or remove tags on a ticket.",
2335
+ description: "Add or remove tags on a ticket. Performs an incremental read-modify-write: it fetches the ticket's current tags, adds those in `add` and deletes those in `remove`, then saves the merged set — tags you don't list are left untouched and duplicates are collapsed. Adding a tag already present, or removing one that is absent, is a no-op (idempotent). Returns the ticket's full tag set after the update. Use this for incremental tag edits; to overwrite the entire tag set at once, or to change tags alongside other fields, use update_ticket instead. Find the ticket id via search_tickets or list_tickets.",
2328
2336
  inputSchema: z.object({
2329
- ticket_id: z.number().int().describe("Ticket ID"),
2330
- add: z.array(z.string()).optional().describe("Tags to add"),
2331
- remove: z.array(z.string()).optional().describe("Tags to remove")
2337
+ ticket_id: z.number().int().describe("Ticket ID — the numeric id of the ticket whose tags to modify. Obtain it from search_tickets or list_tickets."),
2338
+ add: z.array(z.string()).optional().describe("Tags to add. Zendesk tags are single tokens: a value containing spaces is stored as separate tags rather than one tag, so join multi-word tags yourself with an underscore or dash (e.g. \"urgent_request\"). Adding a tag already on the ticket is a no-op. Omit to only remove."),
2339
+ remove: z.array(z.string()).optional().describe("Tags to remove. Removing a tag that is not present is a no-op; tags not listed here stay in place. Omit to only add.")
2332
2340
  }),
2333
2341
  annotations: {
2334
2342
  readOnlyHint: false,
@@ -2361,8 +2369,8 @@ const createTicketTools = (ctx) => {
2361
2369
  title: "List SLA Policies",
2362
2370
  description: "List the configured SLA policies with their filter conditions and per-priority reply/resolution targets. Use this to explain why a given target applies to a ticket and to reconstruct deadlines deterministically instead of hard-coding the policy matrix. Requires an admin token (or a custom role granted the SLA-management permission); a standard agent token gets 403 here, though it can still read live per-ticket SLA via get_ticket / search_tickets.",
2363
2371
  inputSchema: z.object({
2364
- per_page: z.number().int().min(1).max(100).default(100).describe("Results per page"),
2365
- page: z.number().int().min(1).default(1).describe("Page number")
2372
+ per_page: z.number().int().min(1).max(100).default(100).describe(PER_PAGE_DESC),
2373
+ page: z.number().int().min(1).default(1).describe(PAGE_DESC)
2366
2374
  }),
2367
2375
  annotations: {
2368
2376
  readOnlyHint: true,
@@ -2426,9 +2434,9 @@ const createUserTools = (ctx) => {
2426
2434
  title: "Search Zendesk Users",
2427
2435
  description: "Search for users by name, email, or other criteria using Zendesk search query syntax. Returns total count.",
2428
2436
  inputSchema: z.object({
2429
- query: z.string().min(1).describe("Search query"),
2430
- per_page: z.number().int().min(1).max(100).default(100).describe("Results per page"),
2431
- page: z.number().int().min(1).default(1).describe("Page number")
2437
+ query: z.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."),
2438
+ per_page: z.number().int().min(1).max(100).default(100).describe(PER_PAGE_DESC),
2439
+ page: z.number().int().min(1).default(1).describe(PAGE_DESC)
2432
2440
  }),
2433
2441
  annotations: {
2434
2442
  readOnlyHint: true,
@@ -2453,8 +2461,8 @@ const createUserTools = (ctx) => {
2453
2461
  namespace: "users",
2454
2462
  readOnly: true,
2455
2463
  title: "Get Zendesk User",
2456
- description: "Retrieve a user by ID.",
2457
- inputSchema: z.object({ user_id: z.number().int().describe("User ID") }),
2464
+ description: "Retrieve a single user by their numeric id. Returns the full user record (name, email, role, organization, tags). Use search_users when you only have a name or email, or get_current_user for the authenticated identity.",
2465
+ inputSchema: z.object({ user_id: z.number().int().describe("User ID — the numeric id of the Zendesk user to fetch. Obtain it from search_users, or from the requester/assignee fields of a ticket.") }),
2458
2466
  annotations: {
2459
2467
  readOnlyHint: true,
2460
2468
  destructiveHint: false,
@@ -2475,8 +2483,8 @@ const createUserTools = (ctx) => {
2475
2483
  namespace: "users",
2476
2484
  readOnly: true,
2477
2485
  title: "Get Zendesk Organization",
2478
- description: "Retrieve an organization by ID.",
2479
- inputSchema: z.object({ organization_id: z.number().int().describe("Organization ID") }),
2486
+ description: "Retrieve a single organization by its numeric id. Returns full details (name, tags, domains, notes) — more than the name/id that search or list_organizations surface. Use list_organizations to browse or search for a name-based lookup.",
2487
+ inputSchema: z.object({ organization_id: z.number().int().describe("Organization ID — the numeric id of the Zendesk organization to fetch. Obtain it from list_organizations, search, or a user record.") }),
2480
2488
  annotations: {
2481
2489
  readOnlyHint: true,
2482
2490
  destructiveHint: false,