@fruggr/zendesk-mcp-server 1.1.2 → 1.1.3

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/dist/index.js CHANGED
@@ -1,2223 +1,1907 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/auth/api-token.ts
4
- var buildBasicAuthHeader = (email, apiToken) => {
5
- const credentials = `${email}/token:${apiToken}`;
6
- return `Basic ${Buffer.from(credentials).toString("base64")}`;
7
- };
8
-
9
- // src/auth/browser-oauth.ts
10
- import { createHash, randomBytes } from "crypto";
11
- import { createServer } from "http";
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { createServer } from "node:http";
12
4
  import open from "open";
13
-
14
- // src/constants.ts
15
- var CHARACTER_LIMIT = 25e3;
16
- var DEFAULT_PAGE_SIZE = 100;
17
- var MAX_PAGE_SIZE = 100;
18
- var TOKEN_CACHE_TTL_MS = 5 * 60 * 1e3;
19
- var MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
20
- var MAX_EMBEDDED_IMAGE_COUNT = 10;
21
- var MAX_COMMENT_PAGES = Number(process.env["ZENDESK_MAX_COMMENT_PAGES"] ?? 10);
22
- var LARGE_ARTICLE_BODY_CHARS = 3e3;
23
- var LARGE_ARTICLE_SECTION_COUNT = 4;
24
- var getBaseUrl = (subdomain) => `https://${subdomain}.zendesk.com/api/v2`;
25
- var getHelpCenterBaseUrl = (subdomain) => `https://${subdomain}.zendesk.com/api/v2/help_center`;
26
- var getOAuthUrls = (subdomain) => ({
27
- authorizeUrl: `https://${subdomain}.zendesk.com/oauth/authorizations/new`,
28
- tokenUrl: `https://${subdomain}.zendesk.com/oauth/tokens`
5
+ import * as z from "zod/v4";
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import * as cheerio from "cheerio";
8
+ import { toHtml } from "hast-util-to-html";
9
+ import rehypeParse from "rehype-parse";
10
+ import rehypeRaw from "rehype-raw";
11
+ import rehypeRemark from "rehype-remark";
12
+ import rehypeStringify from "rehype-stringify";
13
+ import remarkGfm from "remark-gfm";
14
+ import remarkParse from "remark-parse";
15
+ import remarkRehype from "remark-rehype";
16
+ import remarkStringify from "remark-stringify";
17
+ import { unified } from "unified";
18
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
+ //#region src/auth/api-token.ts
20
+ /**
21
+ * API token authentication for stdio transport.
22
+ * Uses Basic auth: base64(email/token:api_token)
23
+ */
24
+ const buildBasicAuthHeader = (email, apiToken) => {
25
+ const credentials = `${email}/token:${apiToken}`;
26
+ return `Basic ${Buffer.from(credentials).toString("base64")}`;
27
+ };
28
+ //#endregion
29
+ //#region src/constants.ts
30
+ const CHARACTER_LIMIT = 25e3;
31
+ const MAX_COMMENT_PAGES = Number(process.env["ZENDESK_MAX_COMMENT_PAGES"] ?? 10);
32
+ const getBaseUrl = (subdomain) => `https://${subdomain}.zendesk.com/api/v2`;
33
+ const getHelpCenterBaseUrl = (subdomain) => `https://${subdomain}.zendesk.com/api/v2/help_center`;
34
+ const getOAuthUrls = (subdomain) => ({
35
+ authorizeUrl: `https://${subdomain}.zendesk.com/oauth/authorizations/new`,
36
+ tokenUrl: `https://${subdomain}.zendesk.com/oauth/tokens`
29
37
  });
30
-
31
- // src/auth/browser-oauth.ts
32
- var DEFAULT_CALLBACK_PORT = 3e3;
33
- var generateCodeVerifier = () => randomBytes(32).toString("base64url");
34
- var generateCodeChallenge = (verifier) => createHash("sha256").update(verifier).digest("base64url");
35
- var authenticateViaBrowser = (config) => {
36
- const { subdomain, oauthClientId } = config;
37
- const { authorizeUrl, tokenUrl } = getOAuthUrls(subdomain);
38
- const codeVerifier = generateCodeVerifier();
39
- const codeChallenge = generateCodeChallenge(codeVerifier);
40
- return new Promise((resolve, reject) => {
41
- let callbackServer;
42
- callbackServer = createServer(async (req, res) => {
43
- const url = new URL(req.url ?? "/", `http://localhost`);
44
- if (url.pathname !== "/callback") {
45
- res.writeHead(404);
46
- res.end("Not found");
47
- return;
48
- }
49
- const code = url.searchParams.get("code");
50
- const error = url.searchParams.get("error");
51
- if (error) {
52
- const desc = url.searchParams.get("error_description") ?? error;
53
- res.writeHead(400, { "Content-Type": "text/html" });
54
- res.end(`<html><body><h1>Authentication failed</h1><p>${desc}</p></body></html>`);
55
- callbackServer.close();
56
- reject(new Error(`OAuth error: ${desc}`));
57
- return;
58
- }
59
- if (!code) {
60
- res.writeHead(400, { "Content-Type": "text/html" });
61
- res.end("<html><body><h1>Missing authorization code</h1></body></html>");
62
- callbackServer.close();
63
- reject(new Error("Missing authorization code in callback"));
64
- return;
65
- }
66
- try {
67
- const callbackPort = callbackServer.address().port;
68
- const tokenBody = new URLSearchParams({
69
- grant_type: "authorization_code",
70
- code,
71
- client_id: oauthClientId,
72
- redirect_uri: `http://localhost:${callbackPort}/callback`,
73
- code_verifier: codeVerifier
74
- });
75
- const tokenResponse = await fetch(tokenUrl, {
76
- method: "POST",
77
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
78
- body: tokenBody.toString()
79
- });
80
- if (!tokenResponse.ok) {
81
- const errorBody = await tokenResponse.text();
82
- throw new Error(`Token exchange failed (${tokenResponse.status}): ${errorBody}`);
83
- }
84
- const tokenData = await tokenResponse.json();
85
- res.writeHead(200, { "Content-Type": "text/html" });
86
- res.end(
87
- "<html><body><h1>Authentication successful!</h1><p>You can close this tab and return to Claude Code.</p><script>window.close()</script></body></html>"
88
- );
89
- callbackServer.close();
90
- resolve(tokenData);
91
- } catch (err) {
92
- res.writeHead(500, { "Content-Type": "text/html" });
93
- res.end(
94
- `<html><body><h1>Token exchange failed</h1><p>${err instanceof Error ? err.message : String(err)}</p></body></html>`
95
- );
96
- callbackServer.close();
97
- reject(err);
98
- }
99
- });
100
- callbackServer.listen(config.callbackPort ?? DEFAULT_CALLBACK_PORT, () => {
101
- const port = callbackServer.address().port;
102
- const redirectUri = `http://localhost:${port}/callback`;
103
- const params = new URLSearchParams({
104
- response_type: "code",
105
- client_id: oauthClientId,
106
- redirect_uri: redirectUri,
107
- scope: "read write",
108
- code_challenge: codeChallenge,
109
- code_challenge_method: "S256"
110
- });
111
- const authUrl = `${authorizeUrl}?${params.toString()}`;
112
- console.error(`Opening browser for Zendesk authentication...`);
113
- console.error(`If the browser doesn't open, visit: ${authUrl}`);
114
- open(authUrl).catch(() => {
115
- });
116
- });
117
- setTimeout(
118
- () => {
119
- callbackServer.close();
120
- reject(new Error("OAuth authentication timed out (5 min). Please try again."));
121
- },
122
- 5 * 60 * 1e3
123
- ).unref();
124
- });
38
+ //#endregion
39
+ //#region src/auth/browser-oauth.ts
40
+ const DEFAULT_CALLBACK_PORT = 3e3;
41
+ const generateCodeVerifier = () => randomBytes(32).toString("base64url");
42
+ const generateCodeChallenge = (verifier) => createHash("sha256").update(verifier).digest("base64url");
43
+ /**
44
+ * Performs OAuth 2.1 PKCE flow by opening the user's browser.
45
+ * Starts a temporary HTTP server to receive the callback.
46
+ * Returns the access token on success.
47
+ */
48
+ const authenticateViaBrowser = (config) => {
49
+ const { subdomain, oauthClientId } = config;
50
+ const { authorizeUrl, tokenUrl } = getOAuthUrls(subdomain);
51
+ const codeVerifier = generateCodeVerifier();
52
+ const codeChallenge = generateCodeChallenge(codeVerifier);
53
+ return new Promise((resolve, reject) => {
54
+ let callbackServer;
55
+ callbackServer = createServer(async (req, res) => {
56
+ const url = new URL(req.url ?? "/", `http://localhost`);
57
+ if (url.pathname !== "/callback") {
58
+ res.writeHead(404);
59
+ res.end("Not found");
60
+ return;
61
+ }
62
+ const code = url.searchParams.get("code");
63
+ const error = url.searchParams.get("error");
64
+ if (error) {
65
+ const desc = url.searchParams.get("error_description") ?? error;
66
+ res.writeHead(400, { "Content-Type": "text/html" });
67
+ res.end(`<html><body><h1>Authentication failed</h1><p>${desc}</p></body></html>`);
68
+ callbackServer.close();
69
+ reject(/* @__PURE__ */ new Error(`OAuth error: ${desc}`));
70
+ return;
71
+ }
72
+ if (!code) {
73
+ res.writeHead(400, { "Content-Type": "text/html" });
74
+ res.end("<html><body><h1>Missing authorization code</h1></body></html>");
75
+ callbackServer.close();
76
+ reject(/* @__PURE__ */ new Error("Missing authorization code in callback"));
77
+ return;
78
+ }
79
+ try {
80
+ const callbackPort = callbackServer.address().port;
81
+ const tokenBody = new URLSearchParams({
82
+ grant_type: "authorization_code",
83
+ code,
84
+ client_id: oauthClientId,
85
+ redirect_uri: `http://localhost:${callbackPort}/callback`,
86
+ code_verifier: codeVerifier
87
+ });
88
+ const tokenResponse = await fetch(tokenUrl, {
89
+ method: "POST",
90
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
91
+ body: tokenBody.toString()
92
+ });
93
+ if (!tokenResponse.ok) {
94
+ const errorBody = await tokenResponse.text();
95
+ throw new Error(`Token exchange failed (${tokenResponse.status}): ${errorBody}`);
96
+ }
97
+ const tokenData = await tokenResponse.json();
98
+ res.writeHead(200, { "Content-Type": "text/html" });
99
+ res.end("<html><body><h1>Authentication successful!</h1><p>You can close this tab and return to Claude Code.</p><script>window.close()<\/script></body></html>");
100
+ callbackServer.close();
101
+ resolve(tokenData);
102
+ } catch (err) {
103
+ res.writeHead(500, { "Content-Type": "text/html" });
104
+ res.end(`<html><body><h1>Token exchange failed</h1><p>${err instanceof Error ? err.message : String(err)}</p></body></html>`);
105
+ callbackServer.close();
106
+ reject(err);
107
+ }
108
+ });
109
+ callbackServer.listen(config.callbackPort ?? DEFAULT_CALLBACK_PORT, () => {
110
+ const redirectUri = `http://localhost:${callbackServer.address().port}/callback`;
111
+ const authUrl = `${authorizeUrl}?${new URLSearchParams({
112
+ response_type: "code",
113
+ client_id: oauthClientId,
114
+ redirect_uri: redirectUri,
115
+ scope: "read write",
116
+ code_challenge: codeChallenge,
117
+ code_challenge_method: "S256"
118
+ }).toString()}`;
119
+ console.error(`Opening browser for Zendesk authentication...`);
120
+ console.error(`If the browser doesn't open, visit: ${authUrl}`);
121
+ open(authUrl).catch(() => {});
122
+ });
123
+ setTimeout(() => {
124
+ callbackServer.close();
125
+ reject(/* @__PURE__ */ new Error("OAuth authentication timed out (5 min). Please try again."));
126
+ }, 300 * 1e3).unref();
127
+ });
125
128
  };
126
-
127
- // src/auth/token-store.ts
128
- var createTokenStore = (config) => {
129
- let token;
130
- let authPromise;
131
- const setToken = (accessToken, refreshToken) => {
132
- token = { accessToken, refreshToken };
133
- };
134
- const ensureToken = async () => {
135
- if (token) return token;
136
- if (!authPromise) {
137
- authPromise = authenticateViaBrowser({
138
- subdomain: config.subdomain,
139
- oauthClientId: config.oauthClientId
140
- }).then((result) => {
141
- const stored = {
142
- accessToken: result.access_token,
143
- refreshToken: result.refresh_token
144
- };
145
- token = stored;
146
- authPromise = void 0;
147
- return stored;
148
- }).catch((err) => {
149
- authPromise = void 0;
150
- throw err;
151
- });
152
- }
153
- return authPromise;
154
- };
155
- const getToken = async () => {
156
- const stored = await ensureToken();
157
- return stored.accessToken;
158
- };
159
- return { getToken, setToken };
129
+ //#endregion
130
+ //#region src/auth/token-store.ts
131
+ const createTokenStore = (config) => {
132
+ let token;
133
+ let authPromise;
134
+ const setToken = (accessToken, refreshToken) => {
135
+ token = {
136
+ accessToken,
137
+ refreshToken
138
+ };
139
+ };
140
+ const ensureToken = async () => {
141
+ if (token) return token;
142
+ if (!authPromise) authPromise = authenticateViaBrowser({
143
+ subdomain: config.subdomain,
144
+ oauthClientId: config.oauthClientId
145
+ }).then((result) => {
146
+ const stored = {
147
+ accessToken: result.access_token,
148
+ refreshToken: result.refresh_token
149
+ };
150
+ token = stored;
151
+ authPromise = void 0;
152
+ return stored;
153
+ }).catch((err) => {
154
+ authPromise = void 0;
155
+ throw err;
156
+ });
157
+ return authPromise;
158
+ };
159
+ const getToken = async () => {
160
+ return (await ensureToken()).accessToken;
161
+ };
162
+ return {
163
+ getToken,
164
+ setToken
165
+ };
160
166
  };
161
-
162
- // src/config.ts
163
- import * as z from "zod/v4";
164
- var ToolMode = z.enum(["single", "namespace", "all"]);
165
- var LogLevel = z.enum(["debug", "info", "warn", "error"]);
166
- var Namespace = z.enum(["tickets", "help_center", "users"]);
167
- var ConfigSchema = z.object({
168
- subdomain: z.string().min(1, "ZENDESK_SUBDOMAIN is required"),
169
- oauthClientId: z.string().min(1),
170
- zendeskEmail: z.string().optional(),
171
- zendeskApiToken: z.string().optional(),
172
- logLevel: LogLevel,
173
- mode: ToolMode,
174
- readOnly: z.boolean(),
175
- namespaces: z.array(Namespace).optional(),
176
- tools: z.array(z.string()).optional()
167
+ //#endregion
168
+ //#region src/config.ts
169
+ const ToolMode = z.enum([
170
+ "single",
171
+ "namespace",
172
+ "all"
173
+ ]);
174
+ const LogLevel = z.enum([
175
+ "debug",
176
+ "info",
177
+ "warn",
178
+ "error"
179
+ ]);
180
+ const Namespace = z.enum([
181
+ "tickets",
182
+ "help_center",
183
+ "users"
184
+ ]);
185
+ const ConfigSchema = z.object({
186
+ subdomain: z.string().min(1, "ZENDESK_SUBDOMAIN is required"),
187
+ oauthClientId: z.string().min(1),
188
+ zendeskEmail: z.string().optional(),
189
+ zendeskApiToken: z.string().optional(),
190
+ logLevel: LogLevel,
191
+ mode: ToolMode,
192
+ readOnly: z.boolean(),
193
+ namespaces: z.array(Namespace).optional(),
194
+ tools: z.array(z.string()).optional()
177
195
  });
178
- var parseCliArgs = (args) => {
179
- const result = {};
180
- let positionalIndex = 0;
181
- for (let i = 0; i < args.length; i++) {
182
- const arg = args[i];
183
- if (arg === void 0) continue;
184
- const next = args[i + 1];
185
- if (arg === "--mode" && next) {
186
- result.mode = next;
187
- i++;
188
- } else if (arg === "--read-only") {
189
- result.readOnly = true;
190
- } else if (arg === "--namespace" && next) {
191
- result.namespaces = result.namespaces ?? [];
192
- result.namespaces.push(next);
193
- i++;
194
- } else if (arg === "--tool" && next) {
195
- result.tools = result.tools ?? [];
196
- result.tools.push(next);
197
- i++;
198
- } else if (arg === "--log-level" && next) {
199
- result.logLevel = next;
200
- i++;
201
- } else if (!arg.startsWith("-") && positionalIndex === 0) {
202
- result.subdomain = arg;
203
- positionalIndex++;
204
- }
205
- }
206
- return result;
196
+ const parseCliArgs = (args) => {
197
+ const result = {};
198
+ let positionalIndex = 0;
199
+ for (let i = 0; i < args.length; i++) {
200
+ const arg = args[i];
201
+ if (arg === void 0) continue;
202
+ const next = args[i + 1];
203
+ if (arg === "--mode" && next) {
204
+ result.mode = next;
205
+ i++;
206
+ } else if (arg === "--read-only") result.readOnly = true;
207
+ else if (arg === "--namespace" && next) {
208
+ result.namespaces = result.namespaces ?? [];
209
+ result.namespaces.push(next);
210
+ i++;
211
+ } else if (arg === "--tool" && next) {
212
+ result.tools = result.tools ?? [];
213
+ result.tools.push(next);
214
+ i++;
215
+ } else if (arg === "--log-level" && next) {
216
+ result.logLevel = next;
217
+ i++;
218
+ } else if (!arg.startsWith("-") && positionalIndex === 0) {
219
+ result.subdomain = arg;
220
+ positionalIndex++;
221
+ }
222
+ }
223
+ return result;
207
224
  };
208
- var loadConfig = (argv = process.argv.slice(2)) => {
209
- const cli = parseCliArgs(argv);
210
- const subdomain = cli.subdomain ?? process.env["ZENDESK_SUBDOMAIN"] ?? "";
211
- const oauthClientId = process.env["ZENDESK_OAUTH_CLIENT_ID"] ?? (subdomain ? `${subdomain}_zendesk` : "");
212
- const mode = cli.tools?.length ? "all" : cli.mode ?? "namespace";
213
- return ConfigSchema.parse({
214
- subdomain,
215
- oauthClientId,
216
- zendeskEmail: process.env["ZENDESK_EMAIL"],
217
- zendeskApiToken: process.env["ZENDESK_API_TOKEN"],
218
- logLevel: cli.logLevel ?? process.env["LOG_LEVEL"] ?? "info",
219
- mode,
220
- readOnly: cli.readOnly ?? false,
221
- namespaces: cli.namespaces,
222
- tools: cli.tools
223
- });
225
+ const loadConfig = (argv = process.argv.slice(2)) => {
226
+ const cli = parseCliArgs(argv);
227
+ const subdomain = cli.subdomain ?? process.env["ZENDESK_SUBDOMAIN"] ?? "";
228
+ const oauthClientId = process.env["ZENDESK_OAUTH_CLIENT_ID"] ?? (subdomain ? `${subdomain}_zendesk` : "");
229
+ const mode = cli.tools?.length ? "all" : cli.mode ?? "namespace";
230
+ return ConfigSchema.parse({
231
+ subdomain,
232
+ oauthClientId,
233
+ zendeskEmail: process.env["ZENDESK_EMAIL"],
234
+ zendeskApiToken: process.env["ZENDESK_API_TOKEN"],
235
+ logLevel: cli.logLevel ?? process.env["LOG_LEVEL"] ?? "info",
236
+ mode,
237
+ readOnly: cli.readOnly ?? false,
238
+ namespaces: cli.namespaces,
239
+ tools: cli.tools
240
+ });
224
241
  };
225
-
226
- // src/server.ts
227
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
228
- import * as z6 from "zod/v4";
229
-
230
- // src/routing/registry.ts
231
- var filterTools = (allTools, options) => allTools.filter((tool) => {
232
- if (options.readOnly && !tool.readOnly) return false;
233
- if (options.namespaces?.length && !options.namespaces.includes(tool.namespace)) return false;
234
- if (options.tools?.length && !options.tools.includes(tool.name)) return false;
235
- return true;
242
+ //#endregion
243
+ //#region src/routing/registry.ts
244
+ const filterTools = (allTools, options) => allTools.filter((tool) => {
245
+ if (options.readOnly && !tool.readOnly) return false;
246
+ if (options.namespaces?.length && !options.namespaces.includes(tool.namespace)) return false;
247
+ if (options.tools?.length && !options.tools.includes(tool.name)) return false;
248
+ return true;
236
249
  });
237
- var groupByNamespace = (tools) => {
238
- const grouped = /* @__PURE__ */ new Map();
239
- for (const tool of tools) {
240
- const existing = grouped.get(tool.namespace) ?? [];
241
- existing.push(tool);
242
- grouped.set(tool.namespace, existing);
243
- }
244
- return grouped;
250
+ const groupByNamespace = (tools) => {
251
+ const grouped = /* @__PURE__ */ new Map();
252
+ for (const tool of tools) {
253
+ const existing = grouped.get(tool.namespace) ?? [];
254
+ existing.push(tool);
255
+ grouped.set(tool.namespace, existing);
256
+ }
257
+ return grouped;
245
258
  };
246
-
247
- // src/tools/help-center.ts
248
- import * as z2 from "zod/v4";
249
-
250
- // src/client/zendesk-api.ts
251
- var ZendeskApiError = class _ZendeskApiError extends Error {
252
- constructor(status, statusText, body) {
253
- super(_ZendeskApiError.buildMessage(status, statusText, body));
254
- this.status = status;
255
- this.statusText = statusText;
256
- this.body = body;
257
- this.name = "ZendeskApiError";
258
- }
259
- status;
260
- statusText;
261
- body;
262
- static buildMessage(status, statusText, body) {
263
- switch (status) {
264
- case 401:
265
- return "Authentication failed. Your Zendesk token may be expired or invalid. Re-authenticate to get a new token.";
266
- case 403:
267
- return "Permission denied. Your Zendesk account does not have access to this resource.";
268
- case 404:
269
- return `Resource not found. Please verify the ID is correct. (${statusText})`;
270
- case 422:
271
- return `Validation error: ${body}`;
272
- case 429:
273
- return "Rate limit exceeded. Please wait before making more requests.";
274
- default:
275
- return `Zendesk API error ${status}: ${statusText}. ${body}`;
276
- }
277
- }
259
+ //#endregion
260
+ //#region src/client/zendesk-api.ts
261
+ var ZendeskApiError = class ZendeskApiError extends Error {
262
+ status;
263
+ statusText;
264
+ body;
265
+ constructor(status, statusText, body) {
266
+ super(ZendeskApiError.buildMessage(status, statusText, body));
267
+ this.status = status;
268
+ this.statusText = statusText;
269
+ this.body = body;
270
+ this.name = "ZendeskApiError";
271
+ }
272
+ static buildMessage(status, statusText, body) {
273
+ switch (status) {
274
+ case 401: return "Authentication failed. Your Zendesk token may be expired or invalid. Re-authenticate to get a new token.";
275
+ case 403: return "Permission denied. Your Zendesk account does not have access to this resource.";
276
+ case 404: return `Resource not found. Please verify the ID is correct. (${statusText})`;
277
+ case 422: return `Validation error: ${body}`;
278
+ case 429: return "Rate limit exceeded. Please wait before making more requests.";
279
+ default: return `Zendesk API error ${status}: ${statusText}. ${body}`;
280
+ }
281
+ }
278
282
  };
279
- var buildAuthHeader = (token) => token.startsWith("Basic ") ? token : `Bearer ${token}`;
280
- var buildUrl = (base, path, params) => {
281
- const url = new URL(`${base}${path}`);
282
- if (params) {
283
- for (const [key, value] of Object.entries(params)) {
284
- url.searchParams.set(key, value);
285
- }
286
- }
287
- return url.toString();
283
+ const buildAuthHeader = (token) => token.startsWith("Basic ") ? token : `Bearer ${token}`;
284
+ const buildUrl = (base, path, params) => {
285
+ const url = new URL(`${base}${path}`);
286
+ if (params) for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
287
+ return url.toString();
288
288
  };
289
- var executeRequest = async (url, token, options = {}) => {
290
- const { method = "GET", body } = options;
291
- const headers = {
292
- Authorization: buildAuthHeader(token),
293
- Accept: "application/json"
294
- };
295
- if (body) {
296
- headers["Content-Type"] = "application/json";
297
- }
298
- const init = { method, headers };
299
- if (body) {
300
- init.body = JSON.stringify(body);
301
- }
302
- const response = await fetch(url, init);
303
- if (!response.ok) {
304
- const responseBody = await response.text();
305
- throw new ZendeskApiError(response.status, response.statusText, responseBody);
306
- }
307
- if (response.status === 204) {
308
- return {};
309
- }
310
- return response.json();
289
+ const executeRequest = async (url, token, options = {}) => {
290
+ const { method = "GET", body } = options;
291
+ const headers = {
292
+ Authorization: buildAuthHeader(token),
293
+ Accept: "application/json"
294
+ };
295
+ if (body) headers["Content-Type"] = "application/json";
296
+ const init = {
297
+ method,
298
+ headers
299
+ };
300
+ if (body) init.body = JSON.stringify(body);
301
+ const response = await fetch(url, init);
302
+ if (!response.ok) {
303
+ const responseBody = await response.text();
304
+ throw new ZendeskApiError(response.status, response.statusText, responseBody);
305
+ }
306
+ if (response.status === 204) return {};
307
+ return response.json();
311
308
  };
312
- var zendeskGet = (subdomain, token, path, params) => {
313
- const url = buildUrl(getBaseUrl(subdomain), path, params);
314
- return executeRequest(url, token);
309
+ const zendeskGet = (subdomain, token, path, params) => {
310
+ return executeRequest(buildUrl(getBaseUrl(subdomain), path, params), token);
315
311
  };
316
- var zendeskPost = (subdomain, token, path, body) => {
317
- const url = buildUrl(getBaseUrl(subdomain), path);
318
- return executeRequest(url, token, { method: "POST", body });
312
+ const zendeskPost = (subdomain, token, path, body) => {
313
+ return executeRequest(buildUrl(getBaseUrl(subdomain), path), token, {
314
+ method: "POST",
315
+ body
316
+ });
319
317
  };
320
- var zendeskPut = (subdomain, token, path, body) => {
321
- const url = buildUrl(getBaseUrl(subdomain), path);
322
- return executeRequest(url, token, { method: "PUT", body });
318
+ const zendeskPut = (subdomain, token, path, body) => {
319
+ return executeRequest(buildUrl(getBaseUrl(subdomain), path), token, {
320
+ method: "PUT",
321
+ body
322
+ });
323
323
  };
324
- var helpCenterGet = (subdomain, token, path, params) => {
325
- const url = buildUrl(getHelpCenterBaseUrl(subdomain), path, params);
326
- return executeRequest(url, token);
324
+ const helpCenterGet = (subdomain, token, path, params) => {
325
+ return executeRequest(buildUrl(getHelpCenterBaseUrl(subdomain), path, params), token);
327
326
  };
328
- var helpCenterPost = (subdomain, token, path, body) => {
329
- const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
330
- return executeRequest(url, token, { method: "POST", body });
327
+ const helpCenterPost = (subdomain, token, path, body) => {
328
+ return executeRequest(buildUrl(getHelpCenterBaseUrl(subdomain), path), token, {
329
+ method: "POST",
330
+ body
331
+ });
331
332
  };
332
- var helpCenterPut = (subdomain, token, path, body) => {
333
- const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
334
- return executeRequest(url, token, { method: "PUT", body });
333
+ const helpCenterPut = (subdomain, token, path, body) => {
334
+ return executeRequest(buildUrl(getHelpCenterBaseUrl(subdomain), path), token, {
335
+ method: "PUT",
336
+ body
337
+ });
335
338
  };
336
- var fetchZendeskBinary = async (subdomain, token, contentUrl) => {
337
- const expectedHost = `${subdomain}.zendesk.com`;
338
- const headers = {};
339
- if (new URL(contentUrl).hostname === expectedHost) {
340
- headers["Authorization"] = buildAuthHeader(token);
341
- }
342
- const response = await fetch(contentUrl, { headers });
343
- if (!response.ok) {
344
- const body = await response.text();
345
- throw new ZendeskApiError(response.status, response.statusText, body);
346
- }
347
- const contentType = response.headers.get("content-type") ?? "application/octet-stream";
348
- const arrayBuffer = await response.arrayBuffer();
349
- return { data: Buffer.from(arrayBuffer), contentType };
339
+ const fetchZendeskBinary = async (subdomain, token, contentUrl) => {
340
+ const expectedHost = `${subdomain}.zendesk.com`;
341
+ const headers = {};
342
+ if (new URL(contentUrl).hostname === expectedHost) headers["Authorization"] = buildAuthHeader(token);
343
+ const response = await fetch(contentUrl, { headers });
344
+ if (!response.ok) {
345
+ const body = await response.text();
346
+ throw new ZendeskApiError(response.status, response.statusText, body);
347
+ }
348
+ const contentType = response.headers.get("content-type") ?? "application/octet-stream";
349
+ const arrayBuffer = await response.arrayBuffer();
350
+ return {
351
+ data: Buffer.from(arrayBuffer),
352
+ contentType
353
+ };
350
354
  };
351
- var helpCenterUpload = async (subdomain, token, path, formData) => {
352
- const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
353
- const response = await fetch(url, {
354
- method: "POST",
355
- headers: { Authorization: buildAuthHeader(token) },
356
- body: formData
357
- });
358
- if (!response.ok) {
359
- const responseBody = await response.text();
360
- throw new ZendeskApiError(response.status, response.statusText, responseBody);
361
- }
362
- return response.json();
355
+ const helpCenterUpload = async (subdomain, token, path, formData) => {
356
+ const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
357
+ const response = await fetch(url, {
358
+ method: "POST",
359
+ headers: { Authorization: buildAuthHeader(token) },
360
+ body: formData
361
+ });
362
+ if (!response.ok) {
363
+ const responseBody = await response.text();
364
+ throw new ZendeskApiError(response.status, response.statusText, responseBody);
365
+ }
366
+ return response.json();
363
367
  };
364
-
365
- // src/utils/article-sections.ts
366
- import * as cheerio from "cheerio";
367
- import { toHtml } from "hast-util-to-html";
368
- import rehypeParse from "rehype-parse";
369
- import rehypeRaw from "rehype-raw";
370
- import rehypeRemark from "rehype-remark";
371
- import rehypeStringify from "rehype-stringify";
372
- import remarkGfm from "remark-gfm";
373
- import remarkParse from "remark-parse";
374
- import remarkRehype from "remark-rehype";
375
- import remarkStringify from "remark-stringify";
376
- import { unified } from "unified";
377
- var HEADING_LEVELS = /* @__PURE__ */ new Set(["h1", "h2", "h3"]);
378
- var countWords = (text) => {
379
- const trimmed = text.trim();
380
- if (!trimmed) return 0;
381
- return trimmed.split(/\s+/).length;
368
+ //#endregion
369
+ //#region src/utils/article-sections.ts
370
+ const HEADING_LEVELS = new Set([
371
+ "h1",
372
+ "h2",
373
+ "h3"
374
+ ]);
375
+ const countWords = (text) => {
376
+ const trimmed = text.trim();
377
+ if (!trimmed) return 0;
378
+ return trimmed.split(/\s+/).length;
382
379
  };
383
- var textOf = (html) => {
384
- if (!html) return "";
385
- const $ = cheerio.load(`<div>${html}</div>`, null, false);
386
- return $("div").first().text();
380
+ const textOf = (html) => {
381
+ if (!html) return "";
382
+ return cheerio.load(`<div>${html}</div>`, null, false)("div").first().text();
387
383
  };
388
- var parseSections = (html) => {
389
- if (!html || !html.trim()) return [];
390
- const $ = cheerio.load(html, null, false);
391
- const children = $.root().contents().toArray();
392
- const introParts = [];
393
- const sections = [];
394
- let current = null;
395
- for (const node of children) {
396
- const tagName = node.type === "tag" ? node.name.toLowerCase() : "";
397
- if (HEADING_LEVELS.has(tagName)) {
398
- const level = Number.parseInt(tagName.slice(1), 10);
399
- current = {
400
- heading: $(node).text().trim(),
401
- headingTag: tagName,
402
- level,
403
- contentParts: []
404
- };
405
- sections.push(current);
406
- continue;
407
- }
408
- const outer = $.html(node);
409
- if (current) {
410
- current.contentParts.push(outer);
411
- } else {
412
- introParts.push(outer);
413
- }
414
- }
415
- const result = [];
416
- if (introParts.length > 0) {
417
- const introHtml = introParts.join("");
418
- result.push({
419
- index: 0,
420
- heading: "intro",
421
- headingTag: "",
422
- level: 0,
423
- html: introHtml,
424
- wordCount: countWords(textOf(introHtml))
425
- });
426
- }
427
- for (const s of sections) {
428
- const sectionHtml = s.contentParts.join("");
429
- result.push({
430
- index: result.length,
431
- heading: s.heading,
432
- headingTag: s.headingTag,
433
- level: s.level,
434
- html: sectionHtml,
435
- wordCount: countWords(textOf(sectionHtml))
436
- });
437
- }
438
- return result;
384
+ const parseSections = (html) => {
385
+ if (!html || !html.trim()) return [];
386
+ const $ = cheerio.load(html, null, false);
387
+ const children = $.root().contents().toArray();
388
+ const introParts = [];
389
+ const sections = [];
390
+ let current = null;
391
+ for (const node of children) {
392
+ const tagName = node.type === "tag" ? node.name.toLowerCase() : "";
393
+ if (HEADING_LEVELS.has(tagName)) {
394
+ const level = Number.parseInt(tagName.slice(1), 10);
395
+ current = {
396
+ heading: $(node).text().trim(),
397
+ headingTag: tagName,
398
+ level,
399
+ contentParts: []
400
+ };
401
+ sections.push(current);
402
+ continue;
403
+ }
404
+ const outer = $.html(node);
405
+ if (current) current.contentParts.push(outer);
406
+ else introParts.push(outer);
407
+ }
408
+ const result = [];
409
+ if (introParts.length > 0) {
410
+ const introHtml = introParts.join("");
411
+ result.push({
412
+ index: 0,
413
+ heading: "intro",
414
+ headingTag: "",
415
+ level: 0,
416
+ html: introHtml,
417
+ wordCount: countWords(textOf(introHtml))
418
+ });
419
+ }
420
+ for (const s of sections) {
421
+ const sectionHtml = s.contentParts.join("");
422
+ result.push({
423
+ index: result.length,
424
+ heading: s.heading,
425
+ headingTag: s.headingTag,
426
+ level: s.level,
427
+ html: sectionHtml,
428
+ wordCount: countWords(textOf(sectionHtml))
429
+ });
430
+ }
431
+ return result;
439
432
  };
440
- var replaceSectionContent = (html, sectionIndex, newHtml) => {
441
- const sections = parseSections(html);
442
- if (sectionIndex < 0 || sectionIndex >= sections.length) {
443
- throw new Error(
444
- `Section index ${sectionIndex} out of range (valid: 0-${Math.max(0, sections.length - 1)})`
445
- );
446
- }
447
- return sections.map((section, idx) => {
448
- const content = idx === sectionIndex ? newHtml : section.html;
449
- if (section.level === 0) return content;
450
- return `<${section.headingTag}>${section.heading}</${section.headingTag}>${content}`;
451
- }).join("");
433
+ const replaceSectionContent = (html, sectionIndex, newHtml) => {
434
+ const sections = parseSections(html);
435
+ if (sectionIndex < 0 || sectionIndex >= sections.length) throw new Error(`Section index ${sectionIndex} out of range (valid: 0-${Math.max(0, sections.length - 1)})`);
436
+ return sections.map((section, idx) => {
437
+ const content = idx === sectionIndex ? newHtml : section.html;
438
+ if (section.level === 0) return content;
439
+ return `<${section.headingTag}>${section.heading}</${section.headingTag}>${content}`;
440
+ }).join("");
452
441
  };
453
- var keepAsHtml = (_state, node) => ({
454
- type: "html",
455
- value: toHtml(node)
442
+ const keepAsHtml = (_state, node) => ({
443
+ type: "html",
444
+ value: toHtml(node)
456
445
  });
457
- var htmlToMdProcessor = unified().use(rehypeParse, { fragment: true }).use(rehypeRemark, { handlers: { table: keepAsHtml, pre: keepAsHtml } }).use(remarkGfm).use(remarkStringify, { bullet: "-", emphasis: "_", fences: true });
458
- var mdToHtmlProcessor = unified().use(remarkParse).use(remarkGfm).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeRaw).use(rehypeStringify);
459
- var htmlToMarkdown = (html) => {
460
- if (!html) return "";
461
- return String(htmlToMdProcessor.processSync(html));
446
+ const htmlToMdProcessor = unified().use(rehypeParse, { fragment: true }).use(rehypeRemark, { handlers: {
447
+ table: keepAsHtml,
448
+ pre: keepAsHtml
449
+ } }).use(remarkGfm).use(remarkStringify, {
450
+ bullet: "-",
451
+ emphasis: "_",
452
+ fences: true
453
+ });
454
+ const mdToHtmlProcessor = unified().use(remarkParse).use(remarkGfm).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeRaw).use(rehypeStringify);
455
+ const htmlToMarkdown = (html) => {
456
+ if (!html) return "";
457
+ return String(htmlToMdProcessor.processSync(html));
462
458
  };
463
- var markdownToHtml = (markdown) => {
464
- if (!markdown) return "";
465
- return String(mdToHtmlProcessor.processSync(markdown));
459
+ const markdownToHtml = (markdown) => {
460
+ if (!markdown) return "";
461
+ return String(mdToHtmlProcessor.processSync(markdown));
466
462
  };
467
-
468
- // src/utils/formatting.ts
469
- var truncateIfNeeded = (text) => {
470
- if (text.length <= CHARACTER_LIMIT) return text;
471
- return `${text.slice(0, CHARACTER_LIMIT)}
472
-
473
- --- Response truncated (${text.length} chars, limit ${CHARACTER_LIMIT}). Use pagination or filters to reduce results. ---`;
463
+ //#endregion
464
+ //#region src/utils/formatting.ts
465
+ const truncateIfNeeded = (text) => {
466
+ if (text.length <= 25e3) return text;
467
+ return `${text.slice(0, CHARACTER_LIMIT)}\n\n--- Response truncated (${text.length} chars, limit ${CHARACTER_LIMIT}). Use pagination or filters to reduce results. ---`;
474
468
  };
475
- var formatPagination = (meta) => {
476
- const parts = [`Results: ${meta.count}`];
477
- if (meta.has_more) {
478
- parts.push(`More available (cursor: ${meta.after_cursor})`);
479
- }
480
- return parts.join(" | ");
469
+ const formatPagination = (meta) => {
470
+ const parts = [`Results: ${meta.count}`];
471
+ if (meta.has_more) parts.push(`More available (cursor: ${meta.after_cursor})`);
472
+ return parts.join(" | ");
481
473
  };
482
- var formatTicket = (ticket) => [
483
- `## Ticket #${ticket.id}: ${ticket.subject}`,
484
- `- **Status**: ${ticket.status} | **Priority**: ${ticket.priority ?? "none"} | **Type**: ${ticket.type ?? "none"}`,
485
- `- **Requester**: ${ticket.requester_id} | **Assignee**: ${ticket.assignee_id ?? "unassigned"}`,
486
- `- **Tags**: ${ticket.tags.length > 0 ? ticket.tags.join(", ") : "none"}`,
487
- `- **Created**: ${ticket.created_at} | **Updated**: ${ticket.updated_at}`,
488
- ticket.description ? `
489
- ${ticket.description}` : ""
474
+ const formatTicket = (ticket) => [
475
+ `## Ticket #${ticket.id}: ${ticket.subject}`,
476
+ `- **Status**: ${ticket.status} | **Priority**: ${ticket.priority ?? "none"} | **Type**: ${ticket.type ?? "none"}`,
477
+ `- **Requester**: ${ticket.requester_id} | **Assignee**: ${ticket.assignee_id ?? "unassigned"}`,
478
+ `- **Tags**: ${ticket.tags.length > 0 ? ticket.tags.join(", ") : "none"}`,
479
+ `- **Created**: ${ticket.created_at} | **Updated**: ${ticket.updated_at}`,
480
+ ticket.description ? `\n${ticket.description}` : ""
490
481
  ].filter(Boolean).join("\n");
491
- var formatComment = (comment) => {
492
- const lines = [
493
- `### ${comment.public ? "Public comment" : "Internal note"} by ${comment.author_id}`,
494
- `*${comment.created_at}*`
495
- ];
496
- if (comment.attachments?.length) {
497
- const summary = comment.attachments.map((a) => `#${a.id} (${a.content_type})`).join(", ");
498
- lines.push(`Attachments: ${summary}`);
499
- }
500
- lines.push("", comment.body);
501
- return lines.join("\n");
482
+ const formatComment = (comment) => {
483
+ const lines = [`### ${comment.public ? "Public comment" : "Internal note"} by ${comment.author_id}`, `*${comment.created_at}*`];
484
+ if (comment.attachments?.length) {
485
+ const summary = comment.attachments.map((a) => `#${a.id} (${a.content_type})`).join(", ");
486
+ lines.push(`Attachments: ${summary}`);
487
+ }
488
+ lines.push("", comment.body);
489
+ return lines.join("\n");
502
490
  };
503
- var formatUser = (user) => [
504
- `## ${user.name} (${user.id})`,
505
- `- **Email**: ${user.email}`,
506
- `- **Role**: ${user.role}`,
507
- user.role_type != null ? `- **Role type**: ${user.role_type}` : "",
508
- `- **Active**: ${user.active}`,
509
- user.organization_id ? `- **Organization**: ${user.organization_id}` : ""
491
+ const formatUser = (user) => [
492
+ `## ${user.name} (${user.id})`,
493
+ `- **Email**: ${user.email}`,
494
+ `- **Role**: ${user.role}`,
495
+ user.role_type != null ? `- **Role type**: ${user.role_type}` : "",
496
+ `- **Active**: ${user.active}`,
497
+ user.organization_id ? `- **Organization**: ${user.organization_id}` : ""
510
498
  ].filter(Boolean).join("\n");
511
- var formatOrganization = (org) => [
512
- `## ${org.name} (${org.id})`,
513
- org.details ? `- **Details**: ${org.details}` : "",
514
- org.domain_names.length > 0 ? `- **Domains**: ${org.domain_names.join(", ")}` : "",
515
- org.tags.length > 0 ? `- **Tags**: ${org.tags.join(", ")}` : ""
499
+ const formatOrganization = (org) => [
500
+ `## ${org.name} (${org.id})`,
501
+ org.details ? `- **Details**: ${org.details}` : "",
502
+ org.domain_names.length > 0 ? `- **Domains**: ${org.domain_names.join(", ")}` : "",
503
+ org.tags.length > 0 ? `- **Tags**: ${org.tags.join(", ")}` : ""
516
504
  ].filter(Boolean).join("\n");
517
- var formatArticleSummary = (article) => [
518
- `## ${article.title} (${article.id})`,
519
- `- **Locale**: ${article.locale} | **Source locale**: ${article.source_locale}`,
520
- `- **Section**: ${article.section_id} | **Draft**: ${article.draft}`,
521
- article.label_names.length > 0 ? `- **Labels**: ${article.label_names.join(", ")}` : "",
522
- `- **Created**: ${article.created_at} | **Updated**: ${article.updated_at}`
505
+ const formatArticleSummary = (article) => [
506
+ `## ${article.title} (${article.id})`,
507
+ `- **Locale**: ${article.locale} | **Source locale**: ${article.source_locale}`,
508
+ `- **Section**: ${article.section_id} | **Draft**: ${article.draft}`,
509
+ article.label_names.length > 0 ? `- **Labels**: ${article.label_names.join(", ")}` : "",
510
+ `- **Created**: ${article.created_at} | **Updated**: ${article.updated_at}`
523
511
  ].filter(Boolean).join("\n");
524
- var formatArticle = (article) => [formatArticleSummary(article), "", article.body].join("\n");
525
- var formatTranslationSummary = (translation) => [
526
- `## Translation: ${translation.locale} (${translation.id})`,
527
- `- **Title**: ${translation.title}`,
528
- `- **Draft**: ${translation.draft}`,
529
- `- **Updated**: ${translation.updated_at}`
512
+ const formatArticle = (article) => [
513
+ formatArticleSummary(article),
514
+ "",
515
+ article.body
516
+ ].join("\n");
517
+ const formatTranslationSummary = (translation) => [
518
+ `## Translation: ${translation.locale} (${translation.id})`,
519
+ `- **Title**: ${translation.title}`,
520
+ `- **Draft**: ${translation.draft}`,
521
+ `- **Updated**: ${translation.updated_at}`
530
522
  ].join("\n");
531
- var formatTranslation = (translation) => [formatTranslationSummary(translation), "", translation.body].join("\n");
532
- var formatCategory = (category) => `- **${category.name}** (${category.id}) \u2014 ${category.description || "No description"}`;
533
- var formatSection = (section) => `- **${section.name}** (${section.id}) \u2014 Category: ${section.category_id} \u2014 ${section.description || "No description"}`;
534
- var formatPermissionGroup = (group) => `- **${group.name}** (${group.id})${group.built_in ? " \u2014 Built-in" : ""}`;
535
- var formatContentTag = (tag) => `- **${tag.name}** (${tag.id})`;
536
- var formatLabel = (label) => `- **${label.name}** (${label.id})`;
537
- var formatUserSegment = (segment) => `- **${segment.name}** (${segment.id}) \u2014 ${segment.user_type}${segment.built_in ? " \u2014 Built-in" : ""}`;
538
- var formatAttachment = (attachment) => `- **${attachment.file_name}** (${attachment.id}) \u2014 ${attachment.content_type} \u2014 ${attachment.size} bytes`;
539
- var formatList = (items, formatter, meta) => {
540
- const header = meta ? formatPagination(meta) : "";
541
- const body = items.map(formatter).join("\n\n");
542
- const text = [header, body].filter(Boolean).join("\n\n");
543
- return truncateIfNeeded(text);
523
+ const formatTranslation = (translation) => [
524
+ formatTranslationSummary(translation),
525
+ "",
526
+ translation.body
527
+ ].join("\n");
528
+ const formatCategory = (category) => `- **${category.name}** (${category.id}) — ${category.description || "No description"}`;
529
+ const formatSection = (section) => `- **${section.name}** (${section.id}) Category: ${section.category_id}${section.description || "No description"}`;
530
+ const formatPermissionGroup = (group) => `- **${group.name}** (${group.id})${group.built_in ? " — Built-in" : ""}`;
531
+ const formatContentTag = (tag) => `- **${tag.name}** (${tag.id})`;
532
+ const formatLabel = (label) => `- **${label.name}** (${label.id})`;
533
+ const formatUserSegment = (segment) => `- **${segment.name}** (${segment.id}) — ${segment.user_type}${segment.built_in ? " — Built-in" : ""}`;
534
+ const formatAttachment = (attachment) => `- **${attachment.file_name}** (${attachment.id}) — ${attachment.content_type} — ${attachment.size} bytes`;
535
+ const formatList = (items, formatter, meta) => {
536
+ return truncateIfNeeded([meta ? formatPagination(meta) : "", items.map(formatter).join("\n\n")].filter(Boolean).join("\n\n"));
544
537
  };
545
-
546
- // src/utils/pagination.ts
547
- var buildCursorParams = (pageSize, cursor) => {
548
- const params = {
549
- "page[size]": String(pageSize)
550
- };
551
- if (cursor) {
552
- params["page[after]"] = cursor;
553
- }
554
- return params;
538
+ //#endregion
539
+ //#region src/utils/pagination.ts
540
+ const buildCursorParams = (pageSize, cursor) => {
541
+ const params = { "page[size]": String(pageSize) };
542
+ if (cursor) params["page[after]"] = cursor;
543
+ return params;
555
544
  };
556
- var buildOffsetParams = (perPage, page) => {
557
- const params = {
558
- per_page: String(perPage)
559
- };
560
- if (page && page > 1) {
561
- params["page"] = String(page);
562
- }
563
- return params;
545
+ const buildOffsetParams = (perPage, page) => {
546
+ const params = { per_page: String(perPage) };
547
+ if (page && page > 1) params["page"] = String(page);
548
+ return params;
564
549
  };
565
- var extractPaginationMeta = (response) => ({
566
- has_more: response.meta?.has_more ?? response.next_page != null,
567
- after_cursor: response.meta?.after_cursor ?? null,
568
- count: response.count ?? 0
550
+ const extractPaginationMeta = (response) => ({
551
+ has_more: response.meta?.has_more ?? response.next_page != null,
552
+ after_cursor: response.meta?.after_cursor ?? null,
553
+ count: response.count ?? 0
569
554
  });
570
- var extractSearchPaginationMeta = (response, perPage, page) => {
571
- const count = response.count ?? 0;
572
- const has_more = count > page * perPage;
573
- return {
574
- has_more,
575
- after_cursor: has_more ? String(page + 1) : null,
576
- count
577
- };
555
+ const extractSearchPaginationMeta = (response, perPage, page) => {
556
+ const count = response.count ?? 0;
557
+ const has_more = count > page * perPage;
558
+ return {
559
+ has_more,
560
+ after_cursor: has_more ? String(page + 1) : null,
561
+ count
562
+ };
578
563
  };
579
-
580
- // src/tools/help-center.ts
581
- var largeArticleHint = (body, sectionCount) => {
582
- if (body.length < LARGE_ARTICLE_BODY_CHARS && sectionCount < LARGE_ARTICLE_SECTION_COUNT) {
583
- return null;
584
- }
585
- return [
586
- `> \u26A0 Large article (${body.length} chars, ${sectionCount} sections).`,
587
- "> For targeted edits, prefer get_article_outline + get_article_section +",
588
- "> update_article_section to avoid re-sending the full body on each write.",
589
- ""
590
- ].join("\n");
564
+ //#endregion
565
+ //#region src/tools/help-center.ts
566
+ const largeArticleHint = (body, sectionCount) => {
567
+ if (body.length < 3e3 && sectionCount < 4) return null;
568
+ return [
569
+ `> ⚠ Large article (${body.length} chars, ${sectionCount} sections).`,
570
+ "> For targeted edits, prefer get_article_outline + get_article_section +",
571
+ "> update_article_section to avoid re-sending the full body on each write.",
572
+ ""
573
+ ].join("\n");
591
574
  };
592
- var createHelpCenterTools = (ctx) => {
593
- const { subdomain, getToken } = ctx;
594
- return [
595
- {
596
- name: "search_articles",
597
- namespace: "help_center",
598
- readOnly: true,
599
- title: "Search Help Center Articles",
600
- description: "Full-text search across Help Center articles (metadata only, no body). Use get_article for full content. Supports locale filtering. Returns total count.",
601
- inputSchema: z2.object({
602
- query: z2.string().min(1).describe("Search query"),
603
- locale: z2.string().optional().describe('Filter by locale (e.g., "en-us", "fr")'),
604
- per_page: z2.number().int().min(1).max(MAX_PAGE_SIZE).default(DEFAULT_PAGE_SIZE).describe("Results per page"),
605
- page: z2.number().int().min(1).default(1).describe("Page number")
606
- }),
607
- annotations: {
608
- readOnlyHint: true,
609
- destructiveHint: false,
610
- idempotentHint: true,
611
- openWorldHint: true
612
- },
613
- handler: async (params) => {
614
- const { query, locale, per_page, page } = params;
615
- const token = await getToken();
616
- const p = { query, ...buildOffsetParams(per_page, page) };
617
- if (locale) p["locale"] = locale;
618
- const response = await helpCenterGet(
619
- subdomain,
620
- token,
621
- "/articles/search",
622
- p
623
- );
624
- return {
625
- content: [
626
- {
627
- type: "text",
628
- text: formatList(
629
- response.results ?? [],
630
- formatArticleSummary,
631
- extractSearchPaginationMeta(response, per_page, page)
632
- )
633
- }
634
- ]
635
- };
636
- }
637
- },
638
- {
639
- name: "get_article",
640
- namespace: "help_center",
641
- readOnly: true,
642
- title: "Get Help Center Article",
643
- description: "Retrieve an article by ID with full body content. For large articles, prefer get_article_outline + get_article_section to save tokens. Optionally specify locale for a translated version. Returns body (HTML), metadata, source_locale, and list of available translations.",
644
- inputSchema: z2.object({
645
- article_id: z2.number().int().describe("Article ID"),
646
- locale: z2.string().optional().describe("Locale for translated version")
647
- }),
648
- annotations: {
649
- readOnlyHint: true,
650
- destructiveHint: false,
651
- idempotentHint: true,
652
- openWorldHint: true
653
- },
654
- handler: async (params) => {
655
- const { article_id, locale } = params;
656
- const token = await getToken();
657
- const path = locale ? `/${locale}/articles/${article_id}` : `/articles/${article_id}`;
658
- const { article } = await helpCenterGet(
659
- subdomain,
660
- token,
661
- path
662
- );
663
- const { translations } = await helpCenterGet(
664
- subdomain,
665
- token,
666
- `/articles/${article_id}/translations`
667
- );
668
- const hint = largeArticleHint(article.body, parseSections(article.body).length);
669
- const text = (hint ?? "") + formatArticle(article) + `
670
-
671
- **Available translations**: ${translations.map((t) => t.locale).join(", ")}`;
672
- return { content: [{ type: "text", text: truncateIfNeeded(text) }] };
673
- }
674
- },
675
- {
676
- name: "list_categories",
677
- namespace: "help_center",
678
- readOnly: true,
679
- title: "List Help Center Categories",
680
- description: "List all Help Center categories. Optionally filter by locale.",
681
- inputSchema: z2.object({
682
- locale: z2.string().optional(),
683
- page_size: z2.number().int().min(1).max(MAX_PAGE_SIZE).default(DEFAULT_PAGE_SIZE),
684
- cursor: z2.string().optional()
685
- }),
686
- annotations: {
687
- readOnlyHint: true,
688
- destructiveHint: false,
689
- idempotentHint: true,
690
- openWorldHint: true
691
- },
692
- handler: async (params) => {
693
- const { locale, page_size, cursor } = params;
694
- const token = await getToken();
695
- const path = locale ? `/${locale}/categories` : "/categories";
696
- const response = await helpCenterGet(
697
- subdomain,
698
- token,
699
- path,
700
- buildCursorParams(page_size, cursor)
701
- );
702
- return {
703
- content: [
704
- {
705
- type: "text",
706
- text: formatList(
707
- response.categories ?? [],
708
- formatCategory,
709
- extractPaginationMeta(response)
710
- )
711
- }
712
- ]
713
- };
714
- }
715
- },
716
- {
717
- name: "list_sections",
718
- namespace: "help_center",
719
- readOnly: true,
720
- title: "List Help Center Sections",
721
- description: "List sections, optionally filtered by category ID and locale.",
722
- inputSchema: z2.object({
723
- category_id: z2.number().int().optional(),
724
- locale: z2.string().optional(),
725
- page_size: z2.number().int().min(1).max(MAX_PAGE_SIZE).default(DEFAULT_PAGE_SIZE),
726
- cursor: z2.string().optional()
727
- }),
728
- annotations: {
729
- readOnlyHint: true,
730
- destructiveHint: false,
731
- idempotentHint: true,
732
- openWorldHint: true
733
- },
734
- handler: async (params) => {
735
- const { category_id, locale, page_size, cursor } = params;
736
- const token = await getToken();
737
- const path = category_id && locale ? `/${locale}/categories/${category_id}/sections` : category_id ? `/categories/${category_id}/sections` : locale ? `/${locale}/sections` : "/sections";
738
- const response = await helpCenterGet(
739
- subdomain,
740
- token,
741
- path,
742
- buildCursorParams(page_size, cursor)
743
- );
744
- return {
745
- content: [
746
- {
747
- type: "text",
748
- text: formatList(
749
- response.sections ?? [],
750
- formatSection,
751
- extractPaginationMeta(response)
752
- )
753
- }
754
- ]
755
- };
756
- }
757
- },
758
- {
759
- name: "list_articles",
760
- namespace: "help_center",
761
- readOnly: true,
762
- title: "List Help Center Articles",
763
- description: 'List articles (metadata only, no body). Use get_article for full content. Optionally filter by section ID and locale. Supports sort_by ("title", "created_at", "updated_at") and include_translations: true to show available translation locales per article. Note: include_translations must be re-sent on each paginated request.',
764
- inputSchema: z2.object({
765
- section_id: z2.number().int().optional(),
766
- locale: z2.string().optional(),
767
- page_size: z2.number().int().min(1).max(MAX_PAGE_SIZE).default(DEFAULT_PAGE_SIZE),
768
- cursor: z2.string().optional(),
769
- sort_by: z2.enum(["created_at", "updated_at", "position", "title"]).default("position").describe("Sort field"),
770
- sort_order: z2.enum(["asc", "desc"]).default("asc").describe("Sort direction"),
771
- include_translations: z2.boolean().default(false).describe(
772
- "Include available translation locales per article (causes 1 extra API call per article)"
773
- )
774
- }),
775
- annotations: {
776
- readOnlyHint: true,
777
- destructiveHint: false,
778
- idempotentHint: true,
779
- openWorldHint: true
780
- },
781
- handler: async (params) => {
782
- const { section_id, locale, page_size, cursor, sort_by, sort_order, include_translations } = params;
783
- const token = await getToken();
784
- const path = section_id && locale ? `/${locale}/sections/${section_id}/articles` : section_id ? `/sections/${section_id}/articles` : locale ? `/${locale}/articles` : "/articles";
785
- const response = await helpCenterGet(
786
- subdomain,
787
- token,
788
- path,
789
- { ...buildCursorParams(page_size, cursor), sort_by, sort_order }
790
- );
791
- const articles = response.articles ?? [];
792
- if (!include_translations) {
793
- return {
794
- content: [
795
- {
796
- type: "text",
797
- text: formatList(articles, formatArticleSummary, extractPaginationMeta(response))
798
- }
799
- ]
800
- };
801
- }
802
- const formatted = await Promise.all(
803
- articles.map(async (article) => {
804
- const { translations } = await helpCenterGet(
805
- subdomain,
806
- token,
807
- `/articles/${article.id}/translations`
808
- );
809
- const locales = translations.map((t) => t.locale).join(", ");
810
- return `${formatArticleSummary(article)}
811
- - **Translations**: ${locales}`;
812
- })
813
- );
814
- const meta = extractPaginationMeta(response);
815
- const header = meta.count ? `Results: ${meta.count}${meta.has_more ? ` | More available (cursor: ${meta.after_cursor})` : ""}` : "";
816
- const text = [header, ...formatted].filter(Boolean).join("\n\n");
817
- return { content: [{ type: "text", text: truncateIfNeeded(text) }] };
818
- }
819
- },
820
- {
821
- name: "list_article_translations",
822
- namespace: "help_center",
823
- readOnly: true,
824
- title: "List Article Translations",
825
- description: "List all available translations for an article (metadata only, no body: locale, title, draft, updated_at). Use get_article with locale for full translated content.",
826
- inputSchema: z2.object({ article_id: z2.number().int().describe("Article ID") }),
827
- annotations: {
828
- readOnlyHint: true,
829
- destructiveHint: false,
830
- idempotentHint: true,
831
- openWorldHint: true
832
- },
833
- handler: async (params) => {
834
- const { article_id } = params;
835
- const token = await getToken();
836
- const { translations } = await helpCenterGet(
837
- subdomain,
838
- token,
839
- `/articles/${article_id}/translations`
840
- );
841
- return {
842
- content: [{ type: "text", text: formatList(translations, formatTranslationSummary) }]
843
- };
844
- }
845
- },
846
- {
847
- name: "create_article_translation",
848
- namespace: "help_center",
849
- readOnly: false,
850
- title: "Create Article Translation",
851
- description: "Create a translation for an existing article in a specific locale.",
852
- inputSchema: z2.object({
853
- article_id: z2.number().int(),
854
- locale: z2.string().describe('Target locale (e.g., "fr", "de")'),
855
- title: z2.string().min(1),
856
- body: z2.string().min(1).describe("Translated body (HTML)"),
857
- draft: z2.boolean().default(false)
858
- }),
859
- annotations: {
860
- readOnlyHint: false,
861
- destructiveHint: false,
862
- idempotentHint: false,
863
- openWorldHint: true
864
- },
865
- handler: async (params) => {
866
- const { article_id, locale, title, body, draft } = params;
867
- const token = await getToken();
868
- const { translation } = await helpCenterPost(
869
- subdomain,
870
- token,
871
- `/articles/${article_id}/translations`,
872
- { translation: { locale, title, body, draft } }
873
- );
874
- return {
875
- content: [
876
- {
877
- type: "text",
878
- text: `Translation created for article #${article_id} in "${locale}".
879
-
880
- ${formatTranslation(translation)}`
881
- }
882
- ]
883
- };
884
- }
885
- },
886
- {
887
- name: "update_article_translation",
888
- namespace: "help_center",
889
- readOnly: false,
890
- title: "Update Article Translation",
891
- description: "Update article content (title, body) in a specific locale. For targeted edits on one or a few sections, prefer update_article_section \u2014 this tool replaces the FULL body and re-sends the entire article on each write. Use the article's source_locale (from get_article) for the default language, or another locale for translations.",
892
- inputSchema: z2.object({
893
- article_id: z2.number().int(),
894
- locale: z2.string(),
895
- title: z2.string().optional(),
896
- body: z2.string().optional(),
897
- draft: z2.boolean().optional()
898
- }),
899
- annotations: {
900
- readOnlyHint: false,
901
- destructiveHint: false,
902
- idempotentHint: true,
903
- openWorldHint: true
904
- },
905
- handler: async (params) => {
906
- const { article_id, locale, ...updates } = params;
907
- const token = await getToken();
908
- const { translation } = await helpCenterPut(
909
- subdomain,
910
- token,
911
- `/articles/${article_id}/translations/${locale}`,
912
- { translation: updates }
913
- );
914
- return {
915
- content: [
916
- {
917
- type: "text",
918
- text: `Translation updated for article #${article_id} in "${locale}".
919
-
920
- ${formatTranslation(translation)}`
921
- }
922
- ]
923
- };
924
- }
925
- },
926
- {
927
- name: "list_permission_groups",
928
- namespace: "help_center",
929
- readOnly: true,
930
- title: "List Permission Groups",
931
- description: "List all Guide permission groups. Use this to find the permission_group_id required when creating articles.",
932
- inputSchema: z2.object({}),
933
- annotations: {
934
- readOnlyHint: true,
935
- destructiveHint: false,
936
- idempotentHint: true,
937
- openWorldHint: true
938
- },
939
- handler: async () => {
940
- const token = await getToken();
941
- const response = await zendeskGet(subdomain, token, "/guide/permission_groups");
942
- return {
943
- content: [
944
- {
945
- type: "text",
946
- text: formatList(response.permission_groups ?? [], formatPermissionGroup)
947
- }
948
- ]
949
- };
950
- }
951
- },
952
- {
953
- name: "create_article",
954
- namespace: "help_center",
955
- readOnly: false,
956
- title: "Create Help Center Article",
957
- description: "Create a new article in a section. The locale becomes the article's source_locale. Requires a permission_group_id (use list_permission_groups to find available IDs). To add content in other locales afterwards, use create_article_translation.",
958
- inputSchema: z2.object({
959
- section_id: z2.number().int(),
960
- title: z2.string().min(1),
961
- body: z2.string().min(1).describe("Article body (HTML)"),
962
- permission_group_id: z2.number().int().describe("Permission group ID (use list_permission_groups to find it)"),
963
- user_segment_id: z2.number().int().optional().describe(
964
- "User segment ID for visibility (use list_user_segments to find it). Defaults to everyone."
965
- ),
966
- author_id: z2.number().int().optional().describe("Author user ID. Defaults to the authenticated user."),
967
- content_tag_ids: z2.array(z2.string()).optional().describe("Content tag IDs (use list_content_tags to find them)"),
968
- locale: z2.string().optional(),
969
- draft: z2.boolean().default(true),
970
- promoted: z2.boolean().default(false),
971
- label_names: z2.array(z2.string()).optional().describe("Label names for search ranking (use list_labels to see existing labels)")
972
- }),
973
- annotations: {
974
- readOnlyHint: false,
975
- destructiveHint: false,
976
- idempotentHint: false,
977
- openWorldHint: true
978
- },
979
- handler: async (params) => {
980
- const { section_id, ...articleData } = params;
981
- const token = await getToken();
982
- const { article } = await helpCenterPost(
983
- subdomain,
984
- token,
985
- `/sections/${section_id}/articles`,
986
- { article: articleData }
987
- );
988
- return {
989
- content: [
990
- { type: "text", text: `Article #${article.id} created.
991
-
992
- ${formatArticle(article)}` }
993
- ]
994
- };
995
- }
996
- },
997
- {
998
- name: "update_article",
999
- namespace: "help_center",
1000
- readOnly: false,
1001
- title: "Update Help Center Article",
1002
- description: "Update article metadata only (draft, promoted, labels, tags, visibility, section, etc.). Does NOT update content (title, body) \u2014 use update_article_translation for that.",
1003
- inputSchema: z2.object({
1004
- article_id: z2.number().int(),
1005
- draft: z2.boolean().optional(),
1006
- promoted: z2.boolean().optional(),
1007
- label_names: z2.array(z2.string()).optional().describe("Label names for search ranking"),
1008
- content_tag_ids: z2.array(z2.string()).optional().describe("Content tag IDs"),
1009
- user_segment_id: z2.number().int().optional().describe("User segment ID for visibility"),
1010
- author_id: z2.number().int().optional().describe("Author user ID"),
1011
- permission_group_id: z2.number().int().optional().describe("Permission group ID"),
1012
- section_id: z2.number().int().optional()
1013
- }),
1014
- annotations: {
1015
- readOnlyHint: false,
1016
- destructiveHint: false,
1017
- idempotentHint: true,
1018
- openWorldHint: true
1019
- },
1020
- handler: async (params) => {
1021
- const { article_id, ...updates } = params;
1022
- const token = await getToken();
1023
- const { article } = await helpCenterPut(
1024
- subdomain,
1025
- token,
1026
- `/articles/${article_id}`,
1027
- { article: updates }
1028
- );
1029
- return {
1030
- content: [
1031
- { type: "text", text: `Article #${article.id} updated.
1032
-
1033
- ${formatArticle(article)}` }
1034
- ]
1035
- };
1036
- }
1037
- },
1038
- {
1039
- name: "list_content_tags",
1040
- namespace: "help_center",
1041
- readOnly: true,
1042
- title: "List Content Tags",
1043
- description: "List all Guide content tags. Content tags are visible to end users and help them find related articles.",
1044
- inputSchema: z2.object({}),
1045
- annotations: {
1046
- readOnlyHint: true,
1047
- destructiveHint: false,
1048
- idempotentHint: true,
1049
- openWorldHint: true
1050
- },
1051
- handler: async () => {
1052
- const token = await getToken();
1053
- const response = await zendeskGet(
1054
- subdomain,
1055
- token,
1056
- "/guide/content_tags"
1057
- );
1058
- return {
1059
- content: [{ type: "text", text: formatList(response.records ?? [], formatContentTag) }]
1060
- };
1061
- }
1062
- },
1063
- {
1064
- name: "create_content_tag",
1065
- namespace: "help_center",
1066
- readOnly: false,
1067
- title: "Create Content Tag",
1068
- description: "Create a new content tag for Guide articles.",
1069
- inputSchema: z2.object({
1070
- name: z2.string().min(1).describe("Content tag name")
1071
- }),
1072
- annotations: {
1073
- readOnlyHint: false,
1074
- destructiveHint: false,
1075
- idempotentHint: false,
1076
- openWorldHint: true
1077
- },
1078
- handler: async (params) => {
1079
- const { name } = params;
1080
- const token = await getToken();
1081
- const { record: record2 } = await zendeskPost(
1082
- subdomain,
1083
- token,
1084
- "/guide/content_tags",
1085
- { record: { name } }
1086
- );
1087
- return {
1088
- content: [{ type: "text", text: `Content tag created.
1089
-
1090
- ${formatContentTag(record2)}` }]
1091
- };
1092
- }
1093
- },
1094
- {
1095
- name: "list_labels",
1096
- namespace: "help_center",
1097
- readOnly: true,
1098
- title: "List Article Labels",
1099
- description: "List all article labels. Labels improve Help Center search ranking and are not visible to end users.",
1100
- inputSchema: z2.object({}),
1101
- annotations: {
1102
- readOnlyHint: true,
1103
- destructiveHint: false,
1104
- idempotentHint: true,
1105
- openWorldHint: true
1106
- },
1107
- handler: async () => {
1108
- const token = await getToken();
1109
- const response = await helpCenterGet(
1110
- subdomain,
1111
- token,
1112
- "/articles/labels"
1113
- );
1114
- return {
1115
- content: [{ type: "text", text: formatList(response.labels ?? [], formatLabel) }]
1116
- };
1117
- }
1118
- },
1119
- {
1120
- name: "list_user_segments",
1121
- namespace: "help_center",
1122
- readOnly: true,
1123
- title: "List User Segments",
1124
- description: "List all user segments. User segments control article visibility (who can view). Use the ID when creating or updating articles.",
1125
- inputSchema: z2.object({}),
1126
- annotations: {
1127
- readOnlyHint: true,
1128
- destructiveHint: false,
1129
- idempotentHint: true,
1130
- openWorldHint: true
1131
- },
1132
- handler: async () => {
1133
- const token = await getToken();
1134
- const response = await helpCenterGet(subdomain, token, "/user_segments");
1135
- return {
1136
- content: [
1137
- { type: "text", text: formatList(response.user_segments ?? [], formatUserSegment) }
1138
- ]
1139
- };
1140
- }
1141
- },
1142
- {
1143
- name: "list_article_attachments",
1144
- namespace: "help_center",
1145
- readOnly: true,
1146
- title: "List Article Attachments",
1147
- description: "List all attachments for an article.",
1148
- inputSchema: z2.object({
1149
- article_id: z2.number().int().describe("Article ID")
1150
- }),
1151
- annotations: {
1152
- readOnlyHint: true,
1153
- destructiveHint: false,
1154
- idempotentHint: true,
1155
- openWorldHint: true
1156
- },
1157
- handler: async (params) => {
1158
- const { article_id } = params;
1159
- const token = await getToken();
1160
- const response = await helpCenterGet(subdomain, token, `/articles/${article_id}/attachments`);
1161
- return {
1162
- content: [
1163
- {
1164
- type: "text",
1165
- text: formatList(response.article_attachments ?? [], formatAttachment)
1166
- }
1167
- ]
1168
- };
1169
- }
1170
- },
1171
- {
1172
- name: "get_article_outline",
1173
- namespace: "help_center",
1174
- readOnly: true,
1175
- title: "Get Article Outline",
1176
- description: "Return a compact outline of an article (list of sections delimited by h1/h2/h3, with word counts) for the given locale (defaults to source_locale). Includes available translations with their outdated status. Use get_article_section to fetch a specific section.",
1177
- inputSchema: z2.object({
1178
- article_id: z2.number().int().describe("Article ID"),
1179
- locale: z2.string().optional().describe("Locale of the body to outline (defaults to article source_locale)")
1180
- }),
1181
- annotations: {
1182
- readOnlyHint: true,
1183
- destructiveHint: false,
1184
- idempotentHint: true,
1185
- openWorldHint: true
1186
- },
1187
- handler: async (params) => {
1188
- const { article_id, locale } = params;
1189
- const token = await getToken();
1190
- const { article } = await helpCenterGet(
1191
- subdomain,
1192
- token,
1193
- `/articles/${article_id}`
1194
- );
1195
- const effectiveLocale = locale ?? article.source_locale;
1196
- const { translation } = await helpCenterGet(
1197
- subdomain,
1198
- token,
1199
- `/articles/${article_id}/translations/${effectiveLocale}`
1200
- );
1201
- const { translations } = await helpCenterGet(subdomain, token, `/articles/${article_id}/translations`);
1202
- const sections = parseSections(translation.body);
1203
- const outlineLines = sections.length ? sections.map(
1204
- (s) => `- [${s.index}] ${s.headingTag ? `${s.headingTag}: ` : ""}${s.heading} (${s.wordCount} words)`
1205
- ).join("\n") : "_(no sections detected)_";
1206
- const translationsList = translations.map((t) => `- ${t.locale}${t.outdated ? " (outdated)" : ""}`).join("\n");
1207
- const text = [
1208
- `# Outline \u2014 Article #${article_id} (${effectiveLocale})`,
1209
- `**Title**: ${translation.title}`,
1210
- "",
1211
- "## Sections",
1212
- outlineLines,
1213
- "",
1214
- "## Available translations",
1215
- translationsList
1216
- ].join("\n");
1217
- return { content: [{ type: "text", text }] };
1218
- }
1219
- },
1220
- {
1221
- name: "get_article_section",
1222
- namespace: "help_center",
1223
- readOnly: true,
1224
- title: "Get Article Section",
1225
- description: 'Retrieve the content of a single section of an article in a given locale. Use get_article_outline first to discover section indexes. Default format="html" for round-trip safety. Pass format="markdown" only for human review \u2014 the Markdown representation is lossy on some structures (<pre> with <br>, tables with multi-<p> cells are kept as raw HTML to limit the damage, but do not round-trip markdown content back through update_article_section).',
1226
- inputSchema: z2.object({
1227
- article_id: z2.number().int().describe("Article ID"),
1228
- locale: z2.string().describe('Locale of the body (e.g., "en-us", "fr")'),
1229
- section_index: z2.number().int().min(0).describe("0-based index of the section (see get_article_outline)"),
1230
- format: z2.enum(["html", "markdown"]).default("html").describe(
1231
- 'Output format. "html" (default) is round-trip safe. "markdown" is lossy on some HTML structures \u2014 use only for human review, not before update_article_section.'
1232
- )
1233
- }),
1234
- annotations: {
1235
- readOnlyHint: true,
1236
- destructiveHint: false,
1237
- idempotentHint: true,
1238
- openWorldHint: true
1239
- },
1240
- handler: async (params) => {
1241
- const { article_id, locale, section_index, format } = params;
1242
- const token = await getToken();
1243
- const { translation } = await helpCenterGet(
1244
- subdomain,
1245
- token,
1246
- `/articles/${article_id}/translations/${locale}`
1247
- );
1248
- const sections = parseSections(translation.body);
1249
- const section = sections[section_index];
1250
- if (!section) {
1251
- throw new Error(
1252
- `Section index ${section_index} not found. Article has ${sections.length} section(s) (0-${Math.max(0, sections.length - 1)}).`
1253
- );
1254
- }
1255
- const content = format === "markdown" ? htmlToMarkdown(section.html) : section.html;
1256
- const headerLine = section.headingTag ? `## [${section.index}] ${section.headingTag}: ${section.heading}` : `## [${section.index}] ${section.heading}`;
1257
- const text = [
1258
- headerLine,
1259
- `_Locale: ${locale} | Words: ${section.wordCount} | Format: ${format}_`,
1260
- "",
1261
- content
1262
- ].join("\n");
1263
- return { content: [{ type: "text", text: truncateIfNeeded(text) }] };
1264
- }
1265
- },
1266
- {
1267
- name: "update_article_section",
1268
- namespace: "help_center",
1269
- readOnly: false,
1270
- title: "Update Article Section",
1271
- description: 'Replace the content of a single section of an article in a given locale, keeping the rest of the body intact. The server fetches the current body, replaces the targeted section, and PUTs the full reconstructed body via the Translations API. Default format="html" for fidelity. Use format="markdown" only when you control the input and know it does not rely on structures that round-trip poorly (code blocks with line breaks, tables with multi-paragraph cells). The section heading is preserved and is NOT part of the replaced content.',
1272
- inputSchema: z2.object({
1273
- article_id: z2.number().int().describe("Article ID"),
1274
- locale: z2.string().describe("Locale of the translation to update"),
1275
- section_index: z2.number().int().min(0).describe("0-based index of the section to replace (see get_article_outline)"),
1276
- content: z2.string().describe(
1277
- 'New content for the section (heading excluded). HTML by default, Markdown if format="markdown".'
1278
- ),
1279
- format: z2.enum(["html", "markdown"]).default("html").describe(
1280
- 'Input format. "html" (default) is the safe path. "markdown" is converted to HTML server-side but may introduce artifacts on complex content.'
1281
- )
1282
- }),
1283
- annotations: {
1284
- readOnlyHint: false,
1285
- destructiveHint: false,
1286
- idempotentHint: true,
1287
- openWorldHint: true
1288
- },
1289
- handler: async (params) => {
1290
- const { article_id, locale, section_index, content, format } = params;
1291
- const token = await getToken();
1292
- const { translation } = await helpCenterGet(
1293
- subdomain,
1294
- token,
1295
- `/articles/${article_id}/translations/${locale}`
1296
- );
1297
- const newSectionHtml = format === "markdown" ? markdownToHtml(content) : content;
1298
- const newBody = replaceSectionContent(translation.body, section_index, newSectionHtml);
1299
- const { translation: updated } = await helpCenterPut(
1300
- subdomain,
1301
- token,
1302
- `/articles/${article_id}/translations/${locale}`,
1303
- { translation: { body: newBody } }
1304
- );
1305
- const updatedSections = parseSections(updated.body);
1306
- const updatedSection = updatedSections[section_index];
1307
- const newWordCount = updatedSection?.wordCount ?? 0;
1308
- const headingLabel = updatedSection?.heading ?? "(intro)";
1309
- const text = [
1310
- `Section [${section_index}] "${headingLabel}" updated for article #${article_id} (${locale}).`,
1311
- `New word count: ${newWordCount}.`
1312
- ].join("\n");
1313
- return { content: [{ type: "text", text }] };
1314
- }
1315
- },
1316
- {
1317
- name: "compare_translations",
1318
- namespace: "help_center",
1319
- readOnly: true,
1320
- title: "Compare Article Translations",
1321
- description: 'Compare section structure between two locales of the same article, matched by index. Returns a compact table (one row per section) with status: "ok" (both present, source/target word count ratio within 25%), "different" (word count ratio diverges by more than 25% \u2014 size signal only, NOT a semantic divergence: two locales may legitimately differ in verbosity) or "missing" (section absent in target). Useful to spot structurally stale or missing sections; do not interpret "different" as an edit regression on its own.',
1322
- inputSchema: z2.object({
1323
- article_id: z2.number().int().describe("Article ID"),
1324
- source_locale: z2.string().describe("Source (reference) locale"),
1325
- target_locale: z2.string().describe("Target locale to compare against source")
1326
- }),
1327
- annotations: {
1328
- readOnlyHint: true,
1329
- destructiveHint: false,
1330
- idempotentHint: true,
1331
- openWorldHint: true
1332
- },
1333
- handler: async (params) => {
1334
- const { article_id, source_locale, target_locale } = params;
1335
- const token = await getToken();
1336
- const [sourceRes, targetRes] = await Promise.all([
1337
- helpCenterGet(
1338
- subdomain,
1339
- token,
1340
- `/articles/${article_id}/translations/${source_locale}`
1341
- ),
1342
- helpCenterGet(
1343
- subdomain,
1344
- token,
1345
- `/articles/${article_id}/translations/${target_locale}`
1346
- )
1347
- ]);
1348
- const sourceSections = parseSections(sourceRes.translation.body);
1349
- const targetSections = parseSections(targetRes.translation.body);
1350
- const maxLen = Math.max(sourceSections.length, targetSections.length);
1351
- const rows = [];
1352
- rows.push(`| Idx | Heading | Status | Source words | Target words |`);
1353
- rows.push(`| --- | --- | --- | --- | --- |`);
1354
- for (let i = 0; i < maxLen; i += 1) {
1355
- const src = sourceSections[i];
1356
- const tgt = targetSections[i];
1357
- const heading = src?.heading ?? tgt?.heading ?? "";
1358
- const sourceWords = src?.wordCount ?? 0;
1359
- const targetWords = tgt?.wordCount ?? 0;
1360
- let status;
1361
- if (!tgt) status = "missing";
1362
- else if (!src) status = "different";
1363
- else {
1364
- const denom = Math.max(sourceWords, 1);
1365
- const diffRatio = Math.abs(sourceWords - targetWords) / denom;
1366
- status = diffRatio > 0.25 ? "different" : "ok";
1367
- }
1368
- rows.push(`| ${i} | ${heading} | ${status} | ${sourceWords} | ${targetWords} |`);
1369
- }
1370
- const text = [
1371
- `# Translation diff \u2014 Article #${article_id} (${source_locale} \u2192 ${target_locale})`,
1372
- "",
1373
- ...rows
1374
- ].join("\n");
1375
- return { content: [{ type: "text", text }] };
1376
- }
1377
- },
1378
- {
1379
- name: "create_article_attachment",
1380
- namespace: "help_center",
1381
- readOnly: false,
1382
- title: "Create Article Attachment",
1383
- description: "Upload an attachment to an article. Provide file content as base64-encoded string.",
1384
- inputSchema: z2.object({
1385
- article_id: z2.number().int().describe("Article ID"),
1386
- file_name: z2.string().min(1).describe('File name (e.g., "screenshot.png")'),
1387
- file_base64: z2.string().min(1).describe("File content encoded as base64"),
1388
- content_type: z2.string().default("application/octet-stream").describe('MIME type (e.g., "image/png", "application/pdf")')
1389
- }),
1390
- annotations: {
1391
- readOnlyHint: false,
1392
- destructiveHint: false,
1393
- idempotentHint: false,
1394
- openWorldHint: true
1395
- },
1396
- handler: async (params) => {
1397
- const { article_id, file_name, file_base64, content_type } = params;
1398
- const token = await getToken();
1399
- const buffer = Buffer.from(file_base64, "base64");
1400
- const blob = new Blob([buffer], { type: content_type });
1401
- const formData = new FormData();
1402
- formData.append("file", blob, file_name);
1403
- const { article_attachment } = await helpCenterUpload(subdomain, token, `/articles/${article_id}/attachments`, formData);
1404
- return {
1405
- content: [
1406
- {
1407
- type: "text",
1408
- text: `Attachment created for article #${article_id}.
1409
-
1410
- ${formatAttachment(article_attachment)}`
1411
- }
1412
- ]
1413
- };
1414
- }
1415
- }
1416
- ];
575
+ const createHelpCenterTools = (ctx) => {
576
+ const { subdomain, getToken } = ctx;
577
+ return [
578
+ {
579
+ name: "search_articles",
580
+ namespace: "help_center",
581
+ readOnly: true,
582
+ title: "Search Help Center Articles",
583
+ description: "Full-text search across Help Center articles (metadata only, no body). Use get_article for full content. Supports locale filtering. Returns total count.",
584
+ inputSchema: z.object({
585
+ query: z.string().min(1).describe("Search query"),
586
+ locale: z.string().optional().describe("Filter by locale (e.g., \"en-us\", \"fr\")"),
587
+ per_page: z.number().int().min(1).max(100).default(100).describe("Results per page"),
588
+ page: z.number().int().min(1).default(1).describe("Page number")
589
+ }),
590
+ annotations: {
591
+ readOnlyHint: true,
592
+ destructiveHint: false,
593
+ idempotentHint: true,
594
+ openWorldHint: true
595
+ },
596
+ handler: async (params) => {
597
+ const { query, locale, per_page, page } = params;
598
+ const token = await getToken();
599
+ const p = {
600
+ query,
601
+ ...buildOffsetParams(per_page, page)
602
+ };
603
+ if (locale) p["locale"] = locale;
604
+ const response = await helpCenterGet(subdomain, token, "/articles/search", p);
605
+ return { content: [{
606
+ type: "text",
607
+ text: formatList(response.results ?? [], formatArticleSummary, extractSearchPaginationMeta(response, per_page, page))
608
+ }] };
609
+ }
610
+ },
611
+ {
612
+ name: "get_article",
613
+ namespace: "help_center",
614
+ readOnly: true,
615
+ title: "Get Help Center Article",
616
+ description: "Retrieve an article by ID with full body content. For large articles, prefer get_article_outline + get_article_section to save tokens. Optionally specify locale for a translated version. Returns body (HTML), metadata, source_locale, and list of available translations.",
617
+ inputSchema: z.object({
618
+ article_id: z.number().int().describe("Article ID"),
619
+ locale: z.string().optional().describe("Locale for translated version")
620
+ }),
621
+ annotations: {
622
+ readOnlyHint: true,
623
+ destructiveHint: false,
624
+ idempotentHint: true,
625
+ openWorldHint: true
626
+ },
627
+ handler: async (params) => {
628
+ const { article_id, locale } = params;
629
+ const token = await getToken();
630
+ const { article } = await helpCenterGet(subdomain, token, locale ? `/${locale}/articles/${article_id}` : `/articles/${article_id}`);
631
+ const { translations } = await helpCenterGet(subdomain, token, `/articles/${article_id}/translations`);
632
+ return { content: [{
633
+ type: "text",
634
+ text: truncateIfNeeded((largeArticleHint(article.body, parseSections(article.body).length) ?? "") + formatArticle(article) + `\n\n**Available translations**: ${translations.map((t) => t.locale).join(", ")}`)
635
+ }] };
636
+ }
637
+ },
638
+ {
639
+ name: "list_categories",
640
+ namespace: "help_center",
641
+ readOnly: true,
642
+ title: "List Help Center Categories",
643
+ description: "List all Help Center categories. Optionally filter by locale.",
644
+ inputSchema: z.object({
645
+ locale: z.string().optional(),
646
+ page_size: z.number().int().min(1).max(100).default(100),
647
+ cursor: z.string().optional()
648
+ }),
649
+ annotations: {
650
+ readOnlyHint: true,
651
+ destructiveHint: false,
652
+ idempotentHint: true,
653
+ openWorldHint: true
654
+ },
655
+ handler: async (params) => {
656
+ const { locale, page_size, cursor } = params;
657
+ const response = await helpCenterGet(subdomain, await getToken(), locale ? `/${locale}/categories` : "/categories", buildCursorParams(page_size, cursor));
658
+ return { content: [{
659
+ type: "text",
660
+ text: formatList(response.categories ?? [], formatCategory, extractPaginationMeta(response))
661
+ }] };
662
+ }
663
+ },
664
+ {
665
+ name: "list_sections",
666
+ namespace: "help_center",
667
+ readOnly: true,
668
+ title: "List Help Center Sections",
669
+ description: "List sections, optionally filtered by category ID and locale.",
670
+ inputSchema: z.object({
671
+ category_id: z.number().int().optional(),
672
+ locale: z.string().optional(),
673
+ page_size: z.number().int().min(1).max(100).default(100),
674
+ cursor: z.string().optional()
675
+ }),
676
+ annotations: {
677
+ readOnlyHint: true,
678
+ destructiveHint: false,
679
+ idempotentHint: true,
680
+ openWorldHint: true
681
+ },
682
+ handler: async (params) => {
683
+ const { category_id, locale, page_size, cursor } = params;
684
+ const response = await helpCenterGet(subdomain, await getToken(), category_id && locale ? `/${locale}/categories/${category_id}/sections` : category_id ? `/categories/${category_id}/sections` : locale ? `/${locale}/sections` : "/sections", buildCursorParams(page_size, cursor));
685
+ return { content: [{
686
+ type: "text",
687
+ text: formatList(response.sections ?? [], formatSection, extractPaginationMeta(response))
688
+ }] };
689
+ }
690
+ },
691
+ {
692
+ name: "list_articles",
693
+ namespace: "help_center",
694
+ readOnly: true,
695
+ title: "List Help Center Articles",
696
+ description: "List articles (metadata only, no body). Use get_article for full content. Optionally filter by section ID and locale. Supports sort_by (\"title\", \"created_at\", \"updated_at\") and include_translations: true to show available translation locales per article. Note: include_translations must be re-sent on each paginated request.",
697
+ inputSchema: z.object({
698
+ section_id: z.number().int().optional(),
699
+ locale: z.string().optional(),
700
+ page_size: z.number().int().min(1).max(100).default(100),
701
+ cursor: z.string().optional(),
702
+ sort_by: z.enum([
703
+ "created_at",
704
+ "updated_at",
705
+ "position",
706
+ "title"
707
+ ]).default("position").describe("Sort field"),
708
+ sort_order: z.enum(["asc", "desc"]).default("asc").describe("Sort direction"),
709
+ include_translations: z.boolean().default(false).describe("Include available translation locales per article (causes 1 extra API call per article)")
710
+ }),
711
+ annotations: {
712
+ readOnlyHint: true,
713
+ destructiveHint: false,
714
+ idempotentHint: true,
715
+ openWorldHint: true
716
+ },
717
+ handler: async (params) => {
718
+ const { section_id, locale, page_size, cursor, sort_by, sort_order, include_translations } = params;
719
+ const token = await getToken();
720
+ const response = await helpCenterGet(subdomain, token, section_id && locale ? `/${locale}/sections/${section_id}/articles` : section_id ? `/sections/${section_id}/articles` : locale ? `/${locale}/articles` : "/articles", {
721
+ ...buildCursorParams(page_size, cursor),
722
+ sort_by,
723
+ sort_order
724
+ });
725
+ const articles = response.articles ?? [];
726
+ if (!include_translations) return { content: [{
727
+ type: "text",
728
+ text: formatList(articles, formatArticleSummary, extractPaginationMeta(response))
729
+ }] };
730
+ const formatted = await Promise.all(articles.map(async (article) => {
731
+ const { translations } = await helpCenterGet(subdomain, token, `/articles/${article.id}/translations`);
732
+ const locales = translations.map((t) => t.locale).join(", ");
733
+ return `${formatArticleSummary(article)}\n- **Translations**: ${locales}`;
734
+ }));
735
+ const meta = extractPaginationMeta(response);
736
+ return { content: [{
737
+ type: "text",
738
+ text: truncateIfNeeded([meta.count ? `Results: ${meta.count}${meta.has_more ? ` | More available (cursor: ${meta.after_cursor})` : ""}` : "", ...formatted].filter(Boolean).join("\n\n"))
739
+ }] };
740
+ }
741
+ },
742
+ {
743
+ name: "list_article_translations",
744
+ namespace: "help_center",
745
+ readOnly: true,
746
+ title: "List Article Translations",
747
+ description: "List all available translations for an article (metadata only, no body: locale, title, draft, updated_at). Use get_article with locale for full translated content.",
748
+ inputSchema: z.object({ article_id: z.number().int().describe("Article ID") }),
749
+ annotations: {
750
+ readOnlyHint: true,
751
+ destructiveHint: false,
752
+ idempotentHint: true,
753
+ openWorldHint: true
754
+ },
755
+ handler: async (params) => {
756
+ const { article_id } = params;
757
+ const { translations } = await helpCenterGet(subdomain, await getToken(), `/articles/${article_id}/translations`);
758
+ return { content: [{
759
+ type: "text",
760
+ text: formatList(translations, formatTranslationSummary)
761
+ }] };
762
+ }
763
+ },
764
+ {
765
+ name: "create_article_translation",
766
+ namespace: "help_center",
767
+ readOnly: false,
768
+ title: "Create Article Translation",
769
+ description: "Create a translation for an existing article in a specific locale.",
770
+ inputSchema: z.object({
771
+ article_id: z.number().int(),
772
+ locale: z.string().describe("Target locale (e.g., \"fr\", \"de\")"),
773
+ title: z.string().min(1),
774
+ body: z.string().min(1).describe("Translated body (HTML)"),
775
+ draft: z.boolean().default(false)
776
+ }),
777
+ annotations: {
778
+ readOnlyHint: false,
779
+ destructiveHint: false,
780
+ idempotentHint: false,
781
+ openWorldHint: true
782
+ },
783
+ handler: async (params) => {
784
+ const { article_id, locale, title, body, draft } = params;
785
+ const { translation } = await helpCenterPost(subdomain, await getToken(), `/articles/${article_id}/translations`, { translation: {
786
+ locale,
787
+ title,
788
+ body,
789
+ draft
790
+ } });
791
+ return { content: [{
792
+ type: "text",
793
+ text: `Translation created for article #${article_id} in "${locale}".\n\n${formatTranslation(translation)}`
794
+ }] };
795
+ }
796
+ },
797
+ {
798
+ name: "update_article_translation",
799
+ namespace: "help_center",
800
+ readOnly: false,
801
+ title: "Update Article Translation",
802
+ description: "Update article content (title, body) in a specific locale. For targeted edits on one or a few sections, prefer update_article_section — this tool replaces the FULL body and re-sends the entire article on each write. Use the article's source_locale (from get_article) for the default language, or another locale for translations.",
803
+ inputSchema: z.object({
804
+ article_id: z.number().int(),
805
+ locale: z.string(),
806
+ title: z.string().optional(),
807
+ body: z.string().optional(),
808
+ draft: z.boolean().optional()
809
+ }),
810
+ annotations: {
811
+ readOnlyHint: false,
812
+ destructiveHint: false,
813
+ idempotentHint: true,
814
+ openWorldHint: true
815
+ },
816
+ handler: async (params) => {
817
+ const { article_id, locale, ...updates } = params;
818
+ const { translation } = await helpCenterPut(subdomain, await getToken(), `/articles/${article_id}/translations/${locale}`, { translation: updates });
819
+ return { content: [{
820
+ type: "text",
821
+ text: `Translation updated for article #${article_id} in "${locale}".\n\n${formatTranslation(translation)}`
822
+ }] };
823
+ }
824
+ },
825
+ {
826
+ name: "list_permission_groups",
827
+ namespace: "help_center",
828
+ readOnly: true,
829
+ title: "List Permission Groups",
830
+ description: "List all Guide permission groups. Use this to find the permission_group_id required when creating articles.",
831
+ inputSchema: z.object({}),
832
+ annotations: {
833
+ readOnlyHint: true,
834
+ destructiveHint: false,
835
+ idempotentHint: true,
836
+ openWorldHint: true
837
+ },
838
+ handler: async () => {
839
+ return { content: [{
840
+ type: "text",
841
+ text: formatList((await zendeskGet(subdomain, await getToken(), "/guide/permission_groups")).permission_groups ?? [], formatPermissionGroup)
842
+ }] };
843
+ }
844
+ },
845
+ {
846
+ name: "create_article",
847
+ namespace: "help_center",
848
+ readOnly: false,
849
+ title: "Create Help Center Article",
850
+ description: "Create a new article in a section. The locale becomes the article's source_locale. Requires a permission_group_id (use list_permission_groups to find available IDs). To add content in other locales afterwards, use create_article_translation.",
851
+ inputSchema: z.object({
852
+ section_id: z.number().int(),
853
+ title: z.string().min(1),
854
+ body: z.string().min(1).describe("Article body (HTML)"),
855
+ permission_group_id: z.number().int().describe("Permission group ID (use list_permission_groups to find it)"),
856
+ user_segment_id: z.number().int().optional().describe("User segment ID for visibility (use list_user_segments to find it). Defaults to everyone."),
857
+ author_id: z.number().int().optional().describe("Author user ID. Defaults to the authenticated user."),
858
+ content_tag_ids: z.array(z.string()).optional().describe("Content tag IDs (use list_content_tags to find them)"),
859
+ locale: z.string().optional(),
860
+ draft: z.boolean().default(true),
861
+ promoted: z.boolean().default(false),
862
+ label_names: z.array(z.string()).optional().describe("Label names for search ranking (use list_labels to see existing labels)")
863
+ }),
864
+ annotations: {
865
+ readOnlyHint: false,
866
+ destructiveHint: false,
867
+ idempotentHint: false,
868
+ openWorldHint: true
869
+ },
870
+ handler: async (params) => {
871
+ const { section_id, ...articleData } = params;
872
+ const { article } = await helpCenterPost(subdomain, await getToken(), `/sections/${section_id}/articles`, { article: articleData });
873
+ return { content: [{
874
+ type: "text",
875
+ text: `Article #${article.id} created.\n\n${formatArticle(article)}`
876
+ }] };
877
+ }
878
+ },
879
+ {
880
+ name: "update_article",
881
+ namespace: "help_center",
882
+ readOnly: false,
883
+ title: "Update Help Center Article",
884
+ description: "Update article metadata only (draft, promoted, labels, tags, visibility, section, etc.). Does NOT update content (title, body) — use update_article_translation for that.",
885
+ inputSchema: z.object({
886
+ article_id: z.number().int(),
887
+ draft: z.boolean().optional(),
888
+ promoted: z.boolean().optional(),
889
+ label_names: z.array(z.string()).optional().describe("Label names for search ranking"),
890
+ content_tag_ids: z.array(z.string()).optional().describe("Content tag IDs"),
891
+ user_segment_id: z.number().int().optional().describe("User segment ID for visibility"),
892
+ author_id: z.number().int().optional().describe("Author user ID"),
893
+ permission_group_id: z.number().int().optional().describe("Permission group ID"),
894
+ section_id: z.number().int().optional()
895
+ }),
896
+ annotations: {
897
+ readOnlyHint: false,
898
+ destructiveHint: false,
899
+ idempotentHint: true,
900
+ openWorldHint: true
901
+ },
902
+ handler: async (params) => {
903
+ const { article_id, ...updates } = params;
904
+ const { article } = await helpCenterPut(subdomain, await getToken(), `/articles/${article_id}`, { article: updates });
905
+ return { content: [{
906
+ type: "text",
907
+ text: `Article #${article.id} updated.\n\n${formatArticle(article)}`
908
+ }] };
909
+ }
910
+ },
911
+ {
912
+ name: "list_content_tags",
913
+ namespace: "help_center",
914
+ readOnly: true,
915
+ title: "List Content Tags",
916
+ description: "List all Guide content tags. Content tags are visible to end users and help them find related articles.",
917
+ inputSchema: z.object({}),
918
+ annotations: {
919
+ readOnlyHint: true,
920
+ destructiveHint: false,
921
+ idempotentHint: true,
922
+ openWorldHint: true
923
+ },
924
+ handler: async () => {
925
+ return { content: [{
926
+ type: "text",
927
+ text: formatList((await zendeskGet(subdomain, await getToken(), "/guide/content_tags")).records ?? [], formatContentTag)
928
+ }] };
929
+ }
930
+ },
931
+ {
932
+ name: "create_content_tag",
933
+ namespace: "help_center",
934
+ readOnly: false,
935
+ title: "Create Content Tag",
936
+ description: "Create a new content tag for Guide articles.",
937
+ inputSchema: z.object({ name: z.string().min(1).describe("Content tag name") }),
938
+ annotations: {
939
+ readOnlyHint: false,
940
+ destructiveHint: false,
941
+ idempotentHint: false,
942
+ openWorldHint: true
943
+ },
944
+ handler: async (params) => {
945
+ const { name } = params;
946
+ const { record } = await zendeskPost(subdomain, await getToken(), "/guide/content_tags", { record: { name } });
947
+ return { content: [{
948
+ type: "text",
949
+ text: `Content tag created.\n\n${formatContentTag(record)}`
950
+ }] };
951
+ }
952
+ },
953
+ {
954
+ name: "list_labels",
955
+ namespace: "help_center",
956
+ readOnly: true,
957
+ title: "List Article Labels",
958
+ description: "List all article labels. Labels improve Help Center search ranking and are not visible to end users.",
959
+ inputSchema: z.object({}),
960
+ annotations: {
961
+ readOnlyHint: true,
962
+ destructiveHint: false,
963
+ idempotentHint: true,
964
+ openWorldHint: true
965
+ },
966
+ handler: async () => {
967
+ return { content: [{
968
+ type: "text",
969
+ text: formatList((await helpCenterGet(subdomain, await getToken(), "/articles/labels")).labels ?? [], formatLabel)
970
+ }] };
971
+ }
972
+ },
973
+ {
974
+ name: "list_user_segments",
975
+ namespace: "help_center",
976
+ readOnly: true,
977
+ title: "List User Segments",
978
+ description: "List all user segments. User segments control article visibility (who can view). Use the ID when creating or updating articles.",
979
+ inputSchema: z.object({}),
980
+ annotations: {
981
+ readOnlyHint: true,
982
+ destructiveHint: false,
983
+ idempotentHint: true,
984
+ openWorldHint: true
985
+ },
986
+ handler: async () => {
987
+ return { content: [{
988
+ type: "text",
989
+ text: formatList((await helpCenterGet(subdomain, await getToken(), "/user_segments")).user_segments ?? [], formatUserSegment)
990
+ }] };
991
+ }
992
+ },
993
+ {
994
+ name: "list_article_attachments",
995
+ namespace: "help_center",
996
+ readOnly: true,
997
+ title: "List Article Attachments",
998
+ description: "List all attachments for an article.",
999
+ inputSchema: z.object({ article_id: z.number().int().describe("Article ID") }),
1000
+ annotations: {
1001
+ readOnlyHint: true,
1002
+ destructiveHint: false,
1003
+ idempotentHint: true,
1004
+ openWorldHint: true
1005
+ },
1006
+ handler: async (params) => {
1007
+ const { article_id } = params;
1008
+ return { content: [{
1009
+ type: "text",
1010
+ text: formatList((await helpCenterGet(subdomain, await getToken(), `/articles/${article_id}/attachments`)).article_attachments ?? [], formatAttachment)
1011
+ }] };
1012
+ }
1013
+ },
1014
+ {
1015
+ name: "get_article_outline",
1016
+ namespace: "help_center",
1017
+ readOnly: true,
1018
+ title: "Get Article Outline",
1019
+ description: "Return a compact outline of an article (list of sections delimited by h1/h2/h3, with word counts) for the given locale (defaults to source_locale). Includes available translations with their outdated status. Use get_article_section to fetch a specific section.",
1020
+ inputSchema: z.object({
1021
+ article_id: z.number().int().describe("Article ID"),
1022
+ locale: z.string().optional().describe("Locale of the body to outline (defaults to article source_locale)")
1023
+ }),
1024
+ annotations: {
1025
+ readOnlyHint: true,
1026
+ destructiveHint: false,
1027
+ idempotentHint: true,
1028
+ openWorldHint: true
1029
+ },
1030
+ handler: async (params) => {
1031
+ const { article_id, locale } = params;
1032
+ const token = await getToken();
1033
+ const { article } = await helpCenterGet(subdomain, token, `/articles/${article_id}`);
1034
+ const effectiveLocale = locale ?? article.source_locale;
1035
+ const { translation } = await helpCenterGet(subdomain, token, `/articles/${article_id}/translations/${effectiveLocale}`);
1036
+ const { translations } = await helpCenterGet(subdomain, token, `/articles/${article_id}/translations`);
1037
+ const sections = parseSections(translation.body);
1038
+ const outlineLines = sections.length ? sections.map((s) => `- [${s.index}] ${s.headingTag ? `${s.headingTag}: ` : ""}${s.heading} (${s.wordCount} words)`).join("\n") : "_(no sections detected)_";
1039
+ const translationsList = translations.map((t) => `- ${t.locale}${t.outdated ? " (outdated)" : ""}`).join("\n");
1040
+ return { content: [{
1041
+ type: "text",
1042
+ text: [
1043
+ `# Outline — Article #${article_id} (${effectiveLocale})`,
1044
+ `**Title**: ${translation.title}`,
1045
+ "",
1046
+ "## Sections",
1047
+ outlineLines,
1048
+ "",
1049
+ "## Available translations",
1050
+ translationsList
1051
+ ].join("\n")
1052
+ }] };
1053
+ }
1054
+ },
1055
+ {
1056
+ name: "get_article_section",
1057
+ namespace: "help_center",
1058
+ readOnly: true,
1059
+ title: "Get Article Section",
1060
+ description: "Retrieve the content of a single section of an article in a given locale. Use get_article_outline first to discover section indexes. Default format=\"html\" for round-trip safety. Pass format=\"markdown\" only for human review — the Markdown representation is lossy on some structures (<pre> with <br>, tables with multi-<p> cells are kept as raw HTML to limit the damage, but do not round-trip markdown content back through update_article_section).",
1061
+ inputSchema: z.object({
1062
+ article_id: z.number().int().describe("Article ID"),
1063
+ locale: z.string().describe("Locale of the body (e.g., \"en-us\", \"fr\")"),
1064
+ section_index: z.number().int().min(0).describe("0-based index of the section (see get_article_outline)"),
1065
+ format: z.enum(["html", "markdown"]).default("html").describe("Output format. \"html\" (default) is round-trip safe. \"markdown\" is lossy on some HTML structures — use only for human review, not before update_article_section.")
1066
+ }),
1067
+ annotations: {
1068
+ readOnlyHint: true,
1069
+ destructiveHint: false,
1070
+ idempotentHint: true,
1071
+ openWorldHint: true
1072
+ },
1073
+ handler: async (params) => {
1074
+ const { article_id, locale, section_index, format } = params;
1075
+ const { translation } = await helpCenterGet(subdomain, await getToken(), `/articles/${article_id}/translations/${locale}`);
1076
+ const sections = parseSections(translation.body);
1077
+ const section = sections[section_index];
1078
+ if (!section) throw new Error(`Section index ${section_index} not found. Article has ${sections.length} section(s) (0-${Math.max(0, sections.length - 1)}).`);
1079
+ const content = format === "markdown" ? htmlToMarkdown(section.html) : section.html;
1080
+ return { content: [{
1081
+ type: "text",
1082
+ text: truncateIfNeeded([
1083
+ section.headingTag ? `## [${section.index}] ${section.headingTag}: ${section.heading}` : `## [${section.index}] ${section.heading}`,
1084
+ `_Locale: ${locale} | Words: ${section.wordCount} | Format: ${format}_`,
1085
+ "",
1086
+ content
1087
+ ].join("\n"))
1088
+ }] };
1089
+ }
1090
+ },
1091
+ {
1092
+ name: "update_article_section",
1093
+ namespace: "help_center",
1094
+ readOnly: false,
1095
+ title: "Update Article Section",
1096
+ description: "Replace the content of a single section of an article in a given locale, keeping the rest of the body intact. The server fetches the current body, replaces the targeted section, and PUTs the full reconstructed body via the Translations API. Default format=\"html\" for fidelity. Use format=\"markdown\" only when you control the input and know it does not rely on structures that round-trip poorly (code blocks with line breaks, tables with multi-paragraph cells). The section heading is preserved and is NOT part of the replaced content.",
1097
+ inputSchema: z.object({
1098
+ article_id: z.number().int().describe("Article ID"),
1099
+ locale: z.string().describe("Locale of the translation to update"),
1100
+ section_index: z.number().int().min(0).describe("0-based index of the section to replace (see get_article_outline)"),
1101
+ content: z.string().describe("New content for the section (heading excluded). HTML by default, Markdown if format=\"markdown\"."),
1102
+ format: z.enum(["html", "markdown"]).default("html").describe("Input format. \"html\" (default) is the safe path. \"markdown\" is converted to HTML server-side but may introduce artifacts on complex content.")
1103
+ }),
1104
+ annotations: {
1105
+ readOnlyHint: false,
1106
+ destructiveHint: false,
1107
+ idempotentHint: true,
1108
+ openWorldHint: true
1109
+ },
1110
+ handler: async (params) => {
1111
+ const { article_id, locale, section_index, content, format } = params;
1112
+ const token = await getToken();
1113
+ const { translation } = await helpCenterGet(subdomain, token, `/articles/${article_id}/translations/${locale}`);
1114
+ const newSectionHtml = format === "markdown" ? markdownToHtml(content) : content;
1115
+ const newBody = replaceSectionContent(translation.body, section_index, newSectionHtml);
1116
+ const { translation: updated } = await helpCenterPut(subdomain, token, `/articles/${article_id}/translations/${locale}`, { translation: { body: newBody } });
1117
+ const updatedSection = parseSections(updated.body)[section_index];
1118
+ const newWordCount = updatedSection?.wordCount ?? 0;
1119
+ return { content: [{
1120
+ type: "text",
1121
+ text: [`Section [${section_index}] "${updatedSection?.heading ?? "(intro)"}" updated for article #${article_id} (${locale}).`, `New word count: ${newWordCount}.`].join("\n")
1122
+ }] };
1123
+ }
1124
+ },
1125
+ {
1126
+ name: "compare_translations",
1127
+ namespace: "help_center",
1128
+ readOnly: true,
1129
+ title: "Compare Article Translations",
1130
+ description: "Compare section structure between two locales of the same article, matched by index. Returns a compact table (one row per section) with status: \"ok\" (both present, source/target word count ratio within 25%), \"different\" (word count ratio diverges by more than 25% — size signal only, NOT a semantic divergence: two locales may legitimately differ in verbosity) or \"missing\" (section absent in target). Useful to spot structurally stale or missing sections; do not interpret \"different\" as an edit regression on its own.",
1131
+ inputSchema: z.object({
1132
+ article_id: z.number().int().describe("Article ID"),
1133
+ source_locale: z.string().describe("Source (reference) locale"),
1134
+ target_locale: z.string().describe("Target locale to compare against source")
1135
+ }),
1136
+ annotations: {
1137
+ readOnlyHint: true,
1138
+ destructiveHint: false,
1139
+ idempotentHint: true,
1140
+ openWorldHint: true
1141
+ },
1142
+ handler: async (params) => {
1143
+ const { article_id, source_locale, target_locale } = params;
1144
+ const token = await getToken();
1145
+ const [sourceRes, targetRes] = await Promise.all([helpCenterGet(subdomain, token, `/articles/${article_id}/translations/${source_locale}`), helpCenterGet(subdomain, token, `/articles/${article_id}/translations/${target_locale}`)]);
1146
+ const sourceSections = parseSections(sourceRes.translation.body);
1147
+ const targetSections = parseSections(targetRes.translation.body);
1148
+ const maxLen = Math.max(sourceSections.length, targetSections.length);
1149
+ const rows = [];
1150
+ rows.push(`| Idx | Heading | Status | Source words | Target words |`);
1151
+ rows.push(`| --- | --- | --- | --- | --- |`);
1152
+ for (let i = 0; i < maxLen; i += 1) {
1153
+ const src = sourceSections[i];
1154
+ const tgt = targetSections[i];
1155
+ const heading = src?.heading ?? tgt?.heading ?? "";
1156
+ const sourceWords = src?.wordCount ?? 0;
1157
+ const targetWords = tgt?.wordCount ?? 0;
1158
+ let status;
1159
+ if (!tgt) status = "missing";
1160
+ else if (!src) status = "different";
1161
+ else {
1162
+ const denom = Math.max(sourceWords, 1);
1163
+ status = Math.abs(sourceWords - targetWords) / denom > .25 ? "different" : "ok";
1164
+ }
1165
+ rows.push(`| ${i} | ${heading} | ${status} | ${sourceWords} | ${targetWords} |`);
1166
+ }
1167
+ return { content: [{
1168
+ type: "text",
1169
+ text: [
1170
+ `# Translation diff — Article #${article_id} (${source_locale} ${target_locale})`,
1171
+ "",
1172
+ ...rows
1173
+ ].join("\n")
1174
+ }] };
1175
+ }
1176
+ },
1177
+ {
1178
+ name: "create_article_attachment",
1179
+ namespace: "help_center",
1180
+ readOnly: false,
1181
+ title: "Create Article Attachment",
1182
+ description: "Upload an attachment to an article. Provide file content as base64-encoded string.",
1183
+ inputSchema: z.object({
1184
+ article_id: z.number().int().describe("Article ID"),
1185
+ file_name: z.string().min(1).describe("File name (e.g., \"screenshot.png\")"),
1186
+ file_base64: z.string().min(1).describe("File content encoded as base64"),
1187
+ content_type: z.string().default("application/octet-stream").describe("MIME type (e.g., \"image/png\", \"application/pdf\")")
1188
+ }),
1189
+ annotations: {
1190
+ readOnlyHint: false,
1191
+ destructiveHint: false,
1192
+ idempotentHint: false,
1193
+ openWorldHint: true
1194
+ },
1195
+ handler: async (params) => {
1196
+ const { article_id, file_name, file_base64, content_type } = params;
1197
+ const token = await getToken();
1198
+ const buffer = Buffer.from(file_base64, "base64");
1199
+ const blob = new Blob([buffer], { type: content_type });
1200
+ const formData = new FormData();
1201
+ formData.append("file", blob, file_name);
1202
+ const { article_attachment } = await helpCenterUpload(subdomain, token, `/articles/${article_id}/attachments`, formData);
1203
+ return { content: [{
1204
+ type: "text",
1205
+ text: `Attachment created for article #${article_id}.\n\n${formatAttachment(article_attachment)}`
1206
+ }] };
1207
+ }
1208
+ }
1209
+ ];
1417
1210
  };
1418
-
1419
- // src/tools/search.ts
1420
- import * as z3 from "zod/v4";
1421
- var formatSearchResult = (result) => {
1422
- const lines = [`## [${result["result_type"]}] #${result["id"]}`];
1423
- if (result["subject"]) lines.push(`**Subject**: ${result["subject"]}`);
1424
- if (result["name"]) lines.push(`**Name**: ${result["name"]}`);
1425
- if (result["title"]) lines.push(`**Title**: ${result["title"]}`);
1426
- if (result["email"]) lines.push(`**Email**: ${result["email"]}`);
1427
- if (result["status"]) lines.push(`**Status**: ${result["status"]}`);
1428
- if (result["description"]) {
1429
- const desc = String(result["description"]);
1430
- lines.push(desc.length > 200 ? `${desc.slice(0, 200)}...` : desc);
1431
- }
1432
- return lines.join("\n");
1211
+ //#endregion
1212
+ //#region src/tools/search.ts
1213
+ const formatSearchResult = (result) => {
1214
+ const lines = [`## [${result["result_type"]}] #${result["id"]}`];
1215
+ if (result["subject"]) lines.push(`**Subject**: ${result["subject"]}`);
1216
+ if (result["name"]) lines.push(`**Name**: ${result["name"]}`);
1217
+ if (result["title"]) lines.push(`**Title**: ${result["title"]}`);
1218
+ if (result["email"]) lines.push(`**Email**: ${result["email"]}`);
1219
+ if (result["status"]) lines.push(`**Status**: ${result["status"]}`);
1220
+ if (result["description"]) {
1221
+ const desc = String(result["description"]);
1222
+ lines.push(desc.length > 200 ? `${desc.slice(0, 200)}...` : desc);
1223
+ }
1224
+ return lines.join("\n");
1433
1225
  };
1434
- var createSearchTools = (ctx) => {
1435
- const { subdomain, getToken } = ctx;
1436
- return [
1437
- {
1438
- name: "search",
1439
- namespace: "tickets",
1440
- readOnly: true,
1441
- title: "Zendesk Unified Search",
1442
- description: 'Search across tickets, users, and organizations. Supports filters like "type:ticket status:open", "type:user role:agent". Returns total count and paginated results (100 per page). Organization results include name and ID only \u2014 use get_organization for full details (tags, domains, details).',
1443
- inputSchema: z3.object({
1444
- query: z3.string().min(1).describe("Zendesk search query"),
1445
- per_page: z3.number().int().min(1).max(MAX_PAGE_SIZE).default(DEFAULT_PAGE_SIZE).describe("Results per page (max 100)"),
1446
- page: z3.number().int().min(1).default(1).describe("Page number (1-based)")
1447
- }),
1448
- annotations: {
1449
- readOnlyHint: true,
1450
- destructiveHint: false,
1451
- idempotentHint: true,
1452
- openWorldHint: true
1453
- },
1454
- handler: async (params) => {
1455
- const { query, per_page, page } = params;
1456
- const token = await getToken();
1457
- const response = await zendeskGet(
1458
- subdomain,
1459
- token,
1460
- "/search",
1461
- {
1462
- query,
1463
- ...buildOffsetParams(per_page, page)
1464
- }
1465
- );
1466
- const results = response.results ?? [];
1467
- const meta = extractSearchPaginationMeta(response, per_page, page);
1468
- const header = `Total: ${meta.count} | Page ${page} (${results.length} results)${meta.has_more ? ` | Next page: ${meta.after_cursor}` : ""}`;
1469
- const body = results.map(formatSearchResult).join("\n\n");
1470
- return {
1471
- content: [
1472
- { type: "text", text: truncateIfNeeded([header, body].filter(Boolean).join("\n\n")) }
1473
- ]
1474
- };
1475
- }
1476
- }
1477
- ];
1226
+ const createSearchTools = (ctx) => {
1227
+ const { subdomain, getToken } = ctx;
1228
+ return [{
1229
+ name: "search",
1230
+ namespace: "tickets",
1231
+ readOnly: true,
1232
+ title: "Zendesk Unified Search",
1233
+ description: "Search across tickets, users, and organizations. Supports filters like \"type:ticket status:open\", \"type:user role:agent\". Returns total count and paginated results (100 per page). Organization results include name and ID only — use get_organization for full details (tags, domains, details).",
1234
+ inputSchema: z.object({
1235
+ query: z.string().min(1).describe("Zendesk search query"),
1236
+ per_page: z.number().int().min(1).max(100).default(100).describe("Results per page (max 100)"),
1237
+ page: z.number().int().min(1).default(1).describe("Page number (1-based)")
1238
+ }),
1239
+ annotations: {
1240
+ readOnlyHint: true,
1241
+ destructiveHint: false,
1242
+ idempotentHint: true,
1243
+ openWorldHint: true
1244
+ },
1245
+ handler: async (params) => {
1246
+ const { query, per_page, page } = params;
1247
+ const response = await zendeskGet(subdomain, await getToken(), "/search", {
1248
+ query,
1249
+ ...buildOffsetParams(per_page, page)
1250
+ });
1251
+ const results = response.results ?? [];
1252
+ const meta = extractSearchPaginationMeta(response, per_page, page);
1253
+ return { content: [{
1254
+ type: "text",
1255
+ text: truncateIfNeeded([`Total: ${meta.count} | Page ${page} (${results.length} results)${meta.has_more ? ` | Next page: ${meta.after_cursor}` : ""}`, results.map(formatSearchResult).join("\n\n")].filter(Boolean).join("\n\n"))
1256
+ }] };
1257
+ }
1258
+ }];
1478
1259
  };
1479
-
1480
- // src/tools/tickets.ts
1481
- import * as z4 from "zod/v4";
1482
- var formatReference = (attachment) => `**${attachment.file_name}** (id ${attachment.id}, ${attachment.content_type}, ${attachment.size} bytes) \u2014 ${attachment.content_url}`;
1483
- var buildEmbeddedImageBlocks = async (subdomain, token, attachment, reference) => {
1484
- const { data, contentType } = await fetchZendeskBinary(subdomain, token, attachment.content_url);
1485
- return [
1486
- { type: "image", data: data.toString("base64"), mimeType: contentType },
1487
- { type: "text", text: reference }
1488
- ];
1260
+ //#endregion
1261
+ //#region src/tools/tickets.ts
1262
+ const formatReference = (attachment) => `**${attachment.file_name}** (id ${attachment.id}, ${attachment.content_type}, ${attachment.size} bytes) — ${attachment.content_url}`;
1263
+ const buildEmbeddedImageBlocks = async (subdomain, token, attachment, reference) => {
1264
+ const { data, contentType } = await fetchZendeskBinary(subdomain, token, attachment.content_url);
1265
+ return [{
1266
+ type: "image",
1267
+ data: data.toString("base64"),
1268
+ mimeType: contentType
1269
+ }, {
1270
+ type: "text",
1271
+ text: reference
1272
+ }];
1489
1273
  };
1490
- var fetchAllTicketComments = async (subdomain, token, ticketId) => {
1491
- const all = [];
1492
- let cursor;
1493
- let pages = 0;
1494
- while (pages < MAX_COMMENT_PAGES) {
1495
- const response = await zendeskGet(subdomain, token, `/tickets/${ticketId}/comments`, buildCursorParams(MAX_PAGE_SIZE, cursor));
1496
- all.push(...response.comments);
1497
- pages += 1;
1498
- if (!response.meta?.has_more || !response.meta?.after_cursor) break;
1499
- cursor = response.meta.after_cursor;
1500
- }
1501
- return all;
1274
+ const fetchAllTicketComments = async (subdomain, token, ticketId) => {
1275
+ const all = [];
1276
+ let cursor;
1277
+ let pages = 0;
1278
+ while (pages < MAX_COMMENT_PAGES) {
1279
+ const response = await zendeskGet(subdomain, token, `/tickets/${ticketId}/comments`, buildCursorParams(100, cursor));
1280
+ all.push(...response.comments);
1281
+ pages += 1;
1282
+ if (!response.meta?.has_more || !response.meta?.after_cursor) break;
1283
+ cursor = response.meta.after_cursor;
1284
+ }
1285
+ return all;
1502
1286
  };
1503
- var collectAttachmentBlocks = async (subdomain, token, attachments) => {
1504
- const blocks = [];
1505
- let embeddedCount = 0;
1506
- for (const attachment of attachments) {
1507
- const reference = formatReference(attachment);
1508
- const isImage = attachment.content_type.startsWith("image/");
1509
- if (!isImage) {
1510
- blocks.push({ type: "text", text: reference });
1511
- continue;
1512
- }
1513
- let skipReason = null;
1514
- if (attachment.size > MAX_ATTACHMENT_BYTES) {
1515
- skipReason = "skipped: exceeds 5 MB per-image limit";
1516
- } else if (embeddedCount >= MAX_EMBEDDED_IMAGE_COUNT) {
1517
- skipReason = `skipped: max ${MAX_EMBEDDED_IMAGE_COUNT} embedded images reached`;
1518
- }
1519
- if (skipReason) {
1520
- blocks.push({ type: "text", text: `${reference} \u2014 ${skipReason}` });
1521
- continue;
1522
- }
1523
- try {
1524
- blocks.push(...await buildEmbeddedImageBlocks(subdomain, token, attachment, reference));
1525
- embeddedCount += 1;
1526
- } catch (error) {
1527
- const reason = error instanceof ZendeskApiError ? `download failed: ${error.status} ${error.statusText}` : "download failed";
1528
- blocks.push({ type: "text", text: `${reference} \u2014 ${reason}` });
1529
- }
1530
- }
1531
- return blocks;
1287
+ const collectAttachmentBlocks = async (subdomain, token, attachments) => {
1288
+ const blocks = [];
1289
+ let embeddedCount = 0;
1290
+ for (const attachment of attachments) {
1291
+ const reference = formatReference(attachment);
1292
+ if (!attachment.content_type.startsWith("image/")) {
1293
+ blocks.push({
1294
+ type: "text",
1295
+ text: reference
1296
+ });
1297
+ continue;
1298
+ }
1299
+ let skipReason = null;
1300
+ if (attachment.size > 5242880) skipReason = "skipped: exceeds 5 MB per-image limit";
1301
+ else if (embeddedCount >= 10) skipReason = `skipped: max 10 embedded images reached`;
1302
+ if (skipReason) {
1303
+ blocks.push({
1304
+ type: "text",
1305
+ text: `${reference} — ${skipReason}`
1306
+ });
1307
+ continue;
1308
+ }
1309
+ try {
1310
+ blocks.push(...await buildEmbeddedImageBlocks(subdomain, token, attachment, reference));
1311
+ embeddedCount += 1;
1312
+ } catch (error) {
1313
+ const reason = error instanceof ZendeskApiError ? `download failed: ${error.status} ${error.statusText}` : "download failed";
1314
+ blocks.push({
1315
+ type: "text",
1316
+ text: `${reference} — ${reason}`
1317
+ });
1318
+ }
1319
+ }
1320
+ return blocks;
1532
1321
  };
1533
- var createTicketTools = (ctx) => {
1534
- const { subdomain, getToken } = ctx;
1535
- return [
1536
- {
1537
- name: "get_ticket",
1538
- namespace: "tickets",
1539
- readOnly: true,
1540
- title: "Get Zendesk Ticket",
1541
- description: "Retrieve a Zendesk ticket by ID, including its comments if requested. Returns ticket details (subject, status, priority, assignee, tags, description) and optionally all comments/internal notes.",
1542
- inputSchema: z4.object({
1543
- ticket_id: z4.number().int().describe("Ticket ID"),
1544
- include_comments: z4.boolean().default(false).describe("Include ticket comments")
1545
- }),
1546
- annotations: {
1547
- readOnlyHint: true,
1548
- destructiveHint: false,
1549
- idempotentHint: true,
1550
- openWorldHint: true
1551
- },
1552
- handler: async (params) => {
1553
- const { ticket_id, include_comments } = params;
1554
- const token = await getToken();
1555
- const { ticket } = await zendeskGet(
1556
- subdomain,
1557
- token,
1558
- `/tickets/${ticket_id}`
1559
- );
1560
- let text = formatTicket(ticket);
1561
- if (include_comments) {
1562
- const { comments } = await zendeskGet(
1563
- subdomain,
1564
- token,
1565
- `/tickets/${ticket_id}/comments`
1566
- );
1567
- text += `
1568
-
1569
- ---
1570
- # Comments
1571
-
1572
- ${comments.map(formatComment).join("\n\n")}`;
1573
- }
1574
- return { content: [{ type: "text", text: truncateIfNeeded(text) }] };
1575
- }
1576
- },
1577
- {
1578
- name: "get_ticket_attachments",
1579
- namespace: "tickets",
1580
- readOnly: true,
1581
- title: "Get Zendesk Ticket Attachments",
1582
- description: "Retrieve ticket attachments. Images are embedded inline; other files are listed as text references.",
1583
- inputSchema: z4.object({
1584
- ticket_id: z4.number().int().describe("Ticket ID"),
1585
- attachment_ids: z4.array(z4.number().int()).optional().describe(
1586
- "Attachment IDs to fetch directly (e.g. extracted from a previous get_ticket(include_comments=true) call). When provided, skips the comments fetch entirely. When omitted, all attachments of the ticket are returned."
1587
- )
1588
- }),
1589
- annotations: {
1590
- readOnlyHint: true,
1591
- destructiveHint: false,
1592
- idempotentHint: true,
1593
- openWorldHint: true
1594
- },
1595
- handler: async (params) => {
1596
- const { ticket_id, attachment_ids } = params;
1597
- const token = await getToken();
1598
- let attachments;
1599
- if (attachment_ids && attachment_ids.length > 0) {
1600
- attachments = [];
1601
- for (const id of attachment_ids) {
1602
- try {
1603
- const { attachment } = await zendeskGet(
1604
- subdomain,
1605
- token,
1606
- `/attachments/${id}`
1607
- );
1608
- attachments.push(attachment);
1609
- } catch (error) {
1610
- if (!(error instanceof ZendeskApiError) || error.status !== 404) throw error;
1611
- }
1612
- }
1613
- } else {
1614
- const comments = await fetchAllTicketComments(subdomain, token, ticket_id);
1615
- attachments = comments.flatMap((c) => c.attachments ?? []);
1616
- }
1617
- if (attachments.length === 0) {
1618
- return {
1619
- content: [{ type: "text", text: `No attachments found on ticket #${ticket_id}.` }]
1620
- };
1621
- }
1622
- const blocks = await collectAttachmentBlocks(subdomain, token, attachments);
1623
- return {
1624
- content: [
1625
- {
1626
- type: "text",
1627
- text: `# Attachments for ticket #${ticket_id} (${attachments.length} total)`
1628
- },
1629
- ...blocks
1630
- ]
1631
- };
1632
- }
1633
- },
1634
- {
1635
- name: "search_tickets",
1636
- namespace: "tickets",
1637
- readOnly: true,
1638
- title: "Search Zendesk Tickets",
1639
- description: 'Search tickets using Zendesk query syntax (e.g., "status:open assignee:me", "priority:urgent type:incident"). Returns total count.',
1640
- inputSchema: z4.object({
1641
- query: z4.string().min(1).describe("Zendesk search query string"),
1642
- per_page: z4.number().int().min(1).max(MAX_PAGE_SIZE).default(DEFAULT_PAGE_SIZE).describe("Results per page"),
1643
- page: z4.number().int().min(1).default(1).describe("Page number")
1644
- }),
1645
- annotations: {
1646
- readOnlyHint: true,
1647
- destructiveHint: false,
1648
- idempotentHint: true,
1649
- openWorldHint: true
1650
- },
1651
- handler: async (params) => {
1652
- const { query, per_page, page } = params;
1653
- const token = await getToken();
1654
- const response = await zendeskGet(
1655
- subdomain,
1656
- token,
1657
- "/search",
1658
- {
1659
- query: `type:ticket ${query}`,
1660
- ...buildOffsetParams(per_page, page)
1661
- }
1662
- );
1663
- return {
1664
- content: [
1665
- {
1666
- type: "text",
1667
- text: formatList(
1668
- response.results ?? [],
1669
- formatTicket,
1670
- extractSearchPaginationMeta(response, per_page, page)
1671
- )
1672
- }
1673
- ]
1674
- };
1675
- }
1676
- },
1677
- {
1678
- name: "create_ticket",
1679
- namespace: "tickets",
1680
- readOnly: false,
1681
- title: "Create Zendesk Ticket",
1682
- description: "Create a new Zendesk support ticket with subject, description, and optional priority/type/assignee/tags.",
1683
- inputSchema: z4.object({
1684
- subject: z4.string().min(1).describe("Ticket subject"),
1685
- description: z4.string().min(1).describe("Ticket description"),
1686
- priority: z4.enum(["urgent", "high", "normal", "low"]).optional(),
1687
- type: z4.enum(["problem", "incident", "question", "task"]).optional(),
1688
- assignee_id: z4.number().int().optional(),
1689
- group_id: z4.number().int().optional(),
1690
- tags: z4.array(z4.string()).optional(),
1691
- custom_fields: z4.array(z4.object({ id: z4.number().int(), value: z4.unknown() })).optional()
1692
- }),
1693
- annotations: {
1694
- readOnlyHint: false,
1695
- destructiveHint: false,
1696
- idempotentHint: false,
1697
- openWorldHint: true
1698
- },
1699
- handler: async (params) => {
1700
- const { subject, description, ...rest } = params;
1701
- const token = await getToken();
1702
- const { ticket } = await zendeskPost(
1703
- subdomain,
1704
- token,
1705
- "/tickets",
1706
- {
1707
- ticket: { subject, comment: { body: description }, ...rest }
1708
- }
1709
- );
1710
- return {
1711
- content: [
1712
- { type: "text", text: `Ticket #${ticket.id} created.
1713
-
1714
- ${formatTicket(ticket)}` }
1715
- ]
1716
- };
1717
- }
1718
- },
1719
- {
1720
- name: "update_ticket",
1721
- namespace: "tickets",
1722
- readOnly: false,
1723
- title: "Update Zendesk Ticket",
1724
- description: "Update an existing ticket (status, priority, type, assignee, group, subject, tags, custom fields).",
1725
- inputSchema: z4.object({
1726
- ticket_id: z4.number().int().describe("Ticket ID"),
1727
- status: z4.enum(["new", "open", "pending", "hold", "solved", "closed"]).optional(),
1728
- priority: z4.enum(["urgent", "high", "normal", "low"]).optional(),
1729
- type: z4.enum(["problem", "incident", "question", "task"]).optional(),
1730
- assignee_id: z4.number().int().optional(),
1731
- group_id: z4.number().int().optional(),
1732
- subject: z4.string().optional(),
1733
- tags: z4.array(z4.string()).optional(),
1734
- custom_fields: z4.array(z4.object({ id: z4.number().int(), value: z4.unknown() })).optional()
1735
- }),
1736
- annotations: {
1737
- readOnlyHint: false,
1738
- destructiveHint: false,
1739
- idempotentHint: true,
1740
- openWorldHint: true
1741
- },
1742
- handler: async (params) => {
1743
- const { ticket_id, ...updates } = params;
1744
- const token = await getToken();
1745
- const { ticket } = await zendeskPut(
1746
- subdomain,
1747
- token,
1748
- `/tickets/${ticket_id}`,
1749
- { ticket: updates }
1750
- );
1751
- return {
1752
- content: [
1753
- { type: "text", text: `Ticket #${ticket.id} updated.
1754
-
1755
- ${formatTicket(ticket)}` }
1756
- ]
1757
- };
1758
- }
1759
- },
1760
- {
1761
- name: "add_private_note",
1762
- namespace: "tickets",
1763
- readOnly: false,
1764
- title: "Add Private Note",
1765
- description: "Add an internal note (not visible to requester) to a ticket.",
1766
- inputSchema: z4.object({
1767
- ticket_id: z4.number().int().describe("Ticket ID"),
1768
- body: z4.string().min(1).describe("Note content")
1769
- }),
1770
- annotations: {
1771
- readOnlyHint: false,
1772
- destructiveHint: false,
1773
- idempotentHint: false,
1774
- openWorldHint: true
1775
- },
1776
- handler: async (params) => {
1777
- const { ticket_id, body } = params;
1778
- const token = await getToken();
1779
- await zendeskPut(subdomain, token, `/tickets/${ticket_id}`, {
1780
- ticket: { comment: { body, public: false } }
1781
- });
1782
- return { content: [{ type: "text", text: `Private note added to ticket #${ticket_id}.` }] };
1783
- }
1784
- },
1785
- {
1786
- name: "add_public_comment",
1787
- namespace: "tickets",
1788
- readOnly: false,
1789
- title: "Add Public Comment",
1790
- description: "Add a public comment (visible to requester) to a ticket.",
1791
- inputSchema: z4.object({
1792
- ticket_id: z4.number().int().describe("Ticket ID"),
1793
- body: z4.string().min(1).describe("Comment content")
1794
- }),
1795
- annotations: {
1796
- readOnlyHint: false,
1797
- destructiveHint: false,
1798
- idempotentHint: false,
1799
- openWorldHint: true
1800
- },
1801
- handler: async (params) => {
1802
- const { ticket_id, body } = params;
1803
- const token = await getToken();
1804
- await zendeskPut(subdomain, token, `/tickets/${ticket_id}`, {
1805
- ticket: { comment: { body, public: true } }
1806
- });
1807
- return {
1808
- content: [{ type: "text", text: `Public comment added to ticket #${ticket_id}.` }]
1809
- };
1810
- }
1811
- },
1812
- {
1813
- name: "list_tickets",
1814
- namespace: "tickets",
1815
- readOnly: true,
1816
- title: "List Zendesk Tickets",
1817
- description: "List tickets with cursor-based pagination, sorted by most recently updated.",
1818
- inputSchema: z4.object({
1819
- page_size: z4.number().int().min(1).max(MAX_PAGE_SIZE).default(DEFAULT_PAGE_SIZE),
1820
- cursor: z4.string().optional().describe("Pagination cursor")
1821
- }),
1822
- annotations: {
1823
- readOnlyHint: true,
1824
- destructiveHint: false,
1825
- idempotentHint: true,
1826
- openWorldHint: true
1827
- },
1828
- handler: async (params) => {
1829
- const { page_size, cursor } = params;
1830
- const token = await getToken();
1831
- const response = await zendeskGet(
1832
- subdomain,
1833
- token,
1834
- "/tickets",
1835
- buildCursorParams(page_size, cursor)
1836
- );
1837
- return {
1838
- content: [
1839
- {
1840
- type: "text",
1841
- text: formatList(
1842
- response.tickets ?? [],
1843
- formatTicket,
1844
- extractPaginationMeta(response)
1845
- )
1846
- }
1847
- ]
1848
- };
1849
- }
1850
- },
1851
- {
1852
- name: "get_linked_incidents",
1853
- namespace: "tickets",
1854
- readOnly: true,
1855
- title: "Get Linked Incidents",
1856
- description: "Get all incident tickets linked to a problem ticket.",
1857
- inputSchema: z4.object({
1858
- problem_id: z4.number().int().describe("Problem ticket ID")
1859
- }),
1860
- annotations: {
1861
- readOnlyHint: true,
1862
- destructiveHint: false,
1863
- idempotentHint: true,
1864
- openWorldHint: true
1865
- },
1866
- handler: async (params) => {
1867
- const { problem_id } = params;
1868
- const token = await getToken();
1869
- const response = await zendeskGet(
1870
- subdomain,
1871
- token,
1872
- `/tickets/${problem_id}/incidents`
1873
- );
1874
- const incidents = response.tickets ?? [];
1875
- const text = incidents.length > 0 ? `# Incidents linked to problem #${problem_id}
1876
-
1877
- ${incidents.map(formatTicket).join("\n\n")}` : `No incidents linked to problem #${problem_id}.`;
1878
- return { content: [{ type: "text", text: truncateIfNeeded(text) }] };
1879
- }
1880
- },
1881
- {
1882
- name: "manage_tags",
1883
- namespace: "tickets",
1884
- readOnly: false,
1885
- title: "Manage Ticket Tags",
1886
- description: "Add or remove tags on a ticket.",
1887
- inputSchema: z4.object({
1888
- ticket_id: z4.number().int().describe("Ticket ID"),
1889
- add: z4.array(z4.string()).optional().describe("Tags to add"),
1890
- remove: z4.array(z4.string()).optional().describe("Tags to remove")
1891
- }),
1892
- annotations: {
1893
- readOnlyHint: false,
1894
- destructiveHint: false,
1895
- idempotentHint: true,
1896
- openWorldHint: true
1897
- },
1898
- handler: async (params) => {
1899
- const { ticket_id, add, remove } = params;
1900
- const token = await getToken();
1901
- const { ticket } = await zendeskGet(
1902
- subdomain,
1903
- token,
1904
- `/tickets/${ticket_id}`
1905
- );
1906
- const tags = new Set(ticket.tags);
1907
- add?.forEach((t) => {
1908
- tags.add(t);
1909
- });
1910
- remove?.forEach((t) => {
1911
- tags.delete(t);
1912
- });
1913
- const { ticket: updated } = await zendeskPut(
1914
- subdomain,
1915
- token,
1916
- `/tickets/${ticket_id}`,
1917
- { ticket: { tags: [...tags] } }
1918
- );
1919
- return {
1920
- content: [
1921
- {
1922
- type: "text",
1923
- text: `Tags updated on ticket #${ticket_id}. Current: ${updated.tags.join(", ") || "none"}`
1924
- }
1925
- ]
1926
- };
1927
- }
1928
- }
1929
- ];
1322
+ const createTicketTools = (ctx) => {
1323
+ const { subdomain, getToken } = ctx;
1324
+ return [
1325
+ {
1326
+ name: "get_ticket",
1327
+ namespace: "tickets",
1328
+ readOnly: true,
1329
+ title: "Get Zendesk Ticket",
1330
+ description: "Retrieve a Zendesk ticket by ID, including its comments if requested. Returns ticket details (subject, status, priority, assignee, tags, description) and optionally all comments/internal notes.",
1331
+ inputSchema: z.object({
1332
+ ticket_id: z.number().int().describe("Ticket ID"),
1333
+ include_comments: z.boolean().default(false).describe("Include ticket comments")
1334
+ }),
1335
+ annotations: {
1336
+ readOnlyHint: true,
1337
+ destructiveHint: false,
1338
+ idempotentHint: true,
1339
+ openWorldHint: true
1340
+ },
1341
+ handler: async (params) => {
1342
+ const { ticket_id, include_comments } = params;
1343
+ const token = await getToken();
1344
+ const { ticket } = await zendeskGet(subdomain, token, `/tickets/${ticket_id}`);
1345
+ let text = formatTicket(ticket);
1346
+ if (include_comments) {
1347
+ const { comments } = await zendeskGet(subdomain, token, `/tickets/${ticket_id}/comments`);
1348
+ text += `\n\n---\n# Comments\n\n${comments.map(formatComment).join("\n\n")}`;
1349
+ }
1350
+ return { content: [{
1351
+ type: "text",
1352
+ text: truncateIfNeeded(text)
1353
+ }] };
1354
+ }
1355
+ },
1356
+ {
1357
+ name: "get_ticket_attachments",
1358
+ namespace: "tickets",
1359
+ readOnly: true,
1360
+ title: "Get Zendesk Ticket Attachments",
1361
+ description: "Retrieve ticket attachments. Images are embedded inline; other files are listed as text references.",
1362
+ inputSchema: z.object({
1363
+ ticket_id: z.number().int().describe("Ticket ID"),
1364
+ attachment_ids: z.array(z.number().int()).optional().describe("Attachment IDs to fetch directly (e.g. extracted from a previous get_ticket(include_comments=true) call). When provided, skips the comments fetch entirely. When omitted, all attachments of the ticket are returned.")
1365
+ }),
1366
+ annotations: {
1367
+ readOnlyHint: true,
1368
+ destructiveHint: false,
1369
+ idempotentHint: true,
1370
+ openWorldHint: true
1371
+ },
1372
+ handler: async (params) => {
1373
+ const { ticket_id, attachment_ids } = params;
1374
+ const token = await getToken();
1375
+ let attachments;
1376
+ if (attachment_ids && attachment_ids.length > 0) {
1377
+ attachments = [];
1378
+ for (const id of attachment_ids) try {
1379
+ const { attachment } = await zendeskGet(subdomain, token, `/attachments/${id}`);
1380
+ attachments.push(attachment);
1381
+ } catch (error) {
1382
+ if (!(error instanceof ZendeskApiError) || error.status !== 404) throw error;
1383
+ }
1384
+ } else attachments = (await fetchAllTicketComments(subdomain, token, ticket_id)).flatMap((c) => c.attachments ?? []);
1385
+ if (attachments.length === 0) return { content: [{
1386
+ type: "text",
1387
+ text: `No attachments found on ticket #${ticket_id}.`
1388
+ }] };
1389
+ const blocks = await collectAttachmentBlocks(subdomain, token, attachments);
1390
+ return { content: [{
1391
+ type: "text",
1392
+ text: `# Attachments for ticket #${ticket_id} (${attachments.length} total)`
1393
+ }, ...blocks] };
1394
+ }
1395
+ },
1396
+ {
1397
+ name: "search_tickets",
1398
+ namespace: "tickets",
1399
+ readOnly: true,
1400
+ title: "Search Zendesk Tickets",
1401
+ description: "Search tickets using Zendesk query syntax (e.g., \"status:open assignee:me\", \"priority:urgent type:incident\"). Returns total count.",
1402
+ inputSchema: z.object({
1403
+ query: z.string().min(1).describe("Zendesk search query string"),
1404
+ per_page: z.number().int().min(1).max(100).default(100).describe("Results per page"),
1405
+ page: z.number().int().min(1).default(1).describe("Page number")
1406
+ }),
1407
+ annotations: {
1408
+ readOnlyHint: true,
1409
+ destructiveHint: false,
1410
+ idempotentHint: true,
1411
+ openWorldHint: true
1412
+ },
1413
+ handler: async (params) => {
1414
+ const { query, per_page, page } = params;
1415
+ const response = await zendeskGet(subdomain, await getToken(), "/search", {
1416
+ query: `type:ticket ${query}`,
1417
+ ...buildOffsetParams(per_page, page)
1418
+ });
1419
+ return { content: [{
1420
+ type: "text",
1421
+ text: formatList(response.results ?? [], formatTicket, extractSearchPaginationMeta(response, per_page, page))
1422
+ }] };
1423
+ }
1424
+ },
1425
+ {
1426
+ name: "create_ticket",
1427
+ namespace: "tickets",
1428
+ readOnly: false,
1429
+ title: "Create Zendesk Ticket",
1430
+ description: "Create a new Zendesk support ticket with subject, description, and optional priority/type/assignee/tags.",
1431
+ inputSchema: z.object({
1432
+ subject: z.string().min(1).describe("Ticket subject"),
1433
+ description: z.string().min(1).describe("Ticket description"),
1434
+ priority: z.enum([
1435
+ "urgent",
1436
+ "high",
1437
+ "normal",
1438
+ "low"
1439
+ ]).optional(),
1440
+ type: z.enum([
1441
+ "problem",
1442
+ "incident",
1443
+ "question",
1444
+ "task"
1445
+ ]).optional(),
1446
+ assignee_id: z.number().int().optional(),
1447
+ group_id: z.number().int().optional(),
1448
+ tags: z.array(z.string()).optional(),
1449
+ custom_fields: z.array(z.object({
1450
+ id: z.number().int(),
1451
+ value: z.unknown()
1452
+ })).optional()
1453
+ }),
1454
+ annotations: {
1455
+ readOnlyHint: false,
1456
+ destructiveHint: false,
1457
+ idempotentHint: false,
1458
+ openWorldHint: true
1459
+ },
1460
+ handler: async (params) => {
1461
+ const { subject, description, ...rest } = params;
1462
+ const { ticket } = await zendeskPost(subdomain, await getToken(), "/tickets", { ticket: {
1463
+ subject,
1464
+ comment: { body: description },
1465
+ ...rest
1466
+ } });
1467
+ return { content: [{
1468
+ type: "text",
1469
+ text: `Ticket #${ticket.id} created.\n\n${formatTicket(ticket)}`
1470
+ }] };
1471
+ }
1472
+ },
1473
+ {
1474
+ name: "update_ticket",
1475
+ namespace: "tickets",
1476
+ readOnly: false,
1477
+ title: "Update Zendesk Ticket",
1478
+ description: "Update an existing ticket (status, priority, type, assignee, group, subject, tags, custom fields).",
1479
+ inputSchema: z.object({
1480
+ ticket_id: z.number().int().describe("Ticket ID"),
1481
+ status: z.enum([
1482
+ "new",
1483
+ "open",
1484
+ "pending",
1485
+ "hold",
1486
+ "solved",
1487
+ "closed"
1488
+ ]).optional(),
1489
+ priority: z.enum([
1490
+ "urgent",
1491
+ "high",
1492
+ "normal",
1493
+ "low"
1494
+ ]).optional(),
1495
+ type: z.enum([
1496
+ "problem",
1497
+ "incident",
1498
+ "question",
1499
+ "task"
1500
+ ]).optional(),
1501
+ assignee_id: z.number().int().optional(),
1502
+ group_id: z.number().int().optional(),
1503
+ subject: z.string().optional(),
1504
+ tags: z.array(z.string()).optional(),
1505
+ custom_fields: z.array(z.object({
1506
+ id: z.number().int(),
1507
+ value: z.unknown()
1508
+ })).optional()
1509
+ }),
1510
+ annotations: {
1511
+ readOnlyHint: false,
1512
+ destructiveHint: false,
1513
+ idempotentHint: true,
1514
+ openWorldHint: true
1515
+ },
1516
+ handler: async (params) => {
1517
+ const { ticket_id, ...updates } = params;
1518
+ const { ticket } = await zendeskPut(subdomain, await getToken(), `/tickets/${ticket_id}`, { ticket: updates });
1519
+ return { content: [{
1520
+ type: "text",
1521
+ text: `Ticket #${ticket.id} updated.\n\n${formatTicket(ticket)}`
1522
+ }] };
1523
+ }
1524
+ },
1525
+ {
1526
+ name: "add_private_note",
1527
+ namespace: "tickets",
1528
+ readOnly: false,
1529
+ title: "Add Private Note",
1530
+ description: "Add an internal note (not visible to requester) to a ticket.",
1531
+ inputSchema: z.object({
1532
+ ticket_id: z.number().int().describe("Ticket ID"),
1533
+ body: z.string().min(1).describe("Note content")
1534
+ }),
1535
+ annotations: {
1536
+ readOnlyHint: false,
1537
+ destructiveHint: false,
1538
+ idempotentHint: false,
1539
+ openWorldHint: true
1540
+ },
1541
+ handler: async (params) => {
1542
+ const { ticket_id, body } = params;
1543
+ await zendeskPut(subdomain, await getToken(), `/tickets/${ticket_id}`, { ticket: { comment: {
1544
+ body,
1545
+ public: false
1546
+ } } });
1547
+ return { content: [{
1548
+ type: "text",
1549
+ text: `Private note added to ticket #${ticket_id}.`
1550
+ }] };
1551
+ }
1552
+ },
1553
+ {
1554
+ name: "add_public_comment",
1555
+ namespace: "tickets",
1556
+ readOnly: false,
1557
+ title: "Add Public Comment",
1558
+ description: "Add a public comment (visible to requester) to a ticket.",
1559
+ inputSchema: z.object({
1560
+ ticket_id: z.number().int().describe("Ticket ID"),
1561
+ body: z.string().min(1).describe("Comment content")
1562
+ }),
1563
+ annotations: {
1564
+ readOnlyHint: false,
1565
+ destructiveHint: false,
1566
+ idempotentHint: false,
1567
+ openWorldHint: true
1568
+ },
1569
+ handler: async (params) => {
1570
+ const { ticket_id, body } = params;
1571
+ await zendeskPut(subdomain, await getToken(), `/tickets/${ticket_id}`, { ticket: { comment: {
1572
+ body,
1573
+ public: true
1574
+ } } });
1575
+ return { content: [{
1576
+ type: "text",
1577
+ text: `Public comment added to ticket #${ticket_id}.`
1578
+ }] };
1579
+ }
1580
+ },
1581
+ {
1582
+ name: "list_tickets",
1583
+ namespace: "tickets",
1584
+ readOnly: true,
1585
+ title: "List Zendesk Tickets",
1586
+ description: "List tickets with cursor-based pagination, sorted by most recently updated.",
1587
+ inputSchema: z.object({
1588
+ page_size: z.number().int().min(1).max(100).default(100),
1589
+ cursor: z.string().optional().describe("Pagination cursor")
1590
+ }),
1591
+ annotations: {
1592
+ readOnlyHint: true,
1593
+ destructiveHint: false,
1594
+ idempotentHint: true,
1595
+ openWorldHint: true
1596
+ },
1597
+ handler: async (params) => {
1598
+ const { page_size, cursor } = params;
1599
+ const response = await zendeskGet(subdomain, await getToken(), "/tickets", buildCursorParams(page_size, cursor));
1600
+ return { content: [{
1601
+ type: "text",
1602
+ text: formatList(response.tickets ?? [], formatTicket, extractPaginationMeta(response))
1603
+ }] };
1604
+ }
1605
+ },
1606
+ {
1607
+ name: "get_linked_incidents",
1608
+ namespace: "tickets",
1609
+ readOnly: true,
1610
+ title: "Get Linked Incidents",
1611
+ description: "Get all incident tickets linked to a problem ticket.",
1612
+ inputSchema: z.object({ problem_id: z.number().int().describe("Problem ticket ID") }),
1613
+ annotations: {
1614
+ readOnlyHint: true,
1615
+ destructiveHint: false,
1616
+ idempotentHint: true,
1617
+ openWorldHint: true
1618
+ },
1619
+ handler: async (params) => {
1620
+ const { problem_id } = params;
1621
+ const incidents = (await zendeskGet(subdomain, await getToken(), `/tickets/${problem_id}/incidents`)).tickets ?? [];
1622
+ return { content: [{
1623
+ type: "text",
1624
+ text: truncateIfNeeded(incidents.length > 0 ? `# Incidents linked to problem #${problem_id}\n\n${incidents.map(formatTicket).join("\n\n")}` : `No incidents linked to problem #${problem_id}.`)
1625
+ }] };
1626
+ }
1627
+ },
1628
+ {
1629
+ name: "manage_tags",
1630
+ namespace: "tickets",
1631
+ readOnly: false,
1632
+ title: "Manage Ticket Tags",
1633
+ description: "Add or remove tags on a ticket.",
1634
+ inputSchema: z.object({
1635
+ ticket_id: z.number().int().describe("Ticket ID"),
1636
+ add: z.array(z.string()).optional().describe("Tags to add"),
1637
+ remove: z.array(z.string()).optional().describe("Tags to remove")
1638
+ }),
1639
+ annotations: {
1640
+ readOnlyHint: false,
1641
+ destructiveHint: false,
1642
+ idempotentHint: true,
1643
+ openWorldHint: true
1644
+ },
1645
+ handler: async (params) => {
1646
+ const { ticket_id, add, remove } = params;
1647
+ const token = await getToken();
1648
+ const { ticket } = await zendeskGet(subdomain, token, `/tickets/${ticket_id}`);
1649
+ const tags = new Set(ticket.tags);
1650
+ add?.forEach((t) => {
1651
+ tags.add(t);
1652
+ });
1653
+ remove?.forEach((t) => {
1654
+ tags.delete(t);
1655
+ });
1656
+ const { ticket: updated } = await zendeskPut(subdomain, token, `/tickets/${ticket_id}`, { ticket: { tags: [...tags] } });
1657
+ return { content: [{
1658
+ type: "text",
1659
+ text: `Tags updated on ticket #${ticket_id}. Current: ${updated.tags.join(", ") || "none"}`
1660
+ }] };
1661
+ }
1662
+ }
1663
+ ];
1930
1664
  };
1931
-
1932
- // src/tools/users.ts
1933
- import * as z5 from "zod/v4";
1934
- var createUserTools = (ctx) => {
1935
- const { subdomain, getToken } = ctx;
1936
- return [
1937
- {
1938
- name: "get_current_user",
1939
- namespace: "users",
1940
- readOnly: true,
1941
- title: "Get Current Zendesk User",
1942
- description: "Get the currently authenticated Zendesk user. Useful to verify identity and permissions.",
1943
- inputSchema: z5.object({}),
1944
- annotations: {
1945
- readOnlyHint: true,
1946
- destructiveHint: false,
1947
- idempotentHint: true,
1948
- openWorldHint: true
1949
- },
1950
- handler: async () => {
1951
- const token = await getToken();
1952
- const { user } = await zendeskGet(subdomain, token, "/users/me");
1953
- return { content: [{ type: "text", text: formatUser(user) }] };
1954
- }
1955
- },
1956
- {
1957
- name: "search_users",
1958
- namespace: "users",
1959
- readOnly: true,
1960
- title: "Search Zendesk Users",
1961
- description: "Search for users by name, email, or other criteria using Zendesk search query syntax. Returns total count.",
1962
- inputSchema: z5.object({
1963
- query: z5.string().min(1).describe("Search query"),
1964
- per_page: z5.number().int().min(1).max(MAX_PAGE_SIZE).default(DEFAULT_PAGE_SIZE).describe("Results per page"),
1965
- page: z5.number().int().min(1).default(1).describe("Page number")
1966
- }),
1967
- annotations: {
1968
- readOnlyHint: true,
1969
- destructiveHint: false,
1970
- idempotentHint: true,
1971
- openWorldHint: true
1972
- },
1973
- handler: async (params) => {
1974
- const { query, per_page, page } = params;
1975
- const token = await getToken();
1976
- const response = await zendeskGet(
1977
- subdomain,
1978
- token,
1979
- "/search",
1980
- {
1981
- query: `type:user ${query}`,
1982
- ...buildOffsetParams(per_page, page)
1983
- }
1984
- );
1985
- return {
1986
- content: [
1987
- {
1988
- type: "text",
1989
- text: formatList(
1990
- response.results ?? [],
1991
- formatUser,
1992
- extractSearchPaginationMeta(response, per_page, page)
1993
- )
1994
- }
1995
- ]
1996
- };
1997
- }
1998
- },
1999
- {
2000
- name: "get_user",
2001
- namespace: "users",
2002
- readOnly: true,
2003
- title: "Get Zendesk User",
2004
- description: "Retrieve a user by ID.",
2005
- inputSchema: z5.object({ user_id: z5.number().int().describe("User ID") }),
2006
- annotations: {
2007
- readOnlyHint: true,
2008
- destructiveHint: false,
2009
- idempotentHint: true,
2010
- openWorldHint: true
2011
- },
2012
- handler: async (params) => {
2013
- const { user_id } = params;
2014
- const token = await getToken();
2015
- const { user } = await zendeskGet(
2016
- subdomain,
2017
- token,
2018
- `/users/${user_id}`
2019
- );
2020
- return { content: [{ type: "text", text: formatUser(user) }] };
2021
- }
2022
- },
2023
- {
2024
- name: "get_organization",
2025
- namespace: "users",
2026
- readOnly: true,
2027
- title: "Get Zendesk Organization",
2028
- description: "Retrieve an organization by ID.",
2029
- inputSchema: z5.object({ organization_id: z5.number().int().describe("Organization ID") }),
2030
- annotations: {
2031
- readOnlyHint: true,
2032
- destructiveHint: false,
2033
- idempotentHint: true,
2034
- openWorldHint: true
2035
- },
2036
- handler: async (params) => {
2037
- const { organization_id } = params;
2038
- const token = await getToken();
2039
- const { organization } = await zendeskGet(
2040
- subdomain,
2041
- token,
2042
- `/organizations/${organization_id}`
2043
- );
2044
- return { content: [{ type: "text", text: formatOrganization(organization) }] };
2045
- }
2046
- },
2047
- {
2048
- name: "list_organizations",
2049
- namespace: "users",
2050
- readOnly: true,
2051
- title: "List Zendesk Organizations",
2052
- description: "List all organizations with pagination.",
2053
- inputSchema: z5.object({
2054
- page_size: z5.number().int().min(1).max(MAX_PAGE_SIZE).default(DEFAULT_PAGE_SIZE),
2055
- cursor: z5.string().optional()
2056
- }),
2057
- annotations: {
2058
- readOnlyHint: true,
2059
- destructiveHint: false,
2060
- idempotentHint: true,
2061
- openWorldHint: true
2062
- },
2063
- handler: async (params) => {
2064
- const { page_size, cursor } = params;
2065
- const token = await getToken();
2066
- const response = await zendeskGet(
2067
- subdomain,
2068
- token,
2069
- "/organizations",
2070
- buildCursorParams(page_size, cursor)
2071
- );
2072
- return {
2073
- content: [
2074
- {
2075
- type: "text",
2076
- text: formatList(
2077
- response.organizations ?? [],
2078
- formatOrganization,
2079
- extractPaginationMeta(response)
2080
- )
2081
- }
2082
- ]
2083
- };
2084
- }
2085
- }
2086
- ];
1665
+ //#endregion
1666
+ //#region src/tools/users.ts
1667
+ const createUserTools = (ctx) => {
1668
+ const { subdomain, getToken } = ctx;
1669
+ return [
1670
+ {
1671
+ name: "get_current_user",
1672
+ namespace: "users",
1673
+ readOnly: true,
1674
+ title: "Get Current Zendesk User",
1675
+ description: "Get the currently authenticated Zendesk user. Useful to verify identity and permissions.",
1676
+ inputSchema: z.object({}),
1677
+ annotations: {
1678
+ readOnlyHint: true,
1679
+ destructiveHint: false,
1680
+ idempotentHint: true,
1681
+ openWorldHint: true
1682
+ },
1683
+ handler: async () => {
1684
+ const { user } = await zendeskGet(subdomain, await getToken(), "/users/me");
1685
+ return { content: [{
1686
+ type: "text",
1687
+ text: formatUser(user)
1688
+ }] };
1689
+ }
1690
+ },
1691
+ {
1692
+ name: "search_users",
1693
+ namespace: "users",
1694
+ readOnly: true,
1695
+ title: "Search Zendesk Users",
1696
+ description: "Search for users by name, email, or other criteria using Zendesk search query syntax. Returns total count.",
1697
+ inputSchema: z.object({
1698
+ query: z.string().min(1).describe("Search query"),
1699
+ per_page: z.number().int().min(1).max(100).default(100).describe("Results per page"),
1700
+ page: z.number().int().min(1).default(1).describe("Page number")
1701
+ }),
1702
+ annotations: {
1703
+ readOnlyHint: true,
1704
+ destructiveHint: false,
1705
+ idempotentHint: true,
1706
+ openWorldHint: true
1707
+ },
1708
+ handler: async (params) => {
1709
+ const { query, per_page, page } = params;
1710
+ const response = await zendeskGet(subdomain, await getToken(), "/search", {
1711
+ query: `type:user ${query}`,
1712
+ ...buildOffsetParams(per_page, page)
1713
+ });
1714
+ return { content: [{
1715
+ type: "text",
1716
+ text: formatList(response.results ?? [], formatUser, extractSearchPaginationMeta(response, per_page, page))
1717
+ }] };
1718
+ }
1719
+ },
1720
+ {
1721
+ name: "get_user",
1722
+ namespace: "users",
1723
+ readOnly: true,
1724
+ title: "Get Zendesk User",
1725
+ description: "Retrieve a user by ID.",
1726
+ inputSchema: z.object({ user_id: z.number().int().describe("User ID") }),
1727
+ annotations: {
1728
+ readOnlyHint: true,
1729
+ destructiveHint: false,
1730
+ idempotentHint: true,
1731
+ openWorldHint: true
1732
+ },
1733
+ handler: async (params) => {
1734
+ const { user_id } = params;
1735
+ const { user } = await zendeskGet(subdomain, await getToken(), `/users/${user_id}`);
1736
+ return { content: [{
1737
+ type: "text",
1738
+ text: formatUser(user)
1739
+ }] };
1740
+ }
1741
+ },
1742
+ {
1743
+ name: "get_organization",
1744
+ namespace: "users",
1745
+ readOnly: true,
1746
+ title: "Get Zendesk Organization",
1747
+ description: "Retrieve an organization by ID.",
1748
+ inputSchema: z.object({ organization_id: z.number().int().describe("Organization ID") }),
1749
+ annotations: {
1750
+ readOnlyHint: true,
1751
+ destructiveHint: false,
1752
+ idempotentHint: true,
1753
+ openWorldHint: true
1754
+ },
1755
+ handler: async (params) => {
1756
+ const { organization_id } = params;
1757
+ const { organization } = await zendeskGet(subdomain, await getToken(), `/organizations/${organization_id}`);
1758
+ return { content: [{
1759
+ type: "text",
1760
+ text: formatOrganization(organization)
1761
+ }] };
1762
+ }
1763
+ },
1764
+ {
1765
+ name: "list_organizations",
1766
+ namespace: "users",
1767
+ readOnly: true,
1768
+ title: "List Zendesk Organizations",
1769
+ description: "List all organizations with pagination.",
1770
+ inputSchema: z.object({
1771
+ page_size: z.number().int().min(1).max(100).default(100),
1772
+ cursor: z.string().optional()
1773
+ }),
1774
+ annotations: {
1775
+ readOnlyHint: true,
1776
+ destructiveHint: false,
1777
+ idempotentHint: true,
1778
+ openWorldHint: true
1779
+ },
1780
+ handler: async (params) => {
1781
+ const { page_size, cursor } = params;
1782
+ const response = await zendeskGet(subdomain, await getToken(), "/organizations", buildCursorParams(page_size, cursor));
1783
+ return { content: [{
1784
+ type: "text",
1785
+ text: formatList(response.organizations ?? [], formatOrganization, extractPaginationMeta(response))
1786
+ }] };
1787
+ }
1788
+ }
1789
+ ];
2087
1790
  };
2088
-
2089
- // src/tools/index.ts
2090
- var createAllTools = (ctx) => [
2091
- ...createTicketTools(ctx),
2092
- ...createSearchTools(ctx),
2093
- ...createHelpCenterTools(ctx),
2094
- ...createUserTools(ctx)
1791
+ //#endregion
1792
+ //#region src/tools/index.ts
1793
+ const createAllTools = (ctx) => [
1794
+ ...createTicketTools(ctx),
1795
+ ...createSearchTools(ctx),
1796
+ ...createHelpCenterTools(ctx),
1797
+ ...createUserTools(ctx)
2095
1798
  ];
2096
-
2097
- // src/server.ts
2098
- var NAMESPACE_LABELS = {
2099
- tickets: { toolName: "zendesk_tickets", title: "Zendesk Tickets" },
2100
- help_center: { toolName: "zendesk_help_center", title: "Zendesk Help Center" },
2101
- users: { toolName: "zendesk_users", title: "Zendesk Users" }
1799
+ //#endregion
1800
+ //#region src/server.ts
1801
+ const NAMESPACE_LABELS = {
1802
+ tickets: {
1803
+ toolName: "zendesk_tickets",
1804
+ title: "Zendesk Tickets"
1805
+ },
1806
+ help_center: {
1807
+ toolName: "zendesk_help_center",
1808
+ title: "Zendesk Help Center"
1809
+ },
1810
+ users: {
1811
+ toolName: "zendesk_users",
1812
+ title: "Zendesk Users"
1813
+ }
2102
1814
  };
2103
- var summarizeDescription = (description) => {
2104
- const idx = description.indexOf(". ");
2105
- if (idx === -1) return description;
2106
- return description.slice(0, idx + 1);
1815
+ const summarizeDescription = (description) => {
1816
+ const idx = description.indexOf(". ");
1817
+ if (idx === -1) return description;
1818
+ return description.slice(0, idx + 1);
2107
1819
  };
2108
- var buildOperationList = (tools) => tools.map(
2109
- (t) => `- **${t.name}**: ${summarizeDescription(t.description)}${t.readOnly ? "" : " (write)"}`
2110
- ).join("\n");
2111
- var registerProxyTool = (server, toolName, title, tools, handlerMap) => {
2112
- const operationNames = tools.map((t) => t.name);
2113
- const operationList = buildOperationList(tools);
2114
- server.registerTool(
2115
- toolName,
2116
- {
2117
- title,
2118
- description: `${title}. Specify the operation and its parameters.
2119
-
2120
- Available operations:
2121
- ${operationList}`,
2122
- inputSchema: z6.object({
2123
- operation: z6.string().describe(`One of: ${operationNames.join(", ")}`),
2124
- params: z6.record(z6.string(), z6.unknown()).default({}).describe("Operation parameters")
2125
- })
2126
- },
2127
- async ({ operation, params }) => {
2128
- const def = handlerMap.get(operation);
2129
- if (!def) {
2130
- return {
2131
- content: [
2132
- {
2133
- type: "text",
2134
- text: `Unknown operation "${operation}". Available: ${operationNames.join(", ")}`
2135
- }
2136
- ]
2137
- };
2138
- }
2139
- const validated = def.inputSchema.parse(params);
2140
- return def.handler(validated);
2141
- }
2142
- );
1820
+ const buildOperationList = (tools) => tools.map((t) => `- **${t.name}**: ${summarizeDescription(t.description)}${t.readOnly ? "" : " (write)"}`).join("\n");
1821
+ const registerProxyTool = (server, toolName, title, tools, handlerMap) => {
1822
+ const operationNames = tools.map((t) => t.name);
1823
+ const operationList = buildOperationList(tools);
1824
+ server.registerTool(toolName, {
1825
+ title,
1826
+ description: `${title}. Specify the operation and its parameters.\n\nAvailable operations:\n${operationList}`,
1827
+ inputSchema: z.object({
1828
+ operation: z.string().describe(`One of: ${operationNames.join(", ")}`),
1829
+ params: z.record(z.string(), z.unknown()).default({}).describe("Operation parameters")
1830
+ })
1831
+ }, async ({ operation, params }) => {
1832
+ const def = handlerMap.get(operation);
1833
+ if (!def) return { content: [{
1834
+ type: "text",
1835
+ text: `Unknown operation "${operation}". Available: ${operationNames.join(", ")}`
1836
+ }] };
1837
+ const validated = def.inputSchema.parse(params);
1838
+ return def.handler(validated);
1839
+ });
2143
1840
  };
2144
- var createMcpServer = (config, getToken) => {
2145
- const server = new McpServer({
2146
- name: "@digital4better/zendesk-mcp-server",
2147
- version: "0.1.0"
2148
- });
2149
- const allTools = createAllTools({ subdomain: config.subdomain, getToken });
2150
- const filteredTools = filterTools(allTools, {
2151
- readOnly: config.readOnly,
2152
- namespaces: config.namespaces,
2153
- tools: config.tools
2154
- });
2155
- const handlerMap = /* @__PURE__ */ new Map();
2156
- for (const tool of filteredTools) {
2157
- handlerMap.set(tool.name, tool);
2158
- }
2159
- switch (config.mode) {
2160
- case "all": {
2161
- for (const tool of filteredTools) {
2162
- server.registerTool(
2163
- tool.name,
2164
- {
2165
- title: tool.title,
2166
- description: tool.description,
2167
- inputSchema: tool.inputSchema,
2168
- annotations: tool.annotations
2169
- },
2170
- async (params) => tool.handler(params)
2171
- );
2172
- }
2173
- break;
2174
- }
2175
- case "namespace": {
2176
- const grouped = groupByNamespace(filteredTools);
2177
- for (const [namespace, tools] of grouped) {
2178
- const label = NAMESPACE_LABELS[namespace];
2179
- if (label) {
2180
- registerProxyTool(server, label.toolName, label.title, tools, handlerMap);
2181
- }
2182
- }
2183
- break;
2184
- }
2185
- case "single": {
2186
- registerProxyTool(server, "zendesk", "Zendesk", filteredTools, handlerMap);
2187
- break;
2188
- }
2189
- }
2190
- console.error(`Registered ${filteredTools.length} tools in ${config.mode} mode`);
2191
- return server;
1841
+ const createMcpServer = (config, getToken) => {
1842
+ const server = new McpServer({
1843
+ name: "@digital4better/zendesk-mcp-server",
1844
+ version: "0.1.0"
1845
+ });
1846
+ const filteredTools = filterTools(createAllTools({
1847
+ subdomain: config.subdomain,
1848
+ getToken
1849
+ }), {
1850
+ readOnly: config.readOnly,
1851
+ namespaces: config.namespaces,
1852
+ tools: config.tools
1853
+ });
1854
+ const handlerMap = /* @__PURE__ */ new Map();
1855
+ for (const tool of filteredTools) handlerMap.set(tool.name, tool);
1856
+ switch (config.mode) {
1857
+ case "all":
1858
+ for (const tool of filteredTools) server.registerTool(tool.name, {
1859
+ title: tool.title,
1860
+ description: tool.description,
1861
+ inputSchema: tool.inputSchema,
1862
+ annotations: tool.annotations
1863
+ }, async (params) => tool.handler(params));
1864
+ break;
1865
+ case "namespace": {
1866
+ const grouped = groupByNamespace(filteredTools);
1867
+ for (const [namespace, tools] of grouped) {
1868
+ const label = NAMESPACE_LABELS[namespace];
1869
+ if (label) registerProxyTool(server, label.toolName, label.title, tools, handlerMap);
1870
+ }
1871
+ break;
1872
+ }
1873
+ case "single":
1874
+ registerProxyTool(server, "zendesk", "Zendesk", filteredTools, handlerMap);
1875
+ break;
1876
+ }
1877
+ console.error(`Registered ${filteredTools.length} tools in ${config.mode} mode`);
1878
+ return server;
2192
1879
  };
2193
-
2194
- // src/transports/stdio.ts
2195
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2196
- var startStdioTransport = async (server) => {
2197
- const transport = new StdioServerTransport();
2198
- await server.connect(transport);
2199
- console.error("Zendesk MCP server running via stdio");
1880
+ //#endregion
1881
+ //#region src/transports/stdio.ts
1882
+ const startStdioTransport = async (server) => {
1883
+ const transport = new StdioServerTransport();
1884
+ await server.connect(transport);
1885
+ console.error("Zendesk MCP server running via stdio");
2200
1886
  };
2201
-
2202
- // src/index.ts
2203
- var main = async () => {
2204
- const config = loadConfig();
2205
- if (config.zendeskEmail && config.zendeskApiToken) {
2206
- const staticToken = buildBasicAuthHeader(config.zendeskEmail, config.zendeskApiToken);
2207
- const getToken = () => staticToken;
2208
- const server = createMcpServer(config, getToken);
2209
- await startStdioTransport(server);
2210
- } else {
2211
- const tokenStore = createTokenStore({
2212
- subdomain: config.subdomain,
2213
- oauthClientId: config.oauthClientId
2214
- });
2215
- const server = createMcpServer(config, tokenStore.getToken);
2216
- await startStdioTransport(server);
2217
- }
1887
+ //#endregion
1888
+ //#region src/index.ts
1889
+ const main = async () => {
1890
+ const config = loadConfig();
1891
+ if (config.zendeskEmail && config.zendeskApiToken) {
1892
+ const staticToken = buildBasicAuthHeader(config.zendeskEmail, config.zendeskApiToken);
1893
+ const getToken = () => staticToken;
1894
+ await startStdioTransport(createMcpServer(config, getToken));
1895
+ } else await startStdioTransport(createMcpServer(config, createTokenStore({
1896
+ subdomain: config.subdomain,
1897
+ oauthClientId: config.oauthClientId
1898
+ }).getToken));
2218
1899
  };
2219
1900
  main().catch((error) => {
2220
- console.error("Fatal error:", error);
2221
- process.exit(1);
1901
+ console.error("Fatal error:", error);
1902
+ process.exit(1);
2222
1903
  });
1904
+ //#endregion
1905
+ export {};
1906
+
2223
1907
  //# sourceMappingURL=index.js.map