@fruggr/zendesk-mcp-server 3.7.0 → 3.8.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.
@@ -1,16 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { i as __toESM, n as __exportAll, r as __require, t as __commonJSMin } from "./rolldown-runtime-BbXq-jo1.js";
3
- import { A as STARTUP_DOCS, C as WebStandardStreamableHTTPServerTransport, D as OAuthError, E as verifyBearerToken, M as errnoCode, O as OAuthErrorCode, S as DEFAULT_MAX_REQUEST_BODY_SIZE, T as isLegacyRequest, _ as zendeskGet, a as toolEnv, g as createAllTools, h as silentLogger, i as defaultServerFactory, j as createStartupError, k as compareCodeUnits, l as configDir, m as createRequestStateCodec, n as listenedUris, o as watchGrantAlerts, r as createServerCaches, s as startSlaRuntime, t as createListenFeed, v as REQUEST_TIMEOUT_MS, w as createMcpHandler, x as getOAuthUrls, y as deadlineSignal } from "./index.js";
3
+ import { $ as _enum, A as ResourceTemplate, B as stampErrorBrands, D as createMcpHandler, E as WebStandardStreamableHTTPServerTransport, F as ProtocolErrorCode, G as ResultSchema, H as PaginatedRequestParamsSchema, I as ResourceNotFoundError, J as toStrictStandardSchema, K as buildInstructions, L as assertValidCacheHint, M as OAuthError, N as OAuthErrorCode, O as isLegacyRequest, P as ProtocolError, Q as errnoCode, R as brandedHasInstance, S as deadlineSignal, T as DEFAULT_MAX_REQUEST_BODY_SIZE, U as PaginatedResultSchema, V as BaseRequestParamsSchema, W as ResourceSchema, X as STARTUP_DOCS, Y as compareCodeUnits, Z as createStartupError, _ as createAllTools, a as toolEnv, at as object, b as zendeskGet, ct as union, et as array, g as silentLogger, h as readPackageInfo, i as defaultServerFactory, it as number, j as verifyBearerToken, k as McpServer, l as configDir, lt as unknown, m as createRequestStateCodec, n as listenedUris, nt as literal, o as watchGrantAlerts, ot as record, q as Namespace, r as createServerCaches, rt as looseObject, s as startSlaRuntime, st as string, t as createListenFeed, tt as boolean$1, v as createTopologyProvider, w as getOAuthUrls, x as REQUEST_TIMEOUT_MS, y as ZendeskApiError, z as isModernProtocolVersion } from "./index.js";
4
4
  import { a as supportedScopes, i as requestedScope, n as generateCodeVerifier, o as escapeHtml, t as generateCodeChallenge } from "./pkce-DE_v9NQy.js";
5
5
  import * as util from "node:util";
6
6
  import { format, inspect } from "node:util";
7
7
  import * as crypto$2 from "node:crypto";
8
8
  import { createHash, createPrivateKey, createSecretKey, hkdfSync, randomBytes, randomFillSync, randomUUID, timingSafeEqual } from "node:crypto";
9
- import { dirname, join, resolve } from "node:path";
9
+ import { dirname, join, resolve, sep } from "node:path";
10
10
  import { fileURLToPath, pathToFileURL } from "node:url";
