@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 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 by default** — In both transports, the default 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. A static API-token escape hatch is documented below for stdio-only CI / headless contexts; it's refused at boot in HTTP mode.
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 37 individual tools, 3 namespace proxies, or a single unified tool. Choose the mode that fits your LLM's context budget.
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 for all usersthat's the opposite of this server's per-user OAuth model (the API-token escape hatch exists for stdio CI only, and is refused at boot in HTTP).
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 (or API token for CI) | [Quick start: local](#quick-start-local-stdio) |
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; API token refused | [Quick start: remote](#quick-start-remote-http) |
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`** | 37 individual tools (`get_ticket`, `search_articles`, ...) | Clients with good tool selection, full granularity |
53
- | **`namespace`** (default) | 3 proxy tools (`zendesk_tickets`, `zendesk_help_center`, `zendesk_users`) | Balanced context usage, grouped operations |
54
- | **`single`** | 1 proxy tool (`zendesk`) | Minimal context footprint, single entry point |
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 all 36 operations.
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> (10 tools)</summary>
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> (21 tools)</summary>
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> (5 tools)</summary>
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> (1 tool)</summary>
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, all 37 operations in one tool
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
- In stdio, if both `ZENDESK_EMAIL` and `ZENDESK_API_TOKEN` are set, the server uses API token auth; otherwise it uses OAuth 2.1 PKCE. In HTTP mode, API token credentials are refused at boot only per-user OAuth 2.1 PKCE is accepted. Full API-token setup is documented in [`docs/api-token-stdio.md`](docs/api-token-stdio.md).
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 default OAuth 2.1 PKCE flow means each user authenticates with their own
571
- credentials and the server acts with exactly their permissions. API-token auth is
572
- available for headless/CI use (see [Authentication](#authentication)).
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) => token.startsWith("Basic ") ? token : `Bearer ${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 { record } = await zendeskPost(subdomain, await getToken(), "/guide/content_tags", { record: { name } });
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(record)}`
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. A no-op
2246
- * callback (API-token mode) leaves behavior unchanged.
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,