@fruggr/zendesk-mcp-server 2.23.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -11
- package/dist/http-BXXDmcaZ.js +1383 -0
- package/dist/index.js +121 -391
- package/package.json +28 -6
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { dirname, join } from "node:path";
|
|
|
9
9
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
10
|
import { parseArgs } from "node:util";
|
|
11
11
|
import * as z from "zod/v4";
|
|
12
|
-
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/
|
|
12
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server";
|
|
13
13
|
import * as cheerio from "cheerio";
|
|
14
14
|
import { toHtml } from "hast-util-to-html";
|
|
15
15
|
import rehypeParse from "rehype-parse";
|
|
@@ -21,8 +21,7 @@ import remarkParse from "remark-parse";
|
|
|
21
21
|
import remarkRehype from "remark-rehype";
|
|
22
22
|
import remarkStringify from "remark-stringify";
|
|
23
23
|
import { unified } from "unified";
|
|
24
|
-
import { StdioServerTransport } from "@modelcontextprotocol/
|
|
25
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
24
|
+
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
26
25
|
//#region src/utils/logger.ts
|
|
27
26
|
const SEVERITY = {
|
|
28
27
|
debug: 0,
|
|
@@ -118,25 +117,37 @@ const createLogger = (level) => {
|
|
|
118
117
|
};
|
|
119
118
|
};
|
|
120
119
|
//#endregion
|
|
120
|
+
//#region src/utils/env.ts
|
|
121
|
+
/**
|
|
122
|
+
* Reads `name` from the environment. `name` in the result is the variable the
|
|
123
|
+
* value came from, so an error about it names what the operator actually set.
|
|
124
|
+
* Every variable goes through here, so a future rename has a single place to
|
|
125
|
+
* map the old name (AGENTS.md, "Environment variables").
|
|
126
|
+
*/
|
|
127
|
+
const readEnv = (name) => ({
|
|
128
|
+
name,
|
|
129
|
+
value: process.env[name]
|
|
130
|
+
});
|
|
131
|
+
//#endregion
|
|
121
132
|
//#region src/constants.ts
|
|
122
133
|
const positiveIntEnv = (name, fallback) => {
|
|
123
|
-
const raw =
|
|
134
|
+
const raw = readEnv(name).value;
|
|
124
135
|
if (raw === void 0 || raw.trim() === "") return fallback;
|
|
125
136
|
const parsed = Number(raw);
|
|
126
137
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
127
138
|
};
|
|
128
|
-
const CHARACTER_LIMIT = positiveIntEnv("
|
|
129
|
-
const ARTICLE_RESOURCES_SCAN_MAX_PAGES = positiveIntEnv("
|
|
130
|
-
const MAX_ATTACHMENT_BYTES = positiveIntEnv("
|
|
131
|
-
const MAX_EMBEDDED_IMAGE_COUNT = positiveIntEnv("
|
|
139
|
+
const CHARACTER_LIMIT = positiveIntEnv("RESPONSE_CHARACTER_LIMIT", 25e3);
|
|
140
|
+
const ARTICLE_RESOURCES_SCAN_MAX_PAGES = positiveIntEnv("ARTICLE_RESOURCES_SCAN_MAX_PAGES", 20);
|
|
141
|
+
const MAX_ATTACHMENT_BYTES = positiveIntEnv("ATTACHMENT_MAX_BYTES", 5242880);
|
|
142
|
+
const MAX_EMBEDDED_IMAGE_COUNT = positiveIntEnv("EMBEDDED_IMAGES_MAX", 10);
|
|
132
143
|
const STDIO_MAX_MESSAGE_BYTES = 10485760;
|
|
133
144
|
const MESSAGE_CONTENT_BUDGET_BYTES = 10420224;
|
|
134
|
-
const MAX_RESPONSE_BYTES = Math.min(positiveIntEnv("
|
|
145
|
+
const MAX_RESPONSE_BYTES = Math.min(positiveIntEnv("RESPONSE_MAX_BYTES", MESSAGE_CONTENT_BUDGET_BYTES), MESSAGE_CONTENT_BUDGET_BYTES);
|
|
135
146
|
const MAX_BASE64_INPUT_CHARS = MESSAGE_CONTENT_BUDGET_BYTES;
|
|
136
147
|
const MAX_BASE64_INPUT_MB = Number.parseFloat((MAX_BASE64_INPUT_CHARS / 4 * 3 / 1048576).toFixed(2));
|
|
137
|
-
const MAX_COMMENT_PAGES = positiveIntEnv("
|
|
138
|
-
const TICKET_FIELD_SCAN_MAX_PAGES = positiveIntEnv("
|
|
139
|
-
const REORDER_CONFIRM_THRESHOLD = positiveIntEnv("
|
|
148
|
+
const MAX_COMMENT_PAGES = positiveIntEnv("COMMENT_MAX_PAGES", 10);
|
|
149
|
+
const TICKET_FIELD_SCAN_MAX_PAGES = positiveIntEnv("TICKET_FIELD_SCAN_MAX_PAGES", 10);
|
|
150
|
+
const REORDER_CONFIRM_THRESHOLD = positiveIntEnv("REORDER_CONFIRM_THRESHOLD", 20);
|
|
140
151
|
const getBaseUrl = (subdomain) => `https://${subdomain}.zendesk.com/api/v2`;
|
|
141
152
|
const getHelpCenterBaseUrl = (subdomain) => `https://${subdomain}.zendesk.com/api/v2/help_center`;
|
|
142
153
|
const getOAuthUrls = (subdomain) => ({
|
|
@@ -144,6 +155,15 @@ const getOAuthUrls = (subdomain) => ({
|
|
|
144
155
|
tokenUrl: `https://${subdomain}.zendesk.com/oauth/tokens`
|
|
145
156
|
});
|
|
146
157
|
//#endregion
|
|
158
|
+
//#region src/utils/html.ts
|
|
159
|
+
/**
|
|
160
|
+
* Escape a string for safe interpolation into HTML text/attribute context.
|
|
161
|
+
* Both the stdio callback page and the HTTP consent page echo
|
|
162
|
+
* attacker-controllable values (an OAuth `error_description`, a client name
|
|
163
|
+
* from a fetched metadata document); without escaping these are an XSS sink.
|
|
164
|
+
*/
|
|
165
|
+
const escapeHtml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
166
|
+
//#endregion
|
|
147
167
|
//#region src/auth/oauth-scopes.ts
|
|
148
168
|
/**
|
|
149
169
|
* The OAuth scope the server asks Zendesk for, and the one predicate that
|
|
@@ -178,7 +198,7 @@ const supportedScopes = (readOnly) => scopeTokens(requestedScope(readOnly));
|
|
|
178
198
|
* Whether a grant still covers what this process needs: a flat subset test.
|
|
179
199
|
* Coverage, not equality, so a token Zendesk granted wider than requested stays
|
|
180
200
|
* usable. A non-string `granted` is a pre-#283 record, i.e. `read write` — only
|
|
181
|
-
* reachable now through a `
|
|
201
|
+
* reachable now through a `OAUTH_TOKEN_FILE` aimed at one by hand, since the
|
|
182
202
|
* default layout no longer names those files. No scope hierarchy: granular
|
|
183
203
|
* scopes (#284) replace this.
|
|
184
204
|
*/
|
|
@@ -207,17 +227,10 @@ const detectWsl = () => {
|
|
|
207
227
|
* (`docs/decisions/token-file-keying.md`). The `(EADDRINUSE)` marker and `code`
|
|
208
228
|
* are kept for diagnostics/tests.
|
|
209
229
|
*/
|
|
210
|
-
const callbackPortInUseError = (port, cause) => Object.assign(/* @__PURE__ */ new Error(`Cannot start the Zendesk OAuth sign-in: local callback port ${port} is already in use. Another instance of this server is most likely signing in right now: finish that browser window, then retry. If an unrelated program holds the port, set
|
|
230
|
+
const callbackPortInUseError = (port, cause) => Object.assign(/* @__PURE__ */ new Error(`Cannot start the Zendesk OAuth sign-in: local callback port ${port} is already in use. Another instance of this server is most likely signing in right now: finish that browser window, then retry. If an unrelated program holds the port, set OAUTH_CALLBACK_PORT (or --callback-port) to a free port, then register http://localhost:<port>/callback as a redirect URL in your Zendesk OAuth client. (EADDRINUSE)`), {
|
|
211
231
|
code: "EADDRINUSE",
|
|
212
232
|
cause
|
|
213
233
|
});
|
|
214
|
-
/**
|
|
215
|
-
* Escape a string for safe interpolation into HTML text/attribute context.
|
|
216
|
-
* The local callback server echoes attacker-controllable values (the OAuth
|
|
217
|
-
* `error_description` query param, token-exchange error bodies) back into the
|
|
218
|
-
* browser response; without escaping these are a reflected-XSS sink.
|
|
219
|
-
*/
|
|
220
|
-
const escapeHtml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
221
234
|
const errorPage = (title, detail) => `<html><body><h1>${escapeHtml(title)}</h1>${detail === void 0 ? "" : `<p>${escapeHtml(detail)}</p>`}</body></html>`;
|
|
222
235
|
const SUCCESS_PAGE = "<html><body><h1>Authentication successful!</h1><p>You can close this tab and return to your AI assistant.</p><p>This tab will auto-close in <span id=\"t\">10</span>s.</p><script>let n=10;const el=document.getElementById(\"t\");const i=setInterval(()=>{n--;el.textContent=n;if(n<=0){clearInterval(i);window.close();}},1000);<\/script></body></html>";
|
|
223
236
|
const generateCodeVerifier = () => randomBytes(32).toString("base64url");
|
|
@@ -500,11 +513,12 @@ const keyDigest = (key) => createHash("sha256").update(JSON.stringify([
|
|
|
500
513
|
* their own record instead of clobbering a shared file. Why that triple, and
|
|
501
514
|
* why no migration from the old subdomain-only layout:
|
|
502
515
|
* `docs/decisions/token-file-keying.md`.
|
|
503
|
-
* `
|
|
516
|
+
* `OAUTH_TOKEN_FILE` overrides with an explicit path — the way to separate
|
|
504
517
|
* two Zendesk accounts that share a subdomain, client and scope.
|
|
505
518
|
*/
|
|
506
519
|
const resolveTokenPath = (key) => {
|
|
507
|
-
const override =
|
|
520
|
+
const { name, value: override } = readEnv("OAUTH_TOKEN_FILE");
|
|
521
|
+
if (override === "") throw new Error(`Empty ${name}. Set it to a value, or unset it entirely.`);
|
|
508
522
|
if (override) return override;
|
|
509
523
|
const readable = [
|
|
510
524
|
key.subdomain,
|
|
@@ -821,18 +835,37 @@ const ConfigSchema = z.object({
|
|
|
821
835
|
* because they send no Origin header.
|
|
822
836
|
*/
|
|
823
837
|
corsOrigins: z.array(z.string().url().transform((value) => new URL(value).origin).refine((origin) => origin !== "null", { message: "CORS origin must be an http(s) URL with a host" })).default([]),
|
|
824
|
-
callbackPort: z.number().int().min(1).max(65535).optional()
|
|
838
|
+
callbackPort: z.number().int().min(1).max(65535).optional(),
|
|
839
|
+
/**
|
|
840
|
+
* HTTP only: the authorization server's master secret (`OAUTH_MASTER_SECRET`),
|
|
841
|
+
* or a file holding it. Both optional: when absent, one is generated and
|
|
842
|
+
* persisted in the config dir. Belongs to this server, not to Zendesk, hence
|
|
843
|
+
* no `ZENDESK_` prefix. Validated (length, base64) where it is used, so a
|
|
844
|
+
* value never reaches a schema error message.
|
|
845
|
+
*/
|
|
846
|
+
oauthMasterSecret: z.string().optional(),
|
|
847
|
+
oauthMasterSecretFile: z.string().min(1).optional(),
|
|
848
|
+
/** HTTP only: the grant store URI; defaults to a file in the config dir. */
|
|
849
|
+
oauthStore: z.string().url().optional(),
|
|
850
|
+
/** HTTP only: CIMD client ids added to the consent-skip allowlist. */
|
|
851
|
+
oauthTrustedClients: z.array(z.string().url()).default([]),
|
|
852
|
+
/** HTTP only: whether the built-in allowlist (claude.ai, ChatGPT) applies. */
|
|
853
|
+
defaultTrustedClients: z.boolean().default(true)
|
|
825
854
|
});
|
|
826
855
|
const DIGITS_ONLY = /^\d+$/;
|
|
827
856
|
const parsePort = (raw, label) => {
|
|
828
857
|
if (!DIGITS_ONLY.test(raw)) throw new Error(`Invalid ${label} value. Expected an integer 0-65535.`);
|
|
829
858
|
return Number(raw);
|
|
830
859
|
};
|
|
831
|
-
const
|
|
832
|
-
const
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
860
|
+
const readNonEmptyEnv = (name) => {
|
|
861
|
+
const env = readEnv(name);
|
|
862
|
+
if (env.value === "") throw new Error(`Empty ${env.name}. Set it to a value, or unset it entirely.`);
|
|
863
|
+
return env;
|
|
864
|
+
};
|
|
865
|
+
const requireNonEmptyEnv = (name) => readNonEmptyEnv(name).value;
|
|
866
|
+
const portEnv = (name) => {
|
|
867
|
+
const env = readNonEmptyEnv(name);
|
|
868
|
+
return env.value === void 0 ? void 0 : parsePort(env.value, env.name);
|
|
836
869
|
};
|
|
837
870
|
const CLI_OPTIONS = {
|
|
838
871
|
mode: { type: "string" },
|
|
@@ -855,6 +888,13 @@ const CLI_OPTIONS = {
|
|
|
855
888
|
multiple: true
|
|
856
889
|
},
|
|
857
890
|
"callback-port": { type: "string" },
|
|
891
|
+
"oauth-master-secret-file": { type: "string" },
|
|
892
|
+
"oauth-store": { type: "string" },
|
|
893
|
+
"oauth-trusted-client": {
|
|
894
|
+
type: "string",
|
|
895
|
+
multiple: true
|
|
896
|
+
},
|
|
897
|
+
"no-default-trusted-clients": { type: "boolean" },
|
|
858
898
|
"read-only": { type: "boolean" },
|
|
859
899
|
"no-topology": { type: "boolean" },
|
|
860
900
|
"no-promoted-articles": { type: "boolean" },
|
|
@@ -871,14 +911,18 @@ const FIELD_BY_FLAG = /* @__PURE__ */ new Map([
|
|
|
871
911
|
["transport", "transport"],
|
|
872
912
|
["host", "host"],
|
|
873
913
|
["public-url", "publicUrl"],
|
|
874
|
-
["cors-origin", "corsOrigins"]
|
|
914
|
+
["cors-origin", "corsOrigins"],
|
|
915
|
+
["oauth-master-secret-file", "oauthMasterSecretFile"],
|
|
916
|
+
["oauth-store", "oauthStore"],
|
|
917
|
+
["oauth-trusted-client", "oauthTrustedClients"]
|
|
875
918
|
]);
|
|
876
919
|
const STANDALONE_EFFECTS = /* @__PURE__ */ new Map([
|
|
877
920
|
["read-only", { readOnly: true }],
|
|
878
921
|
["no-topology", { topology: false }],
|
|
879
922
|
["no-promoted-articles", { promotedArticles: false }],
|
|
880
923
|
["dev", { dev: true }],
|
|
881
|
-
["print-tools", { printTools: true }]
|
|
924
|
+
["print-tools", { printTools: true }],
|
|
925
|
+
["no-default-trusted-clients", { defaultTrustedClients: false }]
|
|
882
926
|
]);
|
|
883
927
|
const parseCliArgs = (args) => {
|
|
884
928
|
const { values, positionals } = parseArgs({
|
|
@@ -902,23 +946,31 @@ const parseCliArgs = (args) => {
|
|
|
902
946
|
if (values["callback-port"] !== void 0) result.callbackPort = parsePort(values["callback-port"], "--callback-port");
|
|
903
947
|
return result;
|
|
904
948
|
};
|
|
949
|
+
const splitList = (raw) => (raw ?? "").split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
905
950
|
const resolveTransportSettings = (cli) => {
|
|
906
|
-
const corsFromEnv = (process.env["CORS_ORIGIN"]
|
|
951
|
+
const corsFromEnv = splitList(process.env["CORS_ORIGIN"]);
|
|
907
952
|
return {
|
|
908
953
|
transport: cli.transport ?? requireNonEmptyEnv("TRANSPORT") ?? "stdio",
|
|
909
|
-
host: cli.host ?? requireNonEmptyEnv("
|
|
910
|
-
port: cli.port ??
|
|
954
|
+
host: cli.host ?? requireNonEmptyEnv("LISTEN_HOST") ?? "0.0.0.0",
|
|
955
|
+
port: cli.port ?? portEnv("PORT") ?? 3e3,
|
|
911
956
|
publicUrl: cli.publicUrl ?? requireNonEmptyEnv("PUBLIC_URL"),
|
|
912
957
|
corsOrigins: [...cli.corsOrigins ?? [], ...corsFromEnv]
|
|
913
958
|
};
|
|
914
959
|
};
|
|
960
|
+
const resolveOAuthServerSettings = (cli) => ({
|
|
961
|
+
oauthMasterSecret: requireNonEmptyEnv("OAUTH_MASTER_SECRET"),
|
|
962
|
+
oauthMasterSecretFile: cli.oauthMasterSecretFile ?? requireNonEmptyEnv("OAUTH_MASTER_SECRET_FILE"),
|
|
963
|
+
oauthStore: cli.oauthStore ?? requireNonEmptyEnv("OAUTH_STORE"),
|
|
964
|
+
oauthTrustedClients: [...cli.oauthTrustedClients ?? [], ...splitList(readEnv("OAUTH_TRUSTED_CLIENTS").value)],
|
|
965
|
+
defaultTrustedClients: cli.defaultTrustedClients
|
|
966
|
+
});
|
|
915
967
|
const loadConfig = (argv = process.argv.slice(2)) => {
|
|
916
968
|
const cli = parseCliArgs(argv);
|
|
917
969
|
const subdomain = cli.subdomain ?? requireNonEmptyEnv("ZENDESK_SUBDOMAIN") ?? "";
|
|
918
970
|
const oauthClientId = requireNonEmptyEnv("ZENDESK_OAUTH_CLIENT_ID") ?? `${subdomain}_zendesk`;
|
|
919
971
|
const mode = cli.tools?.length ? "all" : cli.mode ?? "namespace";
|
|
920
972
|
const namespaces = cli.namespaces ?? (cli.tools?.length ? [...Namespace.options] : void 0);
|
|
921
|
-
const callbackPort = cli.callbackPort ??
|
|
973
|
+
const callbackPort = cli.callbackPort ?? portEnv("OAUTH_CALLBACK_PORT");
|
|
922
974
|
const hcResourceScheme = cli.hcResourceScheme ?? requireNonEmptyEnv("HC_RESOURCE_SCHEME");
|
|
923
975
|
return ConfigSchema.parse({
|
|
924
976
|
subdomain,
|
|
@@ -934,7 +986,8 @@ const loadConfig = (argv = process.argv.slice(2)) => {
|
|
|
934
986
|
dev: cli.dev ?? false,
|
|
935
987
|
printTools: cli.printTools ?? false,
|
|
936
988
|
callbackPort,
|
|
937
|
-
...resolveTransportSettings(cli)
|
|
989
|
+
...resolveTransportSettings(cli),
|
|
990
|
+
...resolveOAuthServerSettings(cli)
|
|
938
991
|
});
|
|
939
992
|
};
|
|
940
993
|
//#endregion
|
|
@@ -2297,7 +2350,7 @@ const localePrefix = (locale) => locale ? `/${locale}` : "";
|
|
|
2297
2350
|
const articleListPath = (sectionId, locale) => `${localePrefix(locale)}${sectionId ? `/sections/${sectionId}` : ""}/articles`;
|
|
2298
2351
|
const sectionListPath = (categoryId, locale) => `${localePrefix(locale)}${categoryId ? `/categories/${categoryId}` : ""}/sections`;
|
|
2299
2352
|
const scanCostNote = (truncated, pagesScanned, cost) => {
|
|
2300
|
-
if (truncated) return `\n\n_Note: the scan hit its ${ARTICLE_RESOURCES_SCAN_MAX_PAGES}-page cap (${cost}), so promoted articles deeper in the catalog may be missing. This call is costly on this Help Center — avoid repeating it; raise
|
|
2353
|
+
if (truncated) return `\n\n_Note: the scan hit its ${ARTICLE_RESOURCES_SCAN_MAX_PAGES}-page cap (${cost}), so promoted articles deeper in the catalog may be missing. This call is costly on this Help Center — avoid repeating it; raise ARTICLE_RESOURCES_SCAN_MAX_PAGES to widen coverage._`;
|
|
2301
2354
|
if (pagesScanned > 1) return `\n\n_Note: this scan cost ${cost}; this tool performs a fresh scan every call (no caching), so avoid calling it again right away._`;
|
|
2302
2355
|
return "";
|
|
2303
2356
|
};
|
|
@@ -2522,7 +2575,7 @@ const createHelpCenterTools = (ctx) => {
|
|
|
2522
2575
|
namespace: "help_center",
|
|
2523
2576
|
readOnly: true,
|
|
2524
2577
|
title: "List Promoted Help Center Articles",
|
|
2525
|
-
description: "List the promoted (\"featured\") Help Center articles — the small, editorially-curated set surfaced at the top of their sections. Returns metadata only (no body); use get_article for full content. COST: the Help Center API has no server-side promoted filter, so this scans article pages (one Zendesk API request per page, up to
|
|
2578
|
+
description: "List the promoted (\"featured\") Help Center articles — the small, editorially-curated set surfaced at the top of their sections. Returns metadata only (no body); use get_article for full content. COST: the Help Center API has no server-side promoted filter, so this scans article pages (one Zendesk API request per page, up to ARTICLE_RESOURCES_SCAN_MAX_PAGES, default 20) and filters client-side — potentially costly on a large Help Center. Each call performs a fresh, uncached scan, so avoid calling it repeatedly. On a very large Help Center some promoted articles may be omitted, and both the omission and the number of pages scanned are flagged in the output. Lists the default locale. To promote or unpromote an article, use update_article with `promoted` (requires Help Center admin / Guide admin rights).",
|
|
2526
2579
|
inputSchema: z.object({}),
|
|
2527
2580
|
annotations: {
|
|
2528
2581
|
readOnlyHint: true,
|
|
@@ -2900,7 +2953,7 @@ const createHelpCenterTools = (ctx) => {
|
|
|
2900
2953
|
]).describe("Where to move the article relative to its section siblings: \"top\" (becomes first), \"bottom\" (becomes last), or \"before\"/\"after\" a specific reference article. \"before\" and \"after\" require reference_article_id."),
|
|
2901
2954
|
reference_article_id: z.number().int().optional().describe("The sibling article to position next to when target is \"before\" or \"after\" (numeric id from list_articles). Must belong to the same section and differ from article_id; leave it unset for \"top\" or \"bottom\"."),
|
|
2902
2955
|
normalize: z.boolean().default(false).describe("When true, also renumber every article in the section to contiguous positions (0, 1, 2, …) so the stored positions stay tidy. Defaults to false, which writes the fewest positions possible and lets gaps remain. Either way the confirmation threshold still applies."),
|
|
2903
|
-
confirm: z.boolean().default(false).describe("Safety guard for large reorders. When the move would rewrite more article positions than the configured threshold (
|
|
2956
|
+
confirm: z.boolean().default(false).describe("Safety guard for large reorders. When the move would rewrite more article positions than the configured threshold (REORDER_CONFIRM_THRESHOLD, default 20), the tool refuses and reports the count until you pass true here. Has no effect on small reorders.")
|
|
2904
2957
|
}),
|
|
2905
2958
|
annotations: {
|
|
2906
2959
|
readOnlyHint: false,
|
|
@@ -3383,7 +3436,7 @@ const END_USER_FORM_PARAMS = {
|
|
|
3383
3436
|
* form invisible or a required field unenforced, and the caller then gets a
|
|
3384
3437
|
* confidently wrong "no such form" or an opaque Zendesk 422.
|
|
3385
3438
|
*/
|
|
3386
|
-
const fetchAllPages = async ({ subdomain, token, tool, path, extract, params = {}, maxPages = TICKET_FIELD_SCAN_MAX_PAGES, capEnvVar = "
|
|
3439
|
+
const fetchAllPages = async ({ subdomain, token, tool, path, extract, params = {}, maxPages = TICKET_FIELD_SCAN_MAX_PAGES, capEnvVar = "TICKET_FIELD_SCAN_MAX_PAGES", onPage, forbiddenHint }) => {
|
|
3387
3440
|
const items = [];
|
|
3388
3441
|
let page = 1;
|
|
3389
3442
|
while (true) {
|
|
@@ -3440,7 +3493,7 @@ const fetchAllRequestComments = async (subdomain, token, requestId) => {
|
|
|
3440
3493
|
extract: (response) => response.comments,
|
|
3441
3494
|
params: { include: "users" },
|
|
3442
3495
|
maxPages: MAX_COMMENT_PAGES,
|
|
3443
|
-
capEnvVar: "
|
|
3496
|
+
capEnvVar: "COMMENT_MAX_PAGES",
|
|
3444
3497
|
forbiddenHint: OTHER_USERS_REQUEST_HINT,
|
|
3445
3498
|
onPage: (response) => {
|
|
3446
3499
|
for (const user of response.users ?? []) authors.set(user.id, user);
|
|
@@ -5116,10 +5169,10 @@ const registerProxyTool = (server, toolName, title, tools, readOnlyMode, onUnaut
|
|
|
5116
5169
|
return server.registerTool(toolName, {
|
|
5117
5170
|
title,
|
|
5118
5171
|
description: `${prefix}${title}. Specify the operation and its parameters.\n\nAvailable operations:\n${operationList}`,
|
|
5119
|
-
inputSchema: {
|
|
5172
|
+
inputSchema: z.object({
|
|
5120
5173
|
operation: z.string().describe(`One of: ${operationNames.join(", ")}`),
|
|
5121
5174
|
params: z.record(z.string(), z.unknown()).default({}).describe("Operation parameters")
|
|
5122
|
-
},
|
|
5175
|
+
}),
|
|
5123
5176
|
annotations
|
|
5124
5177
|
}, async (args) => dispatch(args));
|
|
5125
5178
|
};
|
|
@@ -5483,358 +5536,34 @@ const renderToolSurface = (config, tools) => {
|
|
|
5483
5536
|
].join("\n");
|
|
5484
5537
|
};
|
|
5485
5538
|
//#endregion
|
|
5486
|
-
//#region src/transports/http.ts
|
|
5487
|
-
const WILDCARD_HOSTS = /* @__PURE__ */ new Set([
|
|
5488
|
-
"0.0.0.0",
|
|
5489
|
-
"::",
|
|
5490
|
-
"*"
|
|
5491
|
-
]);
|
|
5492
|
-
const TRAILING_SLASHES = /\/+$/;
|
|
5493
|
-
const DEFAULT_BROWSER_MCP_CLIENT_ORIGINS = [
|
|
5494
|
-
"https://chatgpt.com",
|
|
5495
|
-
"https://chat.openai.com",
|
|
5496
|
-
"https://claude.ai",
|
|
5497
|
-
"https://gemini.google.com",
|
|
5498
|
-
"https://copilot.microsoft.com",
|
|
5499
|
-
"https://www.perplexity.ai",
|
|
5500
|
-
"https://chat.mistral.ai",
|
|
5501
|
-
"https://grok.com"
|
|
5502
|
-
];
|
|
5503
|
-
const CORS_ALLOWED_METHODS = "GET, POST, DELETE, OPTIONS";
|
|
5504
|
-
const CORS_ALLOWED_HEADERS = "Authorization, Content-Type, Accept, mcp-session-id, mcp-protocol-version, last-event-id";
|
|
5505
|
-
const CORS_EXPOSE_HEADERS = "mcp-session-id";
|
|
5506
|
-
const CORS_MAX_AGE = "600";
|
|
5507
|
-
const LOCALHOST_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
5508
|
-
"localhost",
|
|
5509
|
-
"127.0.0.1",
|
|
5510
|
-
"[::1]"
|
|
5511
|
-
]);
|
|
5512
|
-
const ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
5539
|
+
//#region src/transports/http-peers.ts
|
|
5513
5540
|
/**
|
|
5514
|
-
*
|
|
5515
|
-
*
|
|
5516
|
-
*
|
|
5517
|
-
*
|
|
5518
|
-
* from one of three sanitization points:
|
|
5519
|
-
*
|
|
5520
|
-
* 1. An entry of the hardcoded `DEFAULT_BROWSER_MCP_CLIENT_ORIGINS` array.
|
|
5521
|
-
* 2. An entry of the operator-configured `extraOrigins` array.
|
|
5522
|
-
* 3. A loopback origin rebuilt from validated URL components after the
|
|
5523
|
-
* hostname has been allowlisted against `LOCALHOST_HOSTNAMES`.
|
|
5524
|
-
*
|
|
5525
|
-
* This shape keeps the dataflow from request header to response header
|
|
5526
|
-
* gated by a constant allowlist, which is the pattern CodeQL's
|
|
5527
|
-
* `js/cors-misconfiguration-for-credentials` rule recognises as safe when
|
|
5528
|
-
* combined with `Access-Control-Allow-Credentials: true`.
|
|
5541
|
+
* Packages only the HTTP transport uses, shipped as optional peer dependencies so
|
|
5542
|
+
* a stdio install (npx, bunx) never downloads them. Inlined from `package.json` at
|
|
5543
|
+
* build time, so a version bump there is the only edit. Rationale:
|
|
5544
|
+
* docs/decisions/oauth-authorization-server.md (Packaging).
|
|
5529
5545
|
*/
|
|
5530
|
-
const
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5546
|
+
const HTTP_PEERS = {
|
|
5547
|
+
"@modelcontextprotocol/node": "^2.0.0",
|
|
5548
|
+
"jose": "^6.2.12",
|
|
5549
|
+
"oidc-provider": "~9.12.2"
|
|
5550
|
+
};
|
|
5551
|
+
const installCommand = () => ["npm install @fruggr/zendesk-mcp-server", ...Object.entries(HTTP_PEERS).map(([name, range]) => `${name}@${range}`)].join(" ");
|
|
5552
|
+
const missingPeers = (err) => {
|
|
5553
|
+
if (err?.code !== "ERR_MODULE_NOT_FOUND") return [];
|
|
5554
|
+
const message = String(err.message);
|
|
5555
|
+
return Object.keys(HTTP_PEERS).filter((name) => message.includes(`'${name}'`));
|
|
5556
|
+
};
|
|
5557
|
+
/** Import the HTTP transport, turning a missing optional peer into an actionable error. */
|
|
5558
|
+
const loadHttpTransport = async (importer = () => import("./http-BXXDmcaZ.js")) => {
|
|
5535
5559
|
try {
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
} catch {
|
|
5542
|
-
return;
|
|
5543
|
-
}
|
|
5544
|
-
};
|
|
5545
|
-
const applyCorsHeaders = (req, res, extraOrigins) => {
|
|
5546
|
-
const requestOrigin = req.headers["origin"];
|
|
5547
|
-
if (typeof requestOrigin !== "string" || requestOrigin.length === 0) return;
|
|
5548
|
-
const allowedOrigin = resolveAllowedOrigin(requestOrigin, extraOrigins);
|
|
5549
|
-
if (!allowedOrigin) return;
|
|
5550
|
-
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
5551
|
-
res.setHeader("Vary", "Origin");
|
|
5552
|
-
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
5553
|
-
res.setHeader("Access-Control-Expose-Headers", CORS_EXPOSE_HEADERS);
|
|
5554
|
-
};
|
|
5555
|
-
const handleCorsPreflight = (req, res, extraOrigins) => {
|
|
5556
|
-
if (req.method !== "OPTIONS") return false;
|
|
5557
|
-
applyCorsHeaders(req, res, extraOrigins);
|
|
5558
|
-
if (res.getHeader("Access-Control-Allow-Origin")) {
|
|
5559
|
-
res.setHeader("Access-Control-Allow-Methods", CORS_ALLOWED_METHODS);
|
|
5560
|
-
res.setHeader("Access-Control-Allow-Headers", CORS_ALLOWED_HEADERS);
|
|
5561
|
-
res.setHeader("Access-Control-Max-Age", CORS_MAX_AGE);
|
|
5562
|
-
}
|
|
5563
|
-
res.writeHead(204);
|
|
5564
|
-
res.end();
|
|
5565
|
-
return true;
|
|
5566
|
-
};
|
|
5567
|
-
const resolveResourceUrl = (config, logger = silentLogger) => {
|
|
5568
|
-
if (config.publicUrl) return config.publicUrl.replace(TRAILING_SLASHES, "");
|
|
5569
|
-
if (!WILDCARD_HOSTS.has(config.host)) return `http://${config.host}:${config.port}`;
|
|
5570
|
-
logger.warn("public_url_unset", {
|
|
5571
|
-
host: config.host,
|
|
5572
|
-
advertised: `http://${config.host}:${config.port}`,
|
|
5573
|
-
hint: "OAuth discovery will advertise a non-routable resource identifier and spec-compliant MCP clients may refuse the connection. Set PUBLIC_URL (or --public-url) to the URL clients use to reach this server (e.g. https://your-host.example.com)."
|
|
5574
|
-
});
|
|
5575
|
-
return `http://${config.host}:${config.port}`;
|
|
5576
|
-
};
|
|
5577
|
-
const MISSING_BEARER_MESSAGE = "Missing Authorization: Bearer <zendesk-oauth-token> header. HTTP mode requires per-user OAuth 2.1 PKCE - obtain a token from Zendesk via your MCP client.";
|
|
5578
|
-
const extractBearer = (request) => {
|
|
5579
|
-
const header = request.headers["authorization"];
|
|
5580
|
-
if (typeof header !== "string") return void 0;
|
|
5581
|
-
if (!header.toLowerCase().startsWith("bearer ")) return void 0;
|
|
5582
|
-
return header.slice(7).trim();
|
|
5583
|
-
};
|
|
5584
|
-
const buildOAuthMetadata = (config, logger = silentLogger) => {
|
|
5585
|
-
const { authorizeUrl, tokenUrl } = getOAuthUrls(config.subdomain);
|
|
5586
|
-
const issuer = `https://${config.subdomain}.zendesk.com`;
|
|
5587
|
-
const resource = resolveResourceUrl(config, logger);
|
|
5588
|
-
const scopes = () => supportedScopes(config.readOnly);
|
|
5589
|
-
return {
|
|
5590
|
-
protectedResource: {
|
|
5591
|
-
authorization_servers: [issuer],
|
|
5592
|
-
resource,
|
|
5593
|
-
bearer_methods_supported: ["header"],
|
|
5594
|
-
scopes_supported: scopes()
|
|
5595
|
-
},
|
|
5596
|
-
authorizationServer: {
|
|
5597
|
-
issuer,
|
|
5598
|
-
authorization_endpoint: authorizeUrl,
|
|
5599
|
-
token_endpoint: tokenUrl,
|
|
5600
|
-
response_types_supported: ["code"],
|
|
5601
|
-
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
5602
|
-
code_challenge_methods_supported: ["S256"],
|
|
5603
|
-
token_endpoint_auth_methods_supported: ["none"],
|
|
5604
|
-
scopes_supported: scopes()
|
|
5605
|
-
}
|
|
5606
|
-
};
|
|
5607
|
-
};
|
|
5608
|
-
const sendJson = (res, status, body) => {
|
|
5609
|
-
res.writeHead(status, { "Content-Type": "application/json" });
|
|
5610
|
-
res.end(JSON.stringify(body));
|
|
5611
|
-
};
|
|
5612
|
-
const sendJsonRpcError = (res, status, code, message, headers = {}) => {
|
|
5613
|
-
res.writeHead(status, {
|
|
5614
|
-
"Content-Type": "application/json",
|
|
5615
|
-
...headers
|
|
5616
|
-
});
|
|
5617
|
-
res.end(JSON.stringify({
|
|
5618
|
-
error: {
|
|
5619
|
-
code,
|
|
5620
|
-
message
|
|
5621
|
-
},
|
|
5622
|
-
id: null,
|
|
5623
|
-
jsonrpc: "2.0"
|
|
5624
|
-
}));
|
|
5625
|
-
};
|
|
5626
|
-
const failRequest = (res, err) => {
|
|
5627
|
-
const message = err instanceof Error ? err.message : "Internal Server Error";
|
|
5628
|
-
if (!res.headersSent) {
|
|
5629
|
-
sendJsonRpcError(res, 500, -32603, message);
|
|
5630
|
-
return;
|
|
5631
|
-
}
|
|
5632
|
-
if (!res.writableEnded) res.end();
|
|
5633
|
-
};
|
|
5634
|
-
const sendUnauthorized = (res, resource) => {
|
|
5635
|
-
const wwwAuthenticate = `Bearer resource_metadata="${resource}/.well-known/oauth-protected-resource", error="invalid_token", error_description="${MISSING_BEARER_MESSAGE}"`;
|
|
5636
|
-
sendJsonRpcError(res, 401, -32e3, MISSING_BEARER_MESSAGE, { "WWW-Authenticate": wwwAuthenticate });
|
|
5637
|
-
};
|
|
5638
|
-
const readJsonBody = (req, maxBodyBytes) => new Promise((resolve) => {
|
|
5639
|
-
const chunks = [];
|
|
5640
|
-
let total = 0;
|
|
5641
|
-
let settled = false;
|
|
5642
|
-
const settle = (result) => {
|
|
5643
|
-
if (settled) return;
|
|
5644
|
-
settled = true;
|
|
5645
|
-
resolve(result);
|
|
5646
|
-
};
|
|
5647
|
-
req.on("data", (chunk) => {
|
|
5648
|
-
total += chunk.length;
|
|
5649
|
-
if (total > maxBodyBytes) {
|
|
5650
|
-
req.removeAllListeners("data");
|
|
5651
|
-
settle({
|
|
5652
|
-
ok: false,
|
|
5653
|
-
status: 413,
|
|
5654
|
-
rpcCode: -32600,
|
|
5655
|
-
message: `Request body exceeds ${maxBodyBytes} bytes.`
|
|
5656
|
-
});
|
|
5657
|
-
return;
|
|
5658
|
-
}
|
|
5659
|
-
chunks.push(chunk);
|
|
5660
|
-
});
|
|
5661
|
-
req.on("end", () => {
|
|
5662
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
5663
|
-
if (!raw) {
|
|
5664
|
-
settle({
|
|
5665
|
-
ok: true,
|
|
5666
|
-
value: void 0
|
|
5667
|
-
});
|
|
5668
|
-
return;
|
|
5669
|
-
}
|
|
5670
|
-
try {
|
|
5671
|
-
settle({
|
|
5672
|
-
ok: true,
|
|
5673
|
-
value: JSON.parse(raw)
|
|
5674
|
-
});
|
|
5675
|
-
} catch {
|
|
5676
|
-
settle({
|
|
5677
|
-
ok: false,
|
|
5678
|
-
status: 400,
|
|
5679
|
-
rpcCode: -32700,
|
|
5680
|
-
message: "Parse error: request body is not valid JSON."
|
|
5681
|
-
});
|
|
5682
|
-
}
|
|
5683
|
-
});
|
|
5684
|
-
req.on("error", () => settle({
|
|
5685
|
-
ok: false,
|
|
5686
|
-
status: 400,
|
|
5687
|
-
rpcCode: -32600,
|
|
5688
|
-
message: "Request body could not be read."
|
|
5689
|
-
}));
|
|
5690
|
-
});
|
|
5691
|
-
const respondBodyError = (req, res, failure) => {
|
|
5692
|
-
const headers = failure.status === 413 ? { Connection: "close" } : {};
|
|
5693
|
-
sendJsonRpcError(res, failure.status, failure.rpcCode, failure.message, headers);
|
|
5694
|
-
if (failure.status === 413) {
|
|
5695
|
-
if (res.writableFinished) req.destroy();
|
|
5696
|
-
else res.once("finish", () => req.destroy());
|
|
5560
|
+
return await importer();
|
|
5561
|
+
} catch (err) {
|
|
5562
|
+
const missing = missingPeers(err);
|
|
5563
|
+
if (missing.length === 0) throw err;
|
|
5564
|
+
throw new Error(`The HTTP transport needs optional packages that are not installed (missing: ${missing.join(", ")}). Install them next to the server:\n ${installCommand()}\nstdio does not need them. See docs/http-deployment.md.`, { cause: err });
|
|
5697
5565
|
}
|
|
5698
5566
|
};
|
|
5699
|
-
const SESSION_IDLE_TIMEOUT_MS = 18e5;
|
|
5700
|
-
const SESSION_SWEEP_INTERVAL_MS = 6e4;
|
|
5701
|
-
const startHttpTransport = async (config, logger = silentLogger, options = {}) => {
|
|
5702
|
-
const metadata = buildOAuthMetadata(config, logger);
|
|
5703
|
-
const sessions = /* @__PURE__ */ new Map();
|
|
5704
|
-
const idleTimeoutMs = options.sessionIdleTimeoutMs ?? SESSION_IDLE_TIMEOUT_MS;
|
|
5705
|
-
const maxBodyBytes = options.maxBodyBytes ?? 4194304;
|
|
5706
|
-
const dispatchToSession = async (req, res, sessionId, bearer) => {
|
|
5707
|
-
const session = sessions.get(sessionId);
|
|
5708
|
-
if (!session) return false;
|
|
5709
|
-
session.auth.bearer = bearer;
|
|
5710
|
-
session.lastActivityAt = Date.now();
|
|
5711
|
-
const body = req.method === "POST" ? await readJsonBody(req, maxBodyBytes) : {
|
|
5712
|
-
ok: true,
|
|
5713
|
-
value: void 0
|
|
5714
|
-
};
|
|
5715
|
-
if (!body.ok) {
|
|
5716
|
-
respondBodyError(req, res, body);
|
|
5717
|
-
return true;
|
|
5718
|
-
}
|
|
5719
|
-
await session.transport.handleRequest(req, res, body.value);
|
|
5720
|
-
return true;
|
|
5721
|
-
};
|
|
5722
|
-
const handleMcpRequest = async (req, res) => {
|
|
5723
|
-
const bearer = extractBearer(req);
|
|
5724
|
-
if (!bearer) {
|
|
5725
|
-
sendUnauthorized(res, metadata.protectedResource.resource);
|
|
5726
|
-
return;
|
|
5727
|
-
}
|
|
5728
|
-
const sessionId = typeof req.headers["mcp-session-id"] === "string" ? req.headers["mcp-session-id"] : void 0;
|
|
5729
|
-
if (sessionId && await dispatchToSession(req, res, sessionId, bearer)) return;
|
|
5730
|
-
if (req.method !== "POST") {
|
|
5731
|
-
sendJsonRpcError(res, 400, -32e3, "No active session; initialize via POST first.");
|
|
5732
|
-
return;
|
|
5733
|
-
}
|
|
5734
|
-
const body = await readJsonBody(req, maxBodyBytes);
|
|
5735
|
-
if (!body.ok) {
|
|
5736
|
-
respondBodyError(req, res, body);
|
|
5737
|
-
return;
|
|
5738
|
-
}
|
|
5739
|
-
const auth = { bearer };
|
|
5740
|
-
const server = createMcpServer(config, () => auth.bearer, logger);
|
|
5741
|
-
const transport = new StreamableHTTPServerTransport({
|
|
5742
|
-
sessionIdGenerator: () => randomUUID(),
|
|
5743
|
-
onsessioninitialized: (newId) => {
|
|
5744
|
-
sessions.set(newId, {
|
|
5745
|
-
transport,
|
|
5746
|
-
auth,
|
|
5747
|
-
lastActivityAt: Date.now(),
|
|
5748
|
-
close: async () => {
|
|
5749
|
-
await transport.close();
|
|
5750
|
-
await server.close();
|
|
5751
|
-
}
|
|
5752
|
-
});
|
|
5753
|
-
}
|
|
5754
|
-
});
|
|
5755
|
-
transport.onclose = () => {
|
|
5756
|
-
if (transport.sessionId) sessions.delete(transport.sessionId);
|
|
5757
|
-
};
|
|
5758
|
-
await server.connect(transport);
|
|
5759
|
-
await transport.handleRequest(req, res, body.value);
|
|
5760
|
-
};
|
|
5761
|
-
const staticGetRoutes = {
|
|
5762
|
-
"/.well-known/oauth-protected-resource": metadata.protectedResource,
|
|
5763
|
-
"/.well-known/oauth-authorization-server": metadata.authorizationServer,
|
|
5764
|
-
"/healthz": {
|
|
5765
|
-
status: "ok",
|
|
5766
|
-
subdomain: config.subdomain
|
|
5767
|
-
}
|
|
5768
|
-
};
|
|
5769
|
-
const requestListener = async (req, res) => {
|
|
5770
|
-
try {
|
|
5771
|
-
if (handleCorsPreflight(req, res, config.corsOrigins)) return;
|
|
5772
|
-
applyCorsHeaders(req, res, config.corsOrigins);
|
|
5773
|
-
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
5774
|
-
const staticRoute = req.method === "GET" ? staticGetRoutes[url.pathname] : void 0;
|
|
5775
|
-
if (staticRoute) {
|
|
5776
|
-
sendJson(res, 200, staticRoute);
|
|
5777
|
-
return;
|
|
5778
|
-
}
|
|
5779
|
-
if (url.pathname === "/mcp") {
|
|
5780
|
-
await handleMcpRequest(req, res);
|
|
5781
|
-
return;
|
|
5782
|
-
}
|
|
5783
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
5784
|
-
res.end(JSON.stringify({
|
|
5785
|
-
error: "Not found",
|
|
5786
|
-
path: url.pathname
|
|
5787
|
-
}));
|
|
5788
|
-
} catch (err) {
|
|
5789
|
-
failRequest(res, err);
|
|
5790
|
-
}
|
|
5791
|
-
};
|
|
5792
|
-
const httpServer = createServer((req, res) => {
|
|
5793
|
-
requestListener(req, res);
|
|
5794
|
-
});
|
|
5795
|
-
await new Promise((resolve, reject) => {
|
|
5796
|
-
httpServer.once("error", reject);
|
|
5797
|
-
httpServer.listen(config.port, config.host, () => {
|
|
5798
|
-
httpServer.off("error", reject);
|
|
5799
|
-
resolve();
|
|
5800
|
-
});
|
|
5801
|
-
});
|
|
5802
|
-
const addr = httpServer.address();
|
|
5803
|
-
const boundPort = typeof addr === "object" && addr !== null ? addr.port : config.port;
|
|
5804
|
-
logger.info("http_transport_ready", {
|
|
5805
|
-
host: config.host,
|
|
5806
|
-
port: boundPort
|
|
5807
|
-
});
|
|
5808
|
-
const sweepIdleSessions = async () => {
|
|
5809
|
-
const cutoff = Date.now() - idleTimeoutMs;
|
|
5810
|
-
for (const [id, session] of sessions) {
|
|
5811
|
-
if (session.lastActivityAt > cutoff) continue;
|
|
5812
|
-
sessions.delete(id);
|
|
5813
|
-
try {
|
|
5814
|
-
await session.close();
|
|
5815
|
-
} catch (err) {
|
|
5816
|
-
logger.warn("session_close_failed", {
|
|
5817
|
-
sessionId: id,
|
|
5818
|
-
error: err instanceof Error ? err.message : String(err)
|
|
5819
|
-
});
|
|
5820
|
-
}
|
|
5821
|
-
}
|
|
5822
|
-
};
|
|
5823
|
-
const sweeper = setInterval(() => void sweepIdleSessions(), options.sweepIntervalMs ?? SESSION_SWEEP_INTERVAL_MS);
|
|
5824
|
-
sweeper.unref();
|
|
5825
|
-
return {
|
|
5826
|
-
port: boundPort,
|
|
5827
|
-
close: async () => {
|
|
5828
|
-
clearInterval(sweeper);
|
|
5829
|
-
await Promise.all([...sessions.values()].map((session) => session.close()));
|
|
5830
|
-
sessions.clear();
|
|
5831
|
-
httpServer.closeAllConnections();
|
|
5832
|
-
await new Promise((resolve, reject) => {
|
|
5833
|
-
httpServer.close((err) => err ? reject(err) : resolve());
|
|
5834
|
-
});
|
|
5835
|
-
}
|
|
5836
|
-
};
|
|
5837
|
-
};
|
|
5838
5567
|
//#endregion
|
|
5839
5568
|
//#region src/utils/shutdown.ts
|
|
5840
5569
|
/**
|
|
@@ -5957,6 +5686,7 @@ const main = async () => {
|
|
|
5957
5686
|
return;
|
|
5958
5687
|
}
|
|
5959
5688
|
if (config.dev) logger.warn("dev_mode_ignored_http");
|
|
5689
|
+
const { startHttpTransport } = await loadHttpTransport();
|
|
5960
5690
|
const http = await startHttpTransport(config, logger);
|
|
5961
5691
|
installShutdown({
|
|
5962
5692
|
watchStdin: false,
|
|
@@ -5969,4 +5699,4 @@ main().catch((error) => {
|
|
|
5969
5699
|
process.exit(1);
|
|
5970
5700
|
});
|
|
5971
5701
|
//#endregion
|
|
5972
|
-
export {};
|
|
5702
|
+
export { configDir as a, requestedScope as c, getOAuthUrls as d, silentLogger as f, deadlineSignal as i, supportedScopes as l, zendeskGet as n, generateCodeChallenge as o, REQUEST_TIMEOUT_MS as r, generateCodeVerifier as s, createMcpServer as t, escapeHtml as u };
|