@fruggr/zendesk-mcp-server 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -224,8 +224,11 @@ file per subdomain in your OS config dir —
224
224
  elsewhere; override the path with `ZENDESK_TOKEN_FILE`). It is reused across restarts, so you don't
225
225
  re-authenticate every time the MCP client respawns the server. If the Zendesk
226
226
  OAuth client has token expiration enabled, the stored refresh token is used to
227
- renew access silently; only an expired/invalid refresh token triggers a new
228
- browser sign-in.
227
+ renew access silently **proactively** (the token is refreshed before use when
228
+ it's expired, near expiry, or of unknown age, so the first request after an
229
+ overnight gap never hits a visible auth error) and **periodically** in the
230
+ background so a long-lived, idle session never serves a stale token. Only an
231
+ expired/invalid refresh token triggers a new browser sign-in.
229
232
 
230
233
  > **Port conflict?** If port `27439` is already in use the first tool call returns
231
234
  > a clear error telling you to set `ZENDESK_OAUTH_CALLBACK_PORT` (or
package/dist/index.js CHANGED
@@ -221,7 +221,7 @@ const startBrowserAuth = (config, logger = silentLogger) => {
221
221
  const tokenData = await tokenResponse.json();
222
222
  logger.info("oauth_authenticated");
223
223
  res.writeHead(200, { "Content-Type": "text/html" });
224
- res.end("<html><body><h1>Authentication successful!</h1><p>You can close this tab and return to Claude Code.</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>");
224
+ res.end("<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>");
225
225
  clearTimeout(authTimeout);
226
226
  callbackServer.close();
227
227
  resolveToken(tokenData);
@@ -432,6 +432,7 @@ const clearToken = (path, logger = silentLogger) => {
432
432
  //#endregion
433
433
  //#region src/auth/token-store.ts
434
434
  const EXPIRY_SKEW_MS = 6e4;
435
+ const SCHEDULED_REFRESH_MS = 14400 * 1e3;
435
436
  const createAuthRequiredError = (authorizeUrl) => Object.assign(/* @__PURE__ */ new Error("Zendesk authentication required. A browser window should have opened for you to sign in. If it did not, open this URL in your browser, then retry your request:\n" + authorizeUrl), {
436
437
  name: "AuthRequiredError",
437
438
  authorizeUrl
@@ -444,16 +445,18 @@ const createTokenStore = (config, logger = silentLogger) => {
444
445
  let authorizeUrl;
445
446
  let starting;
446
447
  let refreshing;
448
+ let probedUnknownExpiry = false;
447
449
  const persist = (t) => saveToken(tokenPath, t, logger);
448
450
  const setToken = (accessToken, refreshToken) => {
449
451
  token = {
450
452
  accessToken,
451
453
  refreshToken
452
454
  };
455
+ probedUnknownExpiry = false;
453
456
  persist(token);
454
457
  };
455
- const isExpired = (t) => typeof t.expiresAt === "number" && Date.now() >= t.expiresAt - EXPIRY_SKEW_MS;
456
- const tryRefresh = async (current) => {
458
+ const needsRefresh = (t) => typeof t.expiresAt === "number" ? Date.now() >= t.expiresAt - EXPIRY_SKEW_MS : t.refreshToken !== void 0 && !probedUnknownExpiry;
459
+ const tryRefresh = async (current, { dropOnFailure = true } = {}) => {
457
460
  if (!current.refreshToken) return void 0;
458
461
  try {
459
462
  const result = await refreshAccessToken({
@@ -466,13 +469,16 @@ const createTokenStore = (config, logger = silentLogger) => {
466
469
  refreshToken: result.refresh_token ?? current.refreshToken,
467
470
  expiresAt: expiryFrom(result.expires_in)
468
471
  };
472
+ probedUnknownExpiry = true;
469
473
  persist(token);
470
474
  logger.info("oauth_token_refreshed_cached");
471
475
  return token.accessToken;
472
476
  } catch (err) {
473
477
  logger.warn("oauth_token_refresh_failed", { error: err instanceof Error ? err.message : String(err) });
474
- token = void 0;
475
- clearToken(tokenPath, logger);
478
+ if (dropOnFailure) {
479
+ token = void 0;
480
+ clearToken(tokenPath, logger);
481
+ }
476
482
  return;
477
483
  }
478
484
  };
@@ -490,6 +496,7 @@ const createTokenStore = (config, logger = silentLogger) => {
490
496
  refreshToken: result.refresh_token,
491
497
  expiresAt: expiryFrom(result.expires_in)
492
498
  };
499
+ probedUnknownExpiry = true;
493
500
  persist(token);
494
501
  logger.info("oauth_token_cached");
495
502
  }).catch((err) => {
@@ -505,7 +512,8 @@ const createTokenStore = (config, logger = silentLogger) => {
505
512
  });
506
513
  };
507
514
  const getToken = async () => {
508
- if (token && !isExpired(token)) {
515
+ if (refreshing) await refreshing;
516
+ if (token && !needsRefresh(token)) {
509
517
  logger.debug("oauth_token_cache_hit");
510
518
  return token.accessToken;
511
519
  }
@@ -533,10 +541,18 @@ const createTokenStore = (config, logger = silentLogger) => {
533
541
  }
534
542
  logger.info("oauth_token_invalidated");
535
543
  };
544
+ const scheduledRefresh = setInterval(() => {
545
+ if (token?.refreshToken && !refreshing) refreshing = tryRefresh(token, { dropOnFailure: false }).finally(() => {
546
+ refreshing = void 0;
547
+ });
548
+ }, SCHEDULED_REFRESH_MS);
549
+ scheduledRefresh.unref?.();
550
+ const dispose = () => clearInterval(scheduledRefresh);
536
551
  return {
537
552
  getToken,
538
553
  setToken,
539
- invalidate
554
+ invalidate,
555
+ dispose
540
556
  };
541
557
  };
542
558
  //#endregion
@@ -2376,6 +2392,13 @@ const createAllTools = (ctx) => [
2376
2392
  * refreshes/re-authenticates instead of replaying a revoked token. The callback
2377
2393
  * is omitted only where there is nothing to invalidate (e.g. HTTP per-session
2378
2394
  * bearer, owned by the client).
2395
+ *
2396
+ * Client-visible behaviour on an in-flight revocation: the 401 is a *backstop*,
2397
+ * not a transparent retry. The current call still surfaces the error; recovery
2398
+ * happens on the *next* call, whose `getToken` sees the invalidated token and
2399
+ * silently refreshes (or falls back to browser re-auth if the refresh token is
2400
+ * also dead). Proactive refresh keeps this path rare — it only fires when a
2401
+ * token is revoked between the pre-call refresh check and the request.
2379
2402
  */
2380
2403
  const runHandler = async (def, params, onUnauthorized) => {
2381
2404
  try {