@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 +10 -7
- package/dist/index.js +57 -14
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
# Zendesk MCP Server
|
|
2
2
|
|
|
3
3
|
[](https://glama.ai/mcp/servers/fruggr/zendesk-mcp-server)
|
|
4
|
+
[](https://registry.modelcontextprotocol.io/?search=io.github.fruggr/zendesk-mcp-server)
|
|
4
5
|
[](https://www.npmjs.com/package/@fruggr/zendesk-mcp-server)
|
|
5
6
|
[](LICENSE)
|
|
6
7
|
[](https://nodejs.org)
|
|
7
8
|
[](https://renovatebot.com)
|
|
8
9
|
[](https://github.com/semantic-release/semantic-release)
|
|
9
10
|
|
|
10
|
-
**Bring Zendesk
|
|
11
|
-
[Model Context Protocol](https://modelcontextprotocol.io) (MCP) server
|
|
12
|
-
|
|
13
|
-
|
|
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,
|
|
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`,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
},
|