@fruggr/zendesk-mcp-server 1.9.0 → 2.0.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 +28 -22
- package/dist/index.js +6 -26
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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);
|
|
@@ -1384,10 +1367,10 @@ const createHelpCenterTools = (ctx) => {
|
|
|
1384
1367
|
},
|
|
1385
1368
|
handler: async (params) => {
|
|
1386
1369
|
const { name } = params;
|
|
1387
|
-
const {
|
|
1370
|
+
const { content_tag } = await zendeskPost(subdomain, await getToken(), "/guide/content_tags", { content_tag: { name } });
|
|
1388
1371
|
return { content: [{
|
|
1389
1372
|
type: "text",
|
|
1390
|
-
text: `Content tag created.\n\n${formatContentTag(
|
|
1373
|
+
text: `Content tag created.\n\n${formatContentTag(content_tag)}`
|
|
1391
1374
|
}] };
|
|
1392
1375
|
}
|
|
1393
1376
|
},
|
|
@@ -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,
|