@fruggr/zendesk-mcp-server 3.8.0 → 3.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +38 -15
- package/dist/sbom.cdx.json +4 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@fruggr/zendesk-mcp-server)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://nodejs.org)
|
|
8
|
-
[](https://github.com/fruggr/zendesk-mcp-server/blob/main/docs/http-deployment.md#container)
|
|
9
9
|
|
|
10
10
|
A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that
|
|
11
11
|
puts Zendesk inside your AI assistant, for **both sides of the conversation**.
|
package/dist/index.js
CHANGED
|
@@ -7980,21 +7980,42 @@ const portEnv = (name) => {
|
|
|
7980
7980
|
return env.value === void 0 ? void 0 : parsePort(env.value, env.name);
|
|
7981
7981
|
};
|
|
7982
7982
|
/**
|
|
7983
|
-
* Split a raw
|
|
7984
|
-
*
|
|
7985
|
-
*
|
|
7986
|
-
*
|
|
7987
|
-
* whether an id/subdomain exists is decided lazily, on first use (no token is
|
|
7988
|
-
* available at startup on either transport).
|
|
7983
|
+
* Split a raw comma-separated value (`ZENDESK_BRAND_IDS`, `MCP_NAMESPACES`) into
|
|
7984
|
+
* its entries, trimmed. An empty entry (from `a,,b`, a leading/trailing comma,
|
|
7985
|
+
* or a whitespace-only value) is a misconfiguration, not an ignored blank: it
|
|
7986
|
+
* fails at startup naming the knob, unlike the lenient `splitList`.
|
|
7989
7987
|
*/
|
|
7988
|
+
const splitStrictList = (raw, emptyEntryMessage) => {
|
|
7989
|
+
const entries = raw.split(",").map((entry) => entry.trim());
|
|
7990
|
+
if (entries.some((entry) => entry.length === 0)) throw createStartupError(emptyEntryMessage, STARTUP_DOCS.environment);
|
|
7991
|
+
return entries;
|
|
7992
|
+
};
|
|
7990
7993
|
const parseBrandIds = (raw) => {
|
|
7991
7994
|
if (raw === void 0) return void 0;
|
|
7992
|
-
const
|
|
7993
|
-
if (entries.some((entry) => entry.length === 0)) throw createStartupError("Invalid --brand-ids / ZENDESK_BRAND_IDS value: empty entry. Expected a comma-separated list of brand ids or subdomains, or \"all\".", STARTUP_DOCS.environment);
|
|
7994
|
-
const normalized = entries.map((entry) => entry.toLowerCase());
|
|
7995
|
+
const normalized = splitStrictList(raw, "Invalid --brand-ids / ZENDESK_BRAND_IDS value: empty entry. Expected a comma-separated list of brand ids or subdomains, or \"all\".").map((entry) => entry.toLowerCase());
|
|
7995
7996
|
if (normalized.length > 1 && normalized.includes("all")) throw createStartupError("Invalid --brand-ids / ZENDESK_BRAND_IDS value: \"all\" cannot be combined with brand ids or subdomains.", STARTUP_DOCS.environment);
|
|
7996
7997
|
return [...new Set(normalized)];
|
|
7997
7998
|
};
|
|
7999
|
+
const ALL_NAMESPACES = "all";
|
|
8000
|
+
const expandAllNamespaces = (named, docs) => {
|
|
8001
|
+
const entries = [...new Set(named)];
|
|
8002
|
+
if (!entries.includes(ALL_NAMESPACES)) return entries;
|
|
8003
|
+
if (entries.length > 1) throw createStartupError("Invalid --namespace / MCP_NAMESPACES value: \"all\" cannot be combined with named namespaces.", docs);
|
|
8004
|
+
return [...Namespace.options];
|
|
8005
|
+
};
|
|
8006
|
+
/**
|
|
8007
|
+
* The namespaces the operator asked for, or undefined when neither
|
|
8008
|
+
* `--namespace` nor `MCP_NAMESPACES` is set. The flag wins and leaves the variable
|
|
8009
|
+
* unread. Entries stay strings: the `Namespace` enum in ConfigSchema rejects an
|
|
8010
|
+
* unknown one.
|
|
8011
|
+
*/
|
|
8012
|
+
const resolveNamespaces = (cliValues) => {
|
|
8013
|
+
if (cliValues !== void 0) return expandAllNamespaces(cliValues, STARTUP_DOCS.cliReference);
|
|
8014
|
+
const raw = requireNonEmptyEnv("MCP_NAMESPACES");
|
|
8015
|
+
if (raw === void 0) return void 0;
|
|
8016
|
+
const entries = splitStrictList(raw, "Invalid MCP_NAMESPACES value: empty entry. Expected a comma-separated list of namespaces, or \"all\".");
|
|
8017
|
+
return expandAllNamespaces(entries, STARTUP_DOCS.environment);
|
|
8018
|
+
};
|
|
7998
8019
|
const CLI_OPTIONS = {
|
|
7999
8020
|
mode: { type: "string" },
|
|
8000
8021
|
"brand-ids": { type: "string" },
|
|
@@ -8181,7 +8202,7 @@ const loadConfig = (argv = process.argv.slice(2)) => {
|
|
|
8181
8202
|
const subdomain = cli.subdomain ?? requireNonEmptyEnv("ZENDESK_SUBDOMAIN") ?? "";
|
|
8182
8203
|
const oauthClientId = requireNonEmptyEnv("ZENDESK_OAUTH_CLIENT_ID") ?? `${subdomain}_zendesk`;
|
|
8183
8204
|
const mode = cli.tools?.length ? "all" : cli.mode ?? "namespace";
|
|
8184
|
-
const namespaces = cli.namespaces ?? (cli.tools?.length ? [...Namespace.options] : void 0);
|
|
8205
|
+
const namespaces = resolveNamespaces(cli.namespaces) ?? (cli.tools?.length ? [...Namespace.options] : void 0);
|
|
8185
8206
|
const callbackPort = cli.callbackPort ?? portEnv("OAUTH_CALLBACK_PORT");
|
|
8186
8207
|
const brandIds = cli.brandIds ?? requireNonEmptyEnv("ZENDESK_BRAND_IDS");
|
|
8187
8208
|
const hcResourceScheme = cli.hcResourceScheme ?? requireNonEmptyEnv("HC_RESOURCE_SCHEME");
|
|
@@ -31377,6 +31398,7 @@ const formatAttachmentSuffix = (count) => count ? ` with ${count} attachment(s)`
|
|
|
31377
31398
|
//#endregion
|
|
31378
31399
|
//#region src/tools/requests.ts
|
|
31379
31400
|
const forbidden = (tool, endpoint, error, hint) => new Error(`${tool} reads the end-user Requests surface (${endpoint}), which Zendesk serves to a signed-in requester. The current token was refused (HTTP 403). Either the Help Center is closed to this account, or this token is not a Help-Center-enabled user.${hint ? ` ${hint}` : ""} For the agent-side equivalents use create_ticket, get_ticket, list_tickets or add_public_comment (namespace: tickets).`, { cause: error });
|
|
31401
|
+
const END_USER_COMMENT_FORMAT = "Sent as plain text: neither Markdown nor HTML is rendered for an end user (both show as literal text), so write it without formatting markup.";
|
|
31380
31402
|
const OTHER_USERS_REQUEST_HINT = "A request id that belongs to another user is refused the same way, so check the id is one of your own.";
|
|
31381
31403
|
/** Run `fetch`, rewriting a 403 into guidance naming the end-user surface. */
|
|
31382
31404
|
const withForbiddenGuidance = async (tool, endpoint, fetch, hint) => {
|
|
@@ -31670,7 +31692,7 @@ const createRequestTools = (env) => {
|
|
|
31670
31692
|
description: "Submit a new support request as the signed-in user — the MCP equivalent of the Help Center's \"Submit a request\" form. Returns the created request with its number, which the user can then follow with list_requests and get_request. The submission is checked before sending: an unknown form id, a missing field the form marks required, or a dropdown answer the form does not offer is refused here rather than sent, because Zendesk would answer 201 having silently substituted the default form, accepted an empty subject or dropped the unrecognised value. Call get_request_form first to learn which fields to gather. Priority and type cannot be set by an end user — Zendesk drops them — so triage is left to the agents. This posts as the authenticated user; an agent opening a ticket on someone else's behalf wants create_ticket instead.",
|
|
31671
31693
|
inputSchema: object({
|
|
31672
31694
|
subject: string().min(1).describe("One-line summary of the request, shown as the ticket title to the support agents who pick it up."),
|
|
31673
|
-
body: string().min(1).describe(
|
|
31695
|
+
body: string().min(1).describe(`The request itself: what happened, what was expected, and any steps to reproduce. Becomes the first public comment on the ticket. ${END_USER_COMMENT_FORMAT}`),
|
|
31674
31696
|
form_id: number$1().int().optional().describe("Form id chosen from list_request_forms, deciding which questions apply. Omit to use the account default form."),
|
|
31675
31697
|
custom_fields: array(object({
|
|
31676
31698
|
id: number$1().int().describe("Numeric id of a question on the chosen form, from get_request_form."),
|
|
@@ -31790,7 +31812,7 @@ const createRequestTools = (env) => {
|
|
|
31790
31812
|
description: "Reply on one of the signed-in user's own requests, optionally attaching files. Returns confirmation with the request's status after the reply, which matters because replying on a request that was already solved REOPENS it — Zendesk moves it back to open, and the user should know that is what their message did. The comment is always public: end users cannot post internal notes, and Zendesk silently ignores any attempt to mark one private. A closed request rejects new comments outright. Agents replying to a requester want add_public_comment, or add_private_note for an internal note.",
|
|
31791
31813
|
inputSchema: object({
|
|
31792
31814
|
request_id: number$1().int().describe("Number of the request to reply on, as shown by list_requests."),
|
|
31793
|
-
body: string().min(1).describe(
|
|
31815
|
+
body: string().min(1).describe(`The message to send. Visible to the support agents on the ticket and included in their email notification. ${END_USER_COMMENT_FORMAT}`),
|
|
31794
31816
|
attachments: attachmentsParam("Files to attach to this reply, such as a screenshot or a log, with their content base64-encoded.")
|
|
31795
31817
|
}),
|
|
31796
31818
|
annotations: {
|
|
@@ -32323,6 +32345,7 @@ const formatTicketMetrics = (id, ticket, metric) => {
|
|
|
32323
32345
|
};
|
|
32324
32346
|
//#endregion
|
|
32325
32347
|
//#region src/tools/tickets.ts
|
|
32348
|
+
const AGENT_COMMENT_FORMAT = "Markdown is rendered (**bold**, lists, links): wrap code, identifiers containing _ or * and <placeholders> in backticks so they show literally. HTML is not rendered (tags show as literal text), so do not send HTML.";
|
|
32326
32349
|
const MAX_ATTACHMENT_MB = Number.parseFloat((MAX_ATTACHMENT_BYTES / 1048576).toFixed(2));
|
|
32327
32350
|
const MAX_RESPONSE_MB = Number.parseFloat((MAX_RESPONSE_BYTES / 1048576).toFixed(2));
|
|
32328
32351
|
const blockCost = (block) => block.type === "image" ? Buffer.byteLength(JSON.stringify({
|
|
@@ -32910,7 +32933,7 @@ const createTicketTools = (env) => {
|
|
|
32910
32933
|
description: "Create a new Zendesk support ticket with subject, description, and optional priority/type/assignee/tags. The description becomes the first public comment of the ticket, and the new ticket id is returned. After creation, use update_ticket to change status or assignee, add_public_comment or add_private_note to reply, and manage_tags to adjust tags. Look up valid assignee_id / group_id and custom field ids via search_users or your Zendesk admin settings. Discover custom field ids and their accepted option values with list_ticket_fields.",
|
|
32911
32934
|
inputSchema: object({
|
|
32912
32935
|
subject: string().min(1).describe("Ticket subject — the short summary line shown in ticket lists and search results."),
|
|
32913
|
-
description: string().min(1).describe(
|
|
32936
|
+
description: string().min(1).describe(`Ticket description — the body of the request. It becomes the ticket's first public comment (visible to the requester). ${AGENT_COMMENT_FORMAT}`),
|
|
32914
32937
|
priority: _enum([
|
|
32915
32938
|
"urgent",
|
|
32916
32939
|
"high",
|
|
@@ -33027,7 +33050,7 @@ const createTicketTools = (env) => {
|
|
|
33027
33050
|
description: "Add an internal note (not visible to requester) to a ticket, optionally with file attachments (uploaded via the Zendesk Uploads API and carried on the note). The note is appended to the ticket thread; use add_public_comment instead when the reply should be visible to the requester.",
|
|
33028
33051
|
inputSchema: object({
|
|
33029
33052
|
ticket_id: number$1().int().describe("Ticket ID — the numeric id of the ticket to annotate. Obtain it from search_tickets or list_tickets."),
|
|
33030
|
-
body: string().min(1).describe(
|
|
33053
|
+
body: string().min(1).describe(`Note text (internal, agent-only); not shown to the requester. ${AGENT_COMMENT_FORMAT}`),
|
|
33031
33054
|
attachments: attachmentsParam("Files to attach to this note (base64-encoded content).")
|
|
33032
33055
|
}),
|
|
33033
33056
|
annotations: {
|
|
@@ -33059,7 +33082,7 @@ const createTicketTools = (env) => {
|
|
|
33059
33082
|
description: "Add a public comment (visible to requester) to a ticket, optionally with file attachments (uploaded via the Zendesk Uploads API and carried on the comment). The comment is appended to the ticket thread and emails the requester; use add_private_note instead for an internal, agent-only note.",
|
|
33060
33083
|
inputSchema: object({
|
|
33061
33084
|
ticket_id: number$1().int().describe("Ticket ID — the numeric id of the ticket to reply on. Obtain it from search_tickets or list_tickets."),
|
|
33062
|
-
body: string().min(1).describe(
|
|
33085
|
+
body: string().min(1).describe(`Comment text sent to the requester; visible in the ticket. ${AGENT_COMMENT_FORMAT}`),
|
|
33063
33086
|
attachments: attachmentsParam("Files to attach to this comment (base64-encoded content).")
|
|
33064
33087
|
}),
|
|
33065
33088
|
annotations: {
|
package/dist/sbom.cdx.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.6",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:b8422d8d-453c-5e52-8855-b283836a117a",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
7
|
"component": {
|
|
8
8
|
"type": "application",
|
|
9
|
-
"bom-ref": "pkg:npm/%40fruggr/zendesk-mcp-server@3.
|
|
9
|
+
"bom-ref": "pkg:npm/%40fruggr/zendesk-mcp-server@3.9.1",
|
|
10
10
|
"name": "@fruggr/zendesk-mcp-server",
|
|
11
|
-
"version": "3.
|
|
12
|
-
"purl": "pkg:npm/%40fruggr/zendesk-mcp-server@3.
|
|
11
|
+
"version": "3.9.1",
|
|
12
|
+
"purl": "pkg:npm/%40fruggr/zendesk-mcp-server@3.9.1"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"components": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fruggr/zendesk-mcp-server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.1",
|
|
4
4
|
"mcpName": "io.github.fruggr/zendesk-mcp-server",
|
|
5
5
|
"description": "Deep Zendesk MCP server for your AI assistant: search, draft, update and translate Help Center articles and manage Support tickets end to end — comments, triage and image attachments.",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"typecheck": "node scripts/typecheck.mjs",
|
|
19
19
|
"start": "node dist/index.js",
|
|
20
20
|
"dev": "tsx watch src/index.ts",
|
|
21
|
-
"dev:http": "tsx src/index.ts --transport http --host 127.0.0.1 --port 3000 --public-url http://localhost:3000 --mode all --namespace
|
|
21
|
+
"dev:http": "tsx src/index.ts --transport http --host 127.0.0.1 --port 3000 --public-url http://localhost:3000 --mode all --namespace all",
|
|
22
22
|
"prepare": "npm run build && (lefthook install || true)",
|
|
23
23
|
"prepublishOnly": "npm run build",
|
|
24
24
|
"check": "biome check --error-on-warnings .",
|