@fruggr/zendesk-mcp-server 2.18.0 → 2.19.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.
Files changed (2) hide show
  1. package/dist/index.js +110 -8
  2. package/package.json +6 -6
package/dist/index.js CHANGED
@@ -281,7 +281,6 @@ const startBrowserAuth = (config, logger = silentLogger) => {
281
281
  });
282
282
  const requestedPort = config.callbackPort ?? 27439;
283
283
  const onStartError = (err) => {
284
- clearTimeout(authTimeout);
285
284
  const code = err.code;
286
285
  logger.error("oauth_callback_listen_failed", {
287
286
  port: requestedPort,
@@ -4544,6 +4543,10 @@ const registerReloadTool = (server, reload, logger = silentLogger) => {
4544
4543
  * `reload_tools` tool that hot-reloads edited tool code on demand. stdio only —
4545
4544
  * HTTP builds a per-session server per request, so there is no long-lived
4546
4545
  * server to hot-swap.
4546
+ *
4547
+ * Returns the running server so the caller can close it on shutdown: dev mode
4548
+ * runs over the same stdio transport as normal mode and must exit the same way
4549
+ * when the client disconnects.
4547
4550
  */
4548
4551
  /* v8 ignore start -- runtime bootstrap: binds the reload tool to a real stdio
4549
4552
  transport; the reload machinery it wires up is covered by dev-reload.test.ts */
@@ -4552,6 +4555,7 @@ const startDevServer = async (config, getToken, logger = silentLogger, onUnautho
4552
4555
  registerReloadTool(server, reload, logger);
4553
4556
  await startStdioTransport(server, logger);
4554
4557
  logger.info("dev_mode_enabled");
4558
+ return server;
4555
4559
  };
4556
4560
  /* v8 ignore stop */
4557
4561
  //#endregion
@@ -4907,27 +4911,125 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
4907
4911
  };
4908
4912
  };
4909
4913
  //#endregion
4914
+ //#region src/utils/shutdown.ts
4915
+ /**
4916
+ * How long a shutdown may take before the watchdog forces the exit.
4917
+ *
4918
+ * Generous enough for `server.close()` and an HTTP session drain, short enough
4919
+ * to stay inside the tightest common supervisor grace — `docker stop` allows 10s
4920
+ * and Kubernetes 30s before their own SIGKILL (systemd is far laxer at 90s). A
4921
+ * process killed by its supervisor is exactly the unclean exit this removes.
4922
+ */
4923
+ const SHUTDOWN_GRACE_MS = 3e3;
4924
+ /**
4925
+ * Adapt a `process` to a {@link ShutdownRuntime}. Takes the process as an
4926
+ * argument rather than closing over the global so the adapter itself is
4927
+ * testable — otherwise the one part of this module that touches the real
4928
+ * process would be the one part no test can reach.
4929
+ */
4930
+ const createRuntime = (proc) => ({
4931
+ on: (event, listener) => {
4932
+ proc.on(event, listener);
4933
+ },
4934
+ stdin: { on: (event, listener) => {
4935
+ proc.stdin.on(event, listener);
4936
+ } },
4937
+ exit: (code) => {
4938
+ proc.exit(code);
4939
+ },
4940
+ setTimer: (fn, ms) => {
4941
+ const timer = setTimeout(fn, ms);
4942
+ return {
4943
+ unref: () => void timer.unref(),
4944
+ clear: () => clearTimeout(timer)
4945
+ };
4946
+ }
4947
+ });
4948
+ const defaultRuntime = createRuntime(process);
4949
+ /**
4950
+ * Install the process's one shutdown path and return its trigger.
4951
+ *
4952
+ * Registering a `SIGTERM` handler *removes* Node's default terminate, which
4953
+ * makes the exit our responsibility: a cleanup that stalls on an in-flight
4954
+ * request would otherwise leave a process SIGTERM cannot kill — the very
4955
+ * symptom this exists to remove. Hence the watchdog, which is load-bearing
4956
+ * rather than defensive, and the unconditional `exit` on every path.
4957
+ *
4958
+ * The exit is explicit rather than a drained event loop because the OAuth
4959
+ * callback server (`auth/browser-oauth.ts`) is a listening socket that is not
4960
+ * `unref()`'d: letting the loop drain would keep a disconnected session alive
4961
+ * for up to the 5-minute auth timeout.
4962
+ */
4963
+ const installShutdown = (options) => {
4964
+ const { cleanup, logger, watchStdin, graceMs = SHUTDOWN_GRACE_MS } = options;
4965
+ const runtime = options.runtime ?? defaultRuntime;
4966
+ let started = false;
4967
+ let exited = false;
4968
+ const exitOnce = (code) => {
4969
+ if (exited) return;
4970
+ exited = true;
4971
+ runtime.exit(code);
4972
+ };
4973
+ const shutdown = async (reason) => {
4974
+ if (started) return;
4975
+ started = true;
4976
+ logger.info("shutdown_started", { reason });
4977
+ const watchdog = runtime.setTimer(() => {
4978
+ logger.warn("shutdown_forced", { graceMs });
4979
+ exitOnce(0);
4980
+ }, graceMs);
4981
+ watchdog.unref();
4982
+ try {
4983
+ await cleanup();
4984
+ logger.info("shutdown_complete", { reason });
4985
+ } catch (err) {
4986
+ logger.warn("shutdown_cleanup_failed", { error: err instanceof Error ? err.message : String(err) });
4987
+ } finally {
4988
+ watchdog.clear();
4989
+ exitOnce(0);
4990
+ }
4991
+ };
4992
+ runtime.on("SIGINT", () => void shutdown("SIGINT"));
4993
+ runtime.on("SIGTERM", () => void shutdown("SIGTERM"));
4994
+ if (watchStdin) runtime.stdin.on("end", () => void shutdown("stdin_eof"));
4995
+ return shutdown;
4996
+ };
4997
+ //#endregion
4910
4998
  //#region src/index.ts
4911
4999
  const buildStdioTokenStore = (config, logger) => createTokenStore({
4912
5000
  subdomain: config.subdomain,
4913
5001
  oauthClientId: config.oauthClientId,
4914
5002
  callbackPort: config.callbackPort
4915
5003
  }, logger);
5004
+ const connectStdio = async (config, tokenStore, logger) => {
5005
+ if (config.dev) return startDevServer(config, tokenStore.getToken, logger, tokenStore.invalidate);
5006
+ const server = createMcpServer(config, tokenStore.getToken, logger, tokenStore.invalidate);
5007
+ await startStdioTransport(server, logger);
5008
+ return server;
5009
+ };
4916
5010
  const main = async () => {
4917
5011
  const config = loadConfig();
4918
5012
  const logger = createLogger(config.logLevel);
4919
5013
  if (config.transport === "stdio") {
4920
5014
  const tokenStore = buildStdioTokenStore(config, logger);
4921
- if (config.dev) {
4922
- await startDevServer(config, tokenStore.getToken, logger, tokenStore.invalidate);
4923
- return;
4924
- }
4925
- const server = createMcpServer(config, tokenStore.getToken, logger, tokenStore.invalidate);
4926
- await startStdioTransport(server, logger);
5015
+ const server = await connectStdio(config, tokenStore, logger);
5016
+ installShutdown({
5017
+ watchStdin: true,
5018
+ logger,
5019
+ cleanup: async () => {
5020
+ await server.close();
5021
+ tokenStore.dispose();
5022
+ }
5023
+ });
4927
5024
  return;
4928
5025
  }
4929
5026
  if (config.dev) logger.warn("dev_mode_ignored_http");
4930
- await startHttpTransport(config, logger);
5027
+ const http = await startHttpTransport(config, logger);
5028
+ installShutdown({
5029
+ watchStdin: false,
5030
+ logger,
5031
+ cleanup: http.close
5032
+ });
4931
5033
  };
4932
5034
  main().catch((error) => {
4933
5035
  console.error("Fatal error:", error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fruggr/zendesk-mcp-server",
3
- "version": "2.18.0",
3
+ "version": "2.19.0",
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",
@@ -69,13 +69,13 @@
69
69
  "engines": {
70
70
  "node": ">=20"
71
71
  },
72
- "packageManager": "pnpm@11.21.0+sha512.521705bce689924eac72f5a3587122f362689ef6571e55ba80076fd637c11132ecffada26fad4ea79c485bfddbfd3d5a2a5b05805a77e893de71ec8a6cca3bb1",
72
+ "packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621",
73
73
  "dependencies": {
74
74
  "@modelcontextprotocol/sdk": "1.30.0",
75
75
  "cheerio": "1.2.0",
76
76
  "hast-util-to-html": "9.0.5",
77
77
  "hast-util-to-mdast": "10.1.2",
78
- "open": "11.0.0",
78
+ "open": "11.0.1",
79
79
  "rehype-parse": "9.0.1",
80
80
  "rehype-raw": "7.0.0",
81
81
  "rehype-remark": "10.0.1",
@@ -88,15 +88,15 @@
88
88
  "zod": "4.4.3"
89
89
  },
90
90
  "devDependencies": {
91
- "@biomejs/biome": "2.5.7",
91
+ "@biomejs/biome": "2.5.8",
92
92
  "@semantic-release/changelog": "^7.0.0",
93
93
  "@semantic-release/exec": "^7.1.0",
94
94
  "@semantic-release/git": "^11.0.0",
95
95
  "@semantic-release/github": "^12.0.6",
96
96
  "@semantic-release/npm": "^13.1.5",
97
97
  "@semantic-release/release-notes-generator": "^14.1.1",
98
- "@stryker-mutator/core": "^9.6.1",
99
- "@stryker-mutator/vitest-runner": "^9.6.1",
98
+ "@stryker-mutator/core": "^10.0.0",
99
+ "@stryker-mutator/vitest-runner": "^10.0.0",
100
100
  "@tsconfig/node20": "^20.1.9",
101
101
  "@tsconfig/strictest": "^2.0.8",
102
102
  "@types/hast": "^3.0.4",