11
11
  import { accessSync, chmodSync, constants, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
12
+ import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
12
13
  import { STATUS_CODES, createServer } from "node:http";
13
- import { Http2ServerRequest, constants as constants$1 } from "http2";
14
+ import { Http2ServerRequest, constants as constants$2 } from "http2";
14
15
  import { Readable } from "stream";
15
16
  import crypto$1 from "crypto";
16
17
  import { strict } from "node:assert";
@@ -315,7 +316,7 @@ var drainIncoming = (incoming) => {
315
316
  incomingWithDrainState[incomingDraining] = true;
316
317
  if (incoming instanceof Http2ServerRequest) {
317
318
  try {
318
- incoming.stream?.close?.(constants$1.NGHTTP2_NO_ERROR);
319
+ incoming.stream?.close?.(constants$2.NGHTTP2_NO_ERROR);
319
320
  } catch {}
320
321
  return;
321
322
  }
@@ -505,7 +506,7 @@ var getRequestListener = (fetchCallback, options = {}) => {
505
506
  };
506
507
  };
507
508
  //#endregion
508
- //#region node_modules/.pnpm/@modelcontextprotocol+node@2.1.1_@modelcontextprotocol+server@2.3.1/node_modules/@modelcontextprotocol/node/dist/index.mjs
509
+ //#region node_modules/.pnpm/@modelcontextprotocol+node@2.1.2_@modelcontextprotocol+server@2.4.0/node_modules/@modelcontextprotocol/node/dist/index.mjs
509
510
  /**
510
511
  * Server transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification.
511
512
  * It supports both SSE streaming and direct HTTP responses.
@@ -848,8 +849,8 @@ function internalServerErrorResponse(id) {
848
849
  }, { status: 500 });
849
850
  }
850
851
  //#endregion
851
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/lib/buffer_utils.js
852
- const encoder$1 = new TextEncoder();
852
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/lib/buffer_utils.js
853
+ const encoder$3 = new TextEncoder();
853
854
  const decoder$2 = new TextDecoder();
854
855
  const strictDecoder = new TextDecoder("utf-8", { fatal: !0 });
855
856
  const UINT32_RANGE = 2 ** 32;
@@ -880,7 +881,7 @@ const NON_ASCII = /[^\x00-\x7f]/;
880
881
  function encode$2(string) {
881
882
  if (typeof string == "string" && string.length >= 128) {
882
883
  if (NON_ASCII.test(string)) throw new TypeError("non-ASCII string encountered in encode()");
883
- return encoder$1.encode(string);
884
+ return encoder$3.encode(string);
884
885
  }
885
886
  const bytes = new Uint8Array(string.length);
886
887
  for (let i = 0; i < string.length; i++) {
@@ -890,7 +891,7 @@ function encode$2(string) {
890
891
  }
891
892
  return bytes;
892
893
  }
893
- function encodeBase64(input, url = !1) {
894
+ function encodeBase64$1(input, url = !1) {
894
895
  if (Uint8Array.prototype.toBase64) return input.toBase64({
895
896
  alphabet: url ? "base64url" : "base64",
896
897
  omitPadding: url
@@ -900,7 +901,7 @@ function encodeBase64(input, url = !1) {
900
901
  const encoded = btoa(arr.join(""));
901
902
  return url ? encoded.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_") : encoded;
902
903
  }
903
- function decodeBase64(encoded, url = !1) {
904
+ function decodeBase64$1(encoded, url = !1) {
904
905
  if (Uint8Array.fromBase64) return Uint8Array.fromBase64(encoded, { alphabet: url ? "base64url" : "base64" });
905
906
  if (url) {
906
907
  if (encoded.includes("+") || encoded.includes("/")) throw new TypeError("Invalid base64url");
@@ -915,7 +916,7 @@ async function digest(algorithm, data) {
915
916
  return new Uint8Array(await crypto.subtle.digest(subtleDigest, data));
916
917
  }
917
918
  //#endregion
918
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/util/errors.js
919
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/util/errors.js
919
920
  var errors_exports$1 = /* @__PURE__ */ __exportAll({
920
921
  JOSEAlgNotAllowed: () => JOSEAlgNotAllowed,
921
922
  JOSEError: () => JOSEError,
@@ -1033,20 +1034,20 @@ var JWSSignatureVerificationFailed$1 = class extends JOSEError {
1033
1034
  }
1034
1035
  };
1035
1036
  //#endregion
1036
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/util/base64url.js
1037
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/util/base64url.js
1037
1038
  const invalid$2 = "The input to be decoded is not correctly encoded.";
1038
1039
  function decode$2(input) {
1039
1040
  try {
1040
- return decodeBase64(typeof input == "string" ? input : decoder$2.decode(input), !0);
1041
+ return decodeBase64$1(typeof input == "string" ? input : decoder$2.decode(input), !0);
1041
1042
  } catch (cause) {
1042
1043
  throw new TypeError(invalid$2, { cause });
1043
1044
  }
1044
1045
  }
1045
1046
  function encode$1(input) {
1046
- return encodeBase64(typeof input == "string" ? encoder$1.encode(input) : input, !0);
1047
+ return encodeBase64$1(typeof input == "string" ? encoder$3.encode(input) : input, !0);
1047
1048
  }
1048
1049
  //#endregion
1049
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/lib/validate.js
1050
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/lib/validate.js
1050
1051
  function assertUint8Array(input, label) {
1051
1052
  if (!(input instanceof Uint8Array)) throw new TypeError(`${label} must be an instance of Uint8Array`);
1052
1053
  }
@@ -1138,7 +1139,7 @@ function serializeJoseHeader(Err, header) {
1138
1139
  return [parsed, serialized];
1139
1140
  }
1140
1141
  //#endregion
1141
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/lib/key.js
1142
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/lib/key.js
1142
1143
  const tag = (key) => key[Symbol.toStringTag];
1143
1144
  const jwkMatchesOp = (entry, key, usage) => {
1144
1145
  const { alg } = entry;
@@ -1275,7 +1276,7 @@ async function rawKey(key, expected, usage, extractable = !1) {
1275
1276
  return key instanceof Uint8Array && (key = await crypto.subtle.importKey("raw", key, expected, extractable, [usage])), checkCryptoKey(key, expected, usage), key;
1276
1277
  }
1277
1278
  //#endregion
1278
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/lib/content_encryption.js
1279
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/lib/content_encryption.js
1279
1280
  const generateCek = (enc) => crypto.getRandomValues(new Uint8Array(enc.cekBits >> 3));
1280
1281
  function checkCekLength(cek, expected) {
1281
1282
  const actual = cek.byteLength << 3;
@@ -1362,7 +1363,7 @@ async function decrypt$1(enc, cek, ciphertext, iv, tag, aad) {
1362
1363
  }
1363
1364
  }
1364
1365
  //#endregion
1365
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/lib/key_descriptor.js
1366
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/lib/key_descriptor.js
1366
1367
  function table(entries) {
1367
1368
  const out = { __proto__: null };
1368
1369
  for (const alg in entries) out[alg] = {
@@ -1372,7 +1373,7 @@ function table(entries) {
1372
1373
  return out;
1373
1374
  }
1374
1375
  //#endregion
1375
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/lib/jwe_algorithms.js
1376
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/lib/jwe_algorithms.js
1376
1377
  const wrap = [["encrypt", "wrapKey"], ["decrypt", "unwrapKey"]];
1377
1378
  const derive$1 = [[], ["deriveBits"]];
1378
1379
  const none = [[], []];
@@ -1492,7 +1493,7 @@ function jweEncryption(enc) {
1492
1493
  return (typeof enc == "string" ? ENC[enc] : void 0) ?? unsupported$1("enc", "Encryption Algorithm");
1493
1494
  }
1494
1495
  //#endregion
1495
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/lib/key_management.js
1496
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/lib/key_management.js
1496
1497
  function checkEcdhCryptoKey(key, usage) {
1497
1498
  if (key.algorithm.name !== "ECDH" && key.algorithm.name !== "X25519") throw new TypeError("CryptoKey does not support this operation, its algorithm.name must be ECDH or X25519");
1498
1499
  checkUsage(key, usage);
@@ -1687,7 +1688,7 @@ async function encryptKeyManagement(entry, enc, inputKey, joseHeader, providedCe
1687
1688
  ];
1688
1689
  }
1689
1690
  //#endregion
1690
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/lib/deflate.js
1691
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/lib/deflate.js
1691
1692
  function validateZip(joseHeader, protectedHeader) {
1692
1693
  if (joseHeader.zip !== void 0 && joseHeader.zip !== "DEF") throw new JOSENotSupported("Unsupported JWE \"zip\" (Compression Algorithm) Header Parameter value.");
1693
1694
  if (joseHeader.zip !== void 0 && !protectedHeader?.zip) throw new JWEInvalid("JWE \"zip\" (Compression Algorithm) Header Parameter MUST be in a protected header.");
@@ -1715,7 +1716,7 @@ async function decompress(input, maxLength) {
1715
1716
  return supported("DecompressionStream"), transform(new DecompressionStream("deflate-raw"), input, maxLength);
1716
1717
  }
1717
1718
  //#endregion
1718
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/lib/jwe_decrypt.js
1719
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/lib/jwe_decrypt.js
1719
1720
  function shareJWE(jwe) {
1720
1721
  const { protected: encodedProtected, ciphertext, iv, tag, aad } = jwe;
1721
1722
  let protectedHeader;
@@ -1820,12 +1821,12 @@ async function decryptCompact(jwe, shared, key) {
1820
1821
  }, shared, key);
1821
1822
  }
1822
1823
  //#endregion
1823
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/jwe/compact/decrypt.js
1824
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/jwe/compact/decrypt.js
1824
1825
  async function compactDecrypt(jwe, key, options) {
1825
1826
  return decryptCompact(jwe, prepareDecrypt(options), key);
1826
1827
  }
1827
1828
  //#endregion
1828
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/lib/jwe_encrypt.js
1829
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/lib/jwe_encrypt.js
1829
1830
  function checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader) {
1830
1831
  if (!isDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader)) throw new JWEInvalid("JWE Protected, JWE Shared Unprotected and JWE Per-Recipient Header Parameter names must be disjoint");
1831
1832
  }
@@ -1896,7 +1897,7 @@ function compactJWE(jwe) {
1896
1897
  ].join(".");
1897
1898
  }
1898
1899
  //#endregion
1899
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/lib/jws_algorithms.js
1900
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/lib/jws_algorithms.js
1900
1901
  const sig = [["verify"], ["sign"]];
1901
1902
  function hmac(bits) {
1902
1903
  const subtle = {
@@ -1986,7 +1987,7 @@ function jwsAlgorithm(alg) {
1986
1987
  return entry;
1987
1988
  }
1988
1989
  //#endregion
1989
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/lib/jws_verify.js
1990
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/lib/jws_verify.js
1990
1991
  function prepareVerify(options) {
1991
1992
  return [options && validateAlgorithms("algorithms", options.algorithms), options?.crit];
1992
1993
  }
@@ -2041,13 +2042,13 @@ async function verifyCompact(jws, shared, key) {
2041
2042
  }, shared, key, encodeCompactUnencodedPayload);
2042
2043
  }
2043
2044
  //#endregion
2044
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/jws/compact/verify.js
2045
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/jws/compact/verify.js
2045
2046
  async function compactVerify(jws, key, options) {
2046
2047
  const [result] = await verifyCompact(jws, prepareVerify(options), key);
2047
2048
  return result;
2048
2049
  }
2049
2050
  //#endregion
2050
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/lib/jwt_claims_set.js
2051
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/lib/jwt_claims_set.js
2051
2052
  const epoch = (date) => Math.floor(date.getTime() / 1e3);
2052
2053
  const multipliers = {
2053
2054
  s: 1,
@@ -2144,7 +2145,7 @@ function jwtClaimsSetBytes(producer) {
2144
2145
  const value = claimsSet[claim];
2145
2146
  if (typeof value == "number" && !Number.isFinite(value)) throw new TypeError(`"${claim}" claim must be a finite number`);
2146
2147
  }
2147
- return encoder$1.encode(JSON.stringify(claimsSet));
2148
+ return encoder$3.encode(JSON.stringify(claimsSet));
2148
2149
  }
2149
2150
  var JWTClaimsBuilder = class {
2150
2151
  constructor(claimsSet = {}) {
@@ -2175,7 +2176,7 @@ var JWTClaimsBuilder = class {
2175
2176
  }
2176
2177
  };
2177
2178
  //#endregion
2178
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/jwt/verify.js
2179
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/jwt/verify.js
2179
2180
  async function jwtVerify(jwt, key, options) {
2180
2181
  const [verified, b64] = await verifyCompact(jwt, prepareVerify(options), key);
2181
2182
  if (!b64) throw new JWTInvalid("JWTs MUST NOT use unencoded payload");
@@ -2186,7 +2187,7 @@ async function jwtVerify(jwt, key, options) {
2186
2187
  };
2187
2188
  }
2188
2189
  //#endregion
2189
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/jwe/compact/encrypt.js
2190
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/jwe/compact/encrypt.js
2190
2191
  var CompactEncrypt = class {
2191
2192
  #input;
2192
2193
  constructor(plaintext) {
@@ -2209,7 +2210,7 @@ var CompactEncrypt = class {
2209
2210
  }
2210
2211
  };
2211
2212
  //#endregion
2212
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/lib/jws_sign.js
2213
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/lib/jws_sign.js
2213
2214
  async function createSignature(input, key, rejectUnencoded) {
2214
2215
  let [payload, protectedHeader, unprotectedHeader, crit] = input, encodedProtectedHeader = "";
2215
2216
  if (protectedHeader !== void 0) {
@@ -2231,7 +2232,7 @@ async function createSignature(input, key, rejectUnencoded) {
2231
2232
  let encodedPayload = "", signingPayload = payload, signingInput;
2232
2233
  if (b64) {
2233
2234
  const encodedPayloadCache = input[4];
2234
- encodedPayloadCache ? (encodedPayload = encodedPayloadCache[0] ??= encode$1(payload), signingPayload = encodedPayloadCache[1] ??= encode$2(encodedPayload)) : (encodedPayload = encode$1(payload), signingInput = encoder$1.encode(`${encodedProtectedHeader}.${encodedPayload}`));
2235
+ encodedPayloadCache ? (encodedPayload = encodedPayloadCache[0] ??= encode$1(payload), signingPayload = encodedPayloadCache[1] ??= encode$2(encodedPayload)) : (encodedPayload = encode$1(payload), signingInput = encoder$3.encode(`${encodedProtectedHeader}.${encodedPayload}`));
2235
2236
  }
2236
2237
  signingInput ??= concat(encode$2(encodedProtectedHeader), encode$2("."), signingPayload);
2237
2238
  const signingKey = await rawKey(await prepareKey(entry, key, "sign"), entry.subtle, "sign");
@@ -2252,7 +2253,7 @@ async function createCompactSignature(payload, protectedHeader, crit, key, rejec
2252
2253
  return `${jws.protected}.${jws.payload}.${jws.signature}`;
2253
2254
  }
2254
2255
  //#endregion
2255
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/jws/compact/sign.js
2256
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/jws/compact/sign.js
2256
2257
  var CompactSign = class {
2257
2258
  #payload;
2258
2259
  #protectedHeader;
@@ -2269,7 +2270,7 @@ var CompactSign = class {
2269
2270
  }
2270
2271
  };
2271
2272
  //#endregion
2272
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/lib/key_algorithm.js
2273
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/lib/key_algorithm.js
2273
2274
  function unsupportedAlg(source = "JWK \"alg\" (Algorithm) Parameter") {
2274
2275
  throw new JOSENotSupported(`Invalid or unsupported ${source} value`);
2275
2276
  }
@@ -2277,7 +2278,7 @@ function keyAlgorithm(alg, source) {
2277
2278
  return (typeof alg == "string" ? JWS[alg] ?? JWE[alg] : void 0) ?? unsupportedAlg(source);
2278
2279
  }
2279
2280
  //#endregion
2280
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/key/export.js
2281
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/key/export.js
2281
2282
  async function exportJWK(key) {
2282
2283
  if (isKeyObject(key)) if (key.type === "secret") key = key.export();
2283
2284
  else return key.export({ format: "jwk" });
@@ -2293,7 +2294,7 @@ async function exportJWK(key) {
2293
2294
  return jwk;
2294
2295
  }
2295
2296
  //#endregion
2296
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/jwk/thumbprint.js
2297
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/jwk/thumbprint.js
2297
2298
  const check$2 = (value, description) => {
2298
2299
  if (typeof value != "string" || !value) throw new JWKInvalid(`${description} missing or invalid`);
2299
2300
  };
@@ -2348,7 +2349,7 @@ async function calculateJwkThumbprint(key, digestAlgorithm) {
2348
2349
  return encode$1(await digest(digestAlgorithm, thumbprintInput));
2349
2350
  }
2350
2351
  //#endregion
2351
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/jwk/embedded.js
2352
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/jwk/embedded.js
2352
2353
  async function EmbeddedJWK(protectedHeader, token) {
2353
2354
  const joseHeader = {
2354
2355
  ...protectedHeader,
@@ -2369,7 +2370,7 @@ async function EmbeddedJWK(protectedHeader, token) {
2369
2370
  return key;
2370
2371
  }
2371
2372
  //#endregion
2372
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/jwt/unsecured.js
2373
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/jwt/unsecured.js
2373
2374
  const UnsecuredJWT_base = JWTClaimsBuilder;
2374
2375
  var UnsecuredJWT = class extends UnsecuredJWT_base {
2375
2376
  encode() {
@@ -2396,7 +2397,7 @@ var UnsecuredJWT = class extends UnsecuredJWT_base {
2396
2397
  }
2397
2398
  };
2398
2399
  //#endregion
2399
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/key/import.js
2400
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/key/import.js
2400
2401
  async function importJWK(jwk, alg, options) {
2401
2402
  if (!isObject(jwk)) throw new TypeError("JWK must be an object");
2402
2403
  const normalized = normalizeJwk(jwk), extractable = validateExtractableOption(options?.extractable), { alg: jwkAlg } = normalized;
@@ -2416,7 +2417,7 @@ async function importJWK(jwk, alg, options) {
2416
2417
  }
2417
2418
  }
2418
2419
  //#endregion
2419
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/util/decode_protected_header.js
2420
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/util/decode_protected_header.js
2420
2421
  function decodeProtectedHeader(token) {
2421
2422
  let encodedProtectedHeader;
2422
2423
  if (typeof token == "string") {
@@ -2429,7 +2430,7 @@ function decodeProtectedHeader(token) {
2429
2430
  return parseJoseHeader(encodedProtectedHeader, TypeError, invalid);
2430
2431
  }
2431
2432
  //#endregion
2432
- //#region node_modules/.pnpm/jose@6.2.13/node_modules/jose/dist/webapi/util/decode_jwt.js
2433
+ //#region node_modules/.pnpm/jose@6.2.14/node_modules/jose/dist/webapi/util/decode_jwt.js
2433
2434
  function decodeJwt(jwt) {
2434
2435
  if (typeof jwt != "string") throw new JWTInvalid("JWTs must use Compact JWS serialization, JWT must be a string");
2435
2436
  const { 1: encodedPayload, length } = jwt.split(".");
@@ -2715,308 +2716,6 @@ const renderConsentPage = (view) => {
2715
2716
  /** Plain error page for a failed sign-in. ASCII message, escaped. */
2716
2717
  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>`;
2717
2718
  //#endregion
2718
- //#region src/auth/server/upstream.ts
2719
- /**
2720
- * Zendesk as the upstream identity provider, through the same public PKCE client
2721
- * the stdio flow uses. The server runs PKCE itself; no client secret exists.
2722
- * Why: `docs/decisions/oauth-authorization-server.md` ("Upstream login").
2723
- */
2724
- const ZENDESK_ACCESS_TOKEN_TTL_S = 172800;
2725
- const ZENDESK_REFRESH_TOKEN_TTL_S = 7776e3;
2726
- /** ASCII-only (auth paths end up in headers); never carries the Zendesk body. */
2727
- const upstreamAuthError = (message, status, cause) => Object.assign(new Error(message, cause === void 0 ? void 0 : { cause }), {
2728
- name: "UpstreamAuthError",
2729
- ...status === void 0 ? {} : { status }
2730
- });
2731
- const isUpstreamAuthError = (err) => err instanceof Error && err.name === "UpstreamAuthError";
2732
- const buildZendeskAuthorizeUrl = (upstream, params) => {
2733
- const url = new URL(getOAuthUrls(upstream.subdomain).authorizeUrl);
2734
- url.searchParams.set("response_type", "code");
2735
- url.searchParams.set("client_id", upstream.clientId);
2736
- url.searchParams.set("redirect_uri", params.redirectUri);
2737
- url.searchParams.set("scope", params.scope);
2738
- url.searchParams.set("state", params.state);
2739
- url.searchParams.set("code_challenge", params.codeChallenge);
2740
- url.searchParams.set("code_challenge_method", "S256");
2741
- return url.toString();
2742
- };
2743
- const toTokenSet = (result, previousRefreshToken) => ({
2744
- accessToken: result.access_token,
2745
- refreshToken: result.refresh_token ?? previousRefreshToken,
2746
- expiresAt: result.expires_in === void 0 ? void 0 : Date.now() + result.expires_in * 1e3,
2747
- scope: result.scope
2748
- });
2749
- const requestTokens = async (upstream, grant, logger) => {
2750
- const body = new URLSearchParams({
2751
- client_id: upstream.clientId,
2752
- expires_in: String(ZENDESK_ACCESS_TOKEN_TTL_S),
2753
- refresh_token_expires_in: String(ZENDESK_REFRESH_TOKEN_TTL_S),
2754
- ...grant
2755
- });
2756
- let response;
2757
- try {
2758
- response = await fetch(getOAuthUrls(upstream.subdomain).tokenUrl, {
2759
- method: "POST",
2760
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
2761
- body: body.toString(),
2762
- signal: deadlineSignal(upstream.timeoutMs ?? 3e4)
2763
- });
2764
- } catch (err) {
2765
- throw upstreamAuthError("Zendesk token endpoint unreachable.", void 0, err);
2766
- }
2767
- logger.debug("oauth_upstream_token", {
2768
- grant: grant["grant_type"],
2769
- status: response.status
2770
- });
2771
- if (!response.ok) throw upstreamAuthError(`Zendesk refused the ${grant["grant_type"]} grant (${response.status}).`, response.status);
2772
- return await response.json();
2773
- };
2774
- const exchangeZendeskCode = async (upstream, params, logger = silentLogger) => toTokenSet(await requestTokens(upstream, {
2775
- grant_type: "authorization_code",
2776
- code: params.code,
2777
- redirect_uri: params.redirectUri,
2778
- code_verifier: params.codeVerifier
2779
- }, logger));
2780
- /** Zendesk rotates on refresh: the caller must persist the returned set. */
2781
- const refreshZendeskTokens = async (upstream, refreshToken, logger = silentLogger) => toTokenSet(await requestTokens(upstream, {
2782
- grant_type: "refresh_token",
2783
- refresh_token: refreshToken
2784
- }, logger), refreshToken);
2785
- /** Zendesk is not OIDC: the account is whoever `/users/me` says the token belongs to. */
2786
- const fetchZendeskIdentity = async (upstream, accessToken) => {
2787
- const failure = (cause) => upstreamAuthError("Could not resolve the Zendesk account of the signed-in user.", void 0, cause);
2788
- let user;
2789
- try {
2790
- ({user} = await zendeskGet(upstream.subdomain, accessToken, "/users/me"));
2791
- } catch (err) {
2792
- throw failure(err);
2793
- }
2794
- if (typeof user?.id !== "number") throw failure();
2795
- return {
2796
- id: user.id,
2797
- name: user.name,
2798
- role: user.role
2799
- };
2800
- };
2801
- //#endregion
2802
- //#region src/auth/server/interactions.ts
2803
- const PENDING_TTL_MS = 6e5;
2804
- const CONSENT_MEMORY_TTL_S = 7776e3;
2805
- const sendHtml = (res, status, html) => {
2806
- res.writeHead(status, {
2807
- "Content-Type": "text/html; charset=utf-8",
2808
- "Cache-Control": "no-store"
2809
- });
2810
- res.end(html);
2811
- };
2812
- const redirect = (res, location) => {
2813
- res.writeHead(302, {
2814
- Location: location,
2815
- "Cache-Control": "no-store"
2816
- });
2817
- res.end();
2818
- };
2819
- const EXPIRED = "This sign-in has expired or was opened in another browser. Start the connection again from your MCP client.";
2820
- const consentMemoryKey = (accountId, clientId, redirectUris) => createHash("sha256").update(JSON.stringify([
2821
- accountId,
2822
- clientId,
2823
- [...redirectUris].sort(compareCodeUnits)
2824
- ])).digest("base64url");
2825
- const isExpiredLink = (err) => err instanceof Error && (err.name === "SessionNotFound" || err.name === "InvalidRequest");
2826
- const UID = /^[\w-]{1,64}$/;
2827
- /**
2828
- * The login and consent steps oidc-provider hands off to us. Login is always a
2829
- * round trip to Zendesk (fresh tokens for every grant); consent is skipped for
2830
- * trusted CIMD clients and for a client this user already approved with the
2831
- * same redirect URIs, and shown otherwise (ADR, Consent).
2832
- */
2833
- const createInteractionRoutes = (deps) => {
2834
- const { provider, upstream, issuer } = deps;
2835
- const logger = deps.logger ?? silentLogger;
2836
- const callbackUrl = `${issuer}/oauth/callback`;
2837
- const verifiers = createExpiringMap();
2838
- const upstreamTokens = createExpiringMap();
2839
- const finish = (req, res, result) => provider.interactionFinished(req, res, result, { mergeWithLastSubmission: true });
2840
- const deny = (req, res, description) => finish(req, res, {
2841
- error: "access_denied",
2842
- error_description: description
2843
- });
2844
- const startUpstreamLogin = (res, uid) => {
2845
- const verifier = generateCodeVerifier();
2846
- verifiers.set(uid, verifier, PENDING_TTL_MS);
2847
- redirect(res, buildZendeskAuthorizeUrl(upstream, {
2848
- redirectUri: callbackUrl,
2849
- scope: requestedScope(deps.readOnly),
2850
- state: uid,
2851
- codeChallenge: generateCodeChallenge(verifier)
2852
- }));
2853
- };
2854
- const approve = async (req, res, details, { accountId, clientId }, tokens) => {
2855
- const grant = new provider.Grant({
2856
- accountId,
2857
- clientId
2858
- });
2859
- const missing = details.prompt.details;
2860
- if (missing.missingOIDCScope?.length) grant.addOIDCScope(missing.missingOIDCScope.join(" "));
2861
- for (const [indicator, scopes] of Object.entries(missing.missingResourceScopes ?? {})) grant.addResourceScope(indicator, scopes.join(" "));
2862
- const grantId = await grant.save();
2863
- await deps.zendeskGrants.save(grantId, tokens);
2864
- logger.info("oauth_grant_created", { clientId });
2865
- await finish(req, res, { consent: { grantId } });
2866
- };
2867
- const consentContext = async (details) => {
2868
- const accountId = String(details.session?.accountId);
2869
- const clientId = String(details.params["client_id"]);
2870
- const client = await provider.Client.find(clientId);
2871
- const redirectUris = client?.redirectUris ?? [];
2872
- return {
2873
- client,
2874
- accountId,
2875
- clientId,
2876
- pendingKey: `${accountId}|${clientId}`,
2877
- memoryKey: consentMemoryKey(accountId, clientId, redirectUris),
2878
- redirectUri: String(details.params["redirect_uri"]),
2879
- redirectUris
2880
- };
2881
- };
2882
- const showInteraction = async (req, res, uid) => {
2883
- const details = await provider.interactionDetails(req, res);
2884
- if (details.prompt.name === "login") {
2885
- startUpstreamLogin(res, uid);
2886
- return;
2887
- }
2888
- const context = await consentContext(details);
2889
- if (canSkipConsent({
2890
- clientId: context.clientId,
2891
- redirectUri: context.redirectUri,
2892
- registeredRedirectUris: context.redirectUris
2893
- }, deps.trustedClients) || await deps.consentMemory.get(context.memoryKey) === true) {
2894
- const tokens = upstreamTokens.take(context.pendingKey);
2895
- if (!tokens) return deny(req, res, "The Zendesk sign-in expired. Try again.");
2896
- return approve(req, res, details, context, tokens);
2897
- }
2898
- const missing = details.prompt.details;
2899
- const scopes = [...new Set(Object.values(missing.missingResourceScopes ?? {}).flat())];
2900
- sendHtml(res, 200, renderConsentPage({
2901
- uid,
2902
- clientName: context.client?.clientName,
2903
- clientId: context.clientId,
2904
- redirectUri: context.redirectUri,
2905
- scopes,
2906
- subdomain: upstream.subdomain
2907
- }));
2908
- };
2909
- const completeUpstreamLogin = async (req, res, uid, url) => {
2910
- const details = await provider.interactionDetails(req, res);
2911
- const verifier = verifiers.take(uid);
2912
- const code = url.searchParams.get("code");
2913
- if (!verifier || !code) return deny(req, res, "The Zendesk sign-in was cancelled or refused.");
2914
- try {
2915
- const tokens = await exchangeZendeskCode(upstream, {
2916
- code,
2917
- redirectUri: callbackUrl,
2918
- codeVerifier: verifier
2919
- }, logger);
2920
- const accountId = `zendesk:${(await fetchZendeskIdentity(upstream, tokens.accessToken)).id}`;
2921
- upstreamTokens.set(`${accountId}|${String(details.params["client_id"])}`, tokens, PENDING_TTL_MS);
2922
- await finish(req, res, { login: { accountId } });
2923
- } catch (err) {
2924
- if (!isUpstreamAuthError(err)) throw err;
2925
- logger.warn("oauth_upstream_login_failed", { error: err.message });
2926
- await deny(req, res, err.message);
2927
- }
2928
- };
2929
- const confirm = async (req, res) => {
2930
- const details = await provider.interactionDetails(req, res);
2931
- if (details.prompt.name !== "consent") return sendHtml(res, 400, renderErrorPage("Sign-in failed", EXPIRED));
2932
- const context = await consentContext(details);
2933
- const tokens = upstreamTokens.take(context.pendingKey);
2934
- if (!tokens) return deny(req, res, "The Zendesk sign-in expired. Try again.");
2935
- await deps.consentMemory.set(context.memoryKey, true, CONSENT_MEMORY_TTL_S);
2936
- return approve(req, res, details, context, tokens);
2937
- };
2938
- const routes = async (req, res, url) => {
2939
- const [, , uid = "", action] = url.pathname.split("/");
2940
- if (!UID.test(uid)) return sendHtml(res, 404, renderErrorPage("Not found", EXPIRED));
2941
- if (req.method === "GET" && action === void 0) return showInteraction(req, res, uid);
2942
- if (req.method === "GET" && action === "callback") return completeUpstreamLogin(req, res, uid, url);
2943
- if (req.method === "POST" && action === "confirm") return confirm(req, res);
2944
- if (req.method === "POST" && action === "abort") {
2945
- await provider.interactionDetails(req, res);
2946
- return deny(req, res, "The user declined access.");
2947
- }
2948
- return sendHtml(res, 404, renderErrorPage("Not found", EXPIRED));
2949
- };
2950
- return {
2951
- /** `/oauth/callback`: the one redirect URI registered at Zendesk. */
2952
- upstreamCallback: (res, url) => {
2953
- const uid = url.searchParams.get("state") ?? "";
2954
- if (!UID.test(uid)) {
2955
- sendHtml(res, 400, renderErrorPage("Sign-in failed", EXPIRED));
2956
- return;
2957
- }
2958
- const next = new URL(`/interaction/${uid}/callback`, issuer);
2959
- const code = url.searchParams.get("code");
2960
- if (code) next.searchParams.set("code", code);
2961
- redirect(res, `${next.pathname}${next.search}`);
2962
- },
2963
- /** `/interaction/<uid>[/callback|/confirm|/abort]`. */
2964
- handle: async (req, res, url) => {
2965
- try {
2966
- await routes(req, res, url);
2967
- } catch (err) {
2968
- if (!isExpiredLink(err)) throw err;
2969
- if (!res.headersSent) sendHtml(res, 400, renderErrorPage("Sign-in failed", EXPIRED));
2970
- }
2971
- }
2972
- };
2973
- };
2974
- //#endregion
2975
- //#region src/auth/server/keys.ts
2976
- const MIN_SECRET_BYTES = 32;
2977
- const ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
2978
- const derive = (master, purpose, length = 32) => Buffer.from(hkdfSync("sha256", master, Buffer.alloc(0), `zendesk-mcp/${purpose}/v1`, length));
2979
- const kidFor = (master, purpose) => createHash("sha256").update(derive(master, `kid/${purpose}`, 16)).digest("base64url").slice(0, 16);
2980
- /**
2981
- * Parse `OAUTH_MASTER_SECRET`: base64 entries, comma-separated, newest first.
2982
- * Errors never echo the value.
2983
- */
2984
- const parseSecretList = (raw) => {
2985
- const entries = raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => Buffer.from(entry, "base64"));
2986
- const [first, ...rest] = entries;
2987
- if (!first) throw new Error("OAuth master secret is empty.");
2988
- if (entries.some((secret) => secret.length < 32)) throw new Error(`OAuth master secret must decode to at least 32 bytes of base64 (generate one with: openssl rand -base64 32).`);
2989
- return [first, ...rest];
2990
- };
2991
- const deriveKeySet = (master) => {
2992
- return {
2993
- signingJwk: {
2994
- ...createPrivateKey({
2995
- key: Buffer.concat([ED25519_PKCS8_PREFIX, derive(master, "signing")]),
2996
- format: "der",
2997
- type: "pkcs8"
2998
- }).export({ format: "jwk" }),
2999
- kid: kidFor(master, "signing"),
3000
- use: "sig",
3001
- alg: "EdDSA"
3002
- },
3003
- accessToken: {
3004
- kid: kidFor(master, "access-token"),
3005
- key: createSecretKey(derive(master, "access-token"))
3006
- },
3007
- atRest: {
3008
- kid: kidFor(master, "at-rest"),
3009
- key: createSecretKey(derive(master, "at-rest"))
3010
- },
3011
- cookieKey: derive(master, "cookie").toString("base64url"),
3012
- requestState: derive(master, "request-state")
3013
- };
3014
- };
3015
- const deriveKeyRing = (raw) => {
3016
- const [first, ...rest] = parseSecretList(raw);
3017
- return [deriveKeySet(first), ...rest.map(deriveKeySet)];
3018
- };
3019
- //#endregion
3020
2719
  //#region node_modules/.pnpm/oidc-provider@9.13.0_supports-color@7.2.0/node_modules/oidc-provider/lib/helpers/attention.js
3021
2720
  const SET_BOLD_YELLOW_TEXT = "\x1B[33;1m";
3022
2721
  const SET_BOLD_RED_TEXT = "\x1B[31;1m";
@@ -29313,7 +29012,7 @@ function input(action, csrfToken, code, charset) {
29313
29012
  ${code ? `value="${htmlSafe(code)}" ` : ""}${attributes}type="text" name="user_code" placeholder="Enter code" onfocus="this.select(); this.onfocus = undefined;" autofocus autocomplete="off"></input>
29314
29013
  </form>`;
29315
29014
  }
29316
- function confirm$1(action, csrfToken, code) {
29015
+ function confirm$2(action, csrfToken, code) {
29317
29016
  return `<form id="op.deviceConfirmForm" method="post" action="${action}">
29318
29017
  <input type="hidden" name="xsrf" value="${csrfToken}"/>
29319
29018
  <input type="hidden" name="user_code" value="${htmlSafe(code)}"/>
@@ -32480,7 +32179,7 @@ const init = [
32480
32179
  });
32481
32180
  }
32482
32181
  ];
32483
- const confirm = [
32182
+ const confirm$1 = [
32484
32183
  noCache,
32485
32184
  sessionHandler,
32486
32185
  urlencoded,
@@ -32597,7 +32296,7 @@ const post = [
32597
32296
  const client = await ctx.oidc.provider.Client.find(code.clientId);
32598
32297
  if (!client) throw new InvalidClient("client is invalid", "client not found");
32599
32298
  ctx.oidc.entity("Client", client);
32600
- await userCodeConfirmSource(ctx, confirm$1(ctx.oidc.urlFor("code_verification"), ctx.oidc.session.state.secret, userCode), client, code.deviceInfo, denormalize(normalized, mask));
32299
+ await userCodeConfirmSource(ctx, confirm$2(ctx.oidc.urlFor("code_verification"), ctx.oidc.session.state.secret, userCode), client, code.deviceInfo, denormalize(normalized, mask));
32601
32300
  return;
32602
32301
  }
32603
32302
  code.inFlight = true;
@@ -33230,7 +32929,7 @@ function initializeApp() {
33230
32929
  post$2("introspection", routes.introspection, getErrorHandler("introspection.error"), CORS.client, ...introspection_default);
33231
32930
  options("cors.introspection", routes.introspection, CORS.client, CORS.respond);
33232
32931
  }
33233
- post$2("end_session_confirm", `${routes.end_session}/confirm`, getErrorHandler("end_session_confirm.error"), ...confirm);
32932
+ post$2("end_session_confirm", `${routes.end_session}/confirm`, getErrorHandler("end_session_confirm.error"), ...confirm$1);
33234
32933
  if (features.rpInitiatedLogout.enabled) {
33235
32934
  if (enableHttpPostMethods) post$2("end_session", routes.end_session, getErrorHandler("end_session.error"), ...init);
33236
32935
  get$3("end_session", routes.end_session, getErrorHandler("end_session.error"), ...init);
@@ -36059,10 +35758,10 @@ const PERSISTENT_MODELS = /* @__PURE__ */ new Set([
36059
35758
  "RefreshToken",
36060
35759
  "ZendeskTokens"
36061
35760
  ]);
36062
- const encoder = new TextEncoder();
35761
+ const encoder$2 = new TextEncoder();
36063
35762
  const decoder$1 = new TextDecoder();
36064
35763
  const hashId = (id) => createHash("sha256").update(id).digest("base64url");
36065
- const seal = (ring, value, expiresAt) => new CompactEncrypt(encoder.encode(JSON.stringify(value))).setProtectedHeader({
35764
+ const seal = (ring, value, expiresAt) => new CompactEncrypt(encoder$2.encode(JSON.stringify(value))).setProtectedHeader({
36066
35765
  alg: "dir",
36067
35766
  enc: "A256GCM",
36068
35767
  kid: ring[0].atRest.kid,
@@ -36079,7 +35778,7 @@ const unseal = async (ring, sealed) => {
36079
35778
  expiresAt
36080
35779
  };
36081
35780
  };
36082
- const open = (ring, sealed) => unseal(ring, sealed).catch(() => void 0);
35781
+ const open$1 = (ring, sealed) => unseal(ring, sealed).catch(() => void 0);
36083
35782
  const secondsUntil = (exp) => exp === void 0 ? void 0 : Math.max(exp - Math.floor(Date.now() / 1e3), 1);
36084
35783
  /**
36085
35784
  * One named collection of whole-payload-encrypted records. A record read under
@@ -36092,7 +35791,7 @@ const createSealedCollection = (store, name, ring) => {
36092
35791
  get: async (id) => {
36093
35792
  const sealed = await store.get(key(id));
36094
35793
  if (sealed === void 0) return void 0;
36095
- const record = await open(ring, sealed);
35794
+ const record = await open$1(ring, sealed);
36096
35795
  if (record && !record.current) {
36097
35796
  const { expiresAt } = record;
36098
35797
  const ttlMs = expiresAt === void 0 ? void 0 : expiresAt - Date.now();
@@ -36182,6 +35881,92 @@ const openStore = (uri) => {
36182
35881
  throw new Error(`Unsupported OAuth store scheme "${protocol}". Use file:// or memory://.`);
36183
35882
  };
36184
35883
  //#endregion
35884
+ //#region src/auth/server/upstream.ts
35885
+ /**
35886
+ * Zendesk as the upstream identity provider, through the same public PKCE client
35887
+ * the stdio flow uses. The server runs PKCE itself; no client secret exists.
35888
+ * Why: `docs/decisions/oauth-authorization-server.md` ("Upstream login").
35889
+ */
35890
+ const ZENDESK_ACCESS_TOKEN_TTL_S = 172800;
35891
+ const ZENDESK_REFRESH_TOKEN_TTL_S = 7776e3;
35892
+ /** The Zendesk role that opens the `/mcp/admin` back-office. */
35893
+ const ZENDESK_ADMIN_ROLE = "admin";
35894
+ /** ASCII-only (auth paths end up in headers); never carries the Zendesk body. */
35895
+ const upstreamAuthError = (message, status, cause) => Object.assign(new Error(message, cause === void 0 ? void 0 : { cause }), {
35896
+ name: "UpstreamAuthError",
35897
+ ...status === void 0 ? {} : { status }
35898
+ });
35899
+ const isUpstreamAuthError = (err) => err instanceof Error && err.name === "UpstreamAuthError";
35900
+ const buildZendeskAuthorizeUrl = (upstream, params) => {
35901
+ const url = new URL(getOAuthUrls(upstream.subdomain).authorizeUrl);
35902
+ url.searchParams.set("response_type", "code");
35903
+ url.searchParams.set("client_id", upstream.clientId);
35904
+ url.searchParams.set("redirect_uri", params.redirectUri);
35905
+ url.searchParams.set("scope", params.scope);
35906
+ url.searchParams.set("state", params.state);
35907
+ url.searchParams.set("code_challenge", params.codeChallenge);
35908
+ url.searchParams.set("code_challenge_method", "S256");
35909
+ return url.toString();
35910
+ };
35911
+ const toTokenSet = (result, previousRefreshToken) => ({
35912
+ accessToken: result.access_token,
35913
+ refreshToken: result.refresh_token ?? previousRefreshToken,
35914
+ expiresAt: result.expires_in === void 0 ? void 0 : Date.now() + result.expires_in * 1e3,
35915
+ scope: result.scope
35916
+ });
35917
+ const requestTokens = async (upstream, grant, logger) => {
35918
+ const body = new URLSearchParams({
35919
+ client_id: upstream.clientId,
35920
+ expires_in: String(ZENDESK_ACCESS_TOKEN_TTL_S),
35921
+ refresh_token_expires_in: String(ZENDESK_REFRESH_TOKEN_TTL_S),
35922
+ ...grant
35923
+ });
35924
+ let response;
35925
+ try {
35926
+ response = await fetch(getOAuthUrls(upstream.subdomain).tokenUrl, {
35927
+ method: "POST",
35928
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
35929
+ body: body.toString(),
35930
+ signal: deadlineSignal(upstream.timeoutMs ?? 3e4)
35931
+ });
35932
+ } catch (err) {
35933
+ throw upstreamAuthError("Zendesk token endpoint unreachable.", void 0, err);
35934
+ }
35935
+ logger.debug("oauth_upstream_token", {
35936
+ grant: grant["grant_type"],
35937
+ status: response.status
35938
+ });
35939
+ if (!response.ok) throw upstreamAuthError(`Zendesk refused the ${grant["grant_type"]} grant (${response.status}).`, response.status);
35940
+ return await response.json();
35941
+ };
35942
+ const exchangeZendeskCode = async (upstream, params, logger = silentLogger) => toTokenSet(await requestTokens(upstream, {
35943
+ grant_type: "authorization_code",
35944
+ code: params.code,
35945
+ redirect_uri: params.redirectUri,
35946
+ code_verifier: params.codeVerifier
35947
+ }, logger));
35948
+ /** Zendesk rotates on refresh: the caller must persist the returned set. */
35949
+ const refreshZendeskTokens = async (upstream, refreshToken, logger = silentLogger) => toTokenSet(await requestTokens(upstream, {
35950
+ grant_type: "refresh_token",
35951
+ refresh_token: refreshToken
35952
+ }, logger), refreshToken);
35953
+ /** Zendesk is not OIDC: the account is whoever `/users/me` says the token belongs to. */
35954
+ const fetchZendeskIdentity = async (upstream, accessToken) => {
35955
+ const failure = (cause) => upstreamAuthError("Could not resolve the Zendesk account of the signed-in user.", void 0, cause);
35956
+ let user;
35957
+ try {
35958
+ ({user} = await zendeskGet(upstream.subdomain, accessToken, "/users/me"));
35959
+ } catch (err) {
35960
+ throw failure(err);
35961
+ }
35962
+ if (typeof user?.id !== "number") throw failure();
35963
+ return {
35964
+ id: user.id,
35965
+ name: user.name,
35966
+ role: user.role
35967
+ };
35968
+ };
35969
+ //#endregion
36185
35970
  //#region src/auth/server/zendesk-grant.ts
36186
35971
  const ZENDESK_REFRESH_MARGIN_MS = 3e5;
36187
35972
  const grantUnavailableError = () => Object.assign(/* @__PURE__ */ new Error("The Zendesk authorization behind this grant is no longer valid."), { name: "ZendeskGrantUnavailable" });
@@ -36232,6 +36017,8 @@ const ACCESS_TOKEN_TTL_S = 3600;
36232
36017
  const GRANT_TTL_S = 7776e3;
36233
36018
  const SESSION_TTL_S = 43200;
36234
36019
  const INTERACTION_TTL_S = 600;
36020
+ /** ASCII only: it reaches the client in a token endpoint error. */
36021
+ const ADMIN_ONLY_MESSAGE = "This connector is reserved for Zendesk administrators, and your Zendesk role is not admin.";
36235
36022
  /** Scopes the server advertises and grants; `offline_access` is for clients (ChatGPT) that ask for it. */
36236
36023
  const RESOURCE_SCOPES = ["read", "write"];
36237
36024
  const policyWithUpstreamLogin = () => {
@@ -36297,10 +36084,10 @@ const buildProvider = (options) => {
36297
36084
  defaultResource: () => resource,
36298
36085
  useGrantedResource: () => true,
36299
36086
  getResourceServerInfo: (_ctx, indicator) => {
36300
- if (indicator !== resource) throw new InvalidTarget();
36087
+ if (indicator !== resource && indicator !== options.adminResource) throw new InvalidTarget();
36301
36088
  return {
36302
36089
  scope: scopes,
36303
- audience: resource,
36090
+ audience: indicator,
36304
36091
  accessTokenTTL: ACCESS_TOKEN_TTL_S,
36305
36092
  accessTokenFormat: "jwt",
36306
36093
  jwt: { encrypt: {
@@ -36316,8 +36103,10 @@ const buildProvider = (options) => {
36316
36103
  extraTokenClaims: async (_ctx, token) => {
36317
36104
  if (token.kind !== "AccessToken" || !token.grantId) return void 0;
36318
36105
  try {
36106
+ const zd = await options.zendeskGrants.accessToken(token.grantId);
36107
+ if (token.resourceServer?.audience === options.adminResource && !await options.isZendeskAdmin(zd)) throw new AccessDenied(ADMIN_ONLY_MESSAGE);
36319
36108
  return {
36320
- zd: await options.zendeskGrants.accessToken(token.grantId),
36109
+ zd,
36321
36110
  gid: token.grantId
36322
36111
  };
36323
36112
  } catch (err) {
@@ -36349,6 +36138,230 @@ const buildProvider = (options) => {
36349
36138
  return provider;
36350
36139
  };
36351
36140
  //#endregion
36141
+ //#region src/auth/server/interactions.ts
36142
+ const PENDING_TTL_MS = 6e5;
36143
+ const CONSENT_MEMORY_TTL_S = 7776e3;
36144
+ const sendHtml = (res, status, html) => {
36145
+ res.writeHead(status, {
36146
+ "Content-Type": "text/html; charset=utf-8",
36147
+ "Cache-Control": "no-store"
36148
+ });
36149
+ res.end(html);
36150
+ };
36151
+ const redirect = (res, location) => {
36152
+ res.writeHead(302, {
36153
+ Location: location,
36154
+ "Cache-Control": "no-store"
36155
+ });
36156
+ res.end();
36157
+ };
36158
+ const EXPIRED = "This sign-in has expired or was opened in another browser. Start the connection again from your MCP client.";
36159
+ const consentMemoryKey = (accountId, clientId, redirectUris) => createHash("sha256").update(JSON.stringify([
36160
+ accountId,
36161
+ clientId,
36162
+ [...redirectUris].sort(compareCodeUnits)
36163
+ ])).digest("base64url");
36164
+ const isExpiredLink = (err) => err instanceof Error && (err.name === "SessionNotFound" || err.name === "InvalidRequest");
36165
+ const UID = /^[\w-]{1,64}$/;
36166
+ /**
36167
+ * The login and consent steps oidc-provider hands off to us. Login is always a
36168
+ * round trip to Zendesk (fresh tokens for every grant); consent is skipped for
36169
+ * trusted CIMD clients and for a client this user already approved with the
36170
+ * same redirect URIs, and shown otherwise (ADR, Consent).
36171
+ */
36172
+ const createInteractionRoutes = (deps) => {
36173
+ const { provider, upstream, issuer } = deps;
36174
+ const logger = deps.logger ?? silentLogger;
36175
+ const callbackUrl = `${issuer}/oauth/callback`;
36176
+ const verifiers = createExpiringMap();
36177
+ const upstreamTokens = createExpiringMap();
36178
+ const requestsAdmin = (resource) => (Array.isArray(resource) ? resource : [resource]).includes(deps.adminResource);
36179
+ const finish = (req, res, result) => provider.interactionFinished(req, res, result, { mergeWithLastSubmission: true });
36180
+ const deny = (req, res, description) => finish(req, res, {
36181
+ error: "access_denied",
36182
+ error_description: description
36183
+ });
36184
+ const startUpstreamLogin = (res, uid) => {
36185
+ const verifier = generateCodeVerifier();
36186
+ verifiers.set(uid, verifier, PENDING_TTL_MS);
36187
+ redirect(res, buildZendeskAuthorizeUrl(upstream, {
36188
+ redirectUri: callbackUrl,
36189
+ scope: requestedScope(deps.readOnly),
36190
+ state: uid,
36191
+ codeChallenge: generateCodeChallenge(verifier)
36192
+ }));
36193
+ };
36194
+ const approve = async (req, res, details, { accountId, clientId }, tokens) => {
36195
+ const grant = new provider.Grant({
36196
+ accountId,
36197
+ clientId
36198
+ });
36199
+ const missing = details.prompt.details;
36200
+ if (missing.missingOIDCScope?.length) grant.addOIDCScope(missing.missingOIDCScope.join(" "));
36201
+ for (const [indicator, scopes] of Object.entries(missing.missingResourceScopes ?? {})) grant.addResourceScope(indicator, scopes.join(" "));
36202
+ const grantId = await grant.save();
36203
+ await deps.zendeskGrants.save(grantId, tokens);
36204
+ logger.info("oauth_grant_created", { clientId });
36205
+ await finish(req, res, { consent: { grantId } });
36206
+ };
36207
+ const consentContext = async (details) => {
36208
+ const accountId = String(details.session?.accountId);
36209
+ const clientId = String(details.params["client_id"]);
36210
+ const client = await provider.Client.find(clientId);
36211
+ const redirectUris = client?.redirectUris ?? [];
36212
+ return {
36213
+ client,
36214
+ accountId,
36215
+ clientId,
36216
+ pendingKey: `${accountId}|${clientId}`,
36217
+ memoryKey: consentMemoryKey(accountId, clientId, redirectUris),
36218
+ redirectUri: String(details.params["redirect_uri"]),
36219
+ redirectUris
36220
+ };
36221
+ };
36222
+ const showInteraction = async (req, res, uid) => {
36223
+ const details = await provider.interactionDetails(req, res);
36224
+ if (details.prompt.name === "login") {
36225
+ startUpstreamLogin(res, uid);
36226
+ return;
36227
+ }
36228
+ const context = await consentContext(details);
36229
+ if (canSkipConsent({
36230
+ clientId: context.clientId,
36231
+ redirectUri: context.redirectUri,
36232
+ registeredRedirectUris: context.redirectUris
36233
+ }, deps.trustedClients) || await deps.consentMemory.get(context.memoryKey) === true) {
36234
+ const tokens = upstreamTokens.take(context.pendingKey);
36235
+ if (!tokens) return deny(req, res, "The Zendesk sign-in expired. Try again.");
36236
+ return approve(req, res, details, context, tokens);
36237
+ }
36238
+ const missing = details.prompt.details;
36239
+ const scopes = [...new Set(Object.values(missing.missingResourceScopes ?? {}).flat())];
36240
+ sendHtml(res, 200, renderConsentPage({
36241
+ uid,
36242
+ clientName: context.client?.clientName,
36243
+ clientId: context.clientId,
36244
+ redirectUri: context.redirectUri,
36245
+ scopes,
36246
+ subdomain: upstream.subdomain
36247
+ }));
36248
+ };
36249
+ const completeUpstreamLogin = async (req, res, uid, url) => {
36250
+ const details = await provider.interactionDetails(req, res);
36251
+ const verifier = verifiers.take(uid);
36252
+ const code = url.searchParams.get("code");
36253
+ if (!verifier || !code) return deny(req, res, "The Zendesk sign-in was cancelled or refused.");
36254
+ try {
36255
+ const tokens = await exchangeZendeskCode(upstream, {
36256
+ code,
36257
+ redirectUri: callbackUrl,
36258
+ codeVerifier: verifier
36259
+ }, logger);
36260
+ const identity = await fetchZendeskIdentity(upstream, tokens.accessToken);
36261
+ if (requestsAdmin(details.params["resource"]) && identity.role !== "admin") {
36262
+ logger.info("oauth_admin_sign_in_refused", { role: identity.role ?? "unknown" });
36263
+ return deny(req, res, ADMIN_ONLY_MESSAGE);
36264
+ }
36265
+ const accountId = `zendesk:${identity.id}`;
36266
+ upstreamTokens.set(`${accountId}|${String(details.params["client_id"])}`, tokens, PENDING_TTL_MS);
36267
+ await finish(req, res, { login: { accountId } });
36268
+ } catch (err) {
36269
+ if (!isUpstreamAuthError(err)) throw err;
36270
+ logger.warn("oauth_upstream_login_failed", { error: err.message });
36271
+ await deny(req, res, err.message);
36272
+ }
36273
+ };
36274
+ const confirm = async (req, res) => {
36275
+ const details = await provider.interactionDetails(req, res);
36276
+ if (details.prompt.name !== "consent") return sendHtml(res, 400, renderErrorPage("Sign-in failed", EXPIRED));
36277
+ const context = await consentContext(details);
36278
+ const tokens = upstreamTokens.take(context.pendingKey);
36279
+ if (!tokens) return deny(req, res, "The Zendesk sign-in expired. Try again.");
36280
+ await deps.consentMemory.set(context.memoryKey, true, CONSENT_MEMORY_TTL_S);
36281
+ return approve(req, res, details, context, tokens);
36282
+ };
36283
+ const routes = async (req, res, url) => {
36284
+ const [, , uid = "", action] = url.pathname.split("/");
36285
+ if (!UID.test(uid)) return sendHtml(res, 404, renderErrorPage("Not found", EXPIRED));
36286
+ if (req.method === "GET" && action === void 0) return showInteraction(req, res, uid);
36287
+ if (req.method === "GET" && action === "callback") return completeUpstreamLogin(req, res, uid, url);
36288
+ if (req.method === "POST" && action === "confirm") return confirm(req, res);
36289
+ if (req.method === "POST" && action === "abort") {
36290
+ await provider.interactionDetails(req, res);
36291
+ return deny(req, res, "The user declined access.");
36292
+ }
36293
+ return sendHtml(res, 404, renderErrorPage("Not found", EXPIRED));
36294
+ };
36295
+ return {
36296
+ /** `/oauth/callback`: the one redirect URI registered at Zendesk. */
36297
+ upstreamCallback: (res, url) => {
36298
+ const uid = url.searchParams.get("state") ?? "";
36299
+ if (!UID.test(uid)) {
36300
+ sendHtml(res, 400, renderErrorPage("Sign-in failed", EXPIRED));
36301
+ return;
36302
+ }
36303
+ const next = new URL(`/interaction/${uid}/callback`, issuer);
36304
+ const code = url.searchParams.get("code");
36305
+ if (code) next.searchParams.set("code", code);
36306
+ redirect(res, `${next.pathname}${next.search}`);
36307
+ },
36308
+ /** `/interaction/<uid>[/callback|/confirm|/abort]`. */
36309
+ handle: async (req, res, url) => {
36310
+ try {
36311
+ await routes(req, res, url);
36312
+ } catch (err) {
36313
+ if (!isExpiredLink(err)) throw err;
36314
+ if (!res.headersSent) sendHtml(res, 400, renderErrorPage("Sign-in failed", EXPIRED));
36315
+ }
36316
+ }
36317
+ };
36318
+ };
36319
+ //#endregion
36320
+ //#region src/auth/server/keys.ts
36321
+ const MIN_SECRET_BYTES = 32;
36322
+ const ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
36323
+ const derive = (master, purpose, length = 32) => Buffer.from(hkdfSync("sha256", master, Buffer.alloc(0), `zendesk-mcp/${purpose}/v1`, length));
36324
+ const kidFor = (master, purpose) => createHash("sha256").update(derive(master, `kid/${purpose}`, 16)).digest("base64url").slice(0, 16);
36325
+ /**
36326
+ * Parse `OAUTH_MASTER_SECRET`: base64 entries, comma-separated, newest first.
36327
+ * Errors never echo the value.
36328
+ */
36329
+ const parseSecretList = (raw) => {
36330
+ const entries = raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => Buffer.from(entry, "base64"));
36331
+ const [first, ...rest] = entries;
36332
+ if (!first) throw new Error("OAuth master secret is empty.");
36333
+ if (entries.some((secret) => secret.length < 32)) throw new Error(`OAuth master secret must decode to at least 32 bytes of base64 (generate one with: openssl rand -base64 32).`);
36334
+ return [first, ...rest];
36335
+ };
36336
+ const deriveKeySet = (master) => {
36337
+ return {
36338
+ signingJwk: {
36339
+ ...createPrivateKey({
36340
+ key: Buffer.concat([ED25519_PKCS8_PREFIX, derive(master, "signing")]),
36341
+ format: "der",
36342
+ type: "pkcs8"
36343
+ }).export({ format: "jwk" }),
36344
+ kid: kidFor(master, "signing"),
36345
+ use: "sig",
36346
+ alg: "EdDSA"
36347
+ },
36348
+ accessToken: {
36349
+ kid: kidFor(master, "access-token"),
36350
+ key: createSecretKey(derive(master, "access-token"))
36351
+ },
36352
+ atRest: {
36353
+ kid: kidFor(master, "at-rest"),
36354
+ key: createSecretKey(derive(master, "at-rest"))
36355
+ },
36356
+ cookieKey: derive(master, "cookie").toString("base64url"),
36357
+ requestState: derive(master, "request-state")
36358
+ };
36359
+ };
36360
+ const deriveKeyRing = (raw) => {
36361
+ const [first, ...rest] = parseSecretList(raw);
36362
+ return [deriveKeySet(first), ...rest.map(deriveKeySet)];
36363
+ };
36364
+ //#endregion
36352
36365
  //#region src/auth/server/secret.ts
36353
36366
  const MASTER_SECRET_FILE_NAME = "oauth-master-secret";
36354
36367
  const isWindows = process.platform === "win32";
@@ -36439,6 +36452,7 @@ const createAuthorizationServer = (options) => {
36439
36452
  const { config, issuer, ring, persistent } = options;
36440
36453
  const logger = options.logger ?? silentLogger;
36441
36454
  const resource = `${issuer}/mcp`;
36455
+ const adminResource = `${issuer}/mcp/admin`;
36442
36456
  const upstream = {
36443
36457
  subdomain: config.subdomain,
36444
36458
  clientId: config.oauthClientId
@@ -36462,6 +36476,16 @@ const createAuthorizationServer = (options) => {
36462
36476
  const provider = buildProvider({
36463
36477
  issuer,
36464
36478
  resource,
36479
+ adminResource,
36480
+ isZendeskAdmin: async (zendeskAccessToken) => {
36481
+ try {
36482
+ return (await fetchZendeskIdentity(upstream, zendeskAccessToken)).role === ZENDESK_ADMIN_ROLE;
36483
+ } catch (err) {
36484
+ const { cause } = err;
36485
+ if (cause instanceof ZendeskApiError && cause.status === 401) throw grantUnavailableError();
36486
+ throw err;
36487
+ }
36488
+ },
36465
36489
  ring,
36466
36490
  adapter: createAdapterFactory({ persistent }, ring),
36467
36491
  zendeskGrants,
@@ -36485,6 +36509,7 @@ const createAuthorizationServer = (options) => {
36485
36509
  provider,
36486
36510
  upstream,
36487
36511
  issuer,
36512
+ adminResource,
36488
36513
  readOnly: config.readOnly,
36489
36514
  zendeskGrants,
36490
36515
  consentMemory: createSealedCollection(persistent, "Consent", ring),
@@ -36499,16 +36524,19 @@ const createAuthorizationServer = (options) => {
36499
36524
  const { plaintext } = await compactDecrypt(bearer, keySet.accessToken.key);
36500
36525
  return asVerified(JSON.parse(decoder.decode(plaintext)), issuer);
36501
36526
  };
36527
+ const metadataOf = (of) => ({
36528
+ resource: of,
36529
+ authorization_servers: [issuer],
36530
+ bearer_methods_supported: ["header"],
36531
+ scopes_supported: supportedScopes(config.readOnly)
36532
+ });
36502
36533
  return {
36503
36534
  issuer,
36504
36535
  resource,
36536
+ adminResource,
36505
36537
  requestStateKey: ring[0].requestState,
36506
- protectedResourceMetadata: {
36507
- resource,
36508
- authorization_servers: [issuer],
36509
- bearer_methods_supported: ["header"],
36510
- scopes_supported: supportedScopes(config.readOnly)
36511
- },
36538
+ protectedResourceMetadata: metadataOf(resource),
36539
+ adminProtectedResourceMetadata: metadataOf(adminResource),
36512
36540
  handle: async (req, res) => {
36513
36541
  const url = new URL(req.url ?? "/", issuer);
36514
36542
  if (url.pathname === "/oauth/callback" && req.method === "GET") {
@@ -36568,6 +36596,2359 @@ const prepareAuthorizationServer = async (config, logger = silentLogger) => {
36568
36596
  });
36569
36597
  };
36570
36598
  //#endregion
36599
+ //#region node_modules/.pnpm/@modelcontextprotocol+core@2.4.0/node_modules/@modelcontextprotocol/core/dist/ext/skills/index.mjs
36600
+ /**
36601
+ * Wire constants for the MCP Skills extension (SEP-2640).
36602
+ *
36603
+ * Runtime-neutral: this module imports nothing.
36604
+ */
36605
+ /** The identifier a server advertises in `ServerCapabilities.extensions`. */
36606
+ const SKILLS_EXTENSION_ID = "io.modelcontextprotocol/skills";
36607
+ /** Enumerates the skills a server serves. */
36608
+ const SKILLS_LIST_METHOD = "skills/list";
36609
+ /** Returns one skill's entry by its `SKILL.md` URI, listed or not. */
36610
+ const SKILLS_GET_METHOD = "skills/get";
36611
+ /** Lists the direct children of a directory resource (gated by `directoryRead`). */
36612
+ const RESOURCES_DIRECTORY_READ_METHOD = "resources/directory/read";
36613
+ /** The `mimeType` of a directory resource. */
36614
+ const DIRECTORY_MIME_TYPE = "inode/directory";
36615
+ /**
36616
+ * Per-skill interoperability limits. Hosts MUST accept skills up to these;
36617
+ * servers SHOULD NOT publish skills over them.
36618
+ */
36619
+ const SKILL_LIMITS = Object.freeze({
36620
+ files: 512,
36621
+ bytes: 16777216
36622
+ });
36623
+ /** `sha256:` followed by 64 lowercase hexadecimal characters. */
36624
+ const SKILL_DIGEST_PATTERN = /^sha256:[\da-f]{64}$/;
36625
+ const SkillDigestSchema = string().regex(SKILL_DIGEST_PATTERN, "digest must be \"sha256:\" followed by 64 lowercase hexadecimal characters");
36626
+ const SkillResourceEntrySchema = looseObject({
36627
+ uri: string(),
36628
+ digest: SkillDigestSchema,
36629
+ size: number().int().nonnegative()
36630
+ });
36631
+ const SkillFrontmatterSchema = looseObject({
36632
+ name: string(),
36633
+ description: string()
36634
+ }).refine((frontmatter) => Object.getPrototypeOf(frontmatter) === Object.prototype, "frontmatter must not have a \"__proto__\" field");
36635
+ const SkillResourcesSchema = union([array(SkillResourceEntrySchema), literal("dynamic")]);
36636
+ const SkillSchema = looseObject({
36637
+ uri: string(),
36638
+ frontmatter: SkillFrontmatterSchema,
36639
+ resources: SkillResourcesSchema,
36640
+ _meta: record(string(), unknown()).optional()
36641
+ });
36642
+ const SkillCacheScopeSchema = _enum(["public", "private"]);
36643
+ const cacheFields = {
36644
+ ttlMs: number().int().nonnegative(),
36645
+ cacheScope: SkillCacheScopeSchema
36646
+ };
36647
+ const ListSkillsResultSchema = looseObject({
36648
+ ...PaginatedResultSchema.shape,
36649
+ skills: array(SkillSchema),
36650
+ ...cacheFields
36651
+ });
36652
+ const GetSkillRequestParamsSchema = BaseRequestParamsSchema.extend({ uri: string() });
36653
+ const GetSkillResultSchema = looseObject({
36654
+ ...ResultSchema.shape,
36655
+ skill: SkillSchema,
36656
+ ...cacheFields
36657
+ });
36658
+ const DirectoryResourceSchema = looseObject(ResourceSchema.shape);
36659
+ const ReadResourceDirectoryRequestParamsSchema = PaginatedRequestParamsSchema.extend({ uri: string() });
36660
+ const ReadResourceDirectoryResultSchema = looseObject({
36661
+ ...PaginatedResultSchema.shape,
36662
+ resources: array(DirectoryResourceSchema),
36663
+ ...cacheFields
36664
+ });
36665
+ const SkillsCapabilitySchema = looseObject({ directoryRead: boolean$1().optional() });
36666
+ //#endregion
36667
+ //#region node_modules/.pnpm/@modelcontextprotocol+server@2.4.0/node_modules/@modelcontextprotocol/server/dist/extension-Pm4HwRE6.mjs
36668
+ /**
36669
+ * Thrown when a skill catalog cannot be built: storage failures always, skill problems under `strict`.
36670
+ * Branded, so `instanceof` also matches an instance from another bundled copy of the SDK.
36671
+ */
36672
+ var SkillCatalogError = class extends Error {
36673
+ static {
36674
+ Object.defineProperty(this, "mcpBrand", { value: "mcp.SkillCatalogError" });
36675
+ }
36676
+ static [Symbol.hasInstance](value) {
36677
+ return brandedHasInstance(this, value);
36678
+ }
36679
+ /**
36680
+ * Brand-based type guard: equivalent to `value instanceof this`, as an
36681
+ * explicit static predicate (the axios/AWS-SDK `isInstance` style). Must be
36682
+ * invoked on the class — in callback position write
36683
+ * `v => SkillCatalogError.isInstance(v)`, not `.filter(SkillCatalogError.isInstance)`.
36684
+ */
36685
+ static isInstance(value) {
36686
+ if (typeof this !== "function") throw new TypeError("isInstance must be called on the class (e.g. `SkillCatalogError.isInstance(value)`); for callbacks use `v => SkillCatalogError.isInstance(v)`");
36687
+ return brandedHasInstance(this, value);
36688
+ }
36689
+ problems;
36690
+ constructor(problems) {
36691
+ const lines = problems.map((problem) => ` ${problem.path}: ${problem.message}`).join("\n");
36692
+ super(`${problems.length} skill problem${problems.length === 1 ? "" : "s"}:\n${lines}`);
36693
+ this.name = "SkillCatalogError";
36694
+ this.problems = problems;
36695
+ stampErrorBrands(this, new.target);
36696
+ }
36697
+ };
36698
+ function defaultWarn(warning) {
36699
+ console.warn(`[mcp-sdk] skills: ${warning.path}: ${warning.message}`);
36700
+ }
36701
+ const encoder$1 = new TextEncoder();
36702
+ /** UTF-8 bytes of `text`. */
36703
+ function encodeUtf8(text) {
36704
+ return encoder$1.encode(text);
36705
+ }
36706
+ /** Decodes UTF-8 only if decoding loses nothing (a BOM is kept); otherwise `undefined`. */
36707
+ function decodeUtf8Lossless(bytes) {
36708
+ try {
36709
+ return new TextDecoder("utf-8", {
36710
+ fatal: true,
36711
+ ignoreBOM: true
36712
+ }).decode(bytes);
36713
+ } catch {
36714
+ return;
36715
+ }
36716
+ }
36717
+ /** Standard base64 of `bytes`. */
36718
+ function encodeBase64(bytes) {
36719
+ let binary = "";
36720
+ const chunk = 32768;
36721
+ for (let offset = 0; offset < bytes.length; offset += chunk) binary += String.fromCodePoint(...bytes.subarray(offset, offset + chunk));
36722
+ return btoa(binary);
36723
+ }
36724
+ /** Bytes of a standard base64 string. */
36725
+ function decodeBase64(base64) {
36726
+ const binary = atob(base64);
36727
+ const bytes = new Uint8Array(binary.length);
36728
+ for (let index = 0; index < binary.length; index++) bytes[index] = binary.codePointAt(index);
36729
+ return bytes;
36730
+ }
36731
+ /** `sha256:` followed by the lowercase hex SHA-256 of `bytes` (WebCrypto, every runtime). */
36732
+ async function sha256Digest(bytes) {
36733
+ const { buffer, byteOffset, byteLength } = bytes;
36734
+ const view = buffer instanceof ArrayBuffer ? new Uint8Array(buffer, byteOffset, byteLength) : new Uint8Array(bytes);
36735
+ const hash = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", view));
36736
+ let hex = "";
36737
+ for (const byte of hash) hex += byte.toString(16).padStart(2, "0");
36738
+ return `sha256:${hex}`;
36739
+ }
36740
+ /**
36741
+ * Frontmatter reader for `SKILL.md` (SEP-2640).
36742
+ *
36743
+ * Reads the YAML subset real skills use — block mappings and sequences;
36744
+ * plain, quoted and block scalars; one-line flow collections; comments — with
36745
+ * YAML 1.2 core-schema typing. Anything outside the subset throws a
36746
+ * {@linkcode FrontmatterError} instead of being read differently from a full
36747
+ * YAML parser. Server catalogs and client verification both call it; it is
36748
+ * kept behind this module so it can be swapped for a YAML library.
36749
+ */
36750
+ var FrontmatterError = class extends Error {
36751
+ line;
36752
+ constructor(message, line) {
36753
+ super(line === void 0 ? message : `line ${line}: ${message}`);
36754
+ this.name = "FrontmatterError";
36755
+ this.line = line;
36756
+ }
36757
+ };
36758
+ const MAX_DEPTH = 32;
36759
+ /** Splits `SKILL.md` into its frontmatter YAML and markdown body. */
36760
+ function splitFrontmatter(markdown) {
36761
+ let text = markdown.replaceAll("\r\n", "\n");
36762
+ if (text.codePointAt(0) === 65279) text = text.slice(1);
36763
+ if (!text.startsWith("---\n")) throw new FrontmatterError("SKILL.md must begin with a \"---\" line", 1);
36764
+ const rest = text.slice(4);
36765
+ const close = /^---[\t ]*$/m.exec(rest);
36766
+ if (close === null) throw new FrontmatterError("frontmatter has no closing \"---\" line");
36767
+ let body = rest.slice(close.index + close[0].length);
36768
+ if (body.startsWith("\n")) body = body.slice(1);
36769
+ return {
36770
+ yaml: rest.slice(0, close.index),
36771
+ body
36772
+ };
36773
+ }
36774
+ /** Splits and parses `SKILL.md`. Line numbers in errors count from the file's first line. */
36775
+ function parseSkillMarkdown(markdown) {
36776
+ const { yaml, body } = splitFrontmatter(markdown);
36777
+ return {
36778
+ frontmatter: parseFrontmatterYaml(yaml, 2),
36779
+ body
36780
+ };
36781
+ }
36782
+ /** Parses frontmatter YAML into a JSON object. `firstLine` numbers the first YAML line in errors. */
36783
+ function parseFrontmatterYaml(yaml, firstLine = 1) {
36784
+ return new Parser(toLines(yaml, firstLine)).parseDocument();
36785
+ }
36786
+ function toLines(yaml, firstLine) {
36787
+ const raws = yaml.split("\n");
36788
+ if (raws.at(-1) === "") raws.pop();
36789
+ return raws.map((raw, index) => ({
36790
+ no: firstLine + index,
36791
+ indent: /^ */.exec(raw)?.[0].length ?? 0,
36792
+ text: trimWhiteStart(raw),
36793
+ raw
36794
+ }));
36795
+ }
36796
+ const TAB_INDENT = "tabs cannot be used for indentation";
36797
+ const isSkippable = (line) => line.text === "" || line.text.startsWith("#");
36798
+ const isTabIndented = (line) => line.raw[line.indent] === " ";
36799
+ const isSequenceItem = (text) => /^-(?:[\t ]|$)/.test(text);
36800
+ var Parser = class {
36801
+ index = 0;
36802
+ constructor(lines) {
36803
+ this.lines = lines;
36804
+ }
36805
+ parseDocument() {
36806
+ const first = this.peek();
36807
+ if (first === void 0) return {};
36808
+ if (first.text.startsWith("")) throw new FrontmatterError("a byte order mark inside the frontmatter is not supported", first.no);
36809
+ if (isSequenceItem(first.text) || matchKey(first.text, first.no) === void 0) throw new FrontmatterError("frontmatter must be a mapping of keys to values", first.no);
36810
+ const map = this.parseMapping(first.indent, 0);
36811
+ const extra = this.peek();
36812
+ if (extra !== void 0) throw new FrontmatterError("unexpected content", extra.no);
36813
+ return map;
36814
+ }
36815
+ peek() {
36816
+ while (this.index < this.lines.length && isSkippable(this.lines[this.index])) this.index++;
36817
+ const line = this.lines[this.index];
36818
+ if (line !== void 0 && isTabIndented(line)) throw new FrontmatterError(TAB_INDENT, line.no);
36819
+ return line;
36820
+ }
36821
+ parseMapping(indent, depth) {
36822
+ assertDepth(depth, this.lines[this.index]?.no);
36823
+ const map = {};
36824
+ for (let line = this.peek(); line !== void 0 && line.indent >= indent; line = this.peek()) {
36825
+ if (line.indent > indent) throw new FrontmatterError("unexpected indentation", line.no);
36826
+ if (isSequenceItem(line.text)) break;
36827
+ const match = matchKey(line.text, line.no);
36828
+ if (match === void 0) throw new FrontmatterError("expected \"key: value\"", line.no);
36829
+ if (Object.hasOwn(map, match.key)) throw new FrontmatterError(`duplicate key "${match.key}"`, line.no);
36830
+ this.index++;
36831
+ map[match.key] = this.parseValue(match.rest, line, indent, depth + 1, true);
36832
+ }
36833
+ return map;
36834
+ }
36835
+ parseSequence(indent, depth) {
36836
+ assertDepth(depth, this.lines[this.index]?.no);
36837
+ const items = [];
36838
+ for (let line = this.peek(); line !== void 0 && line.indent >= indent; line = this.peek()) {
36839
+ if (line.indent > indent) throw new FrontmatterError("unexpected indentation", line.no);
36840
+ if (!isSequenceItem(line.text)) break;
36841
+ const after = line.text.slice(1);
36842
+ const rest = trimWhiteStart(after);
36843
+ const column = line.indent + 1 + (after.length - rest.length);
36844
+ if (rest !== "" && !rest.startsWith("#") && (isSequenceItem(rest) || matchKey(rest, line.no) !== void 0)) {
36845
+ if (/^ *\t/.test(after)) throw new FrontmatterError(TAB_INDENT, line.no);
36846
+ this.lines[this.index] = {
36847
+ ...line,
36848
+ indent: column,
36849
+ text: rest
36850
+ };
36851
+ items.push(isSequenceItem(rest) ? this.parseSequence(column, depth + 1) : this.parseMapping(column, depth + 1));
36852
+ continue;
36853
+ }
36854
+ this.index++;
36855
+ items.push(this.parseValue(rest, line, indent, depth + 1, false));
36856
+ }
36857
+ return items;
36858
+ }
36859
+ parseValue(rest, line, parentIndent, depth, allowCompactSequence) {
36860
+ if (rest === "" || rest.startsWith("#")) {
36861
+ const next = this.peek();
36862
+ if (next !== void 0 && next.indent > parentIndent) return this.parseIndented(parentIndent, depth);
36863
+ if (allowCompactSequence && next !== void 0 && next.indent === parentIndent && isSequenceItem(next.text)) return this.parseSequence(parentIndent, depth);
36864
+ return null;
36865
+ }
36866
+ const first = rest[0];
36867
+ if (first === "|" || first === ">") return this.parseBlockScalar(rest, line, parentIndent);
36868
+ if (first === "\"" || first === "'") {
36869
+ const { value, end } = readQuoted(rest, 0, line.no);
36870
+ expectOnlyComment(rest.slice(end), line.no);
36871
+ return value;
36872
+ }
36873
+ if (first === "[" || first === "{") {
36874
+ const { value, end } = readFlow(rest, 0, line.no, depth);
36875
+ expectOnlyComment(rest.slice(end), line.no);
36876
+ return value;
36877
+ }
36878
+ if ("&*!".includes(first)) throw new FrontmatterError("anchors, aliases and tags are not supported", line.no);
36879
+ if ("%@`,]}".includes(first)) throw new FrontmatterError(`"${first}" cannot start a plain value; quote it`, line.no);
36880
+ if (isSequenceItem(rest) || /^\?(?:[\t ]|$)/.test(rest)) throw new FrontmatterError("a nested collection must start on its own line", line.no);
36881
+ return this.parsePlain(rest, line, parentIndent);
36882
+ }
36883
+ parseIndented(parentIndent, depth) {
36884
+ const line = this.peek();
36885
+ if (isSequenceItem(line.text)) return this.parseSequence(line.indent, depth);
36886
+ if (matchKey(line.text, line.no) !== void 0) return this.parseMapping(line.indent, depth);
36887
+ this.index++;
36888
+ return this.parseValue(line.text, line, parentIndent, depth, false);
36889
+ }
36890
+ parsePlain(first, line, parentIndent) {
36891
+ let value = stripComment(first);
36892
+ assertPlain(value, line.no);
36893
+ let commented = COMMENT.test(first);
36894
+ let blanks = 0;
36895
+ while (this.index < this.lines.length) {
36896
+ const next = this.lines[this.index];
36897
+ if (next.text === "") {
36898
+ blanks++;
36899
+ this.index++;
36900
+ continue;
36901
+ }
36902
+ if (next.indent <= parentIndent || next.text.startsWith("#")) break;
36903
+ if (isTabIndented(next)) throw new FrontmatterError(TAB_INDENT, next.no);
36904
+ if (commented) throw new FrontmatterError("a comment ends a plain value; quote a value that continues past it", next.no);
36905
+ const part = stripComment(next.text);
36906
+ assertPlain(part, next.no);
36907
+ commented = COMMENT.test(next.text);
36908
+ value += blanks > 0 ? "\n".repeat(blanks) : " ";
36909
+ value += part;
36910
+ blanks = 0;
36911
+ this.index++;
36912
+ }
36913
+ return resolvePlain(value, line.no);
36914
+ }
36915
+ parseBlockScalar(header, line, parentIndent) {
36916
+ const match = /^([>|])([+-]?)(?:[\t ]+#.*)?[\t ]*$/.exec(header);
36917
+ if (match === null) throw new FrontmatterError(/^[>|][+-]?\d|^[>|]\d/.test(header) ? "block scalar indentation indicators are not supported" : "invalid block scalar header", line.no);
36918
+ const contents = [];
36919
+ let contentIndent = -1;
36920
+ let leadingWidth = 0;
36921
+ while (this.index < this.lines.length) {
36922
+ const next = this.lines[this.index];
36923
+ const spacesOnly = next.indent === next.raw.length;
36924
+ if (contentIndent === -1 && !spacesOnly && next.indent > parentIndent) {
36925
+ contentIndent = next.indent;
36926
+ if (leadingWidth > contentIndent) throw new FrontmatterError("a leading blank line is indented more than the block scalar content", next.no);
36927
+ }
36928
+ if (contentIndent !== -1 && next.indent >= contentIndent) contents.push(next.raw.slice(contentIndent));
36929
+ else if (spacesOnly) {
36930
+ if (contentIndent === -1) leadingWidth = Math.max(leadingWidth, next.indent);
36931
+ contents.push("");
36932
+ } else if (isTabIndented(next)) throw new FrontmatterError(TAB_INDENT, next.no);
36933
+ else break;
36934
+ this.index++;
36935
+ }
36936
+ return foldBlock(contents, match[1] === ">", match[2] ?? "");
36937
+ }
36938
+ };
36939
+ function assertDepth(depth, line) {
36940
+ if (depth > MAX_DEPTH) throw new FrontmatterError(`nesting deeper than ${MAX_DEPTH} levels is not supported`, line);
36941
+ }
36942
+ function matchKey(text, no) {
36943
+ const first = text[0];
36944
+ if (first === "\"" || first === "'") {
36945
+ const { value, end } = readQuoted(text, 0, no);
36946
+ const after = text.slice(end);
36947
+ if (after === ":" || /^:[\t ]/.test(after)) return {
36948
+ key: checkKey(value, no),
36949
+ rest: trimWhiteStart(after.slice(1))
36950
+ };
36951
+ return;
36952
+ }
36953
+ if (first === "?") throw new FrontmatterError("complex keys (\"?\") are not supported", no);
36954
+ if (first === void 0 || "[]{}&*!|>%@`#,".includes(first)) return void 0;
36955
+ for (let index = 0; index < text.length; index++) {
36956
+ const char = text[index];
36957
+ const previous = text[index - 1];
36958
+ if (char === "#" && (previous === " " || previous === " ")) return void 0;
36959
+ if (char === ":") {
36960
+ const next = text[index + 1];
36961
+ if (next === void 0 || next === " " || next === " ") {
36962
+ const key = trimWhiteEnd(text.slice(0, index));
36963
+ if (key === "") return void 0;
36964
+ return {
36965
+ key: resolveKey(key, no),
36966
+ rest: trimWhiteStart(text.slice(index + 1))
36967
+ };
36968
+ }
36969
+ }
36970
+ }
36971
+ }
36972
+ function checkKey(key, no) {
36973
+ if (key === "__proto__") throw new FrontmatterError("the key \"__proto__\" is not supported", no);
36974
+ return key;
36975
+ }
36976
+ function resolveKey(raw, no) {
36977
+ if (typeof resolvePlain(raw, no) !== "string") throw new FrontmatterError(`key "${raw}" must be a string; quote it`, no);
36978
+ return checkKey(raw, no);
36979
+ }
36980
+ function assertPlain(text, no) {
36981
+ if (/:(?:[\t ]|$)/.test(text)) throw new FrontmatterError("a plain value cannot contain \": \"; quote the value", no);
36982
+ }
36983
+ const COMMENT = /[\t ]#/;
36984
+ function stripComment(text) {
36985
+ const match = COMMENT.exec(text);
36986
+ return trimWhiteEnd(match === null ? text : text.slice(0, match.index));
36987
+ }
36988
+ function expectOnlyComment(after, no) {
36989
+ if (trimWhiteStart(after) === "" || /^[\t ]+#/.test(after)) return;
36990
+ throw new FrontmatterError("unexpected text after the value", no);
36991
+ }
36992
+ function trimWhiteStart(text) {
36993
+ return text.slice(skipSpaces(text, 0));
36994
+ }
36995
+ function trimWhiteEnd(text) {
36996
+ let end = text.length;
36997
+ while (end > 0 && (text[end - 1] === " " || text[end - 1] === " ")) end--;
36998
+ return text.slice(0, end);
36999
+ }
37000
+ const NULL = /^(?:~|null|Null|NULL)$/;
37001
+ const TRUE = /^(?:true|True|TRUE)$/;
37002
+ const FALSE = /^(?:false|False|FALSE)$/;
37003
+ const INTEGER = /^[+-]?\d+$/;
37004
+ const OCTAL = /^0o[0-7]+$/;
37005
+ const HEXADECIMAL = /^0x[\dA-Fa-f]+$/;
37006
+ const FLOAT = /^[+-]?(?:\.\d+|\d+(?:\.\d*)?)(?:[Ee][+-]?\d+)?$/;
37007
+ const SPECIAL_FLOAT = /^(?:[+-]?\.(?:inf|Inf|INF)|\.(?:nan|NaN|NAN))$/;
37008
+ function resolvePlain(text, no) {
37009
+ if (text === "" || NULL.test(text)) return null;
37010
+ if (TRUE.test(text)) return true;
37011
+ if (FALSE.test(text)) return false;
37012
+ let number;
37013
+ if (INTEGER.test(text) || FLOAT.test(text)) number = Number(text);
37014
+ else if (OCTAL.test(text)) number = Number.parseInt(text.slice(2), 8);
37015
+ else if (HEXADECIMAL.test(text)) number = Number.parseInt(text.slice(2), 16);
37016
+ else if (SPECIAL_FLOAT.test(text)) number = NaN;
37017
+ if (number === void 0) return text;
37018
+ if (!Number.isFinite(number)) throw new FrontmatterError(`"${text}" cannot be represented in JSON; quote it`, no);
37019
+ return number;
37020
+ }
37021
+ const SIMPLE_ESCAPES = {
37022
+ "0": "\0",
37023
+ a: "\x07",
37024
+ b: "\b",
37025
+ t: " ",
37026
+ " ": " ",
37027
+ n: "\n",
37028
+ v: "\v",
37029
+ f: "\f",
37030
+ r: "\r",
37031
+ e: "\x1B",
37032
+ " ": " ",
37033
+ "\"": "\"",
37034
+ "/": "/",
37035
+ "\\": "\\",
37036
+ N: "…",
37037
+ _: "\xA0",
37038
+ L: "\u2028",
37039
+ P: "\u2029"
37040
+ };
37041
+ const HEX_ESCAPE_WIDTH = {
37042
+ x: 2,
37043
+ u: 4,
37044
+ U: 8
37045
+ };
37046
+ const UNCLOSED_QUOTE = "a quoted value must close on the same line (multi-line quoted values are not supported)";
37047
+ function readQuoted(text, start, no) {
37048
+ const quote = text[start];
37049
+ let value = "";
37050
+ let index = start + 1;
37051
+ while (index < text.length) {
37052
+ const char = text[index];
37053
+ if (quote === "'") {
37054
+ if (char === "'") {
37055
+ if (text[index + 1] === "'") {
37056
+ value += "'";
37057
+ index += 2;
37058
+ continue;
37059
+ }
37060
+ return {
37061
+ value,
37062
+ end: index + 1
37063
+ };
37064
+ }
37065
+ value += char;
37066
+ index++;
37067
+ continue;
37068
+ }
37069
+ if (char === "\"") return {
37070
+ value,
37071
+ end: index + 1
37072
+ };
37073
+ if (char === "\\") {
37074
+ const escape = decodeEscape(text, index, no);
37075
+ value += escape.decoded;
37076
+ index += escape.length;
37077
+ continue;
37078
+ }
37079
+ value += char;
37080
+ index++;
37081
+ }
37082
+ throw new FrontmatterError(UNCLOSED_QUOTE, no);
37083
+ }
37084
+ function decodeEscape(text, index, no) {
37085
+ const code = text[index + 1];
37086
+ if (code === void 0) throw new FrontmatterError(UNCLOSED_QUOTE, no);
37087
+ const simple = SIMPLE_ESCAPES[code];
37088
+ if (simple !== void 0) return {
37089
+ decoded: simple,
37090
+ length: 2
37091
+ };
37092
+ const width = HEX_ESCAPE_WIDTH[code];
37093
+ if (width === void 0) throw new FrontmatterError(`unknown escape "\\${code}"`, no);
37094
+ const hex = text.slice(index + 2, index + 2 + width);
37095
+ if (hex.length !== width || !/^[\dA-Fa-f]+$/.test(hex) || Number.parseInt(hex, 16) > 1114111) throw new FrontmatterError(`invalid escape "\\${code}${hex}"`, no);
37096
+ return {
37097
+ decoded: String.fromCodePoint(Number.parseInt(hex, 16)),
37098
+ length: 2 + width
37099
+ };
37100
+ }
37101
+ function skipSpaces(text, index) {
37102
+ let position = index;
37103
+ while (text[position] === " " || text[position] === " ") position++;
37104
+ return position;
37105
+ }
37106
+ const UNCLOSED_FLOW = "a flow collection must close on the same line";
37107
+ function readFlow(text, start, no, depth) {
37108
+ assertDepth(depth, no);
37109
+ const open = text[start];
37110
+ const close = open === "[" ? "]" : "}";
37111
+ const list = [];
37112
+ const map = {};
37113
+ let index = skipSpaces(text, start + 1);
37114
+ if (text[index] === close) return {
37115
+ value: open === "[" ? list : map,
37116
+ end: index + 1
37117
+ };
37118
+ for (;;) {
37119
+ if (open === "[") {
37120
+ const item = readFlowValue(text, index, no, depth);
37121
+ list.push(item.value);
37122
+ index = skipSpaces(text, item.end);
37123
+ } else {
37124
+ const key = readFlowKey(text, index, no);
37125
+ const item = readFlowValue(text, key.end + 1, no, depth);
37126
+ if (Object.hasOwn(map, key.key)) throw new FrontmatterError(`duplicate key "${key.key}"`, no);
37127
+ map[key.key] = item.value;
37128
+ index = skipSpaces(text, item.end);
37129
+ }
37130
+ const char = text[index];
37131
+ if (char === ",") {
37132
+ index = skipSpaces(text, index + 1);
37133
+ if (text[index] === close) return {
37134
+ value: open === "[" ? list : map,
37135
+ end: index + 1
37136
+ };
37137
+ continue;
37138
+ }
37139
+ if (char === close) return {
37140
+ value: open === "[" ? list : map,
37141
+ end: index + 1
37142
+ };
37143
+ if (char === void 0) throw new FrontmatterError(UNCLOSED_FLOW, no);
37144
+ throw new FrontmatterError(`unexpected "${char}" in a flow collection`, no);
37145
+ }
37146
+ }
37147
+ function readFlowKey(text, start, no) {
37148
+ const index = skipSpaces(text, start);
37149
+ const char = text[index];
37150
+ if (char === void 0) throw new FrontmatterError(UNCLOSED_FLOW, no);
37151
+ if (char === "\"" || char === "'") {
37152
+ const quoted = readQuoted(text, index, no);
37153
+ const colon = skipSpaces(text, quoted.end);
37154
+ if (text[colon] !== ":") throw new FrontmatterError("expected \": \" after a key in a flow mapping", no);
37155
+ return {
37156
+ key: checkKey(quoted.value, no),
37157
+ end: colon
37158
+ };
37159
+ }
37160
+ if (char === "[" || char === "{") throw new FrontmatterError("complex keys (flow collections as keys) are not supported", no);
37161
+ assertFlowPlainStart(text, index, no);
37162
+ const end = scanFlowPlain(text, index);
37163
+ const raw = trimWhiteEnd(text.slice(index, end));
37164
+ const after = text[end + 1];
37165
+ if (raw === "" || text[end] !== ":" || after !== " " && after !== " ") throw new FrontmatterError("expected \"key: value\" in a flow mapping", no);
37166
+ return {
37167
+ key: resolveKey(raw, no),
37168
+ end
37169
+ };
37170
+ }
37171
+ const FLOW_SEPARATORS = " ,[]{}";
37172
+ function assertFlowPlainStart(text, index, no) {
37173
+ const char = text[index];
37174
+ if ("&*!".includes(char)) throw new FrontmatterError("anchors, aliases and tags are not supported", no);
37175
+ if ("|>%@`#".includes(char) || "-?:".includes(char) && FLOW_SEPARATORS.includes(text[index + 1] ?? " ")) throw new FrontmatterError(`"${char}" cannot start a plain value in a flow collection; quote it`, no);
37176
+ }
37177
+ function scanFlowPlain(text, index) {
37178
+ let end = index;
37179
+ while (end < text.length) {
37180
+ const char = text[end];
37181
+ if (",[]{}".includes(char)) break;
37182
+ if (char === "#" && (text[end - 1] === " " || text[end - 1] === " ")) break;
37183
+ if (char === ":" && FLOW_SEPARATORS.includes(text[end + 1] ?? " ")) break;
37184
+ end++;
37185
+ }
37186
+ return end;
37187
+ }
37188
+ function readFlowValue(text, start, no, depth) {
37189
+ const index = skipSpaces(text, start);
37190
+ const char = text[index];
37191
+ if (char === void 0) throw new FrontmatterError(UNCLOSED_FLOW, no);
37192
+ if (char === "[" || char === "{") return readFlow(text, index, no, depth + 1);
37193
+ if (char === "\"" || char === "'") return readQuoted(text, index, no);
37194
+ assertFlowPlainStart(text, index, no);
37195
+ const end = scanFlowPlain(text, index);
37196
+ if (text[end] === ":") throw new FrontmatterError("mappings inside flow collections are not supported; quote the value", no);
37197
+ const raw = trimWhiteEnd(text.slice(index, end));
37198
+ if (raw === "") throw new FrontmatterError("empty value in a flow collection", no);
37199
+ return {
37200
+ value: resolvePlain(raw, no),
37201
+ end
37202
+ };
37203
+ }
37204
+ function foldBlock(contents, folded, chomping) {
37205
+ let result = "";
37206
+ let emptyLines = 0;
37207
+ let didReadContent = false;
37208
+ let atMoreIndented = false;
37209
+ for (const content of contents) {
37210
+ if (content === "") {
37211
+ emptyLines++;
37212
+ continue;
37213
+ }
37214
+ if (folded) if (content.startsWith(" ") || content.startsWith(" ")) {
37215
+ atMoreIndented = true;
37216
+ result += "\n".repeat(didReadContent ? 1 + emptyLines : emptyLines);
37217
+ } else if (atMoreIndented) {
37218
+ atMoreIndented = false;
37219
+ result += "\n".repeat(emptyLines + 1);
37220
+ } else if (emptyLines === 0) {
37221
+ if (didReadContent) result += " ";
37222
+ } else result += "\n".repeat(emptyLines);
37223
+ else result += "\n".repeat(didReadContent ? 1 + emptyLines : emptyLines);
37224
+ result += content;
37225
+ didReadContent = true;
37226
+ emptyLines = 0;
37227
+ }
37228
+ if (chomping === "+") result += "\n".repeat(didReadContent ? 1 + emptyLines : emptyLines);
37229
+ else if (chomping === "" && didReadContent) result += "\n";
37230
+ return result;
37231
+ }
37232
+ const ENCODED_IN_SEGMENT = /[^\w!$&'()*+,.:;=@[\]|~-]/gu;
37233
+ function encodePathText(text) {
37234
+ return text.replaceAll(ENCODED_IN_SEGMENT, (char) => encodeURIComponent(char));
37235
+ }
37236
+ /** Percent-encodes one path segment as WHATWG URL does, plus `%`, `/`, `\` and the dots of `.` and `..`; throws `URIError` on a lone surrogate. */
37237
+ function encodeSegment(segment) {
37238
+ if (segment === "." || segment === "..") return segment.replaceAll(".", "%2E");
37239
+ return encodePathText(segment);
37240
+ }
37241
+ /** `text` without its trailing `/`s; a loop, since `/\/+$/` backtracks polynomially on a long run of `/`. */
37242
+ function trimTrailingSlashes(text) {
37243
+ let end = text.length;
37244
+ while (end > 0 && text.codePointAt(end - 1) === 47) end--;
37245
+ return text.slice(0, end);
37246
+ }
37247
+ /** `base` (`scheme://`, or a URI without a trailing slash) + `/`-joined encoded segments; an authority is encoded strictly. */
37248
+ function joinUri(base, segments) {
37249
+ if (!base.endsWith("://")) return `${trimTrailingSlashes(base)}/${segments.map((segment) => encodeSegment(segment)).join("/")}`;
37250
+ const [authority, ...path] = segments;
37251
+ if (authority === void 0) return base;
37252
+ return [base + encodeURIComponent(authority), ...path.map((segment) => encodeSegment(segment))].join("/");
37253
+ }
37254
+ /** WHATWG-serialised form of `uri`, or `undefined` if it does not parse. */
37255
+ function canonicalUri(uri) {
37256
+ try {
37257
+ return new URL(uri).href;
37258
+ } catch {
37259
+ return;
37260
+ }
37261
+ }
37262
+ /** True when `uri` is already in canonical form. */
37263
+ function isRoundTripUri(uri) {
37264
+ return canonicalUri(uri) === uri;
37265
+ }
37266
+ const MANIFEST_SUFFIX = "/SKILL.md";
37267
+ /** The skill's root directory URI (no trailing slash), or `undefined` if `skillMdUri` is not a `…/SKILL.md` URI in a named directory. */
37268
+ function skillRootUri(skillMdUri) {
37269
+ const canonical = canonicalUri(skillMdUri);
37270
+ if (canonical === void 0 || !canonical.endsWith(MANIFEST_SUFFIX)) return void 0;
37271
+ const root = canonical.slice(0, -9);
37272
+ return root.endsWith("/") ? void 0 : root;
37273
+ }
37274
+ function safeDecode(segment) {
37275
+ try {
37276
+ return decodeURIComponent(segment);
37277
+ } catch {
37278
+ return segment;
37279
+ }
37280
+ }
37281
+ /** The decoded last segment of a directory URI (the authority when the path is empty). */
37282
+ function lastSegmentOf(rootUri) {
37283
+ const url = new URL(rootUri);
37284
+ const path = url.pathname;
37285
+ return safeDecode(path === "" || path === "/" ? url.host : path.slice(path.lastIndexOf("/") + 1));
37286
+ }
37287
+ /** True when `uri` is strictly inside `rootUri` and no segment below it decodes to empty, `.`, `..`, or a name holding `/` or NUL. */
37288
+ function isWithin(rootUri, uri) {
37289
+ const canonical = canonicalUri(uri);
37290
+ if (canonical === void 0 || !canonical.startsWith(`${rootUri}/`)) return false;
37291
+ return canonical.slice(rootUri.length + 1).split("/").every((segment) => {
37292
+ const decoded = safeDecode(segment);
37293
+ return decoded !== "" && decoded !== "." && decoded !== ".." && !decoded.includes("/") && !decoded.includes("\0");
37294
+ });
37295
+ }
37296
+ function decodeOrUndefined(escapes) {
37297
+ try {
37298
+ return decodeURIComponent(escapes);
37299
+ } catch {
37300
+ return;
37301
+ }
37302
+ }
37303
+ function utf8Width(lead) {
37304
+ if (lead >= 240) return 4;
37305
+ if (lead >= 224) return 3;
37306
+ if (lead >= 192) return 2;
37307
+ return 1;
37308
+ }
37309
+ function keyOfEscapes(run) {
37310
+ let key = "";
37311
+ for (let at = 0; at < run.length;) {
37312
+ const width = 3 * utf8Width(Number.parseInt(run.slice(at + 1, at + 3), 16));
37313
+ const decoded = decodeOrUndefined(run.slice(at, at + width));
37314
+ if (decoded === void 0) {
37315
+ key += `%25${run.slice(at + 1, at + 3)}`;
37316
+ at += 3;
37317
+ } else {
37318
+ key += encodePathText(decoded);
37319
+ at += width;
37320
+ }
37321
+ }
37322
+ return key;
37323
+ }
37324
+ const ESCAPE_RUN = /((?:%[\dA-Fa-f]{2})+)/;
37325
+ function keyOfSegment(segment) {
37326
+ return segment.split(ESCAPE_RUN).map((part, index) => index % 2 === 0 ? encodePathText(part) : keyOfEscapes(part)).join("");
37327
+ }
37328
+ /** The one spelling all ways of writing a file's URI below `rootUri` share (`Q&A.md`, `Q%26A.md#x`); `undefined` if it does not parse. */
37329
+ function skillFileKey(rootUri, uri) {
37330
+ let url;
37331
+ try {
37332
+ url = new URL(uri);
37333
+ } catch {
37334
+ return;
37335
+ }
37336
+ url.hash = "";
37337
+ const canonical = url.href;
37338
+ if (canonical.includes("?") || !canonical.startsWith(`${rootUri}/`)) return canonical;
37339
+ return `${rootUri}/${canonical.slice(rootUri.length + 1).split("/").map((segment) => keyOfSegment(segment)).join("/")}`;
37340
+ }
37341
+ /** Code-unit order of two URIs: the one order for entries, directory children and cursors. */
37342
+ function compareUris(a, b) {
37343
+ if (a < b) return -1;
37344
+ if (a > b) return 1;
37345
+ return 0;
37346
+ }
37347
+ /** Agent Skills name rule: lowercase letters, digits and single hyphens (length checked separately). */
37348
+ const SKILL_NAME_PATTERN = /^[\da-z]+(?:-[\da-z]+)*$/;
37349
+ const isStringMap = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((item) => typeof item === "string");
37350
+ function countCharacters(text) {
37351
+ let count = 0;
37352
+ for (let index = 0; index < text.length; index += text.codePointAt(index) > 65535 ? 2 : 1) count++;
37353
+ return count;
37354
+ }
37355
+ /** Structural rules of the stable spec and Agent Skills. Empty array = valid. */
37356
+ function validateSkillEntry(skill) {
37357
+ const problems = [];
37358
+ const root = skillRootUri(skill.uri);
37359
+ if (root === void 0) return [`uri "${skill.uri}" must be a URI ending in /SKILL.md inside a named directory`];
37360
+ const frontmatter = skill.frontmatter;
37361
+ const { name, description } = skill.frontmatter;
37362
+ if (name.length === 0 || name.length > 64 || !SKILL_NAME_PATTERN.test(name)) problems.push(`frontmatter name "${name}" must be 1-64 lowercase letters, digits and single hyphens`);
37363
+ else if (lastSegmentOf(root) !== name) problems.push(`frontmatter name "${name}" must match the last path segment "${lastSegmentOf(root)}"`);
37364
+ if (description === "" || countCharacters(description) > 1024) problems.push("frontmatter description must be 1-1024 characters");
37365
+ const compatibility = frontmatter["compatibility"];
37366
+ if (compatibility !== void 0 && (typeof compatibility !== "string" || compatibility === "" || countCharacters(compatibility) > 500)) problems.push("frontmatter compatibility must be a string of 1-500 characters");
37367
+ if (frontmatter["metadata"] !== void 0 && !isStringMap(frontmatter["metadata"])) problems.push("frontmatter metadata must map strings to strings");
37368
+ if (skill.resources !== "dynamic") {
37369
+ const self = canonicalUri(skill.uri);
37370
+ const seen = /* @__PURE__ */ new Set();
37371
+ let hasSelf = false;
37372
+ for (const resource of skill.resources) {
37373
+ const canonical = canonicalUri(resource.uri);
37374
+ if (canonical === void 0) {
37375
+ problems.push(`resource "${resource.uri}" is not a valid URI`);
37376
+ continue;
37377
+ }
37378
+ if (/[#?]/.test(canonical)) {
37379
+ problems.push(`resource "${resource.uri}" must not have a query or fragment`);
37380
+ continue;
37381
+ }
37382
+ const key = skillFileKey(root, canonical) ?? canonical;
37383
+ if (seen.has(key)) problems.push(`resource "${resource.uri}" is listed more than once`);
37384
+ seen.add(key);
37385
+ if (canonical === self) hasSelf = true;
37386
+ else if (!isWithin(root, resource.uri)) problems.push(`resource "${resource.uri}" is outside the skill directory ${root}`);
37387
+ }
37388
+ if (!hasSelf) problems.push(`resources must include ${skill.uri}`);
37389
+ }
37390
+ return problems;
37391
+ }
37392
+ /** Schema check plus {@linkcode validateSkillEntry}, for values received from elsewhere. */
37393
+ function skillEntryProblems(value) {
37394
+ const parsed = SkillSchema.safeParse(value);
37395
+ if (!parsed.success) return parsed.error.issues.map((issue) => `${issue.path.join(".") || "(entry)"}: ${issue.message}`);
37396
+ return validateSkillEntry(parsed.data);
37397
+ }
37398
+ /** Builds an entry from a skill's files: parses `SKILL.md`, hashes every file, sorts by URI, validates. */
37399
+ async function buildSkillEntry(skillMdUri, files) {
37400
+ const manifest = files.find((file) => file.uri === skillMdUri);
37401
+ if (manifest === void 0) return { problems: ["SKILL.md is missing"] };
37402
+ const text = decodeUtf8Lossless(manifest.bytes);
37403
+ if (text === void 0) return { problems: ["SKILL.md is not valid UTF-8"] };
37404
+ let frontmatter;
37405
+ try {
37406
+ frontmatter = parseSkillMarkdown(text).frontmatter;
37407
+ } catch (error) {
37408
+ if (error instanceof FrontmatterError) return { problems: [`SKILL.md frontmatter: ${error.message}`] };
37409
+ throw error;
37410
+ }
37411
+ if (typeof frontmatter["name"] !== "string" || typeof frontmatter["description"] !== "string") return { problems: ["SKILL.md frontmatter must contain string \"name\" and \"description\""] };
37412
+ const resources = await Promise.all(files.map(async (file) => ({
37413
+ uri: file.uri,
37414
+ digest: await sha256Digest(file.bytes),
37415
+ size: file.bytes.byteLength
37416
+ })));
37417
+ resources.sort((a, b) => compareUris(a.uri, b.uri));
37418
+ const skill = {
37419
+ uri: skillMdUri,
37420
+ frontmatter,
37421
+ resources
37422
+ };
37423
+ return {
37424
+ skill,
37425
+ problems: validateSkillEntry(skill)
37426
+ };
37427
+ }
37428
+ const BY_EXTENSION = {
37429
+ md: "text/markdown",
37430
+ markdown: "text/markdown",
37431
+ txt: "text/plain",
37432
+ json: "application/json",
37433
+ yaml: "application/yaml",
37434
+ yml: "application/yaml",
37435
+ py: "text/x-python",
37436
+ sh: "application/x-sh",
37437
+ bash: "application/x-sh",
37438
+ js: "text/javascript",
37439
+ mjs: "text/javascript",
37440
+ ts: "text/typescript",
37441
+ html: "text/html",
37442
+ css: "text/css",
37443
+ csv: "text/csv",
37444
+ xml: "application/xml",
37445
+ svg: "image/svg+xml",
37446
+ png: "image/png",
37447
+ jpg: "image/jpeg",
37448
+ jpeg: "image/jpeg",
37449
+ gif: "image/gif",
37450
+ webp: "image/webp",
37451
+ pdf: "application/pdf"
37452
+ };
37453
+ /** MIME type from the file extension; `application/octet-stream` when unknown. */
37454
+ function mimeTypeFor(fileName) {
37455
+ const dot = fileName.lastIndexOf(".");
37456
+ if (dot <= 0) return "application/octet-stream";
37457
+ const extension = fileName.slice(dot + 1).toLowerCase();
37458
+ return Object.hasOwn(BY_EXTENSION, extension) ? BY_EXTENSION[extension] : "application/octet-stream";
37459
+ }
37460
+ const basename = (path) => path.slice(path.lastIndexOf("/") + 1);
37461
+ const dirname$1 = (path) => {
37462
+ const slash = path.lastIndexOf("/");
37463
+ return slash === -1 ? "" : path.slice(0, slash);
37464
+ };
37465
+ const errorText$1 = (error) => error instanceof Error ? error.message : String(error);
37466
+ const NOT_FOLLOWED = "symlinks and other special files are not followed; copy the file instead";
37467
+ const isManifest = (entry) => entry.kind !== "directory" && basename(entry.path) === "SKILL.md";
37468
+ function append(lists, key, value) {
37469
+ const list = lists.get(key);
37470
+ if (list === void 0) lists.set(key, [value]);
37471
+ else list.push(value);
37472
+ }
37473
+ function representableUri(uriOf, path) {
37474
+ let uri;
37475
+ try {
37476
+ uri = uriOf(path);
37477
+ } catch (error) {
37478
+ if (error instanceof URIError) return void 0;
37479
+ throw error;
37480
+ }
37481
+ return isRoundTripUri(uri) ? uri : void 0;
37482
+ }
37483
+ async function walk(files) {
37484
+ const tree = [];
37485
+ const visit = async (dir) => {
37486
+ let entries;
37487
+ try {
37488
+ entries = await files.list(dir);
37489
+ } catch (error) {
37490
+ throw new SkillCatalogError([{
37491
+ path: dir === "" ? "." : dir,
37492
+ message: `cannot be listed: ${errorText$1(error)}`
37493
+ }]);
37494
+ }
37495
+ for (const entry of entries.toSorted((a, b) => compareUris(a.name, b.name))) {
37496
+ if (entry.name.startsWith(".")) continue;
37497
+ const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
37498
+ tree.push({
37499
+ path,
37500
+ kind: entry.kind
37501
+ });
37502
+ if (entry.kind === "directory") await visit(path);
37503
+ }
37504
+ };
37505
+ await visit("");
37506
+ return tree;
37507
+ }
37508
+ async function readFile$1(files, path) {
37509
+ try {
37510
+ return await files.read(path);
37511
+ } catch (error) {
37512
+ throw new SkillCatalogError([{
37513
+ path,
37514
+ message: `cannot be read: ${errorText$1(error)}`
37515
+ }]);
37516
+ }
37517
+ }
37518
+ async function buildCatalogSnapshot(files, options) {
37519
+ const tree = await walk(files);
37520
+ const problems = [];
37521
+ const report = (warning) => {
37522
+ if (options.strict) problems.push(warning);
37523
+ else options.warn(warning);
37524
+ };
37525
+ const uriOf = (path) => joinUri("skill://", [...options.prefix, ...path.split("/")]);
37526
+ for (const entry of tree) {
37527
+ const name = basename(entry.path);
37528
+ if (entry.kind === "file" && name !== "SKILL.md" && name.toLowerCase() === "skill.md") options.warn({
37529
+ path: entry.path,
37530
+ message: "did you mean SKILL.md? File names are case-sensitive"
37531
+ });
37532
+ }
37533
+ const skillDirs = new Set(tree.filter((entry) => isManifest(entry)).map((entry) => dirname$1(entry.path)));
37534
+ const insideOf = /* @__PURE__ */ new Map();
37535
+ for (const entry of tree) {
37536
+ let inSkill = false;
37537
+ for (let dir = dirname$1(entry.path); dir !== ""; dir = dirname$1(dir)) {
37538
+ if (!skillDirs.has(dir)) continue;
37539
+ inSkill = true;
37540
+ append(insideOf, dir, entry);
37541
+ }
37542
+ if (!inSkill && entry.kind === "unsupported" && !isManifest(entry)) report({
37543
+ path: entry.path,
37544
+ message: `${NOT_FOLLOWED} (skipped)`
37545
+ });
37546
+ }
37547
+ const built = [];
37548
+ const hashed = /* @__PURE__ */ new Map();
37549
+ const manifests = tree.filter((entry) => isManifest(entry)).toSorted((a, b) => b.path.split("/").length - a.path.split("/").length);
37550
+ for (const entry of manifests) {
37551
+ const dir = dirname$1(entry.path);
37552
+ if (dir === "") {
37553
+ report({
37554
+ path: entry.path,
37555
+ message: "a SKILL.md at the root has no skill path; point loadSkills (or your adapter) at its parent folder (skipped)"
37556
+ });
37557
+ continue;
37558
+ }
37559
+ const skill = await buildOne(files, insideOf.get(dir) ?? [], dir, uriOf, options, report, hashed);
37560
+ if (skill !== void 0) built.push(skill);
37561
+ }
37562
+ if (problems.length > 0) throw new SkillCatalogError(problems);
37563
+ if (built.length === 0) options.warn({
37564
+ path: ".",
37565
+ message: "no skills found (a skill is a folder containing SKILL.md)"
37566
+ });
37567
+ return assemble(built, tree, uriOf);
37568
+ }
37569
+ async function buildOne(files, inside, dir, uriOf, options, report, hashed) {
37570
+ const unsupported = inside.find((entry) => entry.kind === "unsupported");
37571
+ if (unsupported !== void 0) {
37572
+ report({
37573
+ path: unsupported.path,
37574
+ message: `${NOT_FOLLOWED} (skill skipped)`
37575
+ });
37576
+ return;
37577
+ }
37578
+ const manifestPath = `${dir}/SKILL.md`;
37579
+ const manifest = [];
37580
+ const catalogFiles = [];
37581
+ for (const entry of inside) {
37582
+ const uri = representableUri(uriOf, entry.path);
37583
+ if (uri === void 0) {
37584
+ report({
37585
+ path: entry.path,
37586
+ message: "this name cannot be represented in a URI (skill skipped)"
37587
+ });
37588
+ return;
37589
+ }
37590
+ if (entry.kind !== "file") continue;
37591
+ const own = entry.path === manifestPath;
37592
+ let file = own ? void 0 : hashed.get(entry.path);
37593
+ if (file === void 0) {
37594
+ const bytes = await readFile$1(files, entry.path);
37595
+ if (own) manifest.push({
37596
+ uri,
37597
+ bytes
37598
+ });
37599
+ file = {
37600
+ digest: await sha256Digest(bytes),
37601
+ size: bytes.byteLength
37602
+ };
37603
+ hashed.set(entry.path, file);
37604
+ }
37605
+ catalogFiles.push({
37606
+ path: entry.path,
37607
+ uri,
37608
+ ...file,
37609
+ mimeType: mimeTypeFor(basename(entry.path))
37610
+ });
37611
+ }
37612
+ const parsed = await buildSkillEntry(uriOf(manifestPath), manifest);
37613
+ const resources = catalogFiles.map(({ uri, digest, size }) => ({
37614
+ uri,
37615
+ digest,
37616
+ size
37617
+ })).toSorted((a, b) => compareUris(a.uri, b.uri));
37618
+ const skill = parsed.skill === void 0 ? void 0 : {
37619
+ ...parsed.skill,
37620
+ resources
37621
+ };
37622
+ const problems = skill === void 0 ? parsed.problems : validateSkillEntry(skill);
37623
+ if (skill === void 0 || problems.length > 0) {
37624
+ for (const message of problems) report({
37625
+ path: manifestPath,
37626
+ message: `${message} (skill skipped)`
37627
+ });
37628
+ return;
37629
+ }
37630
+ let totalBytes = 0;
37631
+ for (const file of catalogFiles) totalBytes += file.size;
37632
+ if (catalogFiles.length > options.limits.files || totalBytes > options.limits.bytes) {
37633
+ const remedy = options.strict ? "Raise limits to allow it" : "Set strict: true to fail instead, or raise limits";
37634
+ report({
37635
+ path: dir,
37636
+ message: `${catalogFiles.length} files / ${totalBytes} bytes exceeds the limit of ${options.limits.files} files / ${options.limits.bytes} bytes; some hosts may decline it. ${remedy}`
37637
+ });
37638
+ }
37639
+ return {
37640
+ dir,
37641
+ skill,
37642
+ files: catalogFiles,
37643
+ manifestSize: catalogFiles.find((file) => file.path === manifestPath)?.size ?? 0,
37644
+ inside
37645
+ };
37646
+ }
37647
+ function deepFreeze(value) {
37648
+ if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
37649
+ Object.freeze(value);
37650
+ for (const item of Object.values(value)) deepFreeze(item);
37651
+ }
37652
+ return value;
37653
+ }
37654
+ function assemble(built, tree, uriOf) {
37655
+ const ordered = Object.freeze(built.map((item) => deepFreeze(item.skill)).toSorted((a, b) => compareUris(a.uri, b.uri)));
37656
+ const files = /* @__PURE__ */ new Map();
37657
+ const dirPaths = /* @__PURE__ */ new Set();
37658
+ for (const item of built) {
37659
+ for (const file of item.files) files.set(file.uri, file);
37660
+ dirPaths.add(item.dir);
37661
+ for (const entry of item.inside) if (entry.kind === "directory") dirPaths.add(entry.path);
37662
+ }
37663
+ const childrenOf = /* @__PURE__ */ new Map();
37664
+ for (const entry of tree) append(childrenOf, dirname$1(entry.path), entry);
37665
+ const dirs = /* @__PURE__ */ new Map();
37666
+ for (const dirPath of dirPaths) {
37667
+ const children = [];
37668
+ for (const entry of childrenOf.get(dirPath) ?? []) {
37669
+ if (entry.kind === "directory") {
37670
+ children.push({
37671
+ uri: uriOf(entry.path),
37672
+ name: basename(entry.path),
37673
+ mimeType: DIRECTORY_MIME_TYPE
37674
+ });
37675
+ continue;
37676
+ }
37677
+ const file = files.get(uriOf(entry.path));
37678
+ if (entry.kind === "file" && file !== void 0) children.push({
37679
+ uri: file.uri,
37680
+ name: basename(entry.path),
37681
+ mimeType: file.mimeType,
37682
+ size: file.size
37683
+ });
37684
+ }
37685
+ children.sort((a, b) => compareUris(a.uri, b.uri));
37686
+ dirs.set(uriOf(dirPath), children);
37687
+ }
37688
+ const listed = built.map((item) => ({
37689
+ uri: item.skill.uri,
37690
+ name: item.skill.frontmatter.name,
37691
+ description: item.skill.frontmatter.description,
37692
+ mimeType: "text/markdown",
37693
+ size: item.manifestSize
37694
+ })).toSorted((a, b) => compareUris(a.uri, b.uri));
37695
+ return {
37696
+ ordered,
37697
+ skills: new Map(ordered.map((skill) => [skill.uri, skill])),
37698
+ files,
37699
+ dirs,
37700
+ listed
37701
+ };
37702
+ }
37703
+ const trustedSources = /* @__PURE__ */ new WeakSet();
37704
+ const CURSOR_PREFIX = "after:";
37705
+ function encodeCursor(uri) {
37706
+ return encodeBase64(encodeUtf8(CURSOR_PREFIX + uri)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
37707
+ }
37708
+ function decodeCursor(cursor) {
37709
+ try {
37710
+ const base64 = cursor.replaceAll("-", "+").replaceAll("_", "/");
37711
+ const decoded = decodeUtf8Lossless(decodeBase64(base64 + "=".repeat((4 - base64.length % 4) % 4)));
37712
+ return decoded?.startsWith(CURSOR_PREFIX) ? decoded.slice(6) : void 0;
37713
+ } catch {
37714
+ return;
37715
+ }
37716
+ }
37717
+ function paginate(items, cursor, pageSize, key) {
37718
+ let start = 0;
37719
+ if (cursor !== void 0) {
37720
+ const after = decodeCursor(cursor);
37721
+ if (after === void 0) throw new ProtocolError(ProtocolErrorCode.InvalidParams, "Invalid cursor");
37722
+ start = items.findIndex((item) => compareUris(key(item), after) > 0);
37723
+ if (start === -1) start = items.length;
37724
+ }
37725
+ const end = Math.min(items.length, start + pageSize);
37726
+ const page = items.slice(start, end);
37727
+ const last = page.at(-1);
37728
+ return end < items.length && last !== void 0 ? {
37729
+ items: page,
37730
+ nextCursor: encodeCursor(key(last))
37731
+ } : { items: page };
37732
+ }
37733
+ function resolvePageSize(pageSize) {
37734
+ if (pageSize === void 0) return 100;
37735
+ if (pageSize === Number.POSITIVE_INFINITY || Number.isSafeInteger(pageSize) && pageSize > 0) return pageSize;
37736
+ throw new RangeError(`pageSize must be a positive integer or Infinity (got ${pageSize})`);
37737
+ }
37738
+ const errorText = (error) => error instanceof Error ? error.message : String(error);
37739
+ function catalogSource(state, files, pageSize, warn) {
37740
+ const source = {
37741
+ async list({ cursor }) {
37742
+ const page = paginate(state.current.ordered, cursor, pageSize, (skill) => skill.uri);
37743
+ return page.nextCursor === void 0 ? { skills: page.items } : {
37744
+ skills: page.items,
37745
+ nextCursor: page.nextCursor
37746
+ };
37747
+ },
37748
+ async get(uri) {
37749
+ const canonical = canonicalUri(uri);
37750
+ return canonical === void 0 ? void 0 : state.current.skills.get(canonical);
37751
+ },
37752
+ read: (uri) => readChecked(state.current, files, uri, warn),
37753
+ async readDirectory(uri, { cursor }) {
37754
+ const canonical = canonicalUri(uri);
37755
+ const children = canonical === void 0 ? void 0 : state.current.dirs.get(canonical);
37756
+ if (children === void 0) return;
37757
+ const page = paginate(children, cursor, pageSize, (child) => child.uri);
37758
+ return page.nextCursor === void 0 ? { resources: page.items } : {
37759
+ resources: page.items,
37760
+ nextCursor: page.nextCursor
37761
+ };
37762
+ },
37763
+ async listResources() {
37764
+ return [...state.current.listed];
37765
+ }
37766
+ };
37767
+ trustedSources.add(source);
37768
+ return source;
37769
+ }
37770
+ async function readChecked(snapshot, files, uri, warn) {
37771
+ const canonical = canonicalUri(uri);
37772
+ const file = canonical === void 0 ? void 0 : snapshot.files.get(canonical);
37773
+ if (file === void 0) {
37774
+ if (canonical !== void 0 && snapshot.dirs.has(canonical)) throw new ResourceNotFoundError(uri, `${uri} is a directory; use resources/directory/read`);
37775
+ return;
37776
+ }
37777
+ let bytes;
37778
+ try {
37779
+ bytes = await files.read(file.path);
37780
+ } catch (error) {
37781
+ warn({
37782
+ path: file.path,
37783
+ message: `could not be read: ${errorText(error)}`
37784
+ });
37785
+ throw new ProtocolError(ProtocolErrorCode.InternalError, `${uri} could not be read`);
37786
+ }
37787
+ if (bytes.byteLength !== file.size || await sha256Digest(bytes) !== file.digest) {
37788
+ warn({
37789
+ path: file.path,
37790
+ message: "changed since the catalog was loaded; call reload()"
37791
+ });
37792
+ throw new ProtocolError(ProtocolErrorCode.InternalError, `${uri} changed since the skills catalog was loaded`);
37793
+ }
37794
+ const text = decodeUtf8Lossless(bytes);
37795
+ return text === void 0 ? {
37796
+ bytes,
37797
+ mimeType: file.mimeType
37798
+ } : {
37799
+ text,
37800
+ mimeType: file.mimeType
37801
+ };
37802
+ }
37803
+ function splitRelativePath(path, label) {
37804
+ const segments = path.split("/").filter((segment) => segment !== "" && segment !== ".");
37805
+ if (segments.length === 0 || segments.includes("..")) throw new TypeError(`${label} must be a relative path without ".." (got ${JSON.stringify(path)})`);
37806
+ return segments;
37807
+ }
37808
+ function indexRecord(keys) {
37809
+ const dirs = /* @__PURE__ */ new Map();
37810
+ const files = /* @__PURE__ */ new Map();
37811
+ for (const key of keys) {
37812
+ const segments = splitRelativePath(key, "file path");
37813
+ let dir = "";
37814
+ for (const [depth, name] of segments.entries()) {
37815
+ let children = dirs.get(dir);
37816
+ if (children === void 0) {
37817
+ children = /* @__PURE__ */ new Map();
37818
+ dirs.set(dir, children);
37819
+ }
37820
+ children.set(name, {
37821
+ name,
37822
+ kind: depth < segments.length - 1 ? "directory" : "file"
37823
+ });
37824
+ dir = dir === "" ? name : `${dir}/${name}`;
37825
+ }
37826
+ files.set(dir, key);
37827
+ }
37828
+ return {
37829
+ dirs,
37830
+ files
37831
+ };
37832
+ }
37833
+ /**
37834
+ * An in-memory adapter. The record is re-read whenever a walk starts — `list('')` — so `reload()` sees later changes.
37835
+ * Other calls use the index from that read (built on first use if no walk has started), so their cost does not grow
37836
+ * with the record. File contents are always read live.
37837
+ */
37838
+ function memorySkillFiles(files) {
37839
+ let index;
37840
+ const reindex = () => {
37841
+ index = indexRecord(Object.keys(files));
37842
+ return index;
37843
+ };
37844
+ return {
37845
+ async list(dir) {
37846
+ const children = (dir === "" ? reindex() : index ?? reindex()).dirs.get(dir);
37847
+ if (children !== void 0) return Array.from(children.values(), (entry) => ({ ...entry }));
37848
+ if (dir === "") return [];
37849
+ throw new Error(`no such directory: ${dir}`);
37850
+ },
37851
+ async read(path) {
37852
+ const key = Object.hasOwn(files, path) ? path : (index ?? reindex()).files.get(path);
37853
+ const content = key === void 0 ? void 0 : files[key];
37854
+ if (content === void 0) throw new Error(`no such file: ${path}`);
37855
+ return typeof content === "string" ? encodeUtf8(content) : content;
37856
+ }
37857
+ };
37858
+ }
37859
+ const SCHEME_URI = /^[a-z][\d+.a-z-]*:\/\//i;
37860
+ function describeEntry(value) {
37861
+ const uri = value?.uri;
37862
+ return typeof uri === "string" ? uri : "(entry without a uri)";
37863
+ }
37864
+ function toContents(uri, result) {
37865
+ const mimeType = result.mimeType === void 0 ? {} : { mimeType: result.mimeType };
37866
+ return "text" in result ? {
37867
+ uri,
37868
+ ...mimeType,
37869
+ text: result.text
37870
+ } : {
37871
+ uri,
37872
+ ...mimeType,
37873
+ blob: encodeBase64(result.bytes)
37874
+ };
37875
+ }
37876
+ const skillsServers = /* @__PURE__ */ new WeakSet();
37877
+ const afterAttach = /* @__PURE__ */ new WeakMap();
37878
+ const ONE_PER_SERVER = "another skills extension is already attached to this server; attach one per server (to serve several folders or sources, combine them behind one SkillFiles adapter or SkillSource)";
37879
+ /**
37880
+ * The MCP Skills extension (SEP-2640) for one catalog of skills. One instance
37881
+ * serves any number of servers: call {@linkcode SkillsExtension.attach} on each
37882
+ * (before connecting). Build it from a {@linkcode SkillSource}, or with
37883
+ * `SkillsExtension.load(files)` from storage.
37884
+ */
37885
+ var SkillsExtension = class {
37886
+ id = SKILLS_EXTENSION_ID;
37887
+ #source;
37888
+ #trusted;
37889
+ #baseRoot;
37890
+ #cacheHint;
37891
+ #cache;
37892
+ #strict;
37893
+ #warn;
37894
+ #attached = /* @__PURE__ */ new WeakSet();
37895
+ /**
37896
+ * Builds a catalog from storage through a {@linkcode SkillFiles} adapter
37897
+ * (`memorySkillFiles`, `nodeSkillFiles`, or your own).
37898
+ */
37899
+ static load(files, options) {
37900
+ return SkillCatalog.load(files, options);
37901
+ }
37902
+ constructor(options) {
37903
+ if (options.cacheHint !== void 0) assertValidCacheHint(options.cacheHint, "skills cacheHint");
37904
+ const baseUri = options.baseUri ?? "skill://";
37905
+ const canonical = SCHEME_URI.test(baseUri) ? canonicalUri(baseUri) : void 0;
37906
+ if (canonical === void 0) throw new TypeError(`baseUri must be a "scheme://…" URI (got ${JSON.stringify(baseUri)})`);
37907
+ this.#baseRoot = canonical.endsWith("://") ? canonical : `${trimTrailingSlashes(canonical)}/`;
37908
+ this.#source = options.source;
37909
+ this.#trusted = trustedSources.has(options.source);
37910
+ this.#cacheHint = options.cacheHint;
37911
+ this.#cache = {
37912
+ ttlMs: options.cacheHint?.ttlMs ?? 0,
37913
+ cacheScope: options.cacheHint?.cacheScope ?? "private"
37914
+ };
37915
+ this.#strict = options.strict ?? false;
37916
+ this.#warn = options.onWarning ?? defaultWarn;
37917
+ }
37918
+ /**
37919
+ * Serves these skills on `mcp`: registers the skill resource template and
37920
+ * the skills methods, then declares the capability. Must run before
37921
+ * connecting. Calling it again for the same server does nothing. One
37922
+ * skills extension per server: attaching another one throws and leaves the
37923
+ * server as it was, as does attaching to a server that already handles one
37924
+ * of the skills methods.
37925
+ *
37926
+ * @experimental How an extension is wired to a server may change in a minor release.
37927
+ */
37928
+ attach(mcp) {
37929
+ if (this.#attached.has(mcp)) return;
37930
+ if (mcp.isConnected()) throw new Error("attach skills before connecting the server");
37931
+ if (skillsServers.has(mcp)) throw new Error(ONE_PER_SERVER);
37932
+ const server = mcp.server;
37933
+ const directoryRead = this.#source.readDirectory !== void 0;
37934
+ const list = this.#source.listResources === void 0 ? void 0 : (ctx) => this.#listResources(ctx);
37935
+ server.assertCanSetRequestHandler(SKILLS_LIST_METHOD);
37936
+ server.assertCanSetRequestHandler(SKILLS_GET_METHOD);
37937
+ if (directoryRead) server.assertCanSetRequestHandler(RESOURCES_DIRECTORY_READ_METHOD);
37938
+ mcp.registerResource(SKILLS_EXTENSION_ID, new ResourceTemplate(`${this.#baseRoot}{+path}`, { list }), this.#cacheHint === void 0 ? {} : { cacheHint: this.#cacheHint }, (uri, _variables, ctx) => this.#read(uri, ctx));
37939
+ server.setRequestHandler(SKILLS_LIST_METHOD, { params: PaginatedRequestParamsSchema }, (params, ctx) => this.#list(server, params.cursor, ctx));
37940
+ server.setRequestHandler(SKILLS_GET_METHOD, { params: GetSkillRequestParamsSchema }, (params, ctx) => this.#get(server, params.uri, ctx));
37941
+ if (directoryRead) server.setRequestHandler(RESOURCES_DIRECTORY_READ_METHOD, { params: ReadResourceDirectoryRequestParamsSchema }, (params, ctx) => this.#readDirectory(server, params.uri, params.cursor, ctx));
37942
+ server.registerCapabilities({ extensions: { [SKILLS_EXTENSION_ID]: directoryRead ? { directoryRead: true } : {} } });
37943
+ this.#attached.add(mcp);
37944
+ skillsServers.add(mcp);
37945
+ afterAttach.get(this)?.(mcp);
37946
+ }
37947
+ #cacheFields(server) {
37948
+ return isModernProtocolVersion(server.getNegotiatedProtocolVersion() ?? "") ? { ...this.#cache } : {};
37949
+ }
37950
+ #keepValid(skills) {
37951
+ if (this.#trusted) return [...skills];
37952
+ const kept = [];
37953
+ for (const candidate of skills) {
37954
+ const problems = skillEntryProblems(candidate);
37955
+ if (problems.length === 0) {
37956
+ kept.push(candidate);
37957
+ continue;
37958
+ }
37959
+ const message = `invalid skill entry from the source: ${problems.join("; ")}`;
37960
+ if (this.#strict) throw new ProtocolError(ProtocolErrorCode.InternalError, `${describeEntry(candidate)}: ${message}`);
37961
+ this.#warn({
37962
+ path: describeEntry(candidate),
37963
+ message: `${message} (dropped from skills/list)`
37964
+ });
37965
+ }
37966
+ return kept;
37967
+ }
37968
+ async #list(server, cursor, ctx) {
37969
+ const page = await this.#source.list(cursor === void 0 ? {} : { cursor }, ctx);
37970
+ return {
37971
+ skills: this.#keepValid(page.skills),
37972
+ ...page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor },
37973
+ ...this.#cacheFields(server)
37974
+ };
37975
+ }
37976
+ async #get(server, uri, ctx) {
37977
+ const skill = await this.#source.get(uri, ctx);
37978
+ if (skill === void 0) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `No skill is served at ${uri}`);
37979
+ if (!this.#trusted) {
37980
+ const problems = skillEntryProblems(skill);
37981
+ if (problems.length > 0) {
37982
+ const message = `invalid skill entry from the source: ${problems.join("; ")}`;
37983
+ this.#warn({
37984
+ path: uri,
37985
+ message
37986
+ });
37987
+ throw new ProtocolError(ProtocolErrorCode.InternalError, message);
37988
+ }
37989
+ }
37990
+ if (canonicalUri(skill.uri) !== canonicalUri(uri)) throw new ProtocolError(ProtocolErrorCode.InternalError, `the skill source returned ${skill.uri} for ${uri}`);
37991
+ return {
37992
+ skill,
37993
+ ...this.#cacheFields(server)
37994
+ };
37995
+ }
37996
+ async #read(uri, ctx) {
37997
+ const result = await this.#source.read(uri.href, ctx);
37998
+ if (result === void 0) throw new ResourceNotFoundError(uri.href);
37999
+ return { contents: [toContents(uri.href, result)] };
38000
+ }
38001
+ async #readDirectory(server, uri, cursor, ctx) {
38002
+ const page = await this.#source.readDirectory(uri, cursor === void 0 ? {} : { cursor }, ctx);
38003
+ if (page === void 0) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `${uri} is not a directory resource`);
38004
+ return {
38005
+ resources: page.resources,
38006
+ ...page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor },
38007
+ ...this.#cacheFields(server)
38008
+ };
38009
+ }
38010
+ async #listResources(ctx) {
38011
+ const resources = await this.#source.listResources(ctx);
38012
+ const kept = [];
38013
+ for (const resource of resources) if (canonicalUri(resource.uri)?.startsWith(this.#baseRoot)) kept.push(resource);
38014
+ else this.#warn({
38015
+ path: resource.uri,
38016
+ message: `listResources returned a URI outside ${this.#baseRoot}; dropped from resources/list`
38017
+ });
38018
+ return { resources: kept };
38019
+ }
38020
+ };
38021
+ const PRUNE_THRESHOLD = 1024;
38022
+ function resolveLimits(limits) {
38023
+ const resolved = {
38024
+ files: limits?.files ?? SKILL_LIMITS.files,
38025
+ bytes: limits?.bytes ?? SKILL_LIMITS.bytes
38026
+ };
38027
+ for (const [name, value] of Object.entries(resolved)) if (value !== Number.POSITIVE_INFINITY && !(Number.isSafeInteger(value) && value > 0)) throw new RangeError(`limits.${name} must be a positive integer or Infinity (got ${value})`);
38028
+ return resolved;
38029
+ }
38030
+ function listingKey(snapshot) {
38031
+ return JSON.stringify(snapshot.listed.map((resource) => [
38032
+ resource.uri,
38033
+ resource.name,
38034
+ resource.description
38035
+ ]));
38036
+ }
38037
+ function diffSnapshots(previous, next) {
38038
+ const same = (uri) => JSON.stringify(previous.skills.get(uri)) === JSON.stringify(next.skills.get(uri));
38039
+ return {
38040
+ added: [...next.skills.keys()].filter((uri) => !previous.skills.has(uri)),
38041
+ removed: [...previous.skills.keys()].filter((uri) => !next.skills.has(uri)),
38042
+ changed: [...next.skills.keys()].filter((uri) => previous.skills.has(uri) && !same(uri))
38043
+ };
38044
+ }
38045
+ /**
38046
+ * A catalog the SDK built from storage. It holds metadata only: every file
38047
+ * read goes through the adapter and is checked against the published entry.
38048
+ */
38049
+ var SkillCatalog = class SkillCatalog extends SkillsExtension {
38050
+ #state;
38051
+ #files;
38052
+ #build;
38053
+ #servers = /* @__PURE__ */ new Set();
38054
+ #pruneAt = PRUNE_THRESHOLD;
38055
+ #queue = Promise.resolve();
38056
+ constructor(init) {
38057
+ super(init.sourceOptions);
38058
+ this.#state = init.state;
38059
+ this.#files = init.files;
38060
+ this.#build = init.build;
38061
+ afterAttach.set(this, (mcp) => this.#track(mcp));
38062
+ }
38063
+ static async load(files, options = {}) {
38064
+ const pageSize = resolvePageSize(options.pageSize);
38065
+ const build = {
38066
+ prefix: options.prefix === void 0 ? [] : splitRelativePath(options.prefix, "prefix"),
38067
+ limits: resolveLimits(options.limits),
38068
+ strict: options.strict ?? false,
38069
+ warn: options.onWarning ?? defaultWarn
38070
+ };
38071
+ const state = { current: await buildCatalogSnapshot(files, build) };
38072
+ return new SkillCatalog({
38073
+ state,
38074
+ files,
38075
+ build,
38076
+ sourceOptions: {
38077
+ source: catalogSource(state, files, pageSize, build.warn),
38078
+ baseUri: build.prefix.length === 0 ? "skill://" : joinUri("skill://", build.prefix),
38079
+ cacheHint: options.cacheHint,
38080
+ strict: options.strict,
38081
+ onWarning: build.warn
38082
+ }
38083
+ });
38084
+ }
38085
+ /** The skills this catalog serves, sorted by URI. Frozen (deeply): they are exactly what clients receive. */
38086
+ get entries() {
38087
+ return this.#state.current.ordered;
38088
+ }
38089
+ /**
38090
+ * Scans the storage again and swaps the new catalog in at once. All or
38091
+ * nothing: on failure the old catalog keeps serving. Overlapping calls run
38092
+ * one after another.
38093
+ *
38094
+ * When skills are added or removed, or a `SKILL.md`'s name or description
38095
+ * changes, the result has `listChanged: true` and `resources/list_changed`
38096
+ * goes out through every attached server that is still connected (stdio,
38097
+ * stateful 2025-11-25 HTTP sessions). Servers that `createMcpHandler` builds per
38098
+ * request are closed by then, so publish the change on the handler:
38099
+ * `if (changes.listChanged) handler.notify.resourcesChanged()`. Where the
38100
+ * runtime has no `WeakRef` (Cloudflare Workers with a compatibility date
38101
+ * before 2025-05-05), attached servers are not tracked and nothing is sent.
38102
+ */
38103
+ reload() {
38104
+ const run = this.#queue.then(() => this.#reloadNow());
38105
+ this.#queue = run.then(() => {}, () => {});
38106
+ return run;
38107
+ }
38108
+ #track(mcp) {
38109
+ if (typeof WeakRef !== "function") return;
38110
+ if (this.#servers.size >= this.#pruneAt) {
38111
+ for (const reference of this.#servers) if (reference.deref() === void 0) this.#servers.delete(reference);
38112
+ this.#pruneAt = Math.max(PRUNE_THRESHOLD, 2 * this.#servers.size);
38113
+ }
38114
+ this.#servers.add(new WeakRef(mcp));
38115
+ }
38116
+ async #reloadNow() {
38117
+ const previous = this.#state.current;
38118
+ const next = await buildCatalogSnapshot(this.#files, this.#build);
38119
+ this.#state.current = next;
38120
+ const listChanged = listingKey(previous) !== listingKey(next);
38121
+ if (listChanged) this.#notify();
38122
+ return {
38123
+ ...diffSnapshots(previous, next),
38124
+ listChanged
38125
+ };
38126
+ }
38127
+ #notify() {
38128
+ for (const reference of this.#servers) {
38129
+ const mcp = reference.deref();
38130
+ if (mcp === void 0) {
38131
+ this.#servers.delete(reference);
38132
+ continue;
38133
+ }
38134
+ if (!mcp.isConnected()) continue;
38135
+ const sessionId = mcp.server.transport?.sessionId;
38136
+ try {
38137
+ Promise.resolve(mcp.server.sendResourceListChanged()).catch((error) => this.#notifyFailed(sessionId, error));
38138
+ } catch (error) {
38139
+ this.#notifyFailed(sessionId, error);
38140
+ }
38141
+ }
38142
+ }
38143
+ #notifyFailed(sessionId, error) {
38144
+ const to = sessionId === void 0 ? "" : ` to session ${sessionId}`;
38145
+ const reason = error instanceof Error ? error.message : String(error);
38146
+ try {
38147
+ this.#build.warn({
38148
+ path: "resources/list_changed",
38149
+ message: `could not be sent${to}: ${reason}`
38150
+ });
38151
+ } catch {}
38152
+ }
38153
+ };
38154
+ //#endregion
38155
+ //#region node_modules/.pnpm/@modelcontextprotocol+server@2.4.0/node_modules/@modelcontextprotocol/server/dist/ext/skills/index.mjs
38156
+ /**
38157
+ * Builds a valid entry — digests, sizes, frontmatter, rule checks — from one
38158
+ * skill's files. Keys are paths under the skill's folder (`SKILL.md`,
38159
+ * `references/GUIDE.md`). For `SkillSource` authors who publish digests.
38160
+ */
38161
+ async function skillEntry(skillPath, files, options = {}) {
38162
+ const segments = splitRelativePath(skillPath, "skillPath");
38163
+ const baseUri = options.baseUri ?? "skill://";
38164
+ const inputs = Object.entries(files).map(([path, content]) => ({
38165
+ uri: joinUri(baseUri, [...segments, ...splitRelativePath(path, "file path")]),
38166
+ bytes: typeof content === "string" ? encodeUtf8(content) : content
38167
+ }));
38168
+ const { skill, problems } = await buildSkillEntry(joinUri(baseUri, [...segments, "SKILL.md"]), inputs);
38169
+ if (skill === void 0 || problems.length > 0) throw new SkillCatalogError(problems.map((message) => ({
38170
+ path: skillPath,
38171
+ message
38172
+ })));
38173
+ return skill;
38174
+ }
38175
+ //#endregion
38176
+ //#region src/customization/model.ts
38177
+ /** `global` reaches every session; a namespace's own reaches the sessions that expose it. */
38178
+ const INSTRUCTION_SCOPES = ["global", ...Namespace.options];
38179
+ /** Per scope, and for all scopes together: what one session can receive at most. */
38180
+ const INSTRUCTION_MAX_CHARS = 4e3;
38181
+ const INSTRUCTIONS_TOTAL_MAX_CHARS = 12e3;
38182
+ const PROMPT_MAX_CHARS = 32e3;
38183
+ const SKILL_FILE_MAX_CHARS = 64e3;
38184
+ const SKILL_MAX_FILES = 32;
38185
+ const SKILL_TOTAL_MAX_CHARS = 256e3;
38186
+ const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
38187
+ const NAME_MAX = 64;
38188
+ const PATH_SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
38189
+ const PATH_MAX_DEPTH = 4;
38190
+ const SKILL_ENTRY_FILE = "SKILL.md";
38191
+ const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
38192
+ const SINGLE_QUOTED = /^'(.*)'$/;
38193
+ const LINE_BREAK = /\r?\n/;
38194
+ const unquote = (value) => {
38195
+ if (value.startsWith("\"")) try {
38196
+ return JSON.parse(value);
38197
+ } catch {
38198
+ return value;
38199
+ }
38200
+ const single = SINGLE_QUOTED.exec(value);
38201
+ return single ? (single[1] ?? "").replaceAll("''", "'") : value;
38202
+ };
38203
+ /**
38204
+ * The `key: value` lines of a leading `---` block and the body after it. Only
38205
+ * single-line values: a YAML block scalar reads as its indicator, and a skill
38206
+ * using one is refused at write time rather than served half-parsed.
38207
+ */
38208
+ const parseFrontmatter = (text) => {
38209
+ const match = FRONTMATTER.exec(text);
38210
+ if (!match) return {
38211
+ fields: /* @__PURE__ */ new Map(),
38212
+ body: text
38213
+ };
38214
+ const fields = /* @__PURE__ */ new Map();
38215
+ for (const line of (match[1] ?? "").split(LINE_BREAK)) {
38216
+ const colon = line.indexOf(":");
38217
+ if (colon <= 0 || line.startsWith(" ")) continue;
38218
+ const value = line.slice(colon + 1).trim();
38219
+ fields.set(line.slice(0, colon).trim(), unquote(value));
38220
+ }
38221
+ return {
38222
+ fields,
38223
+ body: text.slice(match[0].length)
38224
+ };
38225
+ };
38226
+ const yamlValue = (value) => JSON.stringify(value);
38227
+ const isScope = (value) => INSTRUCTION_SCOPES.includes(value);
38228
+ const MARKDOWN = ".md";
38229
+ /** The scope a top-level `instructions/<scope>.md` holds, if it is one. */
38230
+ const instructionScopeOf = (name) => {
38231
+ const scope = name.endsWith(MARKDOWN) ? name.slice(0, -3) : "";
38232
+ return isScope(scope) ? scope : void 0;
38233
+ };
38234
+ const parsePrompt = (name, text, author) => {
38235
+ const promptName = name.endsWith(MARKDOWN) ? name.slice(0, -3) : "";
38236
+ const { fields, body } = parseFrontmatter(text);
38237
+ const description = fields.get("description");
38238
+ if (!(NAME.test(promptName) && description)) return void 0;
38239
+ return {
38240
+ name: promptName,
38241
+ title: fields.get("title"),
38242
+ description,
38243
+ text: body.trim(),
38244
+ author
38245
+ };
38246
+ };
38247
+ /** Turns the stored tree into entries; anything unexpected is listed, not served. */
38248
+ const parseCustomizations = (tree) => {
38249
+ const instructions = /* @__PURE__ */ new Map();
38250
+ const prompts = /* @__PURE__ */ new Map();
38251
+ const skillFiles = /* @__PURE__ */ new Map();
38252
+ const ignored = [];
38253
+ const takers = {
38254
+ instructions: (name, rest, text, author) => {
38255
+ const scope = rest.length === 0 ? instructionScopeOf(name) : void 0;
38256
+ if (!scope || text.trim() === "") return false;
38257
+ instructions.set(scope, {
38258
+ scope,
38259
+ text: text.trim(),
38260
+ author
38261
+ });
38262
+ return true;
38263
+ },
38264
+ prompts: (name, rest, text, author) => {
38265
+ const prompt = rest.length === 0 ? parsePrompt(name, text, author) : void 0;
38266
+ if (!prompt) return false;
38267
+ prompts.set(prompt.name, prompt);
38268
+ return true;
38269
+ },
38270
+ skills: (name, rest, text) => {
38271
+ if (!NAME.test(name) || rest.length === 0) return false;
38272
+ const files = skillFiles.get(name) ?? /* @__PURE__ */ new Map();
38273
+ files.set(rest.join("/"), text);
38274
+ skillFiles.set(name, files);
38275
+ return true;
38276
+ }
38277
+ };
38278
+ for (const [path, text] of tree.files) {
38279
+ const [kind = "", name = "", ...rest] = path.split("/");
38280
+ if (!(Object.hasOwn(takers, kind) && takers[kind]?.(name, rest, text, tree.authors.get(path)))) ignored.push(path);
38281
+ }
38282
+ const skills = /* @__PURE__ */ new Map();
38283
+ for (const [name, files] of skillFiles) {
38284
+ const entry = files.get(SKILL_ENTRY_FILE);
38285
+ const description = entry ? parseFrontmatter(entry).fields.get("description") : void 0;
38286
+ if (!description || prompts.has(name)) {
38287
+ ignored.push(...[...files.keys()].map((file) => `skills/${name}/${file}`));
38288
+ continue;
38289
+ }
38290
+ skills.set(name, {
38291
+ name,
38292
+ description,
38293
+ files,
38294
+ author: tree.authors.get(`skills/${name}/${SKILL_ENTRY_FILE}`)
38295
+ });
38296
+ }
38297
+ return {
38298
+ instructions,
38299
+ prompts,
38300
+ skills,
38301
+ ignored
38302
+ };
38303
+ };
38304
+ const isCustomizationError = (err) => err instanceof Error && err.name === "CustomizationError";
38305
+ /** A refused edit names the rule it breaks, so the model can fix the call. */
38306
+ const customizationError = (message) => Object.assign(new Error(message), { name: "CustomizationError" });
38307
+ const fail = (message) => {
38308
+ throw customizationError(message);
38309
+ };
38310
+ const checkName = (name, what) => {
38311
+ if (name.length > NAME_MAX || !NAME.test(name)) fail(`Invalid ${what} name "${name}": use lowercase letters, digits and single hyphens (e.g. draft-reply), at most ${NAME_MAX} characters.`);
38312
+ };
38313
+ const checkLength = (text, max, what) => {
38314
+ if (text.length > max) fail(`${what} is ${text.length} characters long; the limit is ${max}.`);
38315
+ };
38316
+ const checkSkillPath = (path) => {
38317
+ const segments = path.split("/");
38318
+ if (segments.length > PATH_MAX_DEPTH || !segments.every((s) => PATH_SEGMENT.test(s))) fail(`Invalid skill file path "${path}": use relative, /-separated names of letters, digits, dots, hyphens and underscores (e.g. references/lexicon.md), at most ${PATH_MAX_DEPTH} levels deep.`);
38319
+ };
38320
+ const instructionsTotal = (current, scope, text) => [...current.instructions.values()].filter((entry) => entry.scope !== scope).reduce((sum, entry) => sum + entry.text.length, text.length);
38321
+ const planInstructions = (current, scope, text) => {
38322
+ const path = `instructions/${scope}.md`;
38323
+ if (text === null) {
38324
+ if (!current.instructions.has(scope)) fail(`No instructions are set for "${scope}".`);
38325
+ return [{
38326
+ path,
38327
+ content: null
38328
+ }];
38329
+ }
38330
+ const trimmed = text.trim();
38331
+ if (trimmed === "") fail("The instructions are empty; delete them instead.");
38332
+ checkLength(trimmed, INSTRUCTION_MAX_CHARS, `The "${scope}" instructions`);
38333
+ const total = instructionsTotal(current, scope, trimmed);
38334
+ if (total > 12e3) fail(`All instructions together would be ${total} characters long; the limit is ${INSTRUCTIONS_TOTAL_MAX_CHARS}, since every session receives them. Shorten them, or move reference material into a skill.`);
38335
+ return [{
38336
+ path,
38337
+ content: `${trimmed}\n`
38338
+ }];
38339
+ };
38340
+ const planPrompt = (current, name, prompt) => {
38341
+ checkName(name, "prompt");
38342
+ const path = `prompts/${name}.md`;
38343
+ if (prompt === null) {
38344
+ if (!current.prompts.has(name)) fail(`No prompt is named "${name}".`);
38345
+ return [{
38346
+ path,
38347
+ content: null
38348
+ }];
38349
+ }
38350
+ if (current.skills.has(name)) fail(`A skill is already named "${name}", and every skill is served as a prompt of its name.`);
38351
+ const description = prompt.description.trim();
38352
+ const text = prompt.text.trim();
38353
+ if (description === "" || description.includes("\n")) fail("A prompt needs a one-line description: it is what clients show in their prompt menu.");
38354
+ if (text === "") fail("The prompt text is empty.");
38355
+ checkLength(text, PROMPT_MAX_CHARS, "The prompt text");
38356
+ const title = prompt.title?.trim();
38357
+ return [{
38358
+ path,
38359
+ content: `---\n${[...title ? [`title: ${yamlValue(title)}`] : [], `description: ${yamlValue(description)}`].join("\n")}\n---\n\n${text}\n`
38360
+ }];
38361
+ };
38362
+ const checkSkillEntry = (name, text) => {
38363
+ const { fields, body } = parseFrontmatter(text);
38364
+ if (fields.get("name") !== name) fail(`SKILL.md must start with a frontmatter block whose name is "${name}" (---, name: ${name}, description: ..., ---).`);
38365
+ const description = fields.get("description") ?? "";
38366
+ if (description === "" || [
38367
+ ">",
38368
+ "|",
38369
+ ">-",
38370
+ "|-"
38371
+ ].includes(description)) fail("SKILL.md needs a one-line description in its frontmatter: it decides when a client loads the skill.");
38372
+ if (body.trim() === "") fail("SKILL.md has no instructions after its frontmatter.");
38373
+ };
38374
+ const planSkill = (current, name, path, text) => {
38375
+ checkName(name, "skill");
38376
+ const skill = current.skills.get(name);
38377
+ const file = path ?? "SKILL.md";
38378
+ checkSkillPath(file);
38379
+ if (text === null) {
38380
+ if (!skill) fail(`No skill is named "${name}".`);
38381
+ if (file === "SKILL.md") return [...skill?.files.keys() ?? []].map((f) => ({
38382
+ path: `skills/${name}/${f}`,
38383
+ content: null
38384
+ }));
38385
+ if (!skill?.files.has(file)) fail(`The skill "${name}" has no file "${file}".`);
38386
+ return [{
38387
+ path: `skills/${name}/${file}`,
38388
+ content: null
38389
+ }];
38390
+ }
38391
+ if (current.prompts.has(name)) fail(`A prompt is already named "${name}", and every skill is also served as a prompt of its name.`);
38392
+ if (file === "SKILL.md") checkSkillEntry(name, text);
38393
+ else if (!skill) fail(`Create the skill's ${SKILL_ENTRY_FILE} first, then add "${file}".`);
38394
+ checkLength(text, SKILL_FILE_MAX_CHARS, `The file "${file}"`);
38395
+ const files = new Map(skill?.files ?? []);
38396
+ files.set(file, text);
38397
+ if (files.size > 32) fail(`A skill holds at most 32 files.`);
38398
+ const total = [...files.values()].reduce((sum, f) => sum + f.length, 0);
38399
+ if (total > 256e3) fail(`The skill would hold ${total} characters; the limit is ${SKILL_TOTAL_MAX_CHARS}.`);
38400
+ return [{
38401
+ path: `skills/${name}/${file}`,
38402
+ content: text
38403
+ }];
38404
+ };
38405
+ /** The file changes an edit makes, or a `CustomizationError` naming the rule it breaks. */
38406
+ const planEdit = (current, edit) => {
38407
+ if (edit.kind === "instructions") return planInstructions(current, edit.scope, edit.text);
38408
+ if (edit.kind === "prompt") return planPrompt(current, edit.name, edit.prompt);
38409
+ return planSkill(current, edit.name, edit.path, edit.text);
38410
+ };
38411
+ //#endregion
38412
+ //#region src/customization/serve.ts
38413
+ /** How much of a skill a derived prompt carries; the rest stays readable as the skill. */
38414
+ const DERIVED_PROMPT_MAX_CHARS = 16e3;
38415
+ const SCOPE_HEADINGS = {
38416
+ tickets: "For tickets",
38417
+ help_center: "For the Help Center",
38418
+ users: "For users and organizations",
38419
+ requests: "For customers' requests",
38420
+ preferences: "For connector preferences"
38421
+ };
38422
+ /**
38423
+ * The admins' instructions a session with `config`'s namespaces receives: the
38424
+ * global ones, then each active namespace's, or `undefined` when none apply.
38425
+ */
38426
+ const buildCustomInstructions = (state, config) => {
38427
+ const blocks = [];
38428
+ const global = state.instructions.get("global");
38429
+ if (global) blocks.push(global.text);
38430
+ for (const namespace of config.namespaces) {
38431
+ const entry = state.instructions.get(namespace);
38432
+ if (entry) blocks.push(`${SCOPE_HEADINGS[namespace]}:\n${entry.text}`);
38433
+ }
38434
+ if (blocks.length === 0) return void 0;
38435
+ return ["Your team's own instructions, set by its Zendesk admins:", ...blocks].join("\n\n");
38436
+ };
38437
+ /** Where a client supporting the Skills extension reads the skill. */
38438
+ const skillUri = (name, file = SKILL_ENTRY_FILE) => `skill://${name}/${file}`;
38439
+ /**
38440
+ * A skill as a prompt, for clients without the Skills extension. The header is
38441
+ * the server's, added on read: it sends a client that has the skill back to it,
38442
+ * and tells the others what the conversion dropped.
38443
+ */
38444
+ const derivedPromptText = (skill) => {
38445
+ const entry = skill.files.get("SKILL.md") ?? "";
38446
+ const body = parseFrontmatter(entry).body.trim();
38447
+ const truncated = body.length > DERIVED_PROMPT_MAX_CHARS;
38448
+ const others = [...skill.files.keys()].filter((file) => file !== SKILL_ENTRY_FILE).sort(compareCodeUnits);
38449
+ return [
38450
+ `[Note from the MCP server] This prompt is the skill "${skill.name}" (${skillUri(skill.name)}) converted for clients that do not support MCP skills.`,
38451
+ `If "${skill.name}" is among the skills available to you, stop reading this prompt: load that skill and follow it instead.`,
38452
+ "Otherwise follow the instructions below, knowing the conversion is lossy: the skill's other files are not included and its scripts cannot run here. If something below does not work as it says, that is a limit of this conversion, not a mistake on your side.",
38453
+ "",
38454
+ "---",
38455
+ "",
38456
+ truncated ? `${body.slice(0, DERIVED_PROMPT_MAX_CHARS)}\n\n[Truncated by the MCP server: the full text is ${skillUri(skill.name)}.]` : body,
38457
+ ...others.length > 0 ? [
38458
+ "",
38459
+ "---",
38460
+ "",
38461
+ "Files of this skill not included here (MCP resources, if your client can read them):",
38462
+ ...others.map((file) => `- ${skillUri(skill.name, file)}`)
38463
+ ] : []
38464
+ ].join("\n");
38465
+ };
38466
+ const userMessage = (text) => ({ messages: [{
38467
+ role: "user",
38468
+ content: {
38469
+ type: "text",
38470
+ text
38471
+ }
38472
+ }] });
38473
+ /** The prompts of `state`: the admins' own, then one per skill. Returns the removers. */
38474
+ const registerPrompts = (server, state) => {
38475
+ return [...[...state.prompts.values()].map((prompt) => server.registerPrompt(prompt.name, {
38476
+ ...prompt.title ? { title: prompt.title } : {},
38477
+ description: prompt.description
38478
+ }, () => userMessage(prompt.text))), ...[...state.skills.values()].map((skill) => server.registerPrompt(skill.name, { description: `${skill.description} (The skill "${skill.name}" as a prompt: use the skill itself when your client supports MCP skills.)` }, () => userMessage(derivedPromptText(skill))))].map((handle) => () => handle.remove());
38479
+ };
38480
+ /**
38481
+ * Serves the customizations on one `/mcp` server: its prompts and the skills.
38482
+ * `live` for a server that outlives an edit (a 2025 session): its prompts are
38483
+ * swapped in place, which notifies the client; per-request servers are rebuilt.
38484
+ */
38485
+ const serveCustomizations = (server, service, { live }) => {
38486
+ if (service.current().skills.size > 0) service.skills.attach(server);
38487
+ server.registerPrompt("customizations-placeholder", {}, () => userMessage("")).remove();
38488
+ let removers = registerPrompts(server, service.current());
38489
+ if (!live) return;
38490
+ const unsubscribe = service.onChange(() => {
38491
+ for (const remove of removers) remove();
38492
+ removers = registerPrompts(server, service.current());
38493
+ });
38494
+ const previous = server.server.onclose;
38495
+ server.server.onclose = () => {
38496
+ unsubscribe();
38497
+ previous?.();
38498
+ };
38499
+ };
38500
+ //#endregion
38501
+ //#region src/customization/setup.ts
38502
+ /**
38503
+ * The `setup` guide served on `/mcp/admin`, as a prompt and as a skill. Its
38504
+ * main use is the first configuration; the same steps serve a later revision.
38505
+ */
38506
+ const SETUP_NAME = "setup";
38507
+ const SETUP_DESCRIPTION = "Set up or revise what this Zendesk MCP server teaches every user's assistant: team instructions, prompts and skills. Use when a Zendesk admin wants to configure, review or change them.";
38508
+ const SETUP_BODY = `# Set up the assistant for your team
38509
+
38510
+ You are helping a Zendesk administrator configure what this MCP server distributes to every user's AI assistant, with the manage_customizations tool.
38511
+
38512
+ ## 1. Start from what already exists
38513
+
38514
+ Before proposing anything:
38515
+
38516
+ 1. Call manage_customizations with action "list". It shows the built-in context the server already sends on its own, and the customizations already configured.
38517
+ 2. Read the built-in context: action "get", kind "built_in", name "instructions" (what every session already receives), then name "topology" (the Help Center structure: locales, categories and sections with ids, segments, permission groups).
38518
+ 3. Summarize both for the admin in a few lines, then ask what they want their team's assistants to do differently.
38519
+
38520
+ ## 2. Pick the right kind for each need
38521
+
38522
+ - Instructions: short, always-on rules every session receives (tone, vocabulary, what never to do). Every session pays for them in context, so keep them to a few lines per scope. Scope "global" reaches every session; a namespace scope (tickets, help_center, users, requests, preferences) reaches only the sessions that expose it.
38523
+ - Prompt: a workflow a user starts on purpose from their client's prompt menu (draft a reply in our style, triage a ticket). It costs nothing until used.
38524
+ - Skill: reference material or a procedure the assistant loads by itself when a task matches its description (style guide, lexicon, translation workflow). Write that description so it says when to use the skill.
38525
+
38526
+ Ask the admin about their team's practices. Never invent rules on their behalf.
38527
+
38528
+ ## 3. Never duplicate what is built in
38529
+
38530
+ Before each set, compare the text with the built-in context you read in step 1. The server already tells every assistant which Help Center it is connected to, its locales, its category and section tree with ids, its segments and permission groups, the user's role, and the SLA alert thresholds. If the admin asks to add any of these (the Help Center URL, the languages, the list of sections...), tell them it is already provided, say where it comes from, and do not write it.
38531
+
38532
+ Also compare new content with the existing customizations, so that two entries never repeat or contradict each other.
38533
+
38534
+ ## 4. Mind the clients that do not support skills
38535
+
38536
+ Many MCP clients do not support MCP skills yet. For them the server also serves every skill as a prompt of the same name, but the conversion is lossy: the prompt carries SKILL.md only, truncated when long; the skill's other files are listed but not included, and scripts cannot run. So:
38537
+
38538
+ - put everything essential in SKILL.md itself;
38539
+ - avoid skills that depend on running code;
38540
+ - warn the admin when a skill they want relies on extra files or code.
38541
+
38542
+ ## 5. Write, then check
38543
+
38544
+ - Show the admin the exact text before each set, and write it only once they agree.
38545
+ - After setting instructions, the tool returns everything a new session receives. Re-read it for repetitions and contradictions with the built-in part.
38546
+ - Instructions reach new sessions only. Prompts and skills reach open sessions on clients that refresh their lists.
38547
+ - Names use lowercase letters, digits and hyphens. A skill's SKILL.md starts with a frontmatter block holding its name and a one-line description.
38548
+ `;
38549
+ const SETUP_SKILL = `---
38550
+ name: ${SETUP_NAME}
38551
+ description: ${JSON.stringify(SETUP_DESCRIPTION)}
38552
+ ---
38553
+
38554
+ ${SETUP_BODY}`;
38555
+ //#endregion
38556
+ //#region src/customization/admin.ts
38557
+ const ADMIN_ONLY = "This back-office is reserved for Zendesk administrators: your Zendesk role is not admin.";
38558
+ /**
38559
+ * The caller, re-read from Zendesk on every call: a token minted for an admin
38560
+ * stays valid up to an hour after a demotion, this check does not.
38561
+ */
38562
+ const requireAdmin = async (subdomain, getToken) => {
38563
+ const { user } = await zendeskGet(subdomain, await getToken(), "/users/me");
38564
+ if (user.role !== "admin") throw new Error(ADMIN_ONLY);
38565
+ return {
38566
+ id: user.id,
38567
+ name: user.name ?? `user ${user.id}`
38568
+ };
38569
+ };
38570
+ const text = (value) => ({ content: [{
38571
+ type: "text",
38572
+ text: value
38573
+ }] });
38574
+ const describeAuthor = (author) => author ? `, last changed by ${author.name} on ${author.at.slice(0, 10)}` : "";
38575
+ /** What a new `/mcp` session receives as `instructions`, built-in part first. */
38576
+ const effectiveInstructions = (config, state) => [buildInstructions(config), buildCustomInstructions(state, config)].filter((block) => block !== void 0).join("\n\n") || "(none)";
38577
+ const listCustomizations = (env) => {
38578
+ const state = env.service.current();
38579
+ return [
38580
+ "## Built in (read only, sent by the server on its own)",
38581
+ "- instructions: what every session already receives. Read it with action \"get\", kind \"built_in\", name \"instructions\".",
38582
+ "- topology: the Help Center structure (locales, categories and sections with ids, segments, permission groups, the caller's role), served to every session as a resource. Read it with kind \"built_in\", name \"topology\".",
38583
+ "",
38584
+ "## Instructions",
38585
+ ...state.instructions.size === 0 ? ["(none)"] : [...state.instructions.values()].map((entry) => `- ${entry.scope}: ${entry.text.length} characters${describeAuthor(entry.author)}`),
38586
+ "",
38587
+ "## Prompts",
38588
+ ...state.prompts.size === 0 ? ["(none)"] : [...state.prompts.values()].map((prompt) => `- ${prompt.name}: ${prompt.description}${describeAuthor(prompt.author)}`),
38589
+ "",
38590
+ "## Skills",
38591
+ ...state.skills.size === 0 ? ["(none)"] : [...state.skills.values()].map((skill) => `- ${skill.name}: ${skill.description} (files: ${sorted(skill.files)})${describeAuthor(skill.author)}`),
38592
+ "",
38593
+ `Limits: ${INSTRUCTION_MAX_CHARS} characters per instructions scope and ${INSTRUCTIONS_TOTAL_MAX_CHARS} for all of them; ${PROMPT_MAX_CHARS} per prompt; a skill's derived prompt carries the first ${DERIVED_PROMPT_MAX_CHARS} characters of its SKILL.md.`
38594
+ ].join("\n");
38595
+ };
38596
+ const ACTIONS = [
38597
+ "list",
38598
+ "get",
38599
+ "set",
38600
+ "delete"
38601
+ ];
38602
+ const KINDS = [
38603
+ "instructions",
38604
+ "prompt",
38605
+ "skill",
38606
+ "built_in"
38607
+ ];
38608
+ const BUILT_IN = ["instructions", "topology"];
38609
+ const inputSchema = object({
38610
+ action: _enum(ACTIONS).describe("list: everything configured plus the built-in context; get: read one entry; set: create or replace one entry; delete: remove one entry."),
38611
+ kind: _enum(KINDS).optional().describe("What the entry is: instructions (always-on text every matching session receives), prompt (a workflow users start from their prompt menu), skill (a SKILL.md, plus optional files, the assistant loads when a task matches), or built_in (what the server already sends, read only). Required except for list."),
38612
+ name: string().optional().describe(`Which entry. For instructions, the scope: ${INSTRUCTION_SCOPES.join(", ")} (global reaches every session, a namespace only sessions exposing it). For prompts and skills, a lowercase-hyphenated name such as draft-reply, shared by both kinds. For built_in: instructions or topology. Required except for list.`),
38613
+ path: string().optional().describe(`Skills only: the file inside the skill, such as references/lexicon.md. Defaults to ${SKILL_ENTRY_FILE}; deleting ${SKILL_ENTRY_FILE} (or giving no path) deletes the whole skill.`),
38614
+ content: string().optional().describe(`For set: the full new text, replacing the old one. Instructions and prompts are plain Markdown; a skill's ${SKILL_ENTRY_FILE} starts with a YAML frontmatter block holding name (the skill name) and a one-line description (quoted if it contains ": ").`),
38615
+ description: string().optional().describe("For set on a prompt: the one-line description clients show in their prompt menu. Required there."),
38616
+ title: string().optional().describe("For set on a prompt: an optional human-readable title shown instead of the name.")
38617
+ });
38618
+ const required = (value, what) => {
38619
+ if (value === void 0 || value === "") throw customizationError(`${what} is required here.`);
38620
+ return value;
38621
+ };
38622
+ const asScope = (name) => {
38623
+ if (!INSTRUCTION_SCOPES.includes(name)) throw customizationError(`Unknown instructions scope "${name}": use one of ${INSTRUCTION_SCOPES.join(", ")}.`);
38624
+ return name;
38625
+ };
38626
+ const sorted = (files) => [...files.keys()].sort(compareCodeUnits).join(", ");
38627
+ const READERS = {
38628
+ built_in: (env, name, _params, topology) => {
38629
+ if (name === "instructions") return buildInstructions(env.config) ?? "(none)";
38630
+ if (name === "topology") return topology().read();
38631
+ throw customizationError(`Unknown built-in "${name}": use ${BUILT_IN.join(" or ")}.`);
38632
+ },
38633
+ instructions: (env, name) => env.service.current().instructions.get(asScope(name))?.text ?? `No instructions are set for "${name}".`,
38634
+ prompt: (env, name) => {
38635
+ const prompt = env.service.current().prompts.get(name);
38636
+ if (!prompt) return `No prompt is named "${name}".`;
38637
+ return [
38638
+ ...prompt.title ? [`Title: ${prompt.title}`] : [],
38639
+ `Description: ${prompt.description}`,
38640
+ "",
38641
+ prompt.text
38642
+ ].join("\n");
38643
+ },
38644
+ skill: (env, name, params) => {
38645
+ const skill = env.service.current().skills.get(name);
38646
+ if (!skill) return `No skill is named "${name}".`;
38647
+ const path = params.path ?? "SKILL.md";
38648
+ const file = skill.files.get(path);
38649
+ if (file === void 0) return `The skill "${name}" has no file "${path}".`;
38650
+ return `Files: ${sorted(skill.files)}\nURI: ${skillUri(name, path)}\n\n${file}`;
38651
+ }
38652
+ };
38653
+ const getEntry = (env, params, topology) => READERS[required(params.kind, "kind")](env, required(params.name, "name"), params, topology);
38654
+ const EDITS = {
38655
+ instructions: (name, content) => ({
38656
+ kind: "instructions",
38657
+ scope: asScope(name),
38658
+ text: content
38659
+ }),
38660
+ prompt: (name, content, params) => ({
38661
+ kind: "prompt",
38662
+ name,
38663
+ prompt: content === null ? null : {
38664
+ description: required(params.description, "description"),
38665
+ text: content,
38666
+ ...params.title === void 0 ? {} : { title: params.title }
38667
+ }
38668
+ }),
38669
+ skill: (name, content, params) => ({
38670
+ kind: "skill",
38671
+ name,
38672
+ ...params.path === void 0 ? {} : { path: params.path },
38673
+ text: content
38674
+ }),
38675
+ built_in: () => {
38676
+ throw customizationError("The built-in context is read only: it comes from the server and Zendesk.");
38677
+ }
38678
+ };
38679
+ const toEdit = (params, deleting) => EDITS[required(params.kind, "kind")](required(params.name, "name"), deleting ? null : required(params.content, "content"), params);
38680
+ const confirm = (env, params, deleting, state) => {
38681
+ const done = deleting ? "Deleted" : "Saved";
38682
+ const what = `${params.kind} "${params.name}"${params.path ? ` (${params.path})` : ""}`;
38683
+ if (params.kind === "instructions") return `${done} ${what}. A new session on /mcp now receives these instructions (the built-in part first); check them for repetitions and contradictions:\n\n${effectiveInstructions(env.config, state)}`;
38684
+ const name = params.name ?? "";
38685
+ if (params.kind === "skill" && state.skills.has(name)) return `${done} ${what}. Clients supporting MCP skills get it as ${skillUri(name)}; the others get the prompt "${name}", carrying ${SKILL_ENTRY_FILE} only.`;
38686
+ if (deleting) return `${done} ${what}. It leaves the prompt menu of clients that refresh their prompt list.`;
38687
+ return `${done} ${what}. It appears in the prompt menu of clients that refresh their prompt list.`;
38688
+ };
38689
+ const createAdminTools = (env) => {
38690
+ const topologyFor = (caller) => () => createTopologyProvider(caller.getToken, env.config.subdomain, void 0, env.config.brandIds);
38691
+ return [{
38692
+ name: "manage_customizations",
38693
+ readOnly: false,
38694
+ title: "Manage Team Customizations",
38695
+ description: "Read and change what this MCP server teaches every user's assistant on /mcp: instructions (always-on text), prompts (workflows users start from their prompt menu) and skills (SKILL.md files the assistant loads when a task matches). list shows them all with who changed each, plus the built-in context the server already sends; get reads one entry, or the built-in instructions and Help Center topology; set creates or replaces an entry; delete removes one. Writes go to this server's store, never to Zendesk. Reserved for Zendesk admins, checked on every call. Before setting anything, read the built-in context and do not repeat it; the setup prompt walks through the whole configuration.",
38696
+ inputSchema,
38697
+ annotations: {
38698
+ readOnlyHint: false,
38699
+ destructiveHint: true,
38700
+ idempotentHint: true,
38701
+ openWorldHint: false
38702
+ },
38703
+ handler: async (raw, caller) => {
38704
+ const params = raw;
38705
+ const admin = await requireAdmin(env.config.subdomain, caller.getToken);
38706
+ try {
38707
+ if (params.action === "list") return text(listCustomizations(env));
38708
+ if (params.action === "get") return text(await getEntry(env, params, topologyFor(caller)));
38709
+ const deleting = params.action === "delete";
38710
+ const state = await env.service.edit(toEdit(params, deleting), {
38711
+ name: `${admin.name} (Zendesk user ${admin.id})`,
38712
+ at: (/* @__PURE__ */ new Date()).toISOString()
38713
+ });
38714
+ return text(confirm(env, params, deleting, state));
38715
+ } catch (err) {
38716
+ if (isCustomizationError(err)) throw new Error(`${err.message} Nothing was changed.`, { cause: err });
38717
+ throw err;
38718
+ }
38719
+ }
38720
+ }];
38721
+ };
38722
+ const ADMIN_INSTRUCTIONS = `This is the back-office of the Zendesk MCP server, reserved for Zendesk admins. It configures what the server teaches every user's assistant on its main endpoint: instructions, prompts and skills. Start with the "${SETUP_NAME}" prompt, or the skill of the same name: it reads what already exists and guides each change.`;
38723
+ /** The `setup` guide as a skill, the same for every admin: built once per process. */
38724
+ let setupSkills;
38725
+ const loadSetupSkill = () => {
38726
+ setupSkills ??= SkillCatalog.load(memorySkillFiles({ [`${SETUP_NAME}/${SKILL_ENTRY_FILE}`]: SETUP_SKILL }));
38727
+ return setupSkills;
38728
+ };
38729
+ /** One `/mcp/admin` server: the admin tools, the `setup` prompt and skill, nothing of Zendesk's. */
38730
+ const createAdminServer = ({ readOnly, tools, setupSkill, caller, requestStateCodec, onUnauthorized }) => {
38731
+ const pkg = readPackageInfo();
38732
+ const server = new McpServer({
38733
+ name: pkg.name,
38734
+ version: pkg.version
38735
+ }, {
38736
+ instructions: ADMIN_INSTRUCTIONS,
38737
+ requestState: { verify: (state, ctx) => requestStateCodec.verify(state, ctx) }
38738
+ });
38739
+ for (const tool of tools.filter((t) => t.readOnly || !readOnly)) server.registerTool(tool.name, {
38740
+ title: tool.title,
38741
+ description: tool.description,
38742
+ inputSchema: toStrictStandardSchema(tool.inputSchema),
38743
+ annotations: tool.annotations
38744
+ }, async (params) => {
38745
+ try {
38746
+ return await tool.handler(params, caller);
38747
+ } catch (err) {
38748
+ if (err instanceof ZendeskApiError && err.status === 401) onUnauthorized?.();
38749
+ throw err;
38750
+ }
38751
+ });
38752
+ server.registerPrompt(SETUP_NAME, {
38753
+ title: "Set up the team assistant",
38754
+ description: SETUP_DESCRIPTION
38755
+ }, () => ({ messages: [{
38756
+ role: "user",
38757
+ content: {
38758
+ type: "text",
38759
+ text: SETUP_BODY
38760
+ }
38761
+ }] }));
38762
+ setupSkill.attach(server);
38763
+ return server;
38764
+ };
38765
+ //#endregion
38766
+ //#region src/customization/backend.ts
38767
+ const AUTHORS_FILE = ".authors.json";
38768
+ const TEMP_FILE = /\.[0-9a-f-]{36}\.tmp$/;
38769
+ /** Nothing on disk: tests, and `memory://` deployments that accept losing it on restart. */
38770
+ const createMemoryBackend = (seed = {}) => {
38771
+ const files = new Map(Object.entries(seed));
38772
+ const authors = /* @__PURE__ */ new Map();
38773
+ return {
38774
+ load: async () => ({
38775
+ files: new Map(files),
38776
+ authors: new Map(authors)
38777
+ }),
38778
+ apply: async (changes, author) => {
38779
+ for (const { path, content } of changes) if (content === null) {
38780
+ files.delete(path);
38781
+ authors.delete(path);
38782
+ } else {
38783
+ files.set(path, content);
38784
+ authors.set(path, author);
38785
+ }
38786
+ }
38787
+ };
38788
+ };
38789
+ const listFiles = async (root, dir = "") => {
38790
+ const entries = await readdir(join(root, dir), { withFileTypes: true }).catch((err) => {
38791
+ if (err.code === "ENOENT") return [];
38792
+ throw err;
38793
+ });
38794
+ return (await Promise.all(entries.map(async (entry) => {
38795
+ const path = dir === "" ? entry.name : `${dir}/${entry.name}`;
38796
+ if (entry.isDirectory()) return listFiles(root, path);
38797
+ return entry.isFile() && !TEMP_FILE.test(entry.name) && path !== AUTHORS_FILE ? [path] : [];
38798
+ }))).flat();
38799
+ };
38800
+ const writeAtomic = async (path, text) => {
38801
+ await mkdir(dirname(path), {
38802
+ recursive: true,
38803
+ mode: 448
38804
+ });
38805
+ const temp = `${path}.${randomUUID()}.tmp`;
38806
+ await writeFile(temp, text, { mode: 384 });
38807
+ await rename(temp, path);
38808
+ };
38809
+ /**
38810
+ * One Markdown file per entry under `root`, readable and versionable as is; the
38811
+ * authors live beside them in `.authors.json`. Each file is replaced by a
38812
+ * rename, so a reader never sees half of one.
38813
+ */
38814
+ const createFileBackend = (dir) => {
38815
+ const root = resolve(dir);
38816
+ const authorsPath = join(root, AUTHORS_FILE);
38817
+ const readAuthors = async () => {
38818
+ try {
38819
+ return JSON.parse(await readFile(authorsPath, "utf8"));
38820
+ } catch {
38821
+ return {};
38822
+ }
38823
+ };
38824
+ const resolvePath = (path) => join(root, ...path.split("/"));
38825
+ return {
38826
+ load: async () => {
38827
+ const paths = await listFiles(root);
38828
+ return {
38829
+ files: new Map(await Promise.all(paths.map(async (path) => [path, await readFile(resolvePath(path), "utf8")]))),
38830
+ authors: new Map(Object.entries(await readAuthors()))
38831
+ };
38832
+ },
38833
+ apply: async (changes, author) => {
38834
+ const authors = await readAuthors();
38835
+ for (const { path, content } of changes) {
38836
+ const target = resolvePath(path);
38837
+ if (!target.startsWith(`${root}${sep}`)) throw new Error(`Invalid path: ${path}`);
38838
+ if (content === null) {
38839
+ await rm(target, { force: true });
38840
+ delete authors[path];
38841
+ } else {
38842
+ await writeAtomic(target, content);
38843
+ authors[path] = author;
38844
+ }
38845
+ }
38846
+ await writeAtomic(authorsPath, `${JSON.stringify(authors, null, 2)}\n`);
38847
+ }
38848
+ };
38849
+ };
38850
+ /** `file://<dir>` or `memory://`, as `CUSTOMIZATION_STORE` names it. */
38851
+ const openCustomizationBackend = (uri) => {
38852
+ const url = new URL(uri);
38853
+ if (url.protocol === "memory:") return createMemoryBackend();
38854
+ if (url.protocol === "file:") return createFileBackend(fileURLToPath(url));
38855
+ throw new Error(`Unsupported CUSTOMIZATION_STORE "${url.protocol}//": use file://<directory> or memory://.`);
38856
+ };
38857
+ //#endregion
38858
+ //#region src/customization/service.ts
38859
+ const encoder = new TextEncoder();
38860
+ /**
38861
+ * The skills of `state()` as the SDK's storage adapter sees them: one folder
38862
+ * per skill at the root. Read live, so `catalog.reload()` picks up an edit.
38863
+ */
38864
+ const skillFiles = (state) => {
38865
+ const paths = () => [...state().skills.values()].flatMap((skill) => [...skill.files.keys()].map((file) => `${skill.name}/${file}`));
38866
+ return {
38867
+ list: async (dir) => {
38868
+ const prefix = dir === "" ? "" : `${dir}/`;
38869
+ const children = /* @__PURE__ */ new Map();
38870
+ for (const path of paths()) {
38871
+ if (!path.startsWith(prefix)) continue;
38872
+ const [name = "", ...rest] = path.slice(prefix.length).split("/");
38873
+ children.set(name, rest.length > 0 ? "directory" : "file");
38874
+ }
38875
+ return [...children].map(([name, kind]) => ({
38876
+ name,
38877
+ kind
38878
+ }));
38879
+ },
38880
+ read: async (path) => {
38881
+ const [name = "", ...rest] = path.split("/");
38882
+ const text = state().skills.get(name)?.files.get(rest.join("/"));
38883
+ if (text === void 0) throw new Error(`No skill file at ${path}`);
38884
+ return encoder.encode(text);
38885
+ }
38886
+ };
38887
+ };
38888
+ /**
38889
+ * The skill as it would be after `changes`, checked by the SDK the way the
38890
+ * catalog will read it (YAML frontmatter, name matching the folder): a skill
38891
+ * it would skip is refused now, with its reason, rather than silently dropped.
38892
+ */
38893
+ const checkSkill = async (state, name, changes) => {
38894
+ const files = new Map(state.skills.get(name)?.files ?? []);
38895
+ for (const { path, content } of changes) {
38896
+ const file = path.slice(`skills/${name}/`.length);
38897
+ if (content === null) files.delete(file);
38898
+ else files.set(file, content);
38899
+ }
38900
+ try {
38901
+ await skillEntry(name, Object.fromEntries(files));
38902
+ } catch (err) {
38903
+ const reason = err instanceof Error ? err.message : String(err);
38904
+ throw customizationError(`The skill would not be served: ${reason} (${SKILL_ENTRY_FILE} frontmatter is YAML: quote a value holding ": ").`);
38905
+ }
38906
+ };
38907
+ const createCustomizationService = async (backend, logger = silentLogger) => {
38908
+ const load = async () => {
38909
+ const state = parseCustomizations(await backend.load());
38910
+ if (state.ignored.length > 0) logger.warn("customizations_ignored", {
38911
+ files: state.ignored,
38912
+ hint: "Files the customization store holds but serves nothing for: a malformed entry, or a file placed by hand."
38913
+ });
38914
+ return state;
38915
+ };
38916
+ let state = await load();
38917
+ const listeners = /* @__PURE__ */ new Set();
38918
+ const skills = await SkillCatalog.load(skillFiles(() => state), { onWarning: (warning) => {
38919
+ if (state.skills.size > 0) logger.warn("customization_skill_skipped", { ...warning });
38920
+ } });
38921
+ let queue = Promise.resolve();
38922
+ const apply = async (edit, author) => {
38923
+ const changes = planEdit(state, edit);
38924
+ if (edit.kind === "skill" && edit.text !== null) await checkSkill(state, edit.name, changes);
38925
+ let failure;
38926
+ try {
38927
+ await backend.apply(changes, author);
38928
+ } catch (error) {
38929
+ failure = { error };
38930
+ }
38931
+ state = await load();
38932
+ await skills.reload();
38933
+ for (const listener of listeners) listener();
38934
+ if (failure) throw failure.error;
38935
+ return state;
38936
+ };
38937
+ return {
38938
+ current: () => state,
38939
+ edit: (edit, author) => {
38940
+ const run = queue.then(() => apply(edit, author));
38941
+ queue = run.catch(() => void 0);
38942
+ return run;
38943
+ },
38944
+ onChange: (listener) => {
38945
+ listeners.add(listener);
38946
+ return () => listeners.delete(listener);
38947
+ },
38948
+ skills
38949
+ };
38950
+ };
38951
+ //#endregion
36571
38952
  //#region src/utils/grant-registry.ts
36572
38953
  const createGrantRegistry = ({ idleTimeoutMs, now = Date.now, onCloseError = () => void 0 }) => {
36573
38954
  const slots = /* @__PURE__ */ new Map();
@@ -36708,7 +39089,7 @@ const SPACES = / +/;
36708
39089
  * resource (`expectedResource`) and the expiry; decrypting and every other
36709
39090
  * claim stay with the authorization server.
36710
39091
  */
36711
- const authenticate = async (request, as) => {
39092
+ const authenticate = async (request, as, resource = as.resource) => {
36712
39093
  const header = request.headers["authorization"];
36713
39094
  const verifier = { verifyAccessToken: async (bearer) => {
36714
39095
  const token = await as.verifyAccessToken(bearer);
@@ -36726,13 +39107,14 @@ const authenticate = async (request, as) => {
36726
39107
  const normalized = typeof header === "string" ? header.replace(SPACES, " ") : void 0;
36727
39108
  return (await verifyBearerToken(normalized, {
36728
39109
  verifier,
36729
- expectedResource: new URL(as.resource)
39110
+ expectedResource: new URL(resource)
36730
39111
  })).extra?.["verified"];
36731
39112
  } catch {
36732
39113
  return;
36733
39114
  }
36734
39115
  };
36735
39116
  const PROTECTED_RESOURCE_PATHS = ["/.well-known/oauth-protected-resource/mcp", "/.well-known/oauth-protected-resource"];
39117
+ const ADMIN_PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource/mcp/admin";
36736
39118
  const sendJson = (res, status, body) => {
36737
39119
  res.writeHead(status, { "Content-Type": "application/json" });
36738
39120
  res.end(JSON.stringify(body));
@@ -36759,8 +39141,8 @@ const failRequest = (res, err) => {
36759
39141
  }
36760
39142
  if (!res.writableEnded) res.end();
36761
39143
  };
36762
- const sendUnauthorized = (res, issuer) => {
36763
- const wwwAuthenticate = `Bearer resource_metadata="${issuer}${PROTECTED_RESOURCE_PATHS[0]}", error="invalid_token", error_description="${MISSING_BEARER_MESSAGE}"`;
39144
+ const sendUnauthorized = (res, issuer, surface) => {
39145
+ const wwwAuthenticate = `Bearer resource_metadata="${issuer}${surface === "admin" ? ADMIN_PROTECTED_RESOURCE_PATH : PROTECTED_RESOURCE_PATHS[0]}", error="invalid_token", error_description="${MISSING_BEARER_MESSAGE}"`;
36764
39146
  sendJsonRpcError(res, 401, -32e3, MISSING_BEARER_MESSAGE, { "WWW-Authenticate": wwwAuthenticate });
36765
39147
  };
36766
39148
  const asNodeRequest = (req) => req;
@@ -36830,6 +39212,12 @@ const SESSION_IDLE_TIMEOUT_MS = 18e5;
36830
39212
  const SESSION_SWEEP_INTERVAL_MS = 6e4;
36831
39213
  const startHttpTransport = async (config, logger = silentLogger, options = {}) => {
36832
39214
  const buildAuthorizationServer = await prepareAuthorizationServer(config, logger);
39215
+ const customizations = await createCustomizationService(openCustomizationBackend(config.customizationStore ?? pathToFileURL(join(configDir(), "customizations")).href), logger);
39216
+ const setupSkill = await loadSetupSkill();
39217
+ const adminTools = createAdminTools({
39218
+ config,
39219
+ service: customizations
39220
+ });
36833
39221
  let authorizationServer;
36834
39222
  const sessions = /* @__PURE__ */ new Map();
36835
39223
  const sla = startSlaRuntime(config);
@@ -36848,9 +39236,9 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
36848
39236
  ...config,
36849
39237
  readOnly: true
36850
39238
  };
36851
- const dispatchToSession = async (req, res, sessionId, token, body) => {
39239
+ const dispatchToSession = async (req, res, sessionId, token, body, surface) => {
36852
39240
  const session = sessions.get(sessionId);
36853
- if (!session || session.subject !== token.subject) return false;
39241
+ if (!session || session.subject !== token.subject || session.surface !== surface) return false;
36854
39242
  if (session.canWrite && !token.canWrite) return false;
36855
39243
  session.auth.zendeskToken = token.zendeskAccessToken;
36856
39244
  session.auth.grantId = token.grantId;
@@ -36858,13 +39246,29 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
36858
39246
  await session.transport.handleRequest(req, res, body);
36859
39247
  return true;
36860
39248
  };
36861
- const openSession = async (req, res, body, token, as) => {
39249
+ const buildServer = (surface, params, live) => surface === "admin" ? createAdminServer({
39250
+ env: {
39251
+ config,
39252
+ service: customizations
39253
+ },
39254
+ readOnly: params.config.readOnly,
39255
+ tools: adminTools,
39256
+ setupSkill,
39257
+ caller: { getToken: params.getToken },
39258
+ requestStateCodec: params.requestStateCodec,
39259
+ onUnauthorized: params.onUnauthorized
39260
+ }) : serverFactory({
39261
+ ...params,
39262
+ extraInstructions: buildCustomInstructions(customizations.current(), params.config),
39263
+ decorate: (server) => serveCustomizations(server, customizations, { live })
39264
+ });
39265
+ const openSession = async (req, res, body, token, as, surface) => {
36862
39266
  const auth = {
36863
39267
  zendeskToken: token.zendeskAccessToken,
36864
39268
  grantId: token.grantId
36865
39269
  };
36866
39270
  const sessionConfig = effectiveConfig(token);
36867
- const server = serverFactory({
39271
+ const server = buildServer(surface, {
36868
39272
  config: sessionConfig,
36869
39273
  getToken: () => auth.zendeskToken,
36870
39274
  logger,
@@ -36878,7 +39282,7 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
36878
39282
  notifyResourceUpdated: (uri) => {
36879
39283
  server.server.sendResourceUpdated({ uri }).catch(() => void 0);
36880
39284
  }
36881
- });
39285
+ }, true);
36882
39286
  const transport = new NodeStreamableHTTPServerTransport({
36883
39287
  sessionIdGenerator: () => randomUUID(),
36884
39288
  onsessioninitialized: (newId) => {
@@ -36886,6 +39290,7 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
36886
39290
  transport,
36887
39291
  auth,
36888
39292
  subject: token.subject,
39293
+ surface,
36889
39294
  canWrite: !sessionConfig.readOnly,
36890
39295
  lastActivityAt: Date.now(),
36891
39296
  close: async () => {
@@ -36900,8 +39305,9 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
36900
39305
  };
36901
39306
  await server.connect(transport);
36902
39307
  await transport.handleRequest(req, res, body);
39308
+ if (transport.sessionId === void 0) await server.close();
36903
39309
  };
36904
- const createGrantEntry = (token, as) => {
39310
+ const createGrantEntry = (token, as, surface) => {
36905
39311
  const { grantId } = token;
36906
39312
  const auth = { zendeskToken: token.zendeskAccessToken };
36907
39313
  const getToken = () => auth.zendeskToken;
@@ -36917,7 +39323,7 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
36917
39323
  const handler = createMcpHandler(({ authInfo }) => {
36918
39324
  const verified = authInfo?.extra?.["verified"];
36919
39325
  if (!verified) throw new Error("2026-07-28 request without a verified token");
36920
- return serverFactory({
39326
+ return buildServer(surface, {
36921
39327
  config: effectiveConfig(verified),
36922
39328
  getToken,
36923
39329
  logger,
@@ -36928,13 +39334,18 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
36928
39334
  slaRuntime: sla.runtime,
36929
39335
  alertPolling: "grant",
36930
39336
  notifyResourceUpdated: (uri) => handler.notify.resourceUpdated(uri)
36931
- });
39337
+ }, false);
36932
39338
  }, {
36933
39339
  legacy: "reject",
36934
39340
  onerror: (err) => logger.debug("mcp_request_rejected", { error: err.message })
36935
39341
  });
36936
39342
  const listens = createListenFeed();
36937
- const stopAlerts = watchGrantAlerts({
39343
+ const main = surface === "main";
39344
+ const stopCustomizations = main ? customizations.onChange(() => {
39345
+ handler.notify.promptsChanged();
39346
+ handler.notify.resourcesChanged();
39347
+ }) : () => void 0;
39348
+ const stopAlerts = main ? watchGrantAlerts({
36938
39349
  config,
36939
39350
  getToken,
36940
39351
  caches,
@@ -36943,7 +39354,7 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
36943
39354
  listens,
36944
39355
  notify: (uri) => handler.notify.resourceUpdated(uri),
36945
39356
  onUnauthorized
36946
- });
39357
+ }) : () => void 0;
36947
39358
  const entry = {
36948
39359
  auth,
36949
39360
  revoked: false,
@@ -36951,6 +39362,7 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
36951
39362
  listens,
36952
39363
  handle: toNodeHandler(handler),
36953
39364
  close: () => {
39365
+ stopCustomizations();
36954
39366
  stopAlerts();
36955
39367
  listens.closeAll();
36956
39368
  return handler.close();
@@ -36958,10 +39370,11 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
36958
39370
  };
36959
39371
  return entry;
36960
39372
  };
36961
- const serveModern = async (req, res, body, token, as) => {
36962
- const entry = grants.acquire(token.grantId, token.subject, () => createGrantEntry(token, as));
39373
+ const serveModern = async (req, res, body, token, as, surface) => {
39374
+ const key = `${surface}:${token.grantId}`;
39375
+ const entry = grants.acquire(key, token.subject, () => createGrantEntry(token, as, surface));
36963
39376
  if (!entry) {
36964
- sendUnauthorized(res, as.issuer);
39377
+ sendUnauthorized(res, as.issuer, surface);
36965
39378
  return;
36966
39379
  }
36967
39380
  entry.auth.zendeskToken = token.zendeskAccessToken;
@@ -36985,22 +39398,22 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
36985
39398
  await entry.handle(request, res, body);
36986
39399
  } finally {
36987
39400
  if (counted) entry.inFlight -= 1;
36988
- if (entry.revoked && entry.inFlight === 0) await grants.close(token.grantId, entry);
39401
+ if (entry.revoked && entry.inFlight === 0) await grants.close(key, entry);
36989
39402
  }
36990
39403
  };
36991
- const serveLegacy = async (req, res, body, token, as) => {
39404
+ const serveLegacy = async (req, res, body, token, as, surface) => {
36992
39405
  const sessionId = typeof req.headers["mcp-session-id"] === "string" ? req.headers["mcp-session-id"] : void 0;
36993
- if (sessionId && await dispatchToSession(req, res, sessionId, token, body)) return;
39406
+ if (sessionId && await dispatchToSession(req, res, sessionId, token, body, surface)) return;
36994
39407
  if (req.method !== "POST") {
36995
39408
  sendJsonRpcError(res, 400, -32e3, "No active session; initialize via POST first.");
36996
39409
  return;
36997
39410
  }
36998
- await openSession(req, res, body, token, as);
39411
+ await openSession(req, res, body, token, as, surface);
36999
39412
  };
37000
- const handleMcpRequest = async (req, res, as) => {
37001
- const token = await authenticate(req, as);
39413
+ const handleMcpRequest = async (req, res, as, surface) => {
39414
+ const token = await authenticate(req, as, surface === "admin" ? as.adminResource : as.resource);
37002
39415
  if (!token) {
37003
- sendUnauthorized(res, as.issuer);
39416
+ sendUnauthorized(res, as.issuer, surface);
37004
39417
  return;
37005
39418
  }
37006
39419
  let body;
@@ -37013,10 +39426,10 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
37013
39426
  body = read.value;
37014
39427
  }
37015
39428
  if (body !== void 0 && !await isLegacyRequest(await toWebRequest(asNodeRequest(req), body), body)) {
37016
- await serveModern(req, res, body, token, as);
39429
+ await serveModern(req, res, body, token, as, surface);
37017
39430
  return;
37018
39431
  }
37019
- await serveLegacy(req, res, body, token, as);
39432
+ await serveLegacy(req, res, body, token, as, surface);
37020
39433
  };
37021
39434
  const route = async (req, res, as) => {
37022
39435
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
@@ -37024,6 +39437,10 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
37024
39437
  sendJson(res, 200, as.protectedResourceMetadata);
37025
39438
  return;
37026
39439
  }
39440
+ if (req.method === "GET" && url.pathname === ADMIN_PROTECTED_RESOURCE_PATH) {
39441
+ sendJson(res, 200, as.adminProtectedResourceMetadata);
39442
+ return;
39443
+ }
37027
39444
  if (req.method === "GET" && url.pathname === "/healthz") {
37028
39445
  sendJson(res, 200, {
37029
39446
  status: "ok",
@@ -37032,7 +39449,11 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
37032
39449
  return;
37033
39450
  }
37034
39451
  if (url.pathname === "/mcp") {
37035
- await handleMcpRequest(req, res, as);
39452
+ await handleMcpRequest(req, res, as, "main");
39453
+ return;
39454
+ }
39455
+ if (url.pathname === "/mcp/admin") {
39456
+ await handleMcpRequest(req, res, as, "admin");
37036
39457
  return;
37037
39458
  }
37038
39459
  await as.handle(req, res);