@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 +5 -2
- package/dist/index.js +30 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
228
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
475
|
-
|
|
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 (
|
|
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 {
|