@fruggr/zendesk-mcp-server 1.9.1 → 2.0.1
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 +28 -22
- package/dist/index.js +40 -60
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,9 +13,9 @@ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that co
|
|
|
13
13
|
|
|
14
14
|
Most Zendesk integrations use a shared admin API key, giving every user full access to every ticket. This server takes a different approach:
|
|
15
15
|
|
|
16
|
-
- **Per-user authentication
|
|
16
|
+
- **Per-user authentication, OAuth-only** — In both transports, auth is OAuth 2.1 PKCE: each user authenticates with their own Zendesk credentials, so the LLM sees exactly what the user is allowed to see. Static API tokens are deliberately **not** supported (see [below](#what-this-server-does-not-do)).
|
|
17
17
|
- **Two deployment shapes, same auth story** — Run it on your laptop as a stdio MCP server (Claude Desktop / Claude Code / VS Code) or deploy it as a private remote MCP server with one user, one Zendesk session per HTTP request.
|
|
18
|
-
- **Context-friendly tool modes** — Expose
|
|
18
|
+
- **Context-friendly tool modes** — Expose every operation as its own tool, group them into namespace proxies, or collapse to a single unified tool. Tools are segmented into namespaces you can selectively enable, so each context loads only the surface it needs.
|
|
19
19
|
- **Section-based article editing** — For large Help Center articles, read and rewrite one section at a time (parsed by h1/h2/h3 headings) instead of shuffling the full HTML body through the LLM. Reduces tokens by 10–100× on targeted edits.
|
|
20
20
|
- **Read-only mode** — Restrict the server to read operations only, ideal for assistants that should never modify data.
|
|
21
21
|
- **Lean stack** — Built on the official `@modelcontextprotocol/sdk` plus `zod`.
|
|
@@ -34,14 +34,23 @@ Most Zendesk integrations use a shared admin API key, giving every user full acc
|
|
|
34
34
|
**Look elsewhere when:**
|
|
35
35
|
|
|
36
36
|
- You need Zendesk products outside Support & Guide (e.g. Talk, Explore analytics, Sell) — those endpoints aren't covered.
|
|
37
|
-
- You need a single shared service account
|
|
37
|
+
- You need a single shared service account, or static API-token auth — this server doesn't support either, by design (see [What this server does *not* do](#what-this-server-does-not-do)).
|
|
38
|
+
|
|
39
|
+
## What this server does *not* do
|
|
40
|
+
|
|
41
|
+
**No API-token authentication.** This server is OAuth 2.1 PKCE only — there is no `ZENDESK_EMAIL` + `ZENDESK_API_TOKEN` (Basic auth) mode, in any transport. This is a deliberate design choice:
|
|
42
|
+
|
|
43
|
+
- **API tokens are insufficiently secure.** A Zendesk API token is a long-lived, static, shared secret that carries the full rights of the issuing user — no per-user scoping, no short expiry, no per-user consent or revocation. OAuth 2.1 PKCE issues per-user, revocable tokens instead, so the LLM only ever sees what the authenticated user is allowed to see.
|
|
44
|
+
- **API tokens don't scale.** A single static credential can't attribute actions to individual users or be revoked granularly, and it makes a multi-user remote deployment unsafe (in HTTP it would expose the issuing user's rights to every caller). OAuth scales naturally: each MCP client carries its own user's token.
|
|
45
|
+
|
|
46
|
+
If you specifically need an API-token / service-account mode (e.g. headless CI with a shared account), use one of the other Zendesk MCP servers that support it — see [Inspiration & related projects](#inspiration--related-projects).
|
|
38
47
|
|
|
39
48
|
## Use cases
|
|
40
49
|
|
|
41
50
|
| Persona | Transport | Auth | Quick start |
|
|
42
51
|
|---------|-----------|------|-------------|
|
|
43
|
-
| **Run it on your laptop** — single user, plugged into Claude Desktop / Claude Code / VS Code | `stdio` (default) | OAuth 2.1 PKCE in your browser
|
|
44
|
-
| **Deploy a private remote MCP server** — one server per Zendesk account, each MCP client carries its own user's OAuth token | `http` | Per-user OAuth 2.1 PKCE bearer in `Authorization:` header
|
|
52
|
+
| **Run it on your laptop** — single user, plugged into Claude Desktop / Claude Code / VS Code | `stdio` (default) | OAuth 2.1 PKCE in your browser | [Quick start: local](#quick-start-local-stdio) |
|
|
53
|
+
| **Deploy a private remote MCP server** — one server per Zendesk account, each MCP client carries its own user's OAuth token | `http` | Per-user OAuth 2.1 PKCE bearer in `Authorization:` header | [Quick start: remote](#quick-start-remote-http) |
|
|
45
54
|
|
|
46
55
|
## Tool modes
|
|
47
56
|
|
|
@@ -49,13 +58,13 @@ The server registers tools in one of three modes, controlled by `--mode`:
|
|
|
49
58
|
|
|
50
59
|
| Mode | Tools exposed | Best for |
|
|
51
60
|
|------|--------------|----------|
|
|
52
|
-
| **`all`** |
|
|
53
|
-
| **`namespace`** (default) |
|
|
54
|
-
| **`single`** |
|
|
61
|
+
| **`all`** | Every operation as its own tool (`get_ticket`, `search_articles`, ...) | Clients with good tool selection, full granularity |
|
|
62
|
+
| **`namespace`** (default) | One proxy tool per namespace (`zendesk_tickets`, `zendesk_help_center`, `zendesk_users`) | Balanced context usage, grouped operations |
|
|
63
|
+
| **`single`** | A single proxy tool (`zendesk`) | Minimal context footprint, single entry point |
|
|
55
64
|
|
|
56
65
|
In `namespace` and `single` modes, the proxy tool accepts `{ "operation": "<tool_name>", "params": { ... } }` and dispatches to the appropriate handler after validating params through the original Zod schema. Proxy descriptions include only the first sentence of each sub-operation to stay compact; the full schema is applied when the operation is actually called.
|
|
57
66
|
|
|
58
|
-
> **Tip:** The `single` mode is particularly useful for models with limited tool slots — one tool handles
|
|
67
|
+
> **Tip:** The `single` mode is particularly useful for models with limited tool slots — one tool handles every operation.
|
|
59
68
|
|
|
60
69
|
### Scoping the surface
|
|
61
70
|
|
|
@@ -74,7 +83,7 @@ zendesk-mcp-server acme --namespace tickets
|
|
|
74
83
|
## Available tools
|
|
75
84
|
|
|
76
85
|
<details>
|
|
77
|
-
<summary><strong>Tickets</strong
|
|
86
|
+
<summary><strong>Tickets</strong></summary>
|
|
78
87
|
|
|
79
88
|
| Tool | Description | Mode |
|
|
80
89
|
|------|-------------|------|
|
|
@@ -92,7 +101,7 @@ zendesk-mcp-server acme --namespace tickets
|
|
|
92
101
|
</details>
|
|
93
102
|
|
|
94
103
|
<details>
|
|
95
|
-
<summary><strong>Help Center</strong
|
|
104
|
+
<summary><strong>Help Center</strong></summary>
|
|
96
105
|
|
|
97
106
|
| Tool | Description | Mode |
|
|
98
107
|
|------|-------------|------|
|
|
@@ -121,7 +130,7 @@ zendesk-mcp-server acme --namespace tickets
|
|
|
121
130
|
</details>
|
|
122
131
|
|
|
123
132
|
<details>
|
|
124
|
-
<summary><strong>Users & Organizations</strong
|
|
133
|
+
<summary><strong>Users & Organizations</strong></summary>
|
|
125
134
|
|
|
126
135
|
| Tool | Description | Mode |
|
|
127
136
|
|------|-------------|------|
|
|
@@ -134,7 +143,7 @@ zendesk-mcp-server acme --namespace tickets
|
|
|
134
143
|
</details>
|
|
135
144
|
|
|
136
145
|
<details>
|
|
137
|
-
<summary><strong>Search</strong
|
|
146
|
+
<summary><strong>Search</strong></summary>
|
|
138
147
|
|
|
139
148
|
| Tool | Description | Mode |
|
|
140
149
|
|------|-------------|------|
|
|
@@ -202,8 +211,6 @@ browser sign-in.
|
|
|
202
211
|
> `--callback-port`) to a free port — remember to register the matching
|
|
203
212
|
> `http://localhost:<port>/callback` redirect URL in your Zendesk OAuth client.
|
|
204
213
|
|
|
205
|
-
> **API token escape hatch (stdio only).** For headless/CI environments where a browser is unavailable, set `ZENDESK_EMAIL` + `ZENDESK_API_TOKEN` (generate the token in **Admin Center → Apps and integrations → APIs → Zendesk API → Token Access**). The MCP server then uses Basic auth instead of starting the OAuth flow. This mode is **refused at boot in HTTP** because a shared static credential would expose every caller to the issuing user's rights.
|
|
206
|
-
|
|
207
214
|
### MCP client wiring
|
|
208
215
|
|
|
209
216
|
<details>
|
|
@@ -428,7 +435,7 @@ Options:
|
|
|
428
435
|
**Examples:**
|
|
429
436
|
|
|
430
437
|
```bash
|
|
431
|
-
# Local single-tool mode — minimal context,
|
|
438
|
+
# Local single-tool mode — minimal context, every operation in one tool
|
|
432
439
|
zendesk-mcp-server acme --mode single
|
|
433
440
|
|
|
434
441
|
# Read-only tickets only
|
|
@@ -450,8 +457,6 @@ zendesk-mcp-server acme --transport http --port 8080 \
|
|
|
450
457
|
| `ZENDESK_OAUTH_CLIENT_ID` | no | `<subdomain>_zendesk` | OAuth client identifier |
|
|
451
458
|
| `ZENDESK_OAUTH_CALLBACK_PORT` | no | `27439` | Local port for the OAuth browser callback (also `--callback-port`). Must match the redirect URL registered in Zendesk. **stdio only**. |
|
|
452
459
|
| `ZENDESK_TOKEN_FILE` | no | OS config dir | Path to the persisted OAuth token file (`0600`). |
|
|
453
|
-
| `ZENDESK_EMAIL` | stdio API-token only | — | Agent email for Basic auth — **refused in HTTP** |
|
|
454
|
-
| `ZENDESK_API_TOKEN` | stdio API-token only | — | Zendesk API token — **refused in HTTP** |
|
|
455
460
|
| `TRANSPORT` | no | `stdio` | `stdio` or `http` |
|
|
456
461
|
| `HOST` | no | `0.0.0.0` | HTTP bind host |
|
|
457
462
|
| `PORT` | no | `3000` | HTTP bind port (`0` to let the OS pick) |
|
|
@@ -459,7 +464,7 @@ zendesk-mcp-server acme --transport http --port 8080 \
|
|
|
459
464
|
| `CORS_ORIGIN` | no | — | Comma-separated browser origins added to the default CORS allowlist |
|
|
460
465
|
| `LOG_LEVEL` | no | `info` | Log verbosity (`debug` surfaces the full OAuth flow trace) |
|
|
461
466
|
|
|
462
|
-
|
|
467
|
+
The server uses per-user OAuth 2.1 PKCE for every transport (local stdio and remote HTTP). There is no static API-token mode — see [What this server does *not* do](#what-this-server-does-not-do).
|
|
463
468
|
|
|
464
469
|
## Troubleshooting
|
|
465
470
|
|
|
@@ -567,9 +572,10 @@ Versions follow [SemVer](https://semver.org/) and are calculated **automatically
|
|
|
567
572
|
## FAQ
|
|
568
573
|
|
|
569
574
|
**Do I need a Zendesk admin API key?**
|
|
570
|
-
No. The
|
|
571
|
-
credentials and the server acts with exactly their
|
|
572
|
-
|
|
575
|
+
No — and the server doesn't support one. The OAuth 2.1 PKCE flow means each user
|
|
576
|
+
authenticates with their own credentials and the server acts with exactly their
|
|
577
|
+
permissions. Static API tokens are intentionally unsupported (see
|
|
578
|
+
[What this server does *not* do](#what-this-server-does-not-do)).
|
|
573
579
|
|
|
574
580
|
**Which Zendesk products are supported?**
|
|
575
581
|
Zendesk Support (tickets, users, organizations) and the Help Center / Guide
|
package/dist/index.js
CHANGED
|
@@ -21,16 +21,6 @@ import remarkStringify from "remark-stringify";
|
|
|
21
21
|
import { unified } from "unified";
|
|
22
22
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
23
23
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
24
|
-
//#region src/auth/api-token.ts
|
|
25
|
-
/**
|
|
26
|
-
* API token authentication for stdio transport.
|
|
27
|
-
* Uses Basic auth: base64(email/token:api_token)
|
|
28
|
-
*/
|
|
29
|
-
const buildBasicAuthHeader = (email, apiToken) => {
|
|
30
|
-
const credentials = `${email}/token:${apiToken}`;
|
|
31
|
-
return `Basic ${Buffer.from(credentials).toString("base64")}`;
|
|
32
|
-
};
|
|
33
|
-
//#endregion
|
|
34
24
|
//#region src/utils/logger.ts
|
|
35
25
|
const SEVERITY = {
|
|
36
26
|
debug: 0,
|
|
@@ -571,8 +561,6 @@ const Transport = z.enum(["stdio", "http"]);
|
|
|
571
561
|
const ConfigSchema = z.object({
|
|
572
562
|
subdomain: z.string().min(1, "ZENDESK_SUBDOMAIN is required"),
|
|
573
563
|
oauthClientId: z.string().min(1),
|
|
574
|
-
zendeskEmail: z.string().optional(),
|
|
575
|
-
zendeskApiToken: z.string().optional(),
|
|
576
564
|
logLevel: LogLevel,
|
|
577
565
|
mode: ToolMode,
|
|
578
566
|
readOnly: z.boolean(),
|
|
@@ -657,14 +645,9 @@ const loadConfig = (argv = process.argv.slice(2)) => {
|
|
|
657
645
|
const corsFromEnv = (process.env["CORS_ORIGIN"] ?? "").split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
658
646
|
const corsOrigins = [...cli.corsOrigins ?? [], ...corsFromEnv];
|
|
659
647
|
const callbackPort = cli.callbackPort ?? parsePortEnv(process.env["ZENDESK_OAUTH_CALLBACK_PORT"], "ZENDESK_OAUTH_CALLBACK_PORT");
|
|
660
|
-
const zendeskEmail = process.env["ZENDESK_EMAIL"];
|
|
661
|
-
const zendeskApiToken = process.env["ZENDESK_API_TOKEN"];
|
|
662
|
-
if (transport === "http" && zendeskEmail && zendeskApiToken) throw new Error("API token authentication (ZENDESK_EMAIL + ZENDESK_API_TOKEN) is not supported in HTTP mode. HTTP mode requires per-user OAuth 2.1 PKCE - unset these variables and configure your MCP client to perform the OAuth flow against Zendesk.");
|
|
663
648
|
return ConfigSchema.parse({
|
|
664
649
|
subdomain,
|
|
665
650
|
oauthClientId,
|
|
666
|
-
zendeskEmail,
|
|
667
|
-
zendeskApiToken,
|
|
668
651
|
logLevel: cli.logLevel ?? process.env["LOG_LEVEL"] ?? "info",
|
|
669
652
|
mode,
|
|
670
653
|
readOnly: cli.readOnly ?? false,
|
|
@@ -702,7 +685,7 @@ var ZendeskApiError = class ZendeskApiError extends Error {
|
|
|
702
685
|
}
|
|
703
686
|
}
|
|
704
687
|
};
|
|
705
|
-
const buildAuthHeader = (token) =>
|
|
688
|
+
const buildAuthHeader = (token) => `Bearer ${token}`;
|
|
706
689
|
const buildUrl = (base, path, params) => {
|
|
707
690
|
const url = new URL(`${base}${path}`);
|
|
708
691
|
if (params) for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
|
|
@@ -1080,11 +1063,11 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1080
1063
|
namespace: "help_center",
|
|
1081
1064
|
readOnly: true,
|
|
1082
1065
|
title: "List Help Center Categories",
|
|
1083
|
-
description: "List all Help Center categories.
|
|
1066
|
+
description: "List all Help Center categories. Categories are the top level of the Guide hierarchy (category → section → article); each entry includes its id, name and locale. Results are cursor-paginated. Pair a returned category id with list_sections to drill down, then list_articles to reach articles. Pass a locale to read category names in that translation.",
|
|
1084
1067
|
inputSchema: z.object({
|
|
1085
|
-
locale: z.string().optional(),
|
|
1086
|
-
page_size: z.number().int().min(1).max(100).default(100),
|
|
1087
|
-
cursor: z.string().optional()
|
|
1068
|
+
locale: z.string().optional().describe("Locale for category names (e.g., \"en-us\", \"fr\"). Defaults to the Help Center default locale."),
|
|
1069
|
+
page_size: z.number().int().min(1).max(100).default(100).describe("Categories per page (1-100, default 100)."),
|
|
1070
|
+
cursor: z.string().optional().describe("Pagination cursor from a previous response; omit for the first page.")
|
|
1088
1071
|
}),
|
|
1089
1072
|
annotations: {
|
|
1090
1073
|
readOnlyHint: true,
|
|
@@ -1106,12 +1089,12 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1106
1089
|
namespace: "help_center",
|
|
1107
1090
|
readOnly: true,
|
|
1108
1091
|
title: "List Help Center Sections",
|
|
1109
|
-
description: "List sections
|
|
1092
|
+
description: "List Help Center sections. Sections are the middle level of the Guide hierarchy (category → section → article) and group related articles; each entry includes its id, name, category_id and locale. Results are cursor-paginated. Pass category_id to list only one category's sections (ids come from list_categories), then use a section id with list_articles. Pass a locale to read section names in that translation.",
|
|
1110
1093
|
inputSchema: z.object({
|
|
1111
|
-
category_id: z.number().int().optional(),
|
|
1112
|
-
locale: z.string().optional(),
|
|
1113
|
-
page_size: z.number().int().min(1).max(100).default(100),
|
|
1114
|
-
cursor: z.string().optional()
|
|
1094
|
+
category_id: z.number().int().optional().describe("Restrict to sections of this category (id from list_categories). Omit to list every section."),
|
|
1095
|
+
locale: z.string().optional().describe("Locale for section names (e.g., \"en-us\", \"fr\"). Defaults to the Help Center default locale."),
|
|
1096
|
+
page_size: z.number().int().min(1).max(100).default(100).describe("Sections per page (1-100, default 100)."),
|
|
1097
|
+
cursor: z.string().optional().describe("Pagination cursor from a previous response; omit for the first page.")
|
|
1115
1098
|
}),
|
|
1116
1099
|
annotations: {
|
|
1117
1100
|
readOnlyHint: true,
|
|
@@ -1206,13 +1189,13 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1206
1189
|
namespace: "help_center",
|
|
1207
1190
|
readOnly: false,
|
|
1208
1191
|
title: "Create Article Translation",
|
|
1209
|
-
description: "Create a translation for an existing article in a specific locale.",
|
|
1192
|
+
description: "Create a translation for an existing article in a specific locale. The article must already exist (create it with create_article); this adds a new localized version and returns the created translation (locale, title, draft state). The target locale must not already have a translation — use update_article_translation to modify an existing one, and list_article_translations to see which locales exist. Provide the full HTML body.",
|
|
1210
1193
|
inputSchema: z.object({
|
|
1211
|
-
article_id: z.number().int(),
|
|
1194
|
+
article_id: z.number().int().describe("ID of the existing article to translate."),
|
|
1212
1195
|
locale: z.string().describe("Target locale (e.g., \"fr\", \"de\")"),
|
|
1213
|
-
title: z.string().min(1),
|
|
1196
|
+
title: z.string().min(1).describe("Translated article title."),
|
|
1214
1197
|
body: z.string().min(1).describe("Translated body (HTML)"),
|
|
1215
|
-
draft: z.boolean().default(false)
|
|
1198
|
+
draft: z.boolean().default(false).describe("Create the translation as a draft (not visible to end users). Defaults to false (published).")
|
|
1216
1199
|
}),
|
|
1217
1200
|
annotations: {
|
|
1218
1201
|
readOnlyHint: false,
|
|
@@ -1374,8 +1357,8 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1374
1357
|
namespace: "help_center",
|
|
1375
1358
|
readOnly: false,
|
|
1376
1359
|
title: "Create Content Tag",
|
|
1377
|
-
description: "Create a new content tag for Guide articles.",
|
|
1378
|
-
inputSchema: z.object({ name: z.string().min(1).describe("Content tag name") }),
|
|
1360
|
+
description: "Create a new content tag for Guide articles. Content tags are end-user visible labels that help readers discover related articles; this returns the created tag with its id. Check list_content_tags first to avoid duplicates, then attach the new id via the content_tag_ids parameter of create_article or update_article. For internal search-ranking labels that are not shown to end users, use article labels (list_labels) instead.",
|
|
1361
|
+
inputSchema: z.object({ name: z.string().min(1).describe("Content tag name as shown to end users (e.g., \"billing\", \"getting-started\").") }),
|
|
1379
1362
|
annotations: {
|
|
1380
1363
|
readOnlyHint: false,
|
|
1381
1364
|
destructiveHint: false,
|
|
@@ -1436,8 +1419,8 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1436
1419
|
namespace: "help_center",
|
|
1437
1420
|
readOnly: true,
|
|
1438
1421
|
title: "List Article Attachments",
|
|
1439
|
-
description: "List all attachments for an article.",
|
|
1440
|
-
inputSchema: z.object({ article_id: z.number().int().describe("
|
|
1422
|
+
description: "List all attachments for an article. Returns attachment metadata only (id, file name, content type, size, URL), not the file bytes; both inline and block attachments are included. This is for Help Center articles — for attachments on support tickets use get_ticket_attachments instead. Upload new files with create_article_attachment.",
|
|
1423
|
+
inputSchema: z.object({ article_id: z.number().int().describe("ID of the Help Center article whose attachments to list.") }),
|
|
1441
1424
|
annotations: {
|
|
1442
1425
|
readOnlyHint: true,
|
|
1443
1426
|
destructiveHint: false,
|
|
@@ -1868,7 +1851,7 @@ const createTicketTools = (ctx) => {
|
|
|
1868
1851
|
namespace: "tickets",
|
|
1869
1852
|
readOnly: false,
|
|
1870
1853
|
title: "Create Zendesk Ticket",
|
|
1871
|
-
description: "Create a new Zendesk support ticket with subject, description, and optional priority/type/assignee/tags.",
|
|
1854
|
+
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.",
|
|
1872
1855
|
inputSchema: z.object({
|
|
1873
1856
|
subject: z.string().min(1).describe("Ticket subject"),
|
|
1874
1857
|
description: z.string().min(1).describe("Ticket description"),
|
|
@@ -1877,20 +1860,20 @@ const createTicketTools = (ctx) => {
|
|
|
1877
1860
|
"high",
|
|
1878
1861
|
"normal",
|
|
1879
1862
|
"low"
|
|
1880
|
-
]).optional(),
|
|
1863
|
+
]).optional().describe("Ticket priority. One of urgent, high, normal, low."),
|
|
1881
1864
|
type: z.enum([
|
|
1882
1865
|
"problem",
|
|
1883
1866
|
"incident",
|
|
1884
1867
|
"question",
|
|
1885
1868
|
"task"
|
|
1886
|
-
]).optional(),
|
|
1887
|
-
assignee_id: z.number().int().optional(),
|
|
1888
|
-
group_id: z.number().int().optional(),
|
|
1889
|
-
tags: z.array(z.string()).optional(),
|
|
1869
|
+
]).optional().describe("Ticket type. One of problem, incident, question, task."),
|
|
1870
|
+
assignee_id: z.number().int().optional().describe("User id of the agent to assign the ticket to."),
|
|
1871
|
+
group_id: z.number().int().optional().describe("Id of the group to assign the ticket to."),
|
|
1872
|
+
tags: z.array(z.string()).optional().describe("Tags to set on the ticket."),
|
|
1890
1873
|
custom_fields: z.array(z.object({
|
|
1891
1874
|
id: z.number().int(),
|
|
1892
1875
|
value: z.unknown()
|
|
1893
|
-
})).optional()
|
|
1876
|
+
})).optional().describe("Custom field values as { id, value } pairs (field ids come from your Zendesk admin settings).")
|
|
1894
1877
|
}),
|
|
1895
1878
|
annotations: {
|
|
1896
1879
|
readOnlyHint: false,
|
|
@@ -1916,7 +1899,7 @@ const createTicketTools = (ctx) => {
|
|
|
1916
1899
|
namespace: "tickets",
|
|
1917
1900
|
readOnly: false,
|
|
1918
1901
|
title: "Update Zendesk Ticket",
|
|
1919
|
-
description: "Update an existing ticket (status, priority, type, assignee, group, subject, tags, custom fields).",
|
|
1902
|
+
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.",
|
|
1920
1903
|
inputSchema: z.object({
|
|
1921
1904
|
ticket_id: z.number().int().describe("Ticket ID"),
|
|
1922
1905
|
status: z.enum([
|
|
@@ -1926,27 +1909,27 @@ const createTicketTools = (ctx) => {
|
|
|
1926
1909
|
"hold",
|
|
1927
1910
|
"solved",
|
|
1928
1911
|
"closed"
|
|
1929
|
-
]).optional(),
|
|
1912
|
+
]).optional().describe("New ticket status. One of new, open, pending, hold, solved, closed."),
|
|
1930
1913
|
priority: z.enum([
|
|
1931
1914
|
"urgent",
|
|
1932
1915
|
"high",
|
|
1933
1916
|
"normal",
|
|
1934
1917
|
"low"
|
|
1935
|
-
]).optional(),
|
|
1918
|
+
]).optional().describe("Ticket priority. One of urgent, high, normal, low."),
|
|
1936
1919
|
type: z.enum([
|
|
1937
1920
|
"problem",
|
|
1938
1921
|
"incident",
|
|
1939
1922
|
"question",
|
|
1940
1923
|
"task"
|
|
1941
|
-
]).optional(),
|
|
1942
|
-
assignee_id: z.number().int().optional(),
|
|
1943
|
-
group_id: z.number().int().optional(),
|
|
1944
|
-
subject: z.string().optional(),
|
|
1945
|
-
tags: z.array(z.string()).optional(),
|
|
1924
|
+
]).optional().describe("Ticket type. One of problem, incident, question, task."),
|
|
1925
|
+
assignee_id: z.number().int().optional().describe("User id of the agent to assign the ticket to."),
|
|
1926
|
+
group_id: z.number().int().optional().describe("Id of the group to assign the ticket to."),
|
|
1927
|
+
subject: z.string().optional().describe("New ticket subject line."),
|
|
1928
|
+
tags: z.array(z.string()).optional().describe("Replaces the full tag set on the ticket. Use manage_tags for incremental add/remove."),
|
|
1946
1929
|
custom_fields: z.array(z.object({
|
|
1947
1930
|
id: z.number().int(),
|
|
1948
1931
|
value: z.unknown()
|
|
1949
|
-
})).optional()
|
|
1932
|
+
})).optional().describe("Custom field values as { id, value } pairs (field ids come from your Zendesk admin settings).")
|
|
1950
1933
|
}),
|
|
1951
1934
|
annotations: {
|
|
1952
1935
|
readOnlyHint: false,
|
|
@@ -2207,10 +2190,10 @@ const createUserTools = (ctx) => {
|
|
|
2207
2190
|
namespace: "users",
|
|
2208
2191
|
readOnly: true,
|
|
2209
2192
|
title: "List Zendesk Organizations",
|
|
2210
|
-
description: "List all organizations with pagination.",
|
|
2193
|
+
description: "List all organizations with pagination. Returns the name and id of each organization plus basic fields; results are cursor-paginated. Use get_organization with an id for full details (tags, domains, notes), or search for query-based lookups by name. Organizations group end users and can be referenced when creating or filtering tickets.",
|
|
2211
2194
|
inputSchema: z.object({
|
|
2212
|
-
page_size: z.number().int().min(1).max(100).default(100),
|
|
2213
|
-
cursor: z.string().optional()
|
|
2195
|
+
page_size: z.number().int().min(1).max(100).default(100).describe("Organizations per page (1-100, default 100)."),
|
|
2196
|
+
cursor: z.string().optional().describe("Pagination cursor from a previous response; omit for the first page.")
|
|
2214
2197
|
}),
|
|
2215
2198
|
annotations: {
|
|
2216
2199
|
readOnlyHint: true,
|
|
@@ -2242,8 +2225,9 @@ const createAllTools = (ctx) => [
|
|
|
2242
2225
|
/**
|
|
2243
2226
|
* Invoke a tool handler, notifying `onUnauthorized` when Zendesk rejects the
|
|
2244
2227
|
* token (401). This lets the OAuth store drop the dead token so the next call
|
|
2245
|
-
* refreshes/re-authenticates instead of replaying a revoked token.
|
|
2246
|
-
*
|
|
2228
|
+
* refreshes/re-authenticates instead of replaying a revoked token. The callback
|
|
2229
|
+
* is omitted only where there is nothing to invalidate (e.g. HTTP per-session
|
|
2230
|
+
* bearer, owned by the client).
|
|
2247
2231
|
*/
|
|
2248
2232
|
const runHandler = async (def, params, onUnauthorized) => {
|
|
2249
2233
|
try {
|
|
@@ -2704,10 +2688,6 @@ const startStdioTransport = async (server, logger = silentLogger) => {
|
|
|
2704
2688
|
//#endregion
|
|
2705
2689
|
//#region src/index.ts
|
|
2706
2690
|
const buildStdioServer = (config, logger) => {
|
|
2707
|
-
if (config.zendeskEmail && config.zendeskApiToken) {
|
|
2708
|
-
const staticToken = buildBasicAuthHeader(config.zendeskEmail, config.zendeskApiToken);
|
|
2709
|
-
return createMcpServer(config, () => staticToken, logger);
|
|
2710
|
-
}
|
|
2711
2691
|
const tokenStore = createTokenStore({
|
|
2712
2692
|
subdomain: config.subdomain,
|
|
2713
2693
|
oauthClientId: config.oauthClientId,
|