@fruggr/zendesk-mcp-server 2.3.1 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,16 +1,19 @@
1
1
  # Zendesk MCP Server
2
2
 
3
3
  [![Glama score](https://glama.ai/mcp/servers/fruggr/zendesk-mcp-server/badges/score.svg)](https://glama.ai/mcp/servers/fruggr/zendesk-mcp-server)
4
+ [![MCP Registry](https://img.shields.io/badge/MCP_Registry-io.github.fruggr%2Fzendesk--mcp--server-0a7ea4)](https://registry.modelcontextprotocol.io/?search=io.github.fruggr/zendesk-mcp-server)
4
5
  [![npm version](https://img.shields.io/npm/v/@fruggr/zendesk-mcp-server?logo=npm&color=cb3837)](https://www.npmjs.com/package/@fruggr/zendesk-mcp-server)
5
6
  [![License: MIT](https://img.shields.io/npm/l/@fruggr/zendesk-mcp-server?color=blue)](LICENSE)
6
7
  [![Node.js](https://img.shields.io/node/v/@fruggr/zendesk-mcp-server?logo=nodedotjs&logoColor=white&color=339933)](https://nodejs.org)
7
8
  [![Renovate enabled](https://img.shields.io/badge/renovate-enabled-brightgreen?logo=renovatebot&logoColor=white)](https://renovatebot.com)
8
9
  [![semantic-release](https://img.shields.io/badge/semantic--release-e10079?logo=semantic-release&logoColor=white)](https://github.com/semantic-release/semantic-release)
9
10
 
10
- **Bring Zendesk Support & the Help Center into your AI assistant.** A
11
- [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that lets
12
- your assistant search articles, answer questions, and create, track and update
13
- tickets in plain language **without switching apps**.
11
+ **Bring Zendesk deep into your AI assistant.** A
12
+ [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for a
13
+ two-way integration: find answers in the Help Center, **draft, update and
14
+ translate** articles (keeping languages in sync), and **manage Support tickets**
15
+ end to end — comments, triage and image attachments — all in plain language,
16
+ **without switching apps**.
14
17
 
15
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),
16
19
  but **vendor-neutral** — it drops into any MCP client (Claude Desktop, Claude
@@ -130,8 +133,8 @@ zendesk-mcp-server acme --namespace tickets
130
133
  | `list_sla_policies` | List SLA policies with filter conditions and per-priority targets (requires an admin token, or a custom role with the SLA-management permission) | read |
131
134
  | `create_ticket` | Create a new ticket with subject, description, priority, tags... | write |
132
135
  | `update_ticket` | Update ticket status, priority, assignee, tags, custom fields | write |
133
- | `add_private_note` | Add an internal note (not visible to requester) | write |
134
- | `add_public_comment` | Add a public comment (visible to requester) | write |
136
+ | `add_private_note` | Add an internal note (not visible to requester), optionally with file attachments | write |
137
+ | `add_public_comment` | Add a public comment (visible to requester), optionally with file attachments | write |
135
138
  | `manage_tags` | Add or remove tags on a ticket | write |
136
139
 
137
140
  </details>
@@ -622,7 +625,7 @@ This project was built with reference to:
622
625
 
623
626
  ## Releases & versioning
624
627
 
625
- Versions follow [SemVer](https://semver.org/) and are calculated **automatically** from commit messages — no one bumps the version by hand. Every merge to `main` triggers [semantic-release](https://github.com/semantic-release/semantic-release), which inspects the new [Conventional Commits](https://www.conventionalcommits.org/) since the previous tag, computes the next version, updates [`CHANGELOG.md`](CHANGELOG.md), publishes to npm, and creates the matching GitHub Release.
628
+ Versions follow [SemVer](https://semver.org/) and are calculated **automatically** from commit messages — no one bumps the version by hand. Every merge to `main` triggers [semantic-release](https://github.com/semantic-release/semantic-release), which inspects the new [Conventional Commits](https://www.conventionalcommits.org/) since the previous tag, computes the next version, updates [`CHANGELOG.md`](CHANGELOG.md), publishes to npm, creates the matching GitHub Release, and mirrors the release into the [official MCP registry](https://registry.modelcontextprotocol.io) as [`io.github.fruggr/zendesk-mcp-server`](https://registry.modelcontextprotocol.io/?search=io.github.fruggr/zendesk-mcp-server) so registry-driven clients discover the new version automatically.
626
629
 
627
630
  | Commit type | Resulting bump |
628
631
  |---|---|
package/dist/index.js CHANGED
@@ -783,6 +783,24 @@ const fetchZendeskBinary = async (subdomain, token, contentUrl) => {
783
783
  contentType
784
784
  };
785
785
  };
786
+ const zendeskUpload = async (subdomain, token, filename, data, contentType, uploadToken) => {
787
+ const params = { filename };
788
+ if (uploadToken) params["token"] = uploadToken;
789
+ const url = buildUrl(getBaseUrl(subdomain), "/uploads", params);
790
+ const response = await fetch(url, {
791
+ method: "POST",
792
+ headers: {
793
+ Authorization: buildAuthHeader(token),
794
+ "Content-Type": contentType
795
+ },
796
+ body: data
797
+ });
798
+ if (!response.ok) {
799
+ const responseBody = await response.text();
800
+ throw new ZendeskApiError(response.status, response.statusText, responseBody);
801
+ }
802
+ return response.json();
803
+ };
786
804
  const helpCenterUpload = async (subdomain, token, path, formData) => {
787
805
  const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
788
806
  const response = await fetch(url, {
@@ -1905,7 +1923,10 @@ const fetchAllTicketComments = async (subdomain, token, ticketId) => {
1905
1923
  let cursor;
1906
1924
  let pages = 0;
1907
1925
  while (pages < MAX_COMMENT_PAGES) {
1908
- const response = await zendeskGet(subdomain, token, `/tickets/${ticketId}/comments`, buildCursorParams(100, cursor));
1926
+ const response = await zendeskGet(subdomain, token, `/tickets/${ticketId}/comments`, {
1927
+ ...buildCursorParams(100, cursor),
1928
+ include_inline_images: "true"
1929
+ });
1909
1930
  all.push(...response.comments);
1910
1931
  pages += 1;
1911
1932
  if (!response.meta?.has_more || !response.meta?.after_cursor) break;
@@ -1969,6 +1990,20 @@ const fetchTicketSla = async (subdomain, token, ticket) => {
1969
1990
  };
1970
1991
  const createTicketTools = (ctx) => {
1971
1992
  const { subdomain, getToken } = ctx;
1993
+ const attachmentSchema = z.object({
1994
+ file_name: z.string().min(1).describe("File name, e.g. \"app.log\" or \"screenshot.png\"."),
1995
+ file_base64: z.string().min(1).base64().describe("File content encoded as base64."),
1996
+ content_type: z.string().min(1).default("application/octet-stream").describe("MIME type, e.g. \"text/plain\", \"image/png\", \"application/pdf\".")
1997
+ });
1998
+ const uploadAttachments = async (token, files) => {
1999
+ let uploadToken;
2000
+ for (const file of files) {
2001
+ const { upload } = await zendeskUpload(subdomain, token, file.file_name, Buffer.from(file.file_base64, "base64"), file.content_type, uploadToken);
2002
+ uploadToken = upload.token;
2003
+ }
2004
+ return uploadToken;
2005
+ };
2006
+ const formatAttachmentSuffix = (count) => count ? ` with ${count} attachment(s)` : "";
1972
2007
  return [
1973
2008
  {
1974
2009
  name: "get_ticket",
@@ -1992,7 +2027,7 @@ const createTicketTools = (ctx) => {
1992
2027
  const { ticket } = await zendeskGet(subdomain, token, `/tickets/${ticket_id}`);
1993
2028
  let text = formatTicket(ticket) + formatSlaBlock(await fetchTicketSla(subdomain, token, ticket));
1994
2029
  if (include_comments) {
1995
- const { comments } = await zendeskGet(subdomain, token, `/tickets/${ticket_id}/comments`);
2030
+ const { comments } = await zendeskGet(subdomain, token, `/tickets/${ticket_id}/comments`, { include_inline_images: "true" });
1996
2031
  text += `\n\n---\n# Comments\n\n${comments.map(formatComment).join("\n\n")}`;
1997
2032
  }
1998
2033
  return { content: [{
@@ -2177,10 +2212,11 @@ const createTicketTools = (ctx) => {
2177
2212
  namespace: "tickets",
2178
2213
  readOnly: false,
2179
2214
  title: "Add Private Note",
2180
- description: "Add an internal note (not visible to requester) to a ticket.",
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).",
2181
2216
  inputSchema: z.object({
2182
2217
  ticket_id: z.number().int().describe("Ticket ID"),
2183
- body: z.string().min(1).describe("Note content")
2218
+ body: z.string().min(1).describe("Note content"),
2219
+ attachments: z.array(attachmentSchema).optional().describe("Files to attach to this note (base64-encoded content).")
2184
2220
  }),
2185
2221
  annotations: {
2186
2222
  readOnlyHint: false,
@@ -2189,14 +2225,17 @@ const createTicketTools = (ctx) => {
2189
2225
  openWorldHint: true
2190
2226
  },
2191
2227
  handler: async (params) => {
2192
- const { ticket_id, body } = params;
2193
- await zendeskPut(subdomain, await getToken(), `/tickets/${ticket_id}`, { ticket: { comment: {
2228
+ const { ticket_id, body, attachments } = params;
2229
+ const token = await getToken();
2230
+ const uploads = attachments?.length ? [await uploadAttachments(token, attachments)] : void 0;
2231
+ await zendeskPut(subdomain, token, `/tickets/${ticket_id}`, { ticket: { comment: {
2194
2232
  body,
2195
- public: false
2233
+ public: false,
2234
+ ...uploads && { uploads }
2196
2235
  } } });
2197
2236
  return { content: [{
2198
2237
  type: "text",
2199
- text: `Private note added to ticket #${ticket_id}.`
2238
+ text: `Private note added to ticket #${ticket_id}${formatAttachmentSuffix(attachments?.length)}.`
2200
2239
  }] };
2201
2240
  }
2202
2241
  },
@@ -2205,10 +2244,11 @@ const createTicketTools = (ctx) => {
2205
2244
  namespace: "tickets",
2206
2245
  readOnly: false,
2207
2246
  title: "Add Public Comment",
2208
- description: "Add a public comment (visible to requester) to a ticket.",
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).",
2209
2248
  inputSchema: z.object({
2210
2249
  ticket_id: z.number().int().describe("Ticket ID"),
2211
- body: z.string().min(1).describe("Comment content")
2250
+ body: z.string().min(1).describe("Comment content"),
2251
+ attachments: z.array(attachmentSchema).optional().describe("Files to attach to this comment (base64-encoded content).")
2212
2252
  }),
2213
2253
  annotations: {
2214
2254
  readOnlyHint: false,
@@ -2217,14 +2257,17 @@ const createTicketTools = (ctx) => {
2217
2257
  openWorldHint: true
2218
2258
  },
2219
2259
  handler: async (params) => {
2220
- const { ticket_id, body } = params;
2221
- await zendeskPut(subdomain, await getToken(), `/tickets/${ticket_id}`, { ticket: { comment: {
2260
+ const { ticket_id, body, attachments } = params;
2261
+ const token = await getToken();
2262
+ const uploads = attachments?.length ? [await uploadAttachments(token, attachments)] : void 0;
2263
+ await zendeskPut(subdomain, token, `/tickets/${ticket_id}`, { ticket: { comment: {
2222
2264
  body,
2223
- public: true
2265
+ public: true,
2266
+ ...uploads && { uploads }
2224
2267
  } } });
2225
2268
  return { content: [{
2226
2269
  type: "text",
2227
- text: `Public comment added to ticket #${ticket_id}.`
2270
+ text: `Public comment added to ticket #${ticket_id}${formatAttachmentSuffix(attachments?.length)}.`
2228
2271
  }] };
2229
2272
  }
2230
2273
  },