@fruggr/zendesk-mcp-server 3.1.2 → 3.2.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.
@@ -30,7 +30,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
30
  SOFTWARE.
31
31
  ```
32
32
 
33
- ## @modelcontextprotocol/core@2.1.0
33
+ ## @modelcontextprotocol/core@2.2.0
34
34
 
35
35
  License: MIT
36
36
 
@@ -476,7 +476,7 @@ CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for
476
476
  the full license text.
477
477
  ```
478
478
 
479
- ## @modelcontextprotocol/server@2.1.0
479
+ ## @modelcontextprotocol/server@2.2.0
480
480
 
481
481
  License: MIT
482
482
 
@@ -2774,14 +2774,14 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
2774
2774
  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2775
2775
  ```
2776
2776
 
2777
- ## mdast-util-gfm-strikethrough@2.0.0
2777
+ ## mdast-util-gfm-strikethrough@2.0.1
2778
2778
 
2779
2779
  License: MIT
2780
2780
 
2781
2781
  ```
2782
2782
  (The MIT License)
2783
2783
 
2784
- Copyright (c) 2020 Titus Wormer <tituswormer@gmail.com>
2784
+ Copyright (c) Titus Wormer <tituswormer@gmail.com>
2785
2785
 
2786
2786
  Permission is hereby granted, free of charge, to any person obtaining
2787
2787
  a copy of this software and associated documentation files (the
@@ -2920,7 +2920,7 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
2920
2920
  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2921
2921
  ```
2922
2922
 
2923
- ## mdast-util-to-markdown@2.1.2
2923
+ ## mdast-util-to-markdown@2.1.3
2924
2924
 
2925
2925
  License: MIT
2926
2926
 
@@ -4111,22 +4111,6 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
4111
4111
  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
4112
4112
  ```
4113
4113
 
4114
- ## powershell-utils@0.1.0
4115
-
4116
- License: MIT
4117
-
4118
- ```
4119
- MIT License
4120
-
4121
- Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
4122
-
4123
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4124
-
4125
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
4126
-
4127
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
4128
- ```
4129
-
4130
4114
  ## powershell-utils@0.2.1
4131
4115
 
4132
4116
  License: MIT
@@ -5198,7 +5182,7 @@ The above copyright notice and this permission notice shall be included in all c
5198
5182
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
5199
5183
  ```
5200
5184
 
5201
- ## wsl-utils@1.0.0
5185
+ ## wsl-utils@1.0.1
5202
5186
 
5203
5187
  License: MIT
5204
5188
 
@@ -16,7 +16,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
16
16
  import { accessSync, chmodSync, constants, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
17
17
  import { Http2ServerRequest, constants as constants$1 } from "http2";
18
18
  import crypto$1 from "crypto";
19
- //#region node_modules/.pnpm/@hono+node-server@1.19.17_hono@4.13.9/node_modules/@hono/node-server/dist/index.mjs
19
+ //#region node_modules/.pnpm/@hono+node-server@1.19.17_hono@4.13.12/node_modules/@hono/node-server/dist/index.mjs
20
20
  var RequestError = class extends Error {
21
21
  constructor(message, options) {
22
22
  super(message, options);
@@ -504,7 +504,7 @@ var getRequestListener = (fetchCallback, options = {}) => {
504
504
  };
505
505
  };
506
506
  //#endregion
507
- //#region node_modules/.pnpm/@modelcontextprotocol+node@2.1.0_@modelcontextprotocol+server@2.1.0_hono@4.13.9/node_modules/@modelcontextprotocol/node/dist/index.mjs
507
+ //#region node_modules/.pnpm/@modelcontextprotocol+node@2.1.0_@modelcontextprotocol+server@2.2.0_hono@4.13.12/node_modules/@modelcontextprotocol/node/dist/index.mjs
508
508
  /**
509
509
  * Server transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification.
510
510
  * It supports both SSE streaming and direct HTTP responses.
@@ -2264,82 +2264,6 @@ function decodeJwt(jwt) {
2264
2264
  return result;
2265
2265
  }
2266
2266
  //#endregion
2267
- //#region src/auth/server/expiring-map.ts
2268
- /**
2269
- * An in-memory map whose entries lapse after their own TTL. Expired entries are
2270
- * swept on every `set`, so the map stays bounded by what was set within the
2271
- * longest TTL, with no timer to hold the process open.
2272
- */
2273
- const createExpiringMap = () => {
2274
- const entries = /* @__PURE__ */ new Map();
2275
- const get = (key) => {
2276
- const entry = entries.get(key);
2277
- return entry && entry.expiresAt > Date.now() ? entry.value : void 0;
2278
- };
2279
- return {
2280
- set: (key, value, ttlMs) => {
2281
- const now = Date.now();
2282
- for (const [k, entry] of entries) if (entry.expiresAt <= now) entries.delete(k);
2283
- entries.set(key, {
2284
- value,
2285
- expiresAt: now + ttlMs
2286
- });
2287
- },
2288
- get,
2289
- /** Read and forget: a pending value is used at most once. */
2290
- take: (key) => {
2291
- const value = get(key);
2292
- entries.delete(key);
2293
- return value;
2294
- },
2295
- /** Live and not-yet-swept entries. */
2296
- size: () => entries.size
2297
- };
2298
- };
2299
- //#endregion
2300
- //#region src/auth/server/consent.ts
2301
- const hostOf = (uri) => {
2302
- try {
2303
- return new URL(uri).host;
2304
- } catch {
2305
- return uri;
2306
- }
2307
- };
2308
- const SCOPE_LABELS = {
2309
- read: "Read your Zendesk data",
2310
- write: "Create and change Zendesk data on your behalf"
2311
- };
2312
- /**
2313
- * The MCP consent screen, shown once per client that is not trusted (DCR,
2314
- * unknown CIMD, loopback redirects). Names the client, the scopes and where the
2315
- * code goes, per the spec's UI requirements. Every interpolated value is
2316
- * escaped: the client name comes from a document anyone can publish.
2317
- */
2318
- const renderConsentPage = (view) => {
2319
- const name = escapeHtml(view.clientName ?? view.clientId);
2320
- const scopes = view.scopes.map((scope) => `<li>${escapeHtml(SCOPE_LABELS[scope] ?? scope)}</li>`).join("");
2321
- const action = `/interaction/${encodeURIComponent(view.uid)}`;
2322
- return [
2323
- "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">",
2324
- "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">",
2325
- "<title>Authorize access to Zendesk</title>",
2326
- "<style>body{font-family:system-ui,sans-serif;max-width:32rem;margin:3rem auto;padding:0 1rem;line-height:1.5}",
2327
- "code{word-break:break-all}button{font:inherit;padding:.5rem 1.25rem;margin-right:.5rem}</style>",
2328
- "</head><body>",
2329
- `<h1>Allow ${name} to use your Zendesk account?</h1>`,
2330
- `<p><strong>${name}</strong> is asking to act as you on <strong>${escapeHtml(view.subdomain)}.zendesk.com</strong>:</p>`,
2331
- `<ul>${scopes}</ul>`,
2332
- `<p>After you allow it, you are sent back to <strong>${escapeHtml(hostOf(view.redirectUri))}</strong>`,
2333
- ` (<code>${escapeHtml(view.redirectUri)}</code>). Only continue if you started this connection yourself.</p>`,
2334
- `<p>Client id: <code>${escapeHtml(view.clientId)}</code></p>`,
2335
- `<form method="post" action="${action}/confirm" style="display:inline"><button type="submit">Allow</button></form>`,
2336
- `<form method="post" action="${action}/abort" style="display:inline"><button type="submit">Deny</button></form>`,
2337
- "</body></html>"
2338
- ].join("");
2339
- };
2340
- /** Plain error page for a failed sign-in. ASCII message, escaped. */
2341
- const renderErrorPage = (title, detail) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Sign-in failed</title></head><body><h1>${escapeHtml(title)}</h1><p>${escapeHtml(detail)}</p></body></html>`;
2342
- //#endregion
2343
2267
  //#region src/auth/server/trusted-clients.ts
2344
2268
  /**
2345
2269
  * Which CIMD clients skip our consent screen, and the fetch shim applied to CIMD
@@ -2425,6 +2349,184 @@ const createCimdFetch = (baseFetch) => async (url, options) => {
2425
2349
  });
2426
2350
  };
2427
2351
  //#endregion
2352
+ //#region src/auth/server/cimd-documents.ts
2353
+ /** How long a kept copy may stand in for its document, after it was kept. */
2354
+ const LAST_GOOD_TTL_S = 604800;
2355
+ const STALE_MAX_AGE_S = 60;
2356
+ const CIMD_FETCH_HINT = "A client document, or a URL it names, could not be fetched: the client cannot sign in or refresh unless a last good copy is served. See https://github.com/fruggr/zendesk-mcp-server/blob/main/docs/troubleshooting.md#http-the-log-shows-oauth_client_fetch_failed";
2357
+ const isTransientFailure = (status) => status === 403 || status === 408 || status === 429 || status >= 500;
2358
+ const messageOf = (err) => {
2359
+ if (!(err instanceof Error)) return String(err);
2360
+ return err.cause instanceof Error ? `${err.message}: ${err.cause.message}` : err.message;
2361
+ };
2362
+ const ownDocument = async (response, url) => {
2363
+ const parsed = await response.json().catch(() => void 0);
2364
+ return parsed?.client_id === url ? parsed : void 0;
2365
+ };
2366
+ /**
2367
+ * A copy is kept when a token is issued, from the client oidc-provider built
2368
+ * and validated: an unauthenticated caller can make the server fetch any URL,
2369
+ * but not write to the grant store, and a rejected document is never kept.
2370
+ */
2371
+ const createCimdDocuments = (baseFetch, { lastGood, logger }) => {
2372
+ const rewrite = createCimdFetch(baseFetch);
2373
+ const stale = /* @__PURE__ */ new Set();
2374
+ const kept = (url) => lastGood.get(url).catch((err) => {
2375
+ logger.warn("oauth_cimd_last_good_read_failed", {
2376
+ url,
2377
+ error: messageOf(err)
2378
+ });
2379
+ });
2380
+ const report = (url, failure, copy) => {
2381
+ logger.warn("oauth_client_fetch_failed", {
2382
+ url,
2383
+ ...failure,
2384
+ fallback: copy ? "last_good" : "none",
2385
+ ...copy && { ageS: Math.floor((Date.now() - copy.fetchedAt) / 1e3) },
2386
+ hint: CIMD_FETCH_HINT
2387
+ });
2388
+ };
2389
+ const serve = (url, copy) => {
2390
+ stale.add(url);
2391
+ return Response.json(copy.document, { headers: { "cache-control": `max-age=${STALE_MAX_AGE_S}` } });
2392
+ };
2393
+ const fallBack = async (url, failure, failed) => {
2394
+ const copy = await kept(url);
2395
+ report(url, failure, copy);
2396
+ if (!copy) return void 0;
2397
+ await failed?.body?.cancel();
2398
+ return serve(url, copy);
2399
+ };
2400
+ const succeeded = async (url, response) => {
2401
+ if (await ownDocument(response.clone(), url) && response.status === 200) {
2402
+ stale.delete(url);
2403
+ return response;
2404
+ }
2405
+ const copy = await kept(url);
2406
+ if (!copy) return response;
2407
+ report(url, {
2408
+ status: response.status,
2409
+ error: "not a client document"
2410
+ }, copy);
2411
+ await response.body?.cancel();
2412
+ return serve(url, copy);
2413
+ };
2414
+ const fetch = async (input, init) => {
2415
+ const url = String(input);
2416
+ let response;
2417
+ try {
2418
+ response = await rewrite(input, init);
2419
+ } catch (err) {
2420
+ const copy = await fallBack(url, { error: messageOf(err) });
2421
+ if (copy) return copy;
2422
+ throw err;
2423
+ }
2424
+ if (response.ok) return succeeded(url, response);
2425
+ if (!isTransientFailure(response.status)) {
2426
+ logger.debug("oauth_client_fetch_failed", {
2427
+ url,
2428
+ status: response.status,
2429
+ fallback: "none"
2430
+ });
2431
+ return response;
2432
+ }
2433
+ return await fallBack(url, { status: response.status }, response) ?? response;
2434
+ };
2435
+ return {
2436
+ fetch,
2437
+ granted: async (client) => {
2438
+ if (client.clientIdMetadataDocument !== true || stale.has(client.clientId)) return;
2439
+ try {
2440
+ await lastGood.set(client.clientId, {
2441
+ document: client.metadata(),
2442
+ fetchedAt: Date.now()
2443
+ }, LAST_GOOD_TTL_S);
2444
+ } catch (err) {
2445
+ logger.warn("oauth_cimd_last_good_store_failed", {
2446
+ url: client.clientId,
2447
+ error: messageOf(err)
2448
+ });
2449
+ }
2450
+ }
2451
+ };
2452
+ };
2453
+ //#endregion
2454
+ //#region src/auth/server/expiring-map.ts
2455
+ /**
2456
+ * An in-memory map whose entries lapse after their own TTL. Expired entries are
2457
+ * swept on every `set`, so the map stays bounded by what was set within the
2458
+ * longest TTL, with no timer to hold the process open.
2459
+ */
2460
+ const createExpiringMap = () => {
2461
+ const entries = /* @__PURE__ */ new Map();
2462
+ const get = (key) => {
2463
+ const entry = entries.get(key);
2464
+ return entry && entry.expiresAt > Date.now() ? entry.value : void 0;
2465
+ };
2466
+ return {
2467
+ set: (key, value, ttlMs) => {
2468
+ const now = Date.now();
2469
+ for (const [k, entry] of entries) if (entry.expiresAt <= now) entries.delete(k);
2470
+ entries.set(key, {
2471
+ value,
2472
+ expiresAt: now + ttlMs
2473
+ });
2474
+ },
2475
+ get,
2476
+ /** Read and forget: a pending value is used at most once. */
2477
+ take: (key) => {
2478
+ const value = get(key);
2479
+ entries.delete(key);
2480
+ return value;
2481
+ },
2482
+ /** Live and not-yet-swept entries. */
2483
+ size: () => entries.size
2484
+ };
2485
+ };
2486
+ //#endregion
2487
+ //#region src/auth/server/consent.ts
2488
+ const hostOf = (uri) => {
2489
+ try {
2490
+ return new URL(uri).host;
2491
+ } catch {
2492
+ return uri;
2493
+ }
2494
+ };
2495
+ const SCOPE_LABELS = {
2496
+ read: "Read your Zendesk data",
2497
+ write: "Create and change Zendesk data on your behalf"
2498
+ };
2499
+ /**
2500
+ * The MCP consent screen, shown once per client that is not trusted (DCR,
2501
+ * unknown CIMD, loopback redirects). Names the client, the scopes and where the
2502
+ * code goes, per the spec's UI requirements. Every interpolated value is
2503
+ * escaped: the client name comes from a document anyone can publish.
2504
+ */
2505
+ const renderConsentPage = (view) => {
2506
+ const name = escapeHtml(view.clientName ?? view.clientId);
2507
+ const scopes = view.scopes.map((scope) => `<li>${escapeHtml(SCOPE_LABELS[scope] ?? scope)}</li>`).join("");
2508
+ const action = `/interaction/${encodeURIComponent(view.uid)}`;
2509
+ return [
2510
+ "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">",
2511
+ "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">",
2512
+ "<title>Authorize access to Zendesk</title>",
2513
+ "<style>body{font-family:system-ui,sans-serif;max-width:32rem;margin:3rem auto;padding:0 1rem;line-height:1.5}",
2514
+ "code{word-break:break-all}button{font:inherit;padding:.5rem 1.25rem;margin-right:.5rem}</style>",
2515
+ "</head><body>",
2516
+ `<h1>Allow ${name} to use your Zendesk account?</h1>`,
2517
+ `<p><strong>${name}</strong> is asking to act as you on <strong>${escapeHtml(view.subdomain)}.zendesk.com</strong>:</p>`,
2518
+ `<ul>${scopes}</ul>`,
2519
+ `<p>After you allow it, you are sent back to <strong>${escapeHtml(hostOf(view.redirectUri))}</strong>`,
2520
+ ` (<code>${escapeHtml(view.redirectUri)}</code>). Only continue if you started this connection yourself.</p>`,
2521
+ `<p>Client id: <code>${escapeHtml(view.clientId)}</code></p>`,
2522
+ `<form method="post" action="${action}/confirm" style="display:inline"><button type="submit">Allow</button></form>`,
2523
+ `<form method="post" action="${action}/abort" style="display:inline"><button type="submit">Deny</button></form>`,
2524
+ "</body></html>"
2525
+ ].join("");
2526
+ };
2527
+ /** Plain error page for a failed sign-in. ASCII message, escaped. */
2528
+ const renderErrorPage = (title, detail) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Sign-in failed</title></head><body><h1>${escapeHtml(title)}</h1><p>${escapeHtml(detail)}</p></body></html>`;
2529
+ //#endregion
2428
2530
  //#region src/auth/server/upstream.ts
2429
2531
  /**
2430
2532
  * Zendesk as the upstream identity provider, through the same public PKCE client
@@ -35898,13 +36000,12 @@ const buildProvider = (options) => {
35898
36000
  const { ring, resource } = options;
35899
36001
  const [current] = ring;
35900
36002
  const scopes = options.allowWrite ? RESOURCE_SCOPES.join(" ") : "read";
35901
- const baseFetch = options.fetch ?? ((url, init) => globalThis.fetch(url, init));
35902
36003
  const provider = new lib_default(options.issuer, {
35903
36004
  adapter: options.adapter,
35904
36005
  jwks: { keys: ring.map((set) => set.signingJwk) },
35905
36006
  cookies: { keys: ring.map((set) => set.cookieKey) },
35906
36007
  clients: [],
35907
- fetch: createCimdFetch(baseFetch),
36008
+ fetch: options.cimdDocuments.fetch,
35908
36009
  scopes: options.allowWrite ? [...RESOURCE_SCOPES, "offline_access"] : ["read", "offline_access"],
35909
36010
  clientDefaults: {
35910
36011
  grant_types: ["authorization_code", "refresh_token"],
@@ -36109,6 +36210,10 @@ const createAuthorizationServer = (options) => {
36109
36210
  hint: "Revoked in this process only. Fix the store before restarting: its next successful write persists the removal."
36110
36211
  });
36111
36212
  };
36213
+ const cimdDocuments = createCimdDocuments(options.fetch ?? ((url, init) => globalThis.fetch(url, init)), {
36214
+ lastGood: createSealedCollection(persistent, "CimdDocument", ring),
36215
+ logger
36216
+ });
36112
36217
  const provider = buildProvider({
36113
36218
  issuer,
36114
36219
  resource,
@@ -36118,7 +36223,10 @@ const createAuthorizationServer = (options) => {
36118
36223
  allowWrite: !config.readOnly,
36119
36224
  isAllowedOrigin: options.isAllowedOrigin,
36120
36225
  behindProxy: config.publicUrl?.startsWith("https:") === true,
36121
- fetch: options.fetch
36226
+ cimdDocuments
36227
+ });
36228
+ provider.on("grant.success", (ctx) => {
36229
+ if (ctx.oidc.client) cimdDocuments.granted(ctx.oidc.client);
36122
36230
  });
36123
36231
  provider.on("server_error", (_ctx, err) => {
36124
36232
  logger.error("oauth_server_error", { error: err.message });