@fruggr/zendesk-mcp-server 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import { parseArgs } from "node:util";
4
- import { randomBytes, randomUUID } from "node:crypto";
4
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
5
5
  import minpath, { dirname, join } from "node:path";
6
6
  import { fileURLToPath, fileURLToPath as urlToPath, pathToFileURL } from "node:url";
7
7
  import minproc, { default as process$2 } from "node:process";
8
- import { readFileSync } from "node:fs";
8
+ import { chmod, mkdir, open, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
9
+ import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
9
11
  //#region \0rolldown/runtime.js
10
12
  var __create$1 = Object.create;
11
13
  var __defProp$1 = Object.defineProperty;
@@ -39,7 +41,7 @@ var __toESM$1 = (mod, isNodeMode, target) => (target = mod != null ? __create$1(
39
41
  }) : target, mod));
40
42
  var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
41
43
  //#endregion
42
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/util.js
44
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/util.js
43
45
  function getEnumValues(entries) {
44
46
  const numericValues = Object.values(entries).filter((v) => typeof v === "number");
45
47
  return Object.entries(entries).filter(([k, _]) => numericValues.indexOf(+k) === -1).map(([_, v]) => v);
@@ -591,7 +593,7 @@ function constantCatch(value) {
591
593
  return fn;
592
594
  }
593
595
  //#endregion
594
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/core.js
596
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/core.js
595
597
  var _a$4;
596
598
  /** A special constant with type `never` */
597
599
  const NEVER = /*@__PURE__*/ Object.freeze({ status: "aborted" });
@@ -699,7 +701,7 @@ function config(newConfig) {
699
701
  return globalConfig;
700
702
  }
701
703
  //#endregion
702
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/doc.js
704
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/doc.js
703
705
  var Doc = class {
704
706
  constructor(args = [], closed = {}) {
705
707
  this.content = [];
@@ -733,7 +735,7 @@ var Doc = class {
733
735
  }
734
736
  };
735
737
  //#endregion
736
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/memoizer.js
738
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/memoizer.js
737
739
  var $ZodCyclicError = class extends Error {
738
740
  constructor() {
739
741
  super(`Cannot parse a reference cycle that closes through a transform`);
@@ -892,7 +894,7 @@ function bucketFor(state, inst) {
892
894
  return bucket;
893
895
  }
894
896
  let handoff;
895
- const open = [];
897
+ const open$1 = [];
896
898
  const memo$2 = {
897
899
  alloc(_inst, payload, empty) {
898
900
  const bucket = handoff;
@@ -903,7 +905,7 @@ const memo$2 = {
903
905
  issues: null
904
906
  };
905
907
  bucket.set(payload.value, entry);
906
- open.push(entry);
908
+ open$1.push(entry);
907
909
  return empty;
908
910
  },
909
911
  guard(inst) {
@@ -969,10 +971,10 @@ const memo$2 = {
969
971
  return payload;
970
972
  }
971
973
  handoff = bucket;
972
- const depth = open.length;
974
+ const depth = open$1.length;
973
975
  const result = base(payload, ctx);
974
976
  handoff = void 0;
975
- const entry = open.length > depth ? open.pop() : void 0;
977
+ const entry = open$1.length > depth ? open$1.pop() : void 0;
976
978
  if (result instanceof Promise) return result.then((r) => {
977
979
  if (entry) entry.issues = r.issues.length ? cloneIssues(r.issues) : NO_ISSUES;
978
980
  return r;
@@ -995,7 +997,7 @@ function isBackEdge(ctx, value) {
995
997
  return backEdges !== void 0 && isRef(value) && backEdges.has(value);
996
998
  }
997
999
  //#endregion
998
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/regexes.js
1000
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/regexes.js
999
1001
  /**
1000
1002
  * @deprecated CUID v1 is deprecated by its authors due to information leakage
1001
1003
  * (timestamps embedded in the id). Use {@link cuid2} instead.
@@ -1070,7 +1072,7 @@ const _null$2 = /^null$/i;
1070
1072
  const lowercase = /^[^A-Z]*$/;
1071
1073
  const uppercase = /^[^a-z]*$/;
1072
1074
  //#endregion
1073
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/checks.js
1075
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/checks.js
1074
1076
  const $ZodCheck = /*@__PURE__*/ $constructor("$ZodCheck", (inst, def) => {
1075
1077
  var _a;
1076
1078
  inst._zod ?? (inst._zod = {});
@@ -1366,7 +1368,7 @@ const $ZodCheckOverwrite = /*@__PURE__*/ $constructor("$ZodCheckOverwrite", (ins
1366
1368
  };
1367
1369
  });
1368
1370
  //#endregion
1369
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/errors.js
1371
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/errors.js
1370
1372
  function _getMessage() {
1371
1373
  const internals = this._zod;
1372
1374
  internals.message ?? (internals.message = JSON.stringify(internals.def, jsonStringifyReplacer, 2));
@@ -1482,7 +1484,7 @@ function formatError(error, mapper = (issue) => issue.message) {
1482
1484
  return fieldErrors;
1483
1485
  }
1484
1486
  //#endregion
1485
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/parse.js
1487
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/parse.js
1486
1488
  function finalizeParams(callee, params) {
1487
1489
  return {
1488
1490
  callee: params?.callee ?? callee,
@@ -1685,14 +1687,14 @@ const _safeDecodeAsync = (_Err) => async (schema, value, _ctx) => {
1685
1687
  return _safeParseAsync(_Err)(schema, value, _ctx);
1686
1688
  };
1687
1689
  //#endregion
1688
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/versions.js
1690
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/versions.js
1689
1691
  const version = {
1690
1692
  major: 4,
1691
1693
  minor: 6,
1692
- patch: 6
1694
+ patch: 7
1693
1695
  };
1694
1696
  //#endregion
1695
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/schemas.js
1697
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/schemas.js
1696
1698
  const $ZodType = /*@__PURE__*/ $constructor("$ZodType", (inst, def) => {
1697
1699
  var _a;
1698
1700
  inst ?? (inst = {});
@@ -3230,7 +3232,7 @@ function handleRefineResult(result, payload, input, inst) {
3230
3232
  }
3231
3233
  }
3232
3234
  //#endregion
3233
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/compile.js
3235
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/compile.js
3234
3236
  /** @internal Sentinel the compiled fast path returns when validation fails. */
3235
3237
  const INVALID = Symbol.for("zod.compile.invalid");
3236
3238
  const FALLBACK_FLAG = Symbol.for("zod.compile.fallback");
@@ -4715,7 +4717,7 @@ function generateTransformCheck(doc, ctx, schema, accessor) {
4715
4717
  return accessor;
4716
4718
  }
4717
4719
  //#endregion
4718
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/locales/en.js
4720
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/locales/en.js
4719
4721
  const error = () => {
4720
4722
  const Sizable = {
4721
4723
  string: {
@@ -4823,7 +4825,7 @@ function en_default() {
4823
4825
  return { localeError: error() };
4824
4826
  }
4825
4827
  //#endregion
4826
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/registries.js
4828
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/registries.js
4827
4829
  var _a$3;
4828
4830
  var $ZodRegistry = class {
4829
4831
  constructor() {
@@ -4870,7 +4872,7 @@ function registry() {
4870
4872
  (_a$3 = globalThis).__zod_globalRegistry ?? (_a$3.__zod_globalRegistry = registry());
4871
4873
  const globalRegistry = globalThis.__zod_globalRegistry;
4872
4874
  //#endregion
4873
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/api.js
4875
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/api.js
4874
4876
  function snapshotChecks(def) {
4875
4877
  if (def.checks) def.checks = [...def.checks];
4876
4878
  return def;
@@ -5399,7 +5401,7 @@ function _check(fn, params) {
5399
5401
  return ch;
5400
5402
  }
5401
5403
  //#endregion
5402
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/to-json-schema.js
5404
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/to-json-schema.js
5403
5405
  function assignProps(target, ...sources) {
5404
5406
  for (const source of sources) for (const key of Reflect.ownKeys(source)) if (Object.prototype.propertyIsEnumerable.call(source, key)) assignProp(target, key, source[key]);
5405
5407
  return target;
@@ -5843,7 +5845,7 @@ const createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params)
5843
5845
  return finalize(ctx, schema);
5844
5846
  };
5845
5847
  //#endregion
5846
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/json-schema-processors.js
5848
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/json-schema-processors.js
5847
5849
  const narrowMin = (agg, key, value) => {
5848
5850
  if (agg[key] === void 0 || value > agg[key]) agg[key] = value;
5849
5851
  };
@@ -6489,7 +6491,7 @@ function toJSONSchema(input, params) {
6489
6491
  return finalize(ctx, input);
6490
6492
  }
6491
6493
  //#endregion
6492
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/compile.js
6494
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/compile.js
6493
6495
  let compiling = false;
6494
6496
  globalConfig.postProcessor = (inst) => {
6495
6497
  if (compiling) return;
@@ -6518,7 +6520,7 @@ globalConfig.postProcessor = (inst) => {
6518
6520
  inst._zod.run = shim;
6519
6521
  };
6520
6522
  //#endregion
6521
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/errors.js
6523
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/classic/errors.js
6522
6524
  const _installedErrorProtos = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]);
6523
6525
  function _lazyMethod(proto, key, make) {
6524
6526
  Object.defineProperty(proto, key, {
@@ -6568,7 +6570,7 @@ const initializer = (inst, issues) => {
6568
6570
  };
6569
6571
  const ZodRealError = /*@__PURE__*/ $constructor("ZodError", initializer, void 0, { Parent: Error });
6570
6572
  //#endregion
6571
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/parse.js
6573
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/classic/parse.js
6572
6574
  const parse$10 = /* @__PURE__ */ _parse(ZodRealError);
6573
6575
  const parseAsync = /* @__PURE__ */ _parseAsync(ZodRealError);
6574
6576
  const safeParse = /* @__PURE__ */ _safeParse(ZodRealError);
@@ -6582,7 +6584,7 @@ const safeDecode = /* @__PURE__ */ _safeDecode(ZodRealError);
6582
6584
  const safeEncodeAsync = /* @__PURE__ */ _safeEncodeAsync(ZodRealError);
6583
6585
  const safeDecodeAsync = /* @__PURE__ */ _safeDecodeAsync(ZodRealError);
6584
6586
  //#endregion
6585
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/schemas.js
6587
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/classic/schemas.js
6586
6588
  function _ensureDefaultLocale() {
6587
6589
  if (!globalConfig.localeError) config(en_default());
6588
6590
  }
@@ -7517,7 +7519,7 @@ function preprocess$1(fn, schema) {
7517
7519
  });
7518
7520
  }
7519
7521
  //#endregion
7520
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/compat.js
7522
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/classic/compat.js
7521
7523
  /** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */
7522
7524
  const ZodIssueCode = {
7523
7525
  invalid_type: "invalid_type",
@@ -7536,7 +7538,7 @@ const ZodIssueCode = {
7536
7538
  var ZodFirstPartyTypeKind;
7537
7539
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
7538
7540
  //#endregion
7539
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/iso.js
7541
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/classic/iso.js
7540
7542
  function datetime(params) {
7541
7543
  return /* @__PURE__ */ _isoDateTime(ZodISODateTime, params);
7542
7544
  }
@@ -7544,7 +7546,7 @@ function date(params) {
7544
7546
  return /* @__PURE__ */ _isoDate(ZodISODate, params);
7545
7547
  }
7546
7548
  //#endregion
7547
- //#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/coerce.js
7549
+ //#region node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/classic/coerce.js
7548
7550
  function number$1(params) {
7549
7551
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
7550
7552
  }
@@ -7561,6 +7563,83 @@ const readEnv = (name) => ({
7561
7563
  value: process.env[name]
7562
7564
  });
7563
7565
  //#endregion
7566
+ //#region src/utils/durations.ts
7567
+ const MINUTES_PER_UNIT = {
7568
+ w: 10080,
7569
+ d: 1440,
7570
+ h: 60,
7571
+ m: 1
7572
+ };
7573
+ const DURATION = /^(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$/;
7574
+ /**
7575
+ * Reads a readable duration (`30m`, `4h`, `2d`, `1w`, `1d4h`) as whole minutes,
7576
+ * case-insensitive. `undefined` for anything else, including a bare number: a
7577
+ * unit-less `4` is ambiguous between minutes and hours.
7578
+ */
7579
+ const parseDuration = (raw) => {
7580
+ const text = raw.trim().toLowerCase();
7581
+ const match = DURATION.exec(text);
7582
+ if (!match || text === "") return void 0;
7583
+ const [, w, d, h, m] = match;
7584
+ return Number(w ?? 0) * MINUTES_PER_UNIT.w + Number(d ?? 0) * MINUTES_PER_UNIT.d + Number(h ?? 0) * MINUTES_PER_UNIT.h + Number(m ?? 0);
7585
+ };
7586
+ /** Why `raw` was refused, with the spelling `parseDuration` reads. */
7587
+ const notADuration = (raw) => `"${raw}" is not a duration. Use a number and a unit: 30m, 4h, 2d, 1w, or a mix like 1d4h.`;
7588
+ /**
7589
+ * Renders a span of minutes in the same spelling `parseDuration` reads, days
7590
+ * being the largest unit (`2d1h`, `45m`). The sign is dropped: callers say
7591
+ * "overdue" or "due" themselves.
7592
+ */
7593
+ const formatMinutes = (minutes) => {
7594
+ let rest = Math.abs(Math.round(minutes));
7595
+ const parts = [];
7596
+ for (const [unit, size] of [["d", MINUTES_PER_UNIT.d], ["h", MINUTES_PER_UNIT.h]]) if (rest >= size) {
7597
+ parts.push(`${Math.floor(rest / size)}${unit}`);
7598
+ rest %= size;
7599
+ }
7600
+ if (rest > 0 || parts.length === 0) parts.push(`${rest}m`);
7601
+ return parts.join("");
7602
+ };
7603
+ //#endregion
7604
+ //#region src/utils/sla-thresholds.ts
7605
+ const REPLY_METRICS = /* @__PURE__ */ new Set(["first_reply_time", "next_reply_time"]);
7606
+ const familyOf = (metric) => REPLY_METRICS.has(metric) ? "reply" : "wait";
7607
+ const DEFAULT_SLA_THRESHOLDS = {
7608
+ reply: {
7609
+ warning: 240,
7610
+ critical: 60
7611
+ },
7612
+ wait: {
7613
+ warning: 2880,
7614
+ critical: 480
7615
+ }
7616
+ };
7617
+ /**
7618
+ * Checks a warning/critical pair given as readable durations. Returns the pair
7619
+ * in minutes, or the reason it is refused: a critical window wider than the
7620
+ * warning one would never show a WARNING.
7621
+ */
7622
+ const toThreshold = (warning, critical) => {
7623
+ const w = parseDuration(warning);
7624
+ const c = parseDuration(critical);
7625
+ if (w === void 0 || c === void 0) return { error: notADuration(w === void 0 ? warning : critical) };
7626
+ if (c > w) return { error: `The critical threshold (${critical}) must not be longer than the warning one (${warning}).` };
7627
+ return { threshold: {
7628
+ warning: w,
7629
+ critical: c
7630
+ } };
7631
+ };
7632
+ /** Reads the `warning,critical` form of the admin variables (`4h,1h`). */
7633
+ const parseThresholdPair = (raw) => {
7634
+ const parts = raw.split(",").map((part) => part.trim());
7635
+ const [warning, critical] = parts;
7636
+ if (parts.length !== 2 || !warning || !critical) return { error: "Expected two durations separated by a comma, warning first: 4h,1h." };
7637
+ return toThreshold(warning, critical);
7638
+ };
7639
+ const formatThreshold = (t) => `warning ${formatMinutes(t.warning)}, critical ${formatMinutes(t.critical)}`;
7640
+ /** One line naming both families' thresholds, as the alerts and the tools report them. */
7641
+ const describeThresholds = (t) => `reply metrics ${formatThreshold(t.reply)}; wait and resolution metrics ${formatThreshold(t.wait)}`;
7642
+ //#endregion
7564
7643
  //#region src/utils/startup-error.ts
7565
7644
  /**
7566
7645
  * Misconfigurations an operator fixes before the server can start. `index.ts`
@@ -7686,7 +7765,8 @@ const Namespace = _enum([
7686
7765
  "tickets",
7687
7766
  "help_center",
7688
7767
  "users",
7689
- "requests"
7768
+ "requests",
7769
+ "preferences"
7690
7770
  ]);
7691
7771
  /**
7692
7772
  * Namespaces exposed when the operator passes no `--namespace` flag.
@@ -7701,9 +7781,18 @@ const Namespace = _enum([
7701
7781
  const DEFAULT_NAMESPACES = [
7702
7782
  "tickets",
7703
7783
  "help_center",
7704
- "users"
7784
+ "users",
7785
+ "preferences"
7705
7786
  ];
7706
7787
  const Transport = _enum(["stdio", "http"]);
7788
+ const SLA_POLL_RANGE = {
7789
+ min: 60,
7790
+ max: 86400
7791
+ };
7792
+ const SlaThresholdSchema = object({
7793
+ warning: number$2().int().min(0),
7794
+ critical: number$2().int().min(0)
7795
+ });
7707
7796
  const ConfigSchema = object({
7708
7797
  subdomain: string$2().min(1, "ZENDESK_SUBDOMAIN is required"),
7709
7798
  oauthClientId: string$2().min(1),
@@ -7809,8 +7898,51 @@ const ConfigSchema = object({
7809
7898
  /** HTTP only: CIMD client ids added to the consent-skip allowlist. */
7810
7899
  oauthTrustedClients: array(string$2().url()).default([]),
7811
7900
  /** HTTP only: whether the built-in allowlist (claude.ai, ChatGPT) applies. */
7812
- defaultTrustedClients: boolean$1().default(true)
7901
+ defaultTrustedClients: boolean$1().default(true),
7902
+ /**
7903
+ * Admin default SLA alert thresholds, in minutes before a breach, per metric
7904
+ * family (`SLA_REPLY_THRESHOLDS` / `SLA_WAIT_THRESHOLDS`, `4h,1h` form).
7905
+ * Parsed in `loadConfig`, so a bad value fails at startup naming the knob.
7906
+ */
7907
+ slaThresholds: object({
7908
+ reply: SlaThresholdSchema,
7909
+ wait: SlaThresholdSchema
7910
+ }).default(DEFAULT_SLA_THRESHOLDS),
7911
+ /**
7912
+ * Whether the server pushes SLA urgency on its own (off by default;
7913
+ * `--sla-push` / `SLA_PUSH=true` turn it on): the `**Urgency**` line in the SLA block of
7914
+ * get_ticket / search_tickets, and the alert resource's polling and update
7915
+ * notifications. Every tool and reading the resource stay available.
7916
+ */
7917
+ slaPush: boolean$1().default(false),
7918
+ /**
7919
+ * How long one user's SLA scope fetch is reused, in seconds (default 60; 0
7920
+ * turns the cache off). Zendesk charges a 304 revalidation as a full request,
7921
+ * so this lifetime is the only quota saving (PR #387 spike 2).
7922
+ */
7923
+ slaCacheTtlSeconds: number$2().int().min(0).default(60),
7924
+ /**
7925
+ * Where per-user SLA state (cache, preferences) lives. Unset: `memory` on
7926
+ * HTTP, `disk` on stdio so the stdio processes of one machine share it.
7927
+ */
7928
+ slaStateStore: _enum(["memory", "disk"]).optional(),
7929
+ /**
7930
+ * Whether each user may set personal SLA alert thresholds with
7931
+ * `configure_sla_alerts` (`SLA_USER_PREFERENCES=off` /
7932
+ * `--no-sla-user-preferences` turn it off: the admin defaults then apply to
7933
+ * everyone).
7934
+ */
7935
+ slaUserPreferences: boolean$1().default(true),
7936
+ /**
7937
+ * How often, in seconds, a subscribed SLA alert resource is re-checked while
7938
+ * at least one subscription is open (default 300, minimum 60, which keeps a
7939
+ * subscriber well inside Zendesk's rate limit; at most a day, past which
7940
+ * `setInterval` would clamp the delay to 1 ms).
7941
+ */
7942
+ slaPollSeconds: number$2().int().min(SLA_POLL_RANGE.min).max(SLA_POLL_RANGE.max).default(300)
7813
7943
  });
7944
+ /** The store a config resolves to; see `slaStateStore`. */
7945
+ const slaStateStoreOf = (config) => config.slaStateStore ?? (config.transport === "http" ? "memory" : "disk");
7814
7946
  const DIGITS_ONLY = /^\d+$/;
7815
7947
  const parsePort = (raw, label) => {
7816
7948
  if (!DIGITS_ONLY.test(raw)) throw createStartupError(`Invalid ${label} value. Expected an integer 0-65535.`, label.startsWith("--") ? STARTUP_DOCS.invocation : STARTUP_DOCS.environment);
@@ -7875,7 +8007,14 @@ const CLI_OPTIONS = {
7875
8007
  "no-topology": { type: "boolean" },
7876
8008
  "no-promoted-articles": { type: "boolean" },
7877
8009
  dev: { type: "boolean" },
7878
- "print-tools": { type: "boolean" }
8010
+ "print-tools": { type: "boolean" },
8011
+ "sla-push": { type: "boolean" },
8012
+ "no-sla-user-preferences": { type: "boolean" },
8013
+ "sla-reply-thresholds": { type: "string" },
8014
+ "sla-wait-thresholds": { type: "string" },
8015
+ "sla-cache-ttl-seconds": { type: "string" },
8016
+ "sla-state-store": { type: "string" },
8017
+ "sla-poll-seconds": { type: "string" }
7879
8018
  };
7880
8019
  new Set(Object.entries(CLI_OPTIONS).filter(([, spec]) => spec.type === "string").map(([name]) => `--${name}`));
7881
8020
  const FIELD_BY_FLAG = /* @__PURE__ */ new Map([
@@ -7890,7 +8029,12 @@ const FIELD_BY_FLAG = /* @__PURE__ */ new Map([
7890
8029
  ["cors-origin", "corsOrigins"],
7891
8030
  ["oauth-master-secret-file", "oauthMasterSecretFile"],
7892
8031
  ["oauth-store", "oauthStore"],
7893
- ["oauth-trusted-client", "oauthTrustedClients"]
8032
+ ["oauth-trusted-client", "oauthTrustedClients"],
8033
+ ["sla-reply-thresholds", "slaReplyThresholds"],
8034
+ ["sla-wait-thresholds", "slaWaitThresholds"],
8035
+ ["sla-cache-ttl-seconds", "slaCacheTtlSeconds"],
8036
+ ["sla-state-store", "slaStateStore"],
8037
+ ["sla-poll-seconds", "slaPollSeconds"]
7894
8038
  ]);
7895
8039
  const STANDALONE_EFFECTS = /* @__PURE__ */ new Map([
7896
8040
  ["read-only", { readOnly: true }],
@@ -7898,7 +8042,9 @@ const STANDALONE_EFFECTS = /* @__PURE__ */ new Map([
7898
8042
  ["no-promoted-articles", { promotedArticles: false }],
7899
8043
  ["dev", { dev: true }],
7900
8044
  ["print-tools", { printTools: true }],
7901
- ["no-default-trusted-clients", { defaultTrustedClients: false }]
8045
+ ["no-default-trusted-clients", { defaultTrustedClients: false }],
8046
+ ["sla-push", { slaPush: true }],
8047
+ ["no-sla-user-preferences", { slaUserPreferences: false }]
7902
8048
  ]);
7903
8049
  const parseStrict = (args) => {
7904
8050
  try {
@@ -7953,6 +8099,59 @@ const resolveOAuthServerSettings = (cli) => ({
7953
8099
  oauthTrustedClients: [...cli.oauthTrustedClients ?? [], ...splitList(readEnv("OAUTH_TRUSTED_CLIENTS").value)],
7954
8100
  defaultTrustedClients: cli.defaultTrustedClients
7955
8101
  });
8102
+ const BOOLEAN_WORDS = /* @__PURE__ */ new Map([
8103
+ ["true", true],
8104
+ ["on", true],
8105
+ ["1", true],
8106
+ ["false", false],
8107
+ ["off", false],
8108
+ ["0", false]
8109
+ ]);
8110
+ const booleanEnv = (name) => {
8111
+ const env = readNonEmptyEnv(name);
8112
+ if (env.value === void 0) return void 0;
8113
+ const value = BOOLEAN_WORDS.get(env.value.trim().toLowerCase());
8114
+ if (value === void 0) throw createStartupError(`Invalid ${env.name} value. Expected true or false (on/off and 1/0 are accepted too).`, STARTUP_DOCS.environment);
8115
+ return value;
8116
+ };
8117
+ const thresholdSetting = (cliValue, envName, flag, fallback) => {
8118
+ const env = readNonEmptyEnv(envName);
8119
+ const raw = cliValue ?? env.value;
8120
+ if (raw === void 0) return fallback;
8121
+ const parsed = parseThresholdPair(raw);
8122
+ if ("error" in parsed) throw createStartupError(`Invalid ${cliValue === void 0 ? env.name : flag} value. ${parsed.error}`, cliValue === void 0 ? STARTUP_DOCS.environment : STARTUP_DOCS.invocation);
8123
+ return parsed.threshold;
8124
+ };
8125
+ const secondsSetting = (cliValue, envName, flag, range) => {
8126
+ const env = readNonEmptyEnv(envName);
8127
+ const raw = cliValue ?? env.value;
8128
+ if (raw === void 0) return void 0;
8129
+ const fail = (expected) => {
8130
+ throw createStartupError(`Invalid ${cliValue === void 0 ? env.name : flag} value. ${expected}`, cliValue === void 0 ? STARTUP_DOCS.environment : STARTUP_DOCS.invocation);
8131
+ };
8132
+ if (!DIGITS_ONLY.test(raw)) fail("Expected a whole number of seconds.");
8133
+ const seconds = Number(raw);
8134
+ if (range && (seconds < range.min || seconds > range.max)) fail(`Expected ${range.min} to ${range.max} seconds.`);
8135
+ return seconds;
8136
+ };
8137
+ const SLA_STATE_STORES = /* @__PURE__ */ new Set(["memory", "disk"]);
8138
+ const stateStoreSetting = (cliValue) => {
8139
+ const env = readNonEmptyEnv("SLA_STATE_STORE");
8140
+ const raw = cliValue ?? env.value;
8141
+ if (raw === void 0 || SLA_STATE_STORES.has(raw)) return raw;
8142
+ throw createStartupError(`Invalid ${cliValue === void 0 ? env.name : "--sla-state-store"} value. Expected memory or disk.`, cliValue === void 0 ? STARTUP_DOCS.environment : STARTUP_DOCS.invocation);
8143
+ };
8144
+ const resolveSlaSettings = (cli) => ({
8145
+ slaThresholds: {
8146
+ reply: thresholdSetting(cli.slaReplyThresholds, "SLA_REPLY_THRESHOLDS", "--sla-reply-thresholds", DEFAULT_SLA_THRESHOLDS.reply),
8147
+ wait: thresholdSetting(cli.slaWaitThresholds, "SLA_WAIT_THRESHOLDS", "--sla-wait-thresholds", DEFAULT_SLA_THRESHOLDS.wait)
8148
+ },
8149
+ slaPush: cli.slaPush ?? booleanEnv("SLA_PUSH"),
8150
+ slaCacheTtlSeconds: secondsSetting(cli.slaCacheTtlSeconds, "SLA_CACHE_TTL_SECONDS", "--sla-cache-ttl-seconds"),
8151
+ slaStateStore: stateStoreSetting(cli.slaStateStore),
8152
+ slaUserPreferences: cli.slaUserPreferences ?? booleanEnv("SLA_USER_PREFERENCES"),
8153
+ slaPollSeconds: secondsSetting(cli.slaPollSeconds, "SLA_POLL_SECONDS", "--sla-poll-seconds", SLA_POLL_RANGE)
8154
+ });
7956
8155
  const loadConfig = (argv = process.argv.slice(2)) => {
7957
8156
  const cli = parseCliArgs(argv);
7958
8157
  const subdomain = cli.subdomain ?? requireNonEmptyEnv("ZENDESK_SUBDOMAIN") ?? "";
@@ -7978,10 +8177,196 @@ const loadConfig = (argv = process.argv.slice(2)) => {
7978
8177
  printTools: cli.printTools ?? false,
7979
8178
  callbackPort,
7980
8179
  ...resolveTransportSettings(cli),
7981
- ...resolveOAuthServerSettings(cli)
8180
+ ...resolveOAuthServerSettings(cli),
8181
+ ...resolveSlaSettings(cli)
7982
8182
  });
7983
8183
  };
7984
8184
  //#endregion
8185
+ //#region src/utils/sla-resource.ts
8186
+ /**
8187
+ * The SLA alert resources. Fixed scheme: unlike the Help Center resources,
8188
+ * nothing about them depends on a deployment.
8189
+ */
8190
+ const SLA_RESOURCE_SCHEME = "zendesk-sla";
8191
+ const SLA_ALERTS_TEMPLATES = {
8192
+ named: `${SLA_RESOURCE_SCHEME}://alerts/{scope}`,
8193
+ view: `${SLA_RESOURCE_SCHEME}://alerts/views/{view}`,
8194
+ query: `${SLA_RESOURCE_SCHEME}://alerts{?query}`
8195
+ };
8196
+ const NAMED_SCOPES = [
8197
+ "mine",
8198
+ "my_groups",
8199
+ "all"
8200
+ ];
8201
+ const namedAlertsUri = (scope) => `${SLA_RESOURCE_SCHEME}://alerts/${scope}`;
8202
+ const DIGITS = /^\d+$/;
8203
+ const VIEW_PATH = /^\/views\/([^/]+)$/;
8204
+ /** Reads an alert resource URI, or `undefined` when it names no alert set. */
8205
+ const parseAlertsUri = (raw) => {
8206
+ let url;
8207
+ try {
8208
+ url = new URL(raw);
8209
+ } catch {
8210
+ return;
8211
+ }
8212
+ if (url.protocol !== `zendesk-sla:` || url.host !== "alerts") return void 0;
8213
+ const path = url.pathname;
8214
+ if (path === "" || path === "/") {
8215
+ const query = url.searchParams.get("query")?.trim();
8216
+ return query ? {
8217
+ kind: "query",
8218
+ query
8219
+ } : void 0;
8220
+ }
8221
+ const named = NAMED_SCOPES.find((scope) => path === `/${scope}`);
8222
+ if (named) return {
8223
+ kind: "scope",
8224
+ scope: named
8225
+ };
8226
+ const view = VIEW_PATH.exec(path)?.[1];
8227
+ if (!view) return void 0;
8228
+ let decoded;
8229
+ try {
8230
+ decoded = decodeURIComponent(view);
8231
+ } catch {
8232
+ return;
8233
+ }
8234
+ return {
8235
+ kind: "view",
8236
+ view: DIGITS.test(decoded) ? Number(decoded) : decoded
8237
+ };
8238
+ };
8239
+ //#endregion
8240
+ //#region src/guidance/instructions.ts
8241
+ /**
8242
+ * URI of the dynamic Help Center topology resource. Single source of truth for
8243
+ * every place that cites it (resource registration, `instructions` blob): the
8244
+ * scheme comes from the config (`--hc-resource-scheme`, default `zendesk-hc`),
8245
+ * the path is fixed. Any future Help Center resource should build its URI the
8246
+ * same way so the whole surface follows the configured scheme.
8247
+ */
8248
+ const topologyResourceUri = (config) => `${config.hcResourceScheme}://topology`;
8249
+ /**
8250
+ * URI template of the pull-only Help Center article resources, built from the
8251
+ * configured scheme exactly like `topologyResourceUri` (`--hc-resource-scheme`,
8252
+ * default `zendesk-hc`). In multi/all brand mode the URI carries a brand
8253
+ * dimension — `<scheme>://brands/{brand}/articles/{id}` — because an article id
8254
+ * alone does not say which brand's Help Center holds it; the brand is named by
8255
+ * id or subdomain, the same idiom the tools' `brand_id` accepts. Unset and
8256
+ * single-brand modes keep `<scheme>://article/{id}`: one Help Center is
8257
+ * addressed, so a brand segment would be noise. The template's `list` callback
8258
+ * enumerates the promoted ("featured") articles (so clients can surface them
8259
+ * for pinning), while any article id can be read on demand.
8260
+ */
8261
+ const articleResourceUriTemplate = (config) => brandScopedArticles(config) ? `${config.hcResourceScheme}://brands/{brand}/articles/{id}` : `${config.hcResourceScheme}://article/{id}`;
8262
+ /**
8263
+ * Build the concrete resource URI for a single article id, under the configured
8264
+ * scheme. Shares the scheme with the template above so the listed URIs always
8265
+ * match the template the read callback is registered under. In multi/all mode
8266
+ * the brand argument (id or subdomain) is required and fills the {brand} slot.
8267
+ */
8268
+ const articleResourceUri = (config, id, brand) => brandScopedArticles(config) ? `${config.hcResourceScheme}://brands/${brand}/articles/${id}` : `${config.hcResourceScheme}://article/${id}`;
8269
+ /**
8270
+ * Whether the article resources address one Help Center among several: the
8271
+ * server was started with several brands, or with `all`. Only then does the URI
8272
+ * carry the brand dimension; unset and single-lock modes name exactly one Help
8273
+ * Center implicitly.
8274
+ */
8275
+ const brandScopedArticles = (config) => {
8276
+ const brandIds = config.brandIds;
8277
+ return brandIds !== void 0 && (brandIds.length > 1 || brandIds[0] === "all");
8278
+ };
8279
+ /**
8280
+ * Whether the `help_center` namespace is active: no `--namespace` filter, or one
8281
+ * that includes it. The shared second half of the two Help Center feature gates
8282
+ * below, so the namespace semantics live in one place.
8283
+ */
8284
+ const helpCenterNamespaceActive = (config) => !config.namespaces?.length || config.namespaces.includes("help_center");
8285
+ /** Whether `--namespace` and `--tool` leave the tool `name` of `namespace` exposed. */
8286
+ const toolExposed = (config, namespace, name) => (!config.namespaces?.length || config.namespaces.includes(namespace)) && (!config.tools?.length || config.tools.includes(name));
8287
+ /**
8288
+ * Whether the SLA alerting (the `zendesk-sla://alerts/...` resources, their
8289
+ * polling, the instructions line) is on: wherever `list_sla_alerts`, which
8290
+ * renders the same report, is exposed.
8291
+ */
8292
+ const slaAlertsEnabled = (config) => toolExposed(config, "tickets", "list_sla_alerts");
8293
+ /**
8294
+ * Whether a user can keep personal SLA thresholds: allowed by the operator, and
8295
+ * `configure_sla_alerts`, the only way to change them, exposed. Otherwise the
8296
+ * server defaults apply, and nothing points a user at a tool they cannot call.
8297
+ */
8298
+ const slaPreferencesEnabled = (config) => config.slaUserPreferences && toolExposed(config, "preferences", "configure_sla_alerts");
8299
+ /**
8300
+ * Whether the Help Center structural context (init instructions + the
8301
+ * topology resource, default `zendesk-hc://topology`) should be exposed. True only when the
8302
+ * feature is enabled (`--no-topology` not set) AND the `help_center` namespace
8303
+ * is active. Shared by the instructions builder and the resource registration in
8304
+ * `server.ts` so both gates stay in sync.
8305
+ */
8306
+ const helpCenterContextEnabled = (config) => config.topology && helpCenterNamespaceActive(config);
8307
+ /**
8308
+ * Whether the read-by-id article resource (`<scheme>://article/{id}`) should be
8309
+ * registered. Available whenever the `help_center` namespace is active — reading
8310
+ * one article is a cheap, on-demand single fetch with NO preloading, so it is
8311
+ * deliberately NOT gated by the promoted-listing flag: `--no-promoted-articles`
8312
+ * turns off the costly pre-listing (below), never the ability to address a known
8313
+ * article id. Not tied to `--no-topology` either (topology is a separate feature).
8314
+ */
8315
+ const articleResourceEnabled = (config) => helpCenterNamespaceActive(config);
8316
+ /**
8317
+ * Whether the promoted-article PRE-LISTING is exposed: the resource `list`
8318
+ * callback's scan, which enumerates the promoted articles for `resources/list`.
8319
+ * A client may trigger it unprompted, and it fans out (a capped scan of
8320
+ * `/articles`, per brand in scope), so `--no-promoted-articles` turns it off.
8321
+ * Read-by-id and the explicit `list_promoted_articles` tool stay available.
8322
+ * `!== false` so an omitted flag on a hand-built Config stays default-on.
8323
+ */
8324
+ const promotedArticlesEnabled = (config) => config.promotedArticles !== false && helpCenterNamespaceActive(config);
8325
+ /**
8326
+ * The one SLA line of the `instructions`: alerts exist, the thresholds in force
8327
+ * by default, and how a user changes theirs. Present whenever the `tickets`
8328
+ * namespace is, push or not: the tool and the resources stay either way.
8329
+ */
8330
+ const buildSlaInstructions = (config) => {
8331
+ if (!slaAlertsEnabled(config)) return void 0;
8332
+ const subscribe = config.slaPush ? ", subscribable for change notifications" : "";
8333
+ const change = slaPreferencesEnabled(config) ? "each user can set their own with configure_sla_alerts" : "the operator set them for everyone";
8334
+ return `SLA alerts: list_sla_alerts lists the SLA metrics breached or about to breach (your tickets, your groups', all, a view or a search), and the ${SLA_RESOURCE_SCHEME}://alerts/... resources hold the same report${subscribe}. Thresholds in force by default: ${describeThresholds(config.slaThresholds)}; ${change}.`;
8335
+ };
8336
+ /**
8337
+ * The static `instructions` blob sent on `initialize`: the Help Center block
8338
+ * and the SLA line, each present only when its namespace is. Deliberately short
8339
+ * and I/O-free: it must not trigger the lazy OAuth/PKCE flow just to connect,
8340
+ * and it stays within a tight token budget. The rich, dynamic topology lives in
8341
+ * the pull-only topology resource (default `zendesk-hc://topology`).
8342
+ */
8343
+ const buildInstructions = (config) => {
8344
+ const blocks = [buildHelpCenterInstructions(config), buildSlaInstructions(config)].filter((block) => block !== void 0);
8345
+ return blocks.length > 0 ? blocks.join("\n\n") : void 0;
8346
+ };
8347
+ const buildHelpCenterInstructions = (config) => {
8348
+ if (!helpCenterContextEnabled(config)) return void 0;
8349
+ let scope;
8350
+ if (config.brandIds === void 0) scope = `the Zendesk Help Center of "${config.subdomain}"`;
8351
+ else if (config.brandIds.length === 1 && config.brandIds[0] === "all") scope = `all brands of the Zendesk Help Center of "${config.subdomain}"`;
8352
+ else if (config.brandIds.length === 1) scope = `brand ${config.brandIds[0]} of the Zendesk Help Center of "${config.subdomain}"`;
8353
+ else scope = `brands ${config.brandIds.join(", ")} of the Zendesk Help Center of "${config.subdomain}"`;
8354
+ const topologyBrandNote = config.brandIds !== void 0 && (config.brandIds.length > 1 || config.brandIds[0] === "all") ? [`The topology describes ${config.brandIds[0] === "all" ? "the account default brand" : `brand ${config.brandIds[0]}`} only; for another brand, use list_categories / list_sections with its brand_id.`] : [];
8355
+ return [
8356
+ `This MCP server is connected to ${scope}.`,
8357
+ "",
8358
+ `When creating or editing Help Center content, the resource ${topologyResourceUri(config)} is useful context:`,
8359
+ "it lists the active locales (and the default one), the category → section tree with IDs,",
8360
+ "the visibility user segments, the permission groups, and your current role.",
8361
+ "Prefer its IDs (section_id, permission_group_id, user_segment_id, locale) over guessing from names.",
8362
+ ...topologyBrandNote,
8363
+ "",
8364
+ "It degrades gracefully: without Guide-admin / Help Center manager rights the permission-groups and",
8365
+ "user-segments sections are marked unavailable (not empty). In that case reuse a permission_group_id",
8366
+ "or user_segment_id from an existing article (get_article) instead."
8367
+ ].join("\n");
8368
+ };
8369
+ //#endregion
7985
8370
  //#region node_modules/.pnpm/@modelcontextprotocol+server@2.3.1/node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
7986
8371
  var __create = Object.create;
7987
8372
  var __defProp = Object.defineProperty;
@@ -15013,7 +15398,32 @@ function buildInputRequired(spec) {
15013
15398
  ...spec.requestState !== void 0 && { requestState: spec.requestState }
15014
15399
  };
15015
15400
  }
15016
- Object.assign(buildInputRequired, {
15401
+ /**
15402
+ * Builder for the input-required return value of multi-round-trip handlers,
15403
+ * with per-kind constructors for the embedded requests
15404
+ * (`inputRequired.elicit`, `inputRequired.elicitUrl`,
15405
+ * `inputRequired.createMessage`, `inputRequired.listRoots`).
15406
+ *
15407
+ * @example Write-once tool requesting confirmation
15408
+ * ```ts
15409
+ * server.registerTool('deploy', { inputSchema: z.object({ env: z.string() }) }, async ({ env }, ctx) => {
15410
+ * const confirmationSchema = z.object({ confirm: z.boolean() });
15411
+ * const confirmed = acceptedContent(ctx.mcpReq.inputResponses, 'confirm', confirmationSchema);
15412
+ * if (confirmed?.confirm !== true) {
15413
+ * return inputRequired({
15414
+ * inputRequests: {
15415
+ * confirm: inputRequired.elicit({
15416
+ * message: `Deploy to ${env}?`,
15417
+ * requestedSchema: confirmationSchema
15418
+ * })
15419
+ * }
15420
+ * });
15421
+ * }
15422
+ * return { content: [{ type: 'text', text: `deployed to ${env}` }] };
15423
+ * });
15424
+ * ```
15425
+ */
15426
+ const inputRequired = Object.assign(buildInputRequired, {
15017
15427
  elicit(params) {
15018
15428
  try {
15019
15429
  return {
@@ -15043,6 +15453,47 @@ Object.assign(buildInputRequired, {
15043
15453
  return { method: "roots/list" };
15044
15454
  }
15045
15455
  });
15456
+ function acceptedContent(responses, key, schema) {
15457
+ const view = inputResponse(responses, key);
15458
+ if (view.kind !== "elicit" || view.action !== "accept" || view.content === void 0) return void 0;
15459
+ if (schema === void 0) return view.content;
15460
+ const outcome = schema["~standard"].validate(view.content);
15461
+ if (outcome instanceof Promise) throw new TypeError("acceptedContent(responses, key, schema) requires a synchronously-validating schema");
15462
+ return outcome.issues === void 0 ? outcome.value : void 0;
15463
+ }
15464
+ /**
15465
+ * Reads one entry of a retried request's `inputResponses`
15466
+ * (`ctx.mcpReq.inputResponses`) as a discriminated view, covering
15467
+ * decline/cancel detection and the non-elicitation response kinds that
15468
+ * {@linkcode acceptedContent} does not surface.
15469
+ *
15470
+ * The values arrive from the client and are not re-validated here — treat
15471
+ * them as untrusted input (validate elicitation content with the
15472
+ * schema-aware {@linkcode acceptedContent} overload where it matters).
15473
+ */
15474
+ function inputResponse(responses, key) {
15475
+ if (responses === void 0 || typeof responses !== "object" || responses === null) return { kind: "missing" };
15476
+ const entry = responses[key];
15477
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return { kind: "missing" };
15478
+ const candidate = entry;
15479
+ if (candidate["action"] === "accept" || candidate["action"] === "decline" || candidate["action"] === "cancel") {
15480
+ const content = candidate["content"];
15481
+ return {
15482
+ kind: "elicit",
15483
+ action: candidate["action"],
15484
+ ...content !== null && typeof content === "object" && !Array.isArray(content) && { content }
15485
+ };
15486
+ }
15487
+ if (Array.isArray(candidate["roots"])) return {
15488
+ kind: "roots",
15489
+ roots: candidate["roots"]
15490
+ };
15491
+ if (typeof candidate["role"] === "string" && candidate["content"] !== void 0) return {
15492
+ kind: "sampling",
15493
+ result: candidate
15494
+ };
15495
+ return { kind: "missing" };
15496
+ }
15046
15497
  /**
15047
15498
  * The message both multi-round-trip loops emit when the round cap is
15048
15499
  * exhausted — the client driver as a typed error, the server-side legacy
@@ -34271,7 +34722,7 @@ var manipulation_exports = /* @__PURE__ */ __exportAll$1({
34271
34722
  insertBefore: () => insertBefore,
34272
34723
  prepend: () => prepend,
34273
34724
  prependTo: () => prependTo,
34274
- remove: () => remove,
34725
+ remove: () => remove$1,
34275
34726
  replaceWith: () => replaceWith,
34276
34727
  text: () => text$9,
34277
34728
  toString: () => toString$2,
@@ -34859,7 +35310,7 @@ function insertBefore(target) {
34859
35310
  * @returns The instance itself.
34860
35311
  * @see {@link https://api.jquery.com/remove/}
34861
35312
  */
34862
- function remove(selector) {
35313
+ function remove$1(selector) {
34863
35314
  domEach(selector ? this.filter(selector) : this, (el) => {
34864
35315
  removeElement(el);
34865
35316
  el.prev = el.next = el.parent = null;
@@ -63658,7 +64109,7 @@ var require_snapshot_utils = /* @__PURE__ */ __commonJSMin$1(((exports, module)
63658
64109
  //#endregion
63659
64110
  //#region node_modules/.pnpm/undici@7.30.0/node_modules/undici/lib/mock/snapshot-recorder.js
63660
64111
  var require_snapshot_recorder = /* @__PURE__ */ __commonJSMin$1(((exports, module) => {
63661
- const { writeFile, readFile, mkdir } = __require("node:fs/promises");
64112
+ const { writeFile: writeFile$1, readFile: readFile$1, mkdir: mkdir$1 } = __require("node:fs/promises");
63662
64113
  const { dirname: dirname$1, resolve: resolve$2 } = __require("node:path");
63663
64114
  const { setTimeout: setTimeout$1, clearTimeout: clearTimeout$1 } = __require("node:timers");
63664
64115
  const { InvalidArgumentError, UndiciError } = require_errors();
@@ -63925,7 +64376,7 @@ var require_snapshot_recorder = /* @__PURE__ */ __commonJSMin$1(((exports, modul
63925
64376
  const path = filePath || this.#snapshotPath;
63926
64377
  if (!path) throw new InvalidArgumentError("Snapshot path is required");
63927
64378
  try {
63928
- const data = await readFile(resolve$2(path), "utf8");
64379
+ const data = await readFile$1(resolve$2(path), "utf8");
63929
64380
  const parsed = JSON.parse(data);
63930
64381
  if (Array.isArray(parsed)) {
63931
64382
  this.#snapshots.clear();
@@ -63946,12 +64397,12 @@ var require_snapshot_recorder = /* @__PURE__ */ __commonJSMin$1(((exports, modul
63946
64397
  const path = filePath || this.#snapshotPath;
63947
64398
  if (!path) throw new InvalidArgumentError("Snapshot path is required");
63948
64399
  const resolvedPath = resolve$2(path);
63949
- await mkdir(dirname$1(resolvedPath), { recursive: true });
64400
+ await mkdir$1(dirname$1(resolvedPath), { recursive: true });
63950
64401
  const data = Array.from(this.#snapshots.entries()).map(([hash, snapshot]) => ({
63951
64402
  hash,
63952
64403
  snapshot
63953
64404
  }));
63954
- await writeFile(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
64405
+ await writeFile$1(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
63955
64406
  }
63956
64407
  /**
63957
64408
  * Clears all recorded snapshots
@@ -77092,7 +77543,7 @@ function tbody(node, index, parent) {
77092
77543
  *
77093
77544
  * @type {Record<'double' | 'name' | 'single' | 'unquoted', Array<[Array<string>, Array<string>]>>}
77094
77545
  */
77095
- const constants$1 = {
77546
+ const constants$2 = {
77096
77547
  name: [[" \n\f\r &/=>".split(""), " \n\f\r \"&'/=>`".split("")], ["\0 \n\f\r \"&'/<=>".split(""), "\0 \n\f\r \"&'/<=>`".split("")]],
77097
77548
  unquoted: [[" \n\f\r &>".split(""), "\0 \n\f\r \"&'<=>`".split("")], ["\0 \n\f\r \"&'<=>`".split(""), "\0 \n\f\r \"&'<=>`".split("")]],
77098
77549
  single: [["&'".split(""), "\"&'`".split("")], ["\0&'".split(""), "\0\"&'`".split("")]],
@@ -77177,18 +77628,18 @@ function serializeAttribute(state, key, value) {
77177
77628
  if (info.overloadedBoolean && (value === info.attribute || value === "")) value = true;
77178
77629
  else if ((info.boolean || info.overloadedBoolean) && (typeof value !== "string" || value === info.attribute || value === "")) value = Boolean(value);
77179
77630
  if (value === null || value === void 0 || value === false || typeof value === "number" && Number.isNaN(value)) return "";
77180
- const name = stringifyEntities(info.attribute, Object.assign({}, state.settings.characterReferences, { subset: constants$1.name[x][y] }));
77631
+ const name = stringifyEntities(info.attribute, Object.assign({}, state.settings.characterReferences, { subset: constants$2.name[x][y] }));
77181
77632
  if (value === true) return name;
77182
77633
  value = Array.isArray(value) ? (info.commaSeparated ? stringify$1 : stringify)(value, { padLeft: !state.settings.tightCommaSeparatedLists }) : String(value);
77183
77634
  if (state.settings.collapseEmptyAttributes && !value) return name;
77184
77635
  if (state.settings.preferUnquoted) result = stringifyEntities(value, Object.assign({}, state.settings.characterReferences, {
77185
77636
  attribute: true,
77186
- subset: constants$1.unquoted[x][y]
77637
+ subset: constants$2.unquoted[x][y]
77187
77638
  }));
77188
77639
  if (result !== value) {
77189
77640
  if (state.settings.quoteSmart && ccount(value, quote) > ccount(value, state.alternative)) quote = state.alternative;
77190
77641
  result = quote + stringifyEntities(value, Object.assign({}, state.settings.characterReferences, {
77191
- subset: (quote === "'" ? constants$1.single : constants$1.double)[x][y],
77642
+ subset: (quote === "'" ? constants$2.single : constants$2.double)[x][y],
77192
77643
  attribute: true
77193
77644
  })) + quote;
77194
77645
  }
@@ -84315,7 +84766,7 @@ function wbr(state, node) {
84315
84766
  */
84316
84767
  const nodeHandlers = {
84317
84768
  comment,
84318
- doctype: ignore$1,
84769
+ doctype: ignore$2,
84319
84770
  root: root$2,
84320
84771
  text: text$5
84321
84772
  };
@@ -84325,43 +84776,43 @@ const nodeHandlers = {
84325
84776
  * Each key is an element name, each value is a `Handler`.
84326
84777
  */
84327
84778
  const handlers$1 = {
84328
- applet: ignore$1,
84329
- area: ignore$1,
84330
- basefont: ignore$1,
84331
- bgsound: ignore$1,
84332
- caption: ignore$1,
84333
- col: ignore$1,
84334
- colgroup: ignore$1,
84335
- command: ignore$1,
84336
- content: ignore$1,
84337
- datalist: ignore$1,
84338
- dialog: ignore$1,
84339
- element: ignore$1,
84340
- embed: ignore$1,
84341
- frame: ignore$1,
84342
- frameset: ignore$1,
84343
- isindex: ignore$1,
84344
- keygen: ignore$1,
84345
- link: ignore$1,
84346
- math: ignore$1,
84347
- menu: ignore$1,
84348
- menuitem: ignore$1,
84349
- meta: ignore$1,
84350
- nextid: ignore$1,
84351
- noembed: ignore$1,
84352
- noframes: ignore$1,
84353
- optgroup: ignore$1,
84354
- option: ignore$1,
84355
- param: ignore$1,
84356
- script: ignore$1,
84357
- shadow: ignore$1,
84358
- source: ignore$1,
84359
- spacer: ignore$1,
84360
- style: ignore$1,
84361
- svg: ignore$1,
84362
- template: ignore$1,
84363
- title: ignore$1,
84364
- track: ignore$1,
84779
+ applet: ignore$2,
84780
+ area: ignore$2,
84781
+ basefont: ignore$2,
84782
+ bgsound: ignore$2,
84783
+ caption: ignore$2,
84784
+ col: ignore$2,
84785
+ colgroup: ignore$2,
84786
+ command: ignore$2,
84787
+ content: ignore$2,
84788
+ datalist: ignore$2,
84789
+ dialog: ignore$2,
84790
+ element: ignore$2,
84791
+ embed: ignore$2,
84792
+ frame: ignore$2,
84793
+ frameset: ignore$2,
84794
+ isindex: ignore$2,
84795
+ keygen: ignore$2,
84796
+ link: ignore$2,
84797
+ math: ignore$2,
84798
+ menu: ignore$2,
84799
+ menuitem: ignore$2,
84800
+ meta: ignore$2,
84801
+ nextid: ignore$2,
84802
+ noembed: ignore$2,
84803
+ noframes: ignore$2,
84804
+ optgroup: ignore$2,
84805
+ option: ignore$2,
84806
+ param: ignore$2,
84807
+ script: ignore$2,
84808
+ shadow: ignore$2,
84809
+ source: ignore$2,
84810
+ spacer: ignore$2,
84811
+ style: ignore$2,
84812
+ svg: ignore$2,
84813
+ template: ignore$2,
84814
+ title: ignore$2,
84815
+ track: ignore$2,
84365
84816
  abbr: all$1,
84366
84817
  acronym: all$1,
84367
84818
  bdi: all$1,
@@ -84494,7 +84945,7 @@ function flow$2(state, node) {
84494
84945
  /**
84495
84946
  * @returns {undefined}
84496
84947
  */
84497
- function ignore$1() {}
84948
+ function ignore$2() {}
84498
84949
  //#endregion
84499
84950
  //#region node_modules/.pnpm/hast-util-to-mdast@10.1.2/node_modules/hast-util-to-mdast/lib/state.js
84500
84951
  /**
@@ -90678,7 +91129,7 @@ function check(tokens, before, after) {
90678
91129
  runs.push(run);
90679
91130
  index = end;
90680
91131
  }
90681
- pair(runs, wrong);
91132
+ pair$1(runs, wrong);
90682
91133
  index = -1;
90683
91134
  while (++index < tokens.length) {
90684
91135
  const involved = wrong.get(tokens[index]);
@@ -90775,7 +91226,7 @@ function improve(items, chosen, mistake, before, after) {
90775
91226
  * @returns {undefined}
90776
91227
  * Nothing, `wrong` is changed.
90777
91228
  */
90778
- function pair(runs, wrong) {
91229
+ function pair$1(runs, wrong) {
90779
91230
  let index = 0;
90780
91231
  while (index < runs.length) {
90781
91232
  const closer = runs[index];
@@ -103417,12 +103868,12 @@ const handlers = {
103417
103868
  tableRow,
103418
103869
  text,
103419
103870
  thematicBreak,
103420
- toml: ignore,
103421
- yaml: ignore,
103422
- definition: ignore,
103423
- footnoteDefinition: ignore
103871
+ toml: ignore$1,
103872
+ yaml: ignore$1,
103873
+ definition: ignore$1,
103874
+ footnoteDefinition: ignore$1
103424
103875
  };
103425
- function ignore() {}
103876
+ function ignore$1() {}
103426
103877
  //#endregion
103427
103878
  //#region node_modules/.pnpm/mdast-util-to-hast@13.2.1/node_modules/mdast-util-to-hast/lib/footer.js
103428
103879
  /**
@@ -105922,18 +106373,30 @@ const formatSlaMetric = (m, now) => {
105922
106373
  }
105923
106374
  return parts.join("; ");
105924
106375
  };
105925
- const formatSlaBlock = (entry) => {
106376
+ const formatSlaBlock = (entry, urgency = "", heading = "### SLA") => {
105926
106377
  if (!entry?.policy_metrics || entry.policy_metrics.length === 0) return "";
105927
106378
  const now = Date.now();
105928
- const lines = ["### SLA"];
106379
+ const lines = [heading];
105929
106380
  const soonest = entry.policy_metrics.filter(hasPendingDeadline).map((m) => ({
105930
106381
  due: m.breach_at,
105931
106382
  at: Date.parse(m.breach_at)
105932
106383
  })).filter((d) => !Number.isNaN(d.at) && d.at > now).reduce((best, d) => best && best.at <= d.at ? best : d, void 0);
105933
106384
  if (soonest) lines.push(`- **Next breach**: ${soonest.due}`);
106385
+ if (urgency) lines.push(urgency);
105934
106386
  for (const m of entry.policy_metrics) lines.push(formatSlaMetric(m, now));
105935
106387
  return `\n\n${lines.join("\n")}`;
105936
106388
  };
106389
+ const formatNextBreachBlock = (metric) => metric ? formatSlaBlock({ policy_metrics: [metric] }, "", "### SLA (next breach only)") : "";
106390
+ /** The next-breaching metric each `/views/{id}/execute` row carries, by ticket id. */
106391
+ const nextBreachByTicket = (rows) => {
106392
+ const byId = /* @__PURE__ */ new Map();
106393
+ for (const row of rows) {
106394
+ const id = row.ticket?.id;
106395
+ const metric = row.ticket?.sla_policy_metric;
106396
+ if (typeof id === "number" && metric) byId.set(id, metric);
106397
+ }
106398
+ return byId;
106399
+ };
105937
106400
  const withName = (id, names) => {
105938
106401
  const n = Number(id);
105939
106402
  if (n === -1) return "System (-1)";
@@ -106305,106 +106768,6 @@ const createArticleResourcesProvider = (getToken, subdomain, onUnauthorized, res
106305
106768
  };
106306
106769
  };
106307
106770
  //#endregion
106308
- //#region src/guidance/instructions.ts
106309
- /**
106310
- * URI of the dynamic Help Center topology resource. Single source of truth for
106311
- * every place that cites it (resource registration, `instructions` blob): the
106312
- * scheme comes from the config (`--hc-resource-scheme`, default `zendesk-hc`),
106313
- * the path is fixed. Any future Help Center resource should build its URI the
106314
- * same way so the whole surface follows the configured scheme.
106315
- */
106316
- const topologyResourceUri = (config) => `${config.hcResourceScheme}://topology`;
106317
- /**
106318
- * URI template of the pull-only Help Center article resources, built from the
106319
- * configured scheme exactly like `topologyResourceUri` (`--hc-resource-scheme`,
106320
- * default `zendesk-hc`). In multi/all brand mode the URI carries a brand
106321
- * dimension — `<scheme>://brands/{brand}/articles/{id}` — because an article id
106322
- * alone does not say which brand's Help Center holds it; the brand is named by
106323
- * id or subdomain, the same idiom the tools' `brand_id` accepts. Unset and
106324
- * single-brand modes keep `<scheme>://article/{id}`: one Help Center is
106325
- * addressed, so a brand segment would be noise. The template's `list` callback
106326
- * enumerates the promoted ("featured") articles (so clients can surface them
106327
- * for pinning), while any article id can be read on demand.
106328
- */
106329
- const articleResourceUriTemplate = (config) => brandScopedArticles(config) ? `${config.hcResourceScheme}://brands/{brand}/articles/{id}` : `${config.hcResourceScheme}://article/{id}`;
106330
- /**
106331
- * Build the concrete resource URI for a single article id, under the configured
106332
- * scheme. Shares the scheme with the template above so the listed URIs always
106333
- * match the template the read callback is registered under. In multi/all mode
106334
- * the brand argument (id or subdomain) is required and fills the {brand} slot.
106335
- */
106336
- const articleResourceUri = (config, id, brand) => brandScopedArticles(config) ? `${config.hcResourceScheme}://brands/${brand}/articles/${id}` : `${config.hcResourceScheme}://article/${id}`;
106337
- /**
106338
- * Whether the article resources address one Help Center among several: the
106339
- * server was started with several brands, or with `all`. Only then does the URI
106340
- * carry the brand dimension; unset and single-lock modes name exactly one Help
106341
- * Center implicitly.
106342
- */
106343
- const brandScopedArticles = (config) => {
106344
- const brandIds = config.brandIds;
106345
- return brandIds !== void 0 && (brandIds.length > 1 || brandIds[0] === "all");
106346
- };
106347
- /**
106348
- * Whether the `help_center` namespace is active: no `--namespace` filter, or one
106349
- * that includes it. The shared second half of the two Help Center feature gates
106350
- * below, so the namespace semantics live in one place.
106351
- */
106352
- const helpCenterNamespaceActive = (config) => !config.namespaces?.length || config.namespaces.includes("help_center");
106353
- /**
106354
- * Whether the Help Center structural context (init instructions + the
106355
- * topology resource, default `zendesk-hc://topology`) should be exposed. True only when the
106356
- * feature is enabled (`--no-topology` not set) AND the `help_center` namespace
106357
- * is active. Shared by the instructions builder and the resource registration in
106358
- * `server.ts` so both gates stay in sync.
106359
- */
106360
- const helpCenterContextEnabled = (config) => config.topology && helpCenterNamespaceActive(config);
106361
- /**
106362
- * Whether the read-by-id article resource (`<scheme>://article/{id}`) should be
106363
- * registered. Available whenever the `help_center` namespace is active — reading
106364
- * one article is a cheap, on-demand single fetch with NO preloading, so it is
106365
- * deliberately NOT gated by the promoted-listing flag: `--no-promoted-articles`
106366
- * turns off the costly pre-listing (below), never the ability to address a known
106367
- * article id. Not tied to `--no-topology` either (topology is a separate feature).
106368
- */
106369
- const articleResourceEnabled = (config) => helpCenterNamespaceActive(config);
106370
- /**
106371
- * Whether the promoted-article PRE-LISTING is exposed: the resource `list`
106372
- * callback's scan, which enumerates the promoted articles for `resources/list`.
106373
- * A client may trigger it unprompted, and it fans out (a capped scan of
106374
- * `/articles`, per brand in scope), so `--no-promoted-articles` turns it off.
106375
- * Read-by-id and the explicit `list_promoted_articles` tool stay available.
106376
- * `!== false` so an omitted flag on a hand-built Config stays default-on.
106377
- */
106378
- const promotedArticlesEnabled = (config) => config.promotedArticles !== false && helpCenterNamespaceActive(config);
106379
- /**
106380
- * The static `instructions` blob sent on `initialize`. Deliberately short and
106381
- * I/O-free: it must not trigger the lazy OAuth/PKCE flow just to connect, and
106382
- * it stays within a tight token budget. The rich, dynamic topology lives in the
106383
- * pull-only topology resource (default `zendesk-hc://topology`) referenced here.
106384
- */
106385
- const buildInstructions = (config) => {
106386
- if (!helpCenterContextEnabled(config)) return void 0;
106387
- let scope;
106388
- if (config.brandIds === void 0) scope = `the Zendesk Help Center of "${config.subdomain}"`;
106389
- else if (config.brandIds.length === 1 && config.brandIds[0] === "all") scope = `all brands of the Zendesk Help Center of "${config.subdomain}"`;
106390
- else if (config.brandIds.length === 1) scope = `brand ${config.brandIds[0]} of the Zendesk Help Center of "${config.subdomain}"`;
106391
- else scope = `brands ${config.brandIds.join(", ")} of the Zendesk Help Center of "${config.subdomain}"`;
106392
- const topologyBrandNote = config.brandIds !== void 0 && (config.brandIds.length > 1 || config.brandIds[0] === "all") ? [`The topology describes ${config.brandIds[0] === "all" ? "the account default brand" : `brand ${config.brandIds[0]}`} only; for another brand, use list_categories / list_sections with its brand_id.`] : [];
106393
- return [
106394
- `This MCP server is connected to ${scope}.`,
106395
- "",
106396
- `When creating or editing Help Center content, the resource ${topologyResourceUri(config)} is useful context:`,
106397
- "it lists the active locales (and the default one), the category → section tree with IDs,",
106398
- "the visibility user segments, the permission groups, and your current role.",
106399
- "Prefer its IDs (section_id, permission_group_id, user_segment_id, locale) over guessing from names.",
106400
- ...topologyBrandNote,
106401
- "",
106402
- "It degrades gracefully: without Guide-admin / Help Center manager rights the permission-groups and",
106403
- "user-segments sections are marked unavailable (not empty). In that case reuse a permission_group_id",
106404
- "or user_segment_id from an existing article (get_article) instead."
106405
- ].join("\n");
106406
- };
106407
- //#endregion
106408
106771
  //#region src/guidance/topology.ts
106409
106772
  /**
106410
106773
  * Resolve an admin-gated fetch to a sentinel on HTTP 403 instead of rejecting.
@@ -106561,6 +106924,9 @@ const createTopologyProvider = (getToken, subdomain, onUnauthorized, brandIds, r
106561
106924
  return promise;
106562
106925
  } };
106563
106926
  };
106927
+ //#endregion
106928
+ //#region src/routing/proxy-schema.ts
106929
+ const writes = (t) => !t.readOnly || t.annotations?.readOnlyHint === false;
106564
106930
  const PARAMS_FIELD_DESCRIPTION = "Parameters of the chosen operation, named exactly as its line in \"operation\" lists them (* = required). An unknown name is rejected.";
106565
106931
  const summarizeDescription = (description) => {
106566
106932
  const idx = description.indexOf(". ");
@@ -106571,7 +106937,7 @@ const operationSignature = (tool) => {
106571
106937
  const params = Object.entries(tool.inputSchema.shape).map(([key, field]) => field.safeParse(void 0).success ? key : `${key}*`);
106572
106938
  return `${tool.name}(${params.join(", ")})`;
106573
106939
  };
106574
- const buildOperationList = (tools) => tools.map((t) => `- ${operationSignature(t)}: ${summarizeDescription(t.description)}${t.readOnly ? "" : " (write)"}`).join("\n");
106940
+ const buildOperationList = (tools) => tools.map((t) => `- ${operationSignature(t)}: ${summarizeDescription(t.description)}${writes(t) ? " (write)" : ""}`).join("\n");
106575
106941
  const buildOperationFieldDescription = (tools) => `One of the operations below. Each line reads name(parameters): summary, where the parameters are the exact names "params" accepts and * marks a required one; (write) marks an operation that changes data.\n${buildOperationList(tools)}`;
106576
106942
  const buildProxyDescription = ({ title, tools, readOnly }) => {
106577
106943
  const head = `${readOnly ? "[RO] " : ""}${title}. Specify the operation and its parameters. The "operation" field lists every operation with a one-line summary and the parameter names "params" accepts (* = required); an unknown parameter is rejected with the list of valid ones.`;
@@ -106629,6 +106995,10 @@ const NAMESPACE_LABELS = {
106629
106995
  requests: {
106630
106996
  toolName: "zendesk_requests",
106631
106997
  title: "Zendesk Requests"
106998
+ },
106999
+ preferences: {
107000
+ toolName: "connector_preferences",
107001
+ title: "Connector preferences (this MCP server, not Zendesk)"
106632
107002
  }
106633
107003
  };
106634
107004
  //#endregion
@@ -108066,6 +108436,126 @@ const createHelpCenterTools = (ctx) => {
108066
108436
  ];
108067
108437
  };
108068
108438
  //#endregion
108439
+ //#region src/utils/sla-preferences.ts
108440
+ const SLA_FAMILIES = ["reply", "wait"];
108441
+ const FAMILY_LABEL = {
108442
+ reply: "Reply metrics (first_reply_time, next_reply_time)",
108443
+ wait: "Wait and resolution metrics (every other metric)"
108444
+ };
108445
+ /** Personal thresholds where set (and allowed), the server default elsewhere. */
108446
+ const effectiveThresholds = (admin, prefs) => ({
108447
+ reply: prefs?.reply ?? admin.reply,
108448
+ wait: prefs?.wait ?? admin.wait
108449
+ });
108450
+ const isEmptyChange = (change) => Object.values(change).every((value) => value === void 0);
108451
+ const readDuration = (raw, field) => {
108452
+ if (raw === void 0) return void 0;
108453
+ const minutes = parseDuration(raw);
108454
+ return minutes === void 0 ? `${field}: ${notADuration(raw)}` : minutes;
108455
+ };
108456
+ const changeFamily = (family, current, change) => {
108457
+ const warning = readDuration(change[`${family}_warning`], `${family}_warning`);
108458
+ const critical = readDuration(change[`${family}_critical`], `${family}_critical`);
108459
+ if (typeof warning === "string") return { error: warning };
108460
+ if (typeof critical === "string") return { error: critical };
108461
+ const given = warning !== void 0 || critical !== void 0;
108462
+ if (change.reset === family || change.reset === "all") return given ? { error: `Either reset the ${family} thresholds or set them, not both.` } : "reset";
108463
+ if (!given) return "keep";
108464
+ const threshold = {
108465
+ warning: warning ?? current.warning,
108466
+ critical: critical ?? current.critical
108467
+ };
108468
+ if (threshold.critical > threshold.warning) return { error: `The ${family} critical threshold (${formatMinutes(threshold.critical)}) must not be longer than its warning one (${formatMinutes(threshold.warning)}).` };
108469
+ return threshold;
108470
+ };
108471
+ /**
108472
+ * Applies a change to the stored preferences. A half given alone keeps the
108473
+ * other half in force (personal, else server default); a family reset goes
108474
+ * back to the server default, and `all` also forgets the user was introduced. Returns the new preferences, or why the change
108475
+ * is refused.
108476
+ */
108477
+ const applyPreferenceChange = (admin, prefs, change) => {
108478
+ const current = effectiveThresholds(admin, prefs);
108479
+ const next = change.reset === "all" ? {} : { ...prefs };
108480
+ for (const family of SLA_FAMILIES) {
108481
+ const outcome = changeFamily(family, current[family], change);
108482
+ if (outcome === "reset") delete next[family];
108483
+ else if (outcome === "keep") continue;
108484
+ else if ("error" in outcome) return outcome;
108485
+ else next[family] = outcome;
108486
+ }
108487
+ return { preferences: next };
108488
+ };
108489
+ const sourceOf = (family, prefs, allowed, admin) => allowed && prefs?.[family] ? `your preference (server default: ${formatThreshold(admin[family])})` : "server default";
108490
+ /**
108491
+ * The thresholds in force for the caller, where each comes from, and how they
108492
+ * are applied. Shared by `configure_sla_alerts` and the alert tool's
108493
+ * first-call explanation.
108494
+ */
108495
+ const describeEffectiveThresholds = (admin, prefs, allowed) => {
108496
+ const inForce = effectiveThresholds(admin, allowed ? prefs : void 0);
108497
+ return [
108498
+ "SLA alert thresholds in force for you:",
108499
+ ...SLA_FAMILIES.map((family) => `- ${FAMILY_LABEL[family]}: ${formatThreshold(inForce[family])} — ${sourceOf(family, prefs, allowed, admin)}`),
108500
+ "A running metric turns WARNING when its breach is closer than the warning threshold, CRITICAL when closer than the critical one, and BREACHED once its deadline has passed. Paused and achieved metrics never alert.",
108501
+ allowed ? "Change them with configure_sla_alerts (reply_warning, reply_critical, wait_warning, wait_critical), or go back to the server defaults with reset." : "Personal thresholds are turned off on this server (SLA_USER_PREFERENCES=off): the server defaults apply to everyone."
108502
+ ].join("\n");
108503
+ };
108504
+ //#endregion
108505
+ //#region src/tools/preferences.ts
108506
+ /** How every SLA duration parameter is spelled, for their descriptions. */
108507
+ const DURATION_HINT = "A readable duration: 30m, 4h, 2d, 1w, or a mix like 1d4h.";
108508
+ /** The caller's SLA service, or the plain error of a server that wires none. */
108509
+ const requireSla = (sla) => {
108510
+ if (!sla) throw new Error("SLA alerting is not available on this server.");
108511
+ return sla;
108512
+ };
108513
+ const createPreferenceTools = (ctx) => [{
108514
+ name: "configure_sla_alerts",
108515
+ namespace: "preferences",
108516
+ readOnly: true,
108517
+ title: "Configure SLA Alert Thresholds",
108518
+ description: "Read or change your personal SLA alert thresholds: how close to a breach a running SLA metric must be to count as WARNING or CRITICAL. Called with no parameters it changes nothing and reports the thresholds in force for you and where each comes from (your preference or the server default). With parameters it saves your own thresholds per metric family (reply: first_reply_time and next_reply_time; wait: every other metric) on this MCP server — nothing is written to Zendesk — and they then drive list_sla_alerts and the Urgency line of get_ticket / search_tickets for you only; reset goes back to the server defaults. When the operator turned personal thresholds off (SLA_USER_PREFERENCES=off), only the read works and the reply says so.",
108519
+ inputSchema: object({
108520
+ reply_warning: string$2().optional().describe(`Time left before a reply metric (first_reply_time, next_reply_time) breaches at which it turns WARNING. ${DURATION_HINT} Omit to keep the current value.`),
108521
+ reply_critical: string$2().optional().describe(`Time left before a reply metric breaches at which it turns CRITICAL; must not exceed the reply warning threshold. ${DURATION_HINT} Omit to keep the current value.`),
108522
+ wait_warning: string$2().optional().describe(`Time left before a wait or resolution metric (requester_wait_time, agent_work_time and every other non-reply metric) breaches at which it turns WARNING. ${DURATION_HINT} Omit to keep the current value.`),
108523
+ wait_critical: string$2().optional().describe(`Time left before a wait or resolution metric breaches at which it turns CRITICAL; must not exceed the wait warning threshold. ${DURATION_HINT} Omit to keep the current value.`),
108524
+ reset: _enum([
108525
+ "reply",
108526
+ "wait",
108527
+ "all"
108528
+ ]).optional().describe("Drop your personal thresholds for one family (\"reply\" or \"wait\") or both (\"all\") and follow the server defaults again; \"all\" also makes your next list_sla_alerts call explain the thresholds again. Cannot be combined with new values for the same family.")
108529
+ }),
108530
+ annotations: {
108531
+ readOnlyHint: false,
108532
+ destructiveHint: false,
108533
+ idempotentHint: true,
108534
+ openWorldHint: false
108535
+ },
108536
+ handler: async (params) => {
108537
+ const sla = requireSla(ctx.sla);
108538
+ const change = params;
108539
+ const prefs = await sla.preferences();
108540
+ const describe = (current) => describeEffectiveThresholds(sla.adminThresholds, current, sla.userPreferencesAllowed);
108541
+ if (isEmptyChange(change)) return { content: [{
108542
+ type: "text",
108543
+ text: describe(prefs)
108544
+ }] };
108545
+ if (!sla.userPreferencesAllowed) return { content: [{
108546
+ type: "text",
108547
+ text: `Nothing was changed.\n\n${describe(prefs)}`
108548
+ }] };
108549
+ const applied = applyPreferenceChange(sla.adminThresholds, prefs, change);
108550
+ if ("error" in applied) throw new Error(`${applied.error} Nothing was changed.`);
108551
+ await sla.savePreferences(applied.preferences);
108552
+ return { content: [{
108553
+ type: "text",
108554
+ text: `Your SLA alert thresholds were saved.\n\n${describe(applied.preferences)}`
108555
+ }] };
108556
+ }
108557
+ }];
108558
+ //#endregion
108069
108559
  //#region src/tools/attachments.ts
108070
108560
  /**
108071
108561
  * File-attachment input, shared by every tool that can carry files on a comment.
@@ -108638,6 +109128,406 @@ const createSearchTools = (ctx) => {
108638
109128
  }];
108639
109129
  };
108640
109130
  //#endregion
109131
+ //#region src/utils/elicitation.ts
109132
+ /**
109133
+ * Whether a client can answer a form elicitation. A bare `elicitation: {}`
109134
+ * predates the modes and counts as form support (spec rule the SDK follows).
109135
+ */
109136
+ const supportsFormElicitation = (capabilities) => {
109137
+ const elicitation = capabilities?.elicitation;
109138
+ if (!elicitation) return false;
109139
+ return Object.keys(elicitation).length === 0 || "form" in elicitation;
109140
+ };
109141
+ const inputRequiredError = (result) => Object.assign(/* @__PURE__ */ new Error("The tool needs input from the user before it can answer."), {
109142
+ name: "InputRequiredError",
109143
+ inputRequired: result
109144
+ });
109145
+ const isInputRequiredError = (err) => err instanceof Error && err.name === "InputRequiredError" && "inputRequired" in err;
109146
+ //#endregion
109147
+ //#region src/utils/sla-urgency.ts
109148
+ /** Most urgent first: the order alerts are grouped and ranked in. */
109149
+ const URGENCY_LEVELS = [
109150
+ "BREACHED",
109151
+ "CRITICAL",
109152
+ "WARNING"
109153
+ ];
109154
+ const MINUTE_MS = 6e4;
109155
+ /**
109156
+ * The urgency of one live SLA metric, or `undefined` when it does not call for
109157
+ * attention: not running (paused, achieved, fulfilled), no readable deadline,
109158
+ * or further away than its family's warning threshold. A deadline already
109159
+ * passed is BREACHED whatever the thresholds.
109160
+ */
109161
+ const classifyMetric = (m, thresholds, now) => {
109162
+ if (!hasPendingDeadline(m)) return void 0;
109163
+ const minutesLeft = Math.round((Date.parse(m.breach_at) - now) / MINUTE_MS) + 0;
109164
+ const { warning, critical } = thresholds[familyOf(m.metric)];
109165
+ let level;
109166
+ if (minutesLeft < 0) level = "BREACHED";
109167
+ else if (minutesLeft <= critical) level = "CRITICAL";
109168
+ else if (minutesLeft <= warning) level = "WARNING";
109169
+ else return void 0;
109170
+ return {
109171
+ metric: m.metric,
109172
+ level,
109173
+ breachAt: m.breach_at,
109174
+ minutesLeft
109175
+ };
109176
+ };
109177
+ /** Orders by level (BREACHED first), then by the soonest breach. */
109178
+ const compareUrgency = (a, b) => URGENCY_LEVELS.indexOf(a.level) - URGENCY_LEVELS.indexOf(b.level) || Date.parse(a.breachAt) - Date.parse(b.breachAt);
109179
+ /** The single most urgent running metric of a ticket, if any calls for attention. */
109180
+ const ticketUrgency = (entry, thresholds, now) => {
109181
+ let worst;
109182
+ for (const m of entry?.policy_metrics ?? []) {
109183
+ const u = classifyMetric(m, thresholds, now);
109184
+ if (u && (!worst || compareUrgency(u, worst) < 0)) worst = u;
109185
+ }
109186
+ return worst;
109187
+ };
109188
+ /** `due in 45m` or `overdue by 2h`. */
109189
+ const formatDueIn = (u) => u.level === "BREACHED" ? `overdue by ${formatMinutes(u.minutesLeft)}` : `due in ${formatMinutes(u.minutesLeft)}`;
109190
+ /**
109191
+ * The `**Urgency**` line pushed into a ticket's SLA block, or '' when nothing is
109192
+ * urgent, so the caller can pass it on unconditionally.
109193
+ */
109194
+ const formatUrgencyLine = (u) => u ? `- **Urgency**: ${u.level} — ${u.metric} ${formatDueIn(u)}` : "";
109195
+ //#endregion
109196
+ //#region src/utils/sla-alerts.ts
109197
+ const toAlertTicket = (ticket) => ({
109198
+ id: ticket.id,
109199
+ subject: ticket.subject,
109200
+ status: ticket.status,
109201
+ priority: ticket.priority,
109202
+ metrics: ticket.slas?.policy_metrics ?? []
109203
+ });
109204
+ /** Several fetches into one snapshot, each ticket once (a ticket can sit in two groups). */
109205
+ const mergeSnapshots = (snapshots) => {
109206
+ const byId = /* @__PURE__ */ new Map();
109207
+ for (const snapshot of snapshots) for (const ticket of snapshot.tickets) if (!byId.has(ticket.id)) byId.set(ticket.id, ticket);
109208
+ return {
109209
+ tickets: [...byId.values()],
109210
+ truncated: snapshots.some((s) => s.truncated),
109211
+ unread: snapshots.reduce((sum, s) => sum + s.unread, 0)
109212
+ };
109213
+ };
109214
+ /**
109215
+ * Every running metric that is breached or inside its family's thresholds, one
109216
+ * alert per metric, most urgent level first and soonest breach first within it.
109217
+ */
109218
+ const selectAlerts = (tickets, thresholds, now, filter) => tickets.flatMap((ticket) => ticket.metrics.flatMap((m) => {
109219
+ const urgency = classifyMetric(m, thresholds, now);
109220
+ return urgency ? [{
109221
+ ticket,
109222
+ urgency
109223
+ }] : [];
109224
+ })).filter(({ urgency }) => urgency.level === "BREACHED" ? filter.includeBreached : filter.withinMinutes === void 0 || urgency.minutesLeft <= filter.withinMinutes).sort((a, b) => URGENCY_LEVELS.indexOf(a.urgency.level) - URGENCY_LEVELS.indexOf(b.urgency.level) || Date.parse(a.urgency.breachAt) - Date.parse(b.urgency.breachAt) || a.ticket.id - b.ticket.id);
109225
+ /**
109226
+ * What a subscriber is notified about: which ticket, which metric, which
109227
+ * level. The minutes counting down are left out on purpose, or every poll
109228
+ * would read as a change.
109229
+ */
109230
+ const alertFingerprint = (alerts) => alerts.map(({ ticket, urgency }) => `${ticket.id}:${urgency.metric}:${urgency.level}`).sort((a, b) => a.localeCompare(b)).join("|");
109231
+ const formatAlert = ({ ticket, urgency }) => `- #${ticket.id} ${urgency.metric} ${formatDueIn(urgency)} (due ${urgency.breachAt}) — "${ticket.subject}" [${ticket.status}, ${ticket.priority ?? "no priority"}]`;
109232
+ /**
109233
+ * The compact report: a summary line, the thresholds, then one line per alert
109234
+ * grouped by level. Past `maxLines` alerts, the rest are counted, not listed.
109235
+ */
109236
+ const renderAlerts = (alerts, report) => {
109237
+ const counts = URGENCY_LEVELS.map((level) => `${alerts.filter((a) => a.urgency.level === level).length} ${level.toLowerCase()}`).join(", ");
109238
+ const lines = [`SLA alerts for ${report.scopeLabel}: ${counts} (${report.checked} ticket(s) checked).`, `Thresholds: ${report.thresholds}.`];
109239
+ if (report.truncated) {
109240
+ const howMany = report.unread > 0 ? `${report.unread} more ticket(s)` : "More tickets";
109241
+ lines.push(`Truncated: ${howMany} in this scope were not read. Narrow it with query or view to see them.`);
109242
+ }
109243
+ for (const note of report.notes) lines.push(note);
109244
+ if (alerts.length === 0) {
109245
+ lines.push("", "No running SLA metric is breached or within its thresholds.");
109246
+ return lines.join("\n");
109247
+ }
109248
+ const shown = alerts.slice(0, report.maxLines);
109249
+ for (const level of URGENCY_LEVELS) {
109250
+ const group = shown.filter((a) => a.urgency.level === level);
109251
+ if (group.length > 0) lines.push("", `## ${level}`, ...group.map(formatAlert));
109252
+ }
109253
+ const hidden = alerts.length - shown.length;
109254
+ if (hidden > 0) lines.push("", `${hidden} more alert(s) not listed. Narrow the scope with query or view, or shorten within.`);
109255
+ return lines.join("\n");
109256
+ };
109257
+ //#endregion
109258
+ //#region src/utils/sla-scope.ts
109259
+ const SCOPE_QUERIES = {
109260
+ mine: "assignee:me status<solved",
109261
+ all: "status<solved"
109262
+ };
109263
+ const groupQuery = (groupId) => `group:${groupId} status<solved`;
109264
+ /** The cache key of a scope: same tickets, same key, whoever asks. */
109265
+ const scopeKey = (scope) => {
109266
+ if (scope.kind === "scope") return `scope:${scope.scope}`;
109267
+ if (scope.kind === "view") return `view:${scope.viewId}`;
109268
+ return `query:${scope.query.trim()}`;
109269
+ };
109270
+ const scopeLabel = (scope) => {
109271
+ if (scope.kind === "view") return `view ${scope.viewId}`;
109272
+ if (scope.kind === "query") return `the tickets matching "${scope.query.trim()}"`;
109273
+ return {
109274
+ mine: "your assigned unsolved tickets",
109275
+ my_groups: "your groups' unsolved tickets",
109276
+ all: "all unsolved tickets"
109277
+ }[scope.scope];
109278
+ };
109279
+ const PAGE_SIZE = 100;
109280
+ const readPages = async (fetchPage) => {
109281
+ const tickets = [];
109282
+ let total;
109283
+ let more = true;
109284
+ for (let page = 1; more && page <= 5; page += 1) {
109285
+ const response = await fetchPage(page);
109286
+ tickets.push(...response.tickets);
109287
+ total = response.count ?? total;
109288
+ more = response.next_page != null && response.tickets.length > 0;
109289
+ }
109290
+ return {
109291
+ tickets: tickets.map(toAlertTicket),
109292
+ truncated: more,
109293
+ unread: more ? Math.max((total ?? 0) - tickets.length, 0) : 0
109294
+ };
109295
+ };
109296
+ const pageParams = (page) => ({
109297
+ per_page: String(PAGE_SIZE),
109298
+ page: String(page)
109299
+ });
109300
+ const readSearch = (get, query) => readPages(async (page) => {
109301
+ const response = await get("/search", {
109302
+ query: `type:ticket ${query}`,
109303
+ include: "tickets(slas)",
109304
+ ...pageParams(page)
109305
+ });
109306
+ return {
109307
+ ...response,
109308
+ tickets: response.results ?? []
109309
+ };
109310
+ });
109311
+ const readView = (get, viewId) => readPages(async (page) => {
109312
+ const response = await get(`/views/${viewId}/tickets`, {
109313
+ include: "slas",
109314
+ ...pageParams(page)
109315
+ });
109316
+ return {
109317
+ ...response,
109318
+ tickets: response.tickets ?? []
109319
+ };
109320
+ });
109321
+ /** The ids of the groups `userId` belongs to, every page read. */
109322
+ const readGroupIds = async (get, userId) => {
109323
+ const ids = [];
109324
+ let cursor;
109325
+ do {
109326
+ const params = { "page[size]": String(PAGE_SIZE) };
109327
+ if (cursor) params["page[after]"] = cursor;
109328
+ const response = await get(`/users/${userId}/group_memberships`, params);
109329
+ ids.push(...(response.group_memberships ?? []).map((m) => m.group_id));
109330
+ cursor = response.meta?.has_more ? response.meta.after_cursor ?? void 0 : void 0;
109331
+ } while (cursor);
109332
+ return [...new Set(ids)];
109333
+ };
109334
+ /**
109335
+ * Reads the tickets of a scope with their live SLA, bounded by
109336
+ * `MAX_SCOPE_PAGES` per search or view and `MAX_SCOPE_GROUPS` group searches. Every call goes through `get`, so
109337
+ * through the client's retry policy.
109338
+ */
109339
+ const fetchScope = async (scope, get, userId) => {
109340
+ if (scope.kind === "view") return readView(get, scope.viewId);
109341
+ if (scope.kind === "query") return readSearch(get, scope.query.trim());
109342
+ if (scope.scope !== "my_groups") return readSearch(get, SCOPE_QUERIES[scope.scope]);
109343
+ const groupIds = await readGroupIds(get, await userId());
109344
+ const snapshots = [];
109345
+ for (const id of groupIds.slice(0, 10)) snapshots.push(await readSearch(get, groupQuery(id)));
109346
+ if (groupIds.length > 10) snapshots.push({
109347
+ tickets: [],
109348
+ truncated: true,
109349
+ unread: 0
109350
+ });
109351
+ return mergeSnapshots(snapshots);
109352
+ };
109353
+ /** Where the thresholds in force come from, as the report states it. */
109354
+ const thresholdsSource = (sla, prefs, overridden) => {
109355
+ if (overridden) return "overridden for this call";
109356
+ if (!sla.userPreferencesAllowed) return "server default";
109357
+ return prefs?.reply || prefs?.wait ? "yours; change them with configure_sla_alerts" : "server default; set your own with configure_sla_alerts";
109358
+ };
109359
+ /**
109360
+ * One alert check, as the tool, a resource read and the poller all run it:
109361
+ * the scope read through the caller's cache, the alerts selected and
109362
+ * rendered, and their fingerprint for change detection.
109363
+ */
109364
+ const checkAlerts = async (input) => {
109365
+ const { sla, get, target } = input;
109366
+ const snapshot = await sla.cached(scopeKey(target), () => fetchScope(target, get, sla.userId));
109367
+ const alerts = selectAlerts(snapshot.tickets, input.thresholds, Date.now(), input.filter);
109368
+ return {
109369
+ fingerprint: alertFingerprint(alerts),
109370
+ text: renderAlerts(alerts, {
109371
+ scopeLabel: scopeLabel(target),
109372
+ checked: snapshot.tickets.length,
109373
+ truncated: snapshot.truncated,
109374
+ unread: snapshot.unread,
109375
+ thresholds: `${describeThresholds(input.thresholds)} (${input.source})`,
109376
+ notes: input.notes,
109377
+ maxLines: 100
109378
+ })
109379
+ };
109380
+ };
109381
+ //#endregion
109382
+ //#region src/utils/sla-alerts-elicitation.ts
109383
+ /** The key the alert tool's one elicitation is asked and answered under. */
109384
+ const ALERTS_ELICITATION_KEY = "sla_alerts";
109385
+ const SCOPE_CHOICES = [
109386
+ {
109387
+ const: "mine",
109388
+ title: "My assigned tickets"
109389
+ },
109390
+ {
109391
+ const: "my_groups",
109392
+ title: "My groups' tickets"
109393
+ },
109394
+ {
109395
+ const: "all",
109396
+ title: "All unsolved tickets"
109397
+ }
109398
+ ];
109399
+ const pair = (t) => `${formatMinutes(t.warning)},${formatMinutes(t.critical)}`;
109400
+ /**
109401
+ * The one form the alert tool may show: the scope when none was given, and on
109402
+ * a user's first call the thresholds, explained and editable. `undefined` when
109403
+ * there is nothing to ask.
109404
+ */
109405
+ const buildAlertsElicitation = (options) => {
109406
+ const { askScope, introduce, thresholds } = options;
109407
+ if (!askScope && !introduce) return void 0;
109408
+ const properties = {};
109409
+ const message = [];
109410
+ if (askScope) {
109411
+ message.push("Which tickets should be checked for SLA alerts?");
109412
+ properties["scope"] = {
109413
+ type: "string",
109414
+ title: "Tickets to check",
109415
+ oneOf: [...SCOPE_CHOICES],
109416
+ default: "mine"
109417
+ };
109418
+ }
109419
+ if (introduce) {
109420
+ message.push(`A running SLA metric is flagged WARNING when its breach is closer than the warning threshold, CRITICAL when closer than the critical one, and BREACHED once overdue. In force for you: reply metrics (first_reply_time, next_reply_time) ${pair(thresholds.reply)}, every other metric ${pair(thresholds.wait)}, as warning,critical. Edits here apply to this call only; configure_sla_alerts keeps them.`);
109421
+ properties["reply_thresholds"] = {
109422
+ type: "string",
109423
+ title: "Reply metrics: warning,critical",
109424
+ default: pair(thresholds.reply)
109425
+ };
109426
+ properties["wait_thresholds"] = {
109427
+ type: "string",
109428
+ title: "Wait and resolution metrics: warning,critical",
109429
+ default: pair(thresholds.wait)
109430
+ };
109431
+ }
109432
+ return {
109433
+ message: message.join("\n\n"),
109434
+ requestedSchema: {
109435
+ type: "object",
109436
+ properties,
109437
+ ...askScope ? { required: ["scope"] } : {}
109438
+ }
109439
+ };
109440
+ };
109441
+ const isNamedScope = (value) => SCOPE_CHOICES.some((choice) => choice.const === value);
109442
+ /**
109443
+ * Reads an accepted answer to that form. The content comes from the client
109444
+ * unvalidated, so every field is checked here; a bad one is ignored with a
109445
+ * note rather than failing the whole call.
109446
+ */
109447
+ const readAlertsAnswer = (content, thresholds) => {
109448
+ const answer = {
109449
+ change: {},
109450
+ notes: []
109451
+ };
109452
+ if (isNamedScope(content["scope"])) answer.scope = content["scope"];
109453
+ for (const family of ["reply", "wait"]) {
109454
+ const raw = content[`${family}_thresholds`];
109455
+ if (typeof raw !== "string" || raw.trim() === pair(thresholds[family])) continue;
109456
+ const parsed = parseThresholdPair(raw);
109457
+ if ("error" in parsed) {
109458
+ answer.notes.push(`Your ${family} thresholds "${raw}" were not applied: ${parsed.error}`);
109459
+ continue;
109460
+ }
109461
+ answer.change[`${family}_warning`] = formatMinutes(parsed.threshold.warning);
109462
+ answer.change[`${family}_critical`] = formatMinutes(parsed.threshold.critical);
109463
+ }
109464
+ return answer;
109465
+ };
109466
+ /**
109467
+ * Applies the per-call `warning` / `critical` overrides to both families. A
109468
+ * pair given together must hold critical <= warning. A half given alone wins:
109469
+ * a lone warning lowers each family's critical to it at most, a lone critical
109470
+ * raises each family's warning to it at least.
109471
+ */
109472
+ const overrideThresholds = (thresholds, warning, critical) => {
109473
+ if (warning !== void 0 && critical !== void 0) {
109474
+ const both = toThreshold(warning, critical);
109475
+ return "error" in both ? both : { thresholds: {
109476
+ reply: both.threshold,
109477
+ wait: both.threshold
109478
+ } };
109479
+ }
109480
+ const w = warning === void 0 ? void 0 : parseDuration(warning);
109481
+ if (warning !== void 0 && w === void 0) return { error: notADuration(warning) };
109482
+ const c = critical === void 0 ? void 0 : parseDuration(critical);
109483
+ if (critical !== void 0 && c === void 0) return { error: notADuration(critical) };
109484
+ const apply = (t) => ({
109485
+ warning: w ?? Math.max(t.warning, c ?? 0),
109486
+ critical: c ?? Math.min(t.critical, w ?? t.critical)
109487
+ });
109488
+ return { thresholds: {
109489
+ reply: apply(thresholds.reply),
109490
+ wait: apply(thresholds.wait)
109491
+ } };
109492
+ };
109493
+ //#endregion
109494
+ //#region src/utils/ticket-metrics.ts
109495
+ const NOT_RECORDED = "not recorded";
109496
+ const DURATIONS = [
109497
+ ["First reply", "reply_time_in_minutes"],
109498
+ ["First resolution", "first_resolution_time_in_minutes"],
109499
+ ["Full resolution", "full_resolution_time_in_minutes"],
109500
+ ["Agent wait", "agent_wait_time_in_minutes"],
109501
+ ["Requester wait", "requester_wait_time_in_minutes"],
109502
+ ["On hold", "on_hold_time_in_minutes"]
109503
+ ];
109504
+ const half = (minutes) => minutes === null ? "not reached yet" : formatMinutes(minutes);
109505
+ const formatPair = (pair) => {
109506
+ if (!pair) return NOT_RECORDED;
109507
+ if (pair.calendar === null && pair.business === null) return "not reached yet";
109508
+ return `calendar ${half(pair.calendar)}, business ${half(pair.business)}`;
109509
+ };
109510
+ const count = (value) => value === void 0 ? NOT_RECORDED : `${value}`;
109511
+ const when = (value) => value ?? "not yet";
109512
+ /**
109513
+ * One ticket's elapsed metrics (`ticket_metric`): durations in calendar and
109514
+ * business minutes, counters and key timestamps. These are facts recorded
109515
+ * whether or not an SLA policy applies, unlike the live SLA countdown.
109516
+ */
109517
+ const formatTicketMetrics = (id, ticket, metric) => {
109518
+ if (!ticket) return `## Ticket #${id}\n- Not found, or not visible to you.`;
109519
+ const header = `## Ticket #${id}: ${ticket.subject}`;
109520
+ if (!metric) return `${header}\n- No metrics recorded for this ticket.`;
109521
+ return [
109522
+ header,
109523
+ ...DURATIONS.map(([label, key]) => `- **${label}**: ${formatPair(metric[key])}`),
109524
+ `- **Reopens**: ${count(metric.reopens)} | **Replies**: ${count(metric.replies)} | **Assignee stations**: ${count(metric.assignee_stations)} | **Group stations**: ${count(metric.group_stations)}`,
109525
+ `- **Created**: ${metric.created_at ?? NOT_RECORDED} | **First assigned**: ${when(metric.initially_assigned_at)} | **Assigned**: ${when(metric.assigned_at)} | **Solved**: ${when(metric.solved_at)}`,
109526
+ `- **Latest comment**: ${when(metric.latest_comment_added_at)} | **Status changed**: ${when(metric.status_updated_at)}`,
109527
+ `- **Last update by requester**: ${when(metric.requester_updated_at)} | **by assignee**: ${when(metric.assignee_updated_at)}`
109528
+ ].join("\n");
109529
+ };
109530
+ //#endregion
108641
109531
  //#region src/tools/tickets.ts
108642
109532
  const MAX_ATTACHMENT_MB = Number.parseFloat((MAX_ATTACHMENT_BYTES / 1048576).toFixed(2));
108643
109533
  const MAX_RESPONSE_MB = Number.parseFloat((MAX_RESPONSE_BYTES / 1048576).toFixed(2));
@@ -108980,6 +109870,12 @@ const formatMacroPreviewDiff = (ticketId, macroId, before, result) => {
108980
109870
  };
108981
109871
  const createTicketTools = (ctx) => {
108982
109872
  const { subdomain, getToken } = ctx;
109873
+ const urgencyRenderer = async () => {
109874
+ const sla = ctx.sla;
109875
+ if (!sla?.push) return () => "";
109876
+ const thresholds = await sla.thresholds().catch(() => sla.adminThresholds);
109877
+ return (entry) => formatUrgencyLine(ticketUrgency(entry, thresholds, Date.now()));
109878
+ };
108983
109879
  const subscriberIdList = (description, max) => {
108984
109880
  const ids = array(number$2().int().positive());
108985
109881
  return (max === void 0 ? ids : ids.max(max)).optional().describe(description);
@@ -108990,7 +109886,7 @@ const createTicketTools = (ctx) => {
108990
109886
  namespace: "tickets",
108991
109887
  readOnly: true,
108992
109888
  title: "Get Zendesk Ticket",
108993
- description: "Retrieve a Zendesk ticket by ID, including its live SLA state (per-metric stage and breach countdown) when an SLA policy applies, plus its comments if requested. Returns ticket details (subject, status, priority, assignee, tags, description) and optionally all comments/internal notes. It also lists the followers and email CCs on the ticket, resolved to names, which is how you tell who a ticket update actually reaches: followers are notified of updates including internal notes, email CCs take part in the public correspondence, and being the requester makes someone neither. Both lists come back empty on an account where the \"CCs and followers\" setting is not enabled in Zendesk. The per-ticket Show endpoint exposes no SLA, so the SLA block is resolved via a scoped search and may be absent for a very high-volume requester or a just-updated ticket; SLA targets and policy conditions live in list_sla_policies. This returns the ticket as it stands now; for the history of changes behind that state (who changed what, and when), use get_ticket_history. The comment thread is appended in one block — the first page of comments Zendesk returns, cut past the response character limit — so on a long ticket read it with list_ticket_comments, which pages the comments and returns the newest first.",
109889
+ description: "Retrieve a Zendesk ticket by ID, including its live SLA state (per-metric stage and breach countdown) when an SLA policy applies, plus its comments if requested. Returns ticket details (subject, status, priority, assignee, tags, description) and optionally all comments/internal notes. It also lists the followers and email CCs on the ticket, resolved to names, which is how you tell who a ticket update actually reaches: followers are notified of updates including internal notes, email CCs take part in the public correspondence, and being the requester makes someone neither. Both lists come back empty on an account where the \"CCs and followers\" setting is not enabled in Zendesk. The per-ticket Show endpoint exposes no SLA, so the SLA block is resolved via a scoped search and may be absent for a very high-volume requester or a just-updated ticket; SLA targets and policy conditions live in list_sla_policies. When the operator turned SLA push on, the SLA block also carries an Urgency line naming the most urgent running metric as BREACHED, CRITICAL or WARNING against the caller's alert thresholds. This returns the ticket as it stands now; for the history of changes behind that state (who changed what, and when), use get_ticket_history. The comment thread is appended in one block — the first page of comments Zendesk returns, cut past the response character limit — so on a long ticket read it with list_ticket_comments, which pages the comments and returns the newest first.",
108994
109890
  inputSchema: object({
108995
109891
  ticket_id: number$2().int().describe("Ticket ID — the numeric id of the ticket to fetch. Obtain it from search_tickets or list_tickets."),
108996
109892
  include_comments: boolean$1().default(false).describe("When true, appends the full public comment and internal note thread to the response. Defaults to false to keep the payload small; enable it when you need the conversation, not just the ticket fields. On a long thread prefer list_ticket_comments — this flag appends one unpaginated block, so comments past Zendesk's first page are absent and the rest is cut at the response character limit.")
@@ -109005,14 +109901,18 @@ const createTicketTools = (ctx) => {
109005
109901
  const { ticket_id, include_comments } = params;
109006
109902
  const token = await getToken();
109007
109903
  const { ticket } = await zendeskGet(subdomain, token, `/tickets/${ticket_id}`);
109008
- const [sla, thread] = await Promise.all([fetchTicketSla(subdomain, token, ticket), include_comments ? zendeskGet(subdomain, token, `/tickets/${ticket_id}/comments`, {
109009
- include: "users",
109010
- include_inline_images: "true"
109011
- }) : void 0]);
109904
+ const [sla, thread, urgency] = await Promise.all([
109905
+ fetchTicketSla(subdomain, token, ticket),
109906
+ include_comments ? zendeskGet(subdomain, token, `/tickets/${ticket_id}/comments`, {
109907
+ include: "users",
109908
+ include_inline_images: "true"
109909
+ }) : void 0,
109910
+ urgencyRenderer()
109911
+ ]);
109012
109912
  const comments = thread?.comments ?? [];
109013
109913
  const names = await resolveUserDisplayNames(subdomain, token, [...collectSubscriberIds(ticket), ...comments.map((comment) => comment.author_id)], thread?.users);
109014
109914
  const commentsBlock = thread ? `\n\n---\n# Comments\n\n${comments.map((comment) => formatComment(comment, names)).join("\n\n")}` : "";
109015
- const text = formatTicket(ticket) + formatSlaBlock(sla) + formatSubscribersBlock(ticket, names) + commentsBlock;
109915
+ const text = formatTicket(ticket) + formatSlaBlock(sla, urgency(sla)) + formatSubscribersBlock(ticket, names) + commentsBlock;
109016
109916
  const advice = include_comments ? `get_ticket appends the thread as one unpaginated block; read it page by page with list_ticket_comments (ticket_id: ${ticket_id}, sort_order: "desc") to get the newest comments first.` : "get_ticket takes no pagination or filter parameters, so this response cannot be narrowed from the call.";
109017
109917
  return { content: [{
109018
109918
  type: "text",
@@ -109148,7 +110048,7 @@ const createTicketTools = (ctx) => {
109148
110048
  namespace: "tickets",
109149
110049
  readOnly: true,
109150
110050
  title: "Search Zendesk Tickets",
109151
- description: "Search tickets using Zendesk query syntax, returning each result with its live SLA state (per-metric stage and breach countdown) when an SLA policy applies. Examples: \"status:open assignee:me\", \"priority:urgent ticket_type:incident\". Returns total count, so queue triage like \"breaching today\" works without a per-ticket fetch.",
110051
+ description: "Search tickets using Zendesk query syntax, returning each result with its live SLA state (per-metric stage and breach countdown) when an SLA policy applies. Examples: \"status:open assignee:me\", \"priority:urgent ticket_type:incident\". Returns total count, so queue triage like \"breaching today\" works without a per-ticket fetch. When the operator turned SLA push on, each SLA block also carries an Urgency line (BREACHED, CRITICAL or WARNING) against the caller's alert thresholds.",
109152
110052
  inputSchema: object({
109153
110053
  query: string$2().min(1).describe("Zendesk ticket search query — field filters like \"status:open\", \"assignee:me\", \"priority:urgent ticket_type:incident\", combined with free text. A \"type:ticket\" scope is added automatically, so filter the ticket kind with ticket_type: (e.g. ticket_type:incident), never type: (which the API rejects here)."),
109154
110054
  per_page: number$2().int().min(1).max(100).default(100).describe(PER_PAGE_DESC),
@@ -109163,12 +110063,12 @@ const createTicketTools = (ctx) => {
109163
110063
  handler: async (params) => {
109164
110064
  const { query, per_page, page } = params;
109165
110065
  const token = await getToken();
109166
- const response = await zendeskGet(subdomain, token, "/search", {
110066
+ const [response, urgency] = await Promise.all([zendeskGet(subdomain, token, "/search", {
109167
110067
  query: `type:ticket ${query}`,
109168
110068
  include: "tickets(slas)",
109169
110069
  ...buildOffsetParams(per_page, page)
109170
- });
109171
- const formatTicketWithSla = (ticket) => formatTicket(ticket) + formatSlaBlock(ticket.slas);
110070
+ }), urgencyRenderer()]);
110071
+ const formatTicketWithSla = (ticket) => formatTicket(ticket) + formatSlaBlock(ticket.slas, urgency(ticket.slas));
109172
110072
  return { content: [{
109173
110073
  type: "text",
109174
110074
  text: formatList(response.results ?? [], formatTicketWithSla, extractSearchPaginationMeta(response, per_page, page))
@@ -109482,6 +110382,35 @@ const createTicketTools = (ctx) => {
109482
110382
  }] };
109483
110383
  }
109484
110384
  },
110385
+ {
110386
+ name: "get_ticket_metrics",
110387
+ namespace: "tickets",
110388
+ readOnly: true,
110389
+ title: "Get Ticket Metrics",
110390
+ description: "Read the elapsed-time metrics (reply, resolution, wait and on-hold times, reopens, replies) of up to 100 tickets in one call. Each ticket gets first reply, first and full resolution, agent wait, requester wait and on-hold times in calendar and business minutes, plus reopen, reply, assignee-station and group-station counts and the key timestamps (assigned, solved, latest comment). These are facts Zendesk records on every ticket, so they are available even on tickets no SLA policy covers; they say how long things took, not how long is left — for the live breach countdown use get_ticket, search_tickets or list_sla_alerts. A duration still running reads \"not reached yet\". Read-only; tickets the caller cannot see are reported as such rather than failing the call.",
110391
+ inputSchema: object({ ticket_ids: array(number$2().int().positive()).min(1).max(100).describe("Ticket IDs to read metrics for, 1 to 100 numeric ids (duplicates are read once), as returned by search_tickets, list_tickets or get_view_tickets. The answer keeps this order.") }),
110392
+ annotations: {
110393
+ readOnlyHint: true,
110394
+ destructiveHint: false,
110395
+ idempotentHint: true,
110396
+ openWorldHint: true
110397
+ },
110398
+ handler: async (params) => {
110399
+ const ids = [...new Set(params.ticket_ids)];
110400
+ const token = await getToken();
110401
+ const response = await zendeskGet(subdomain, token, "/tickets/show_many", {
110402
+ ids: ids.join(","),
110403
+ include: "metric_sets"
110404
+ });
110405
+ const tickets = new Map((response.tickets ?? []).map((t) => [t.id, t]));
110406
+ const metrics = new Map((response.metric_sets ?? []).map((m) => [m.ticket_id, m]));
110407
+ const text = ids.map((id) => formatTicketMetrics(id, tickets.get(id), metrics.get(id))).join("\n\n");
110408
+ return { content: [{
110409
+ type: "text",
110410
+ text: truncateIfNeeded(text, "Pass fewer ticket_ids per call.")
110411
+ }] };
110412
+ }
110413
+ },
109485
110414
  {
109486
110415
  name: "list_ticket_fields",
109487
110416
  namespace: "tickets",
@@ -109545,7 +110474,7 @@ const createTicketTools = (ctx) => {
109545
110474
  namespace: "tickets",
109546
110475
  readOnly: true,
109547
110476
  title: "Get Tickets In A View",
109548
- description: "Read the tickets inside a Zendesk view, in the view's own configured sort order — the same order the agent sees in the Zendesk UI — which is the natural way to work a named queue like \"Unassigned tickets\" or \"Breaching today\". Accepts the view by title or by numeric id (discover both with list_views); a title is matched case-insensitively against the agent's active views, and on no match the available titles are returned so you can retry in one step. Tickets come back with the same fields as list_tickets and are cursor-paginated; there is no live SLA block here (use search_tickets when you need per-ticket SLA state), and sort_by/sort_order override the view's order when you want a different cut.",
110477
+ description: "Read the tickets inside a Zendesk view, in the view's own configured sort order — the same order the agent sees in the Zendesk UI — which is the natural way to work a named queue like \"Unassigned tickets\" or \"Breaching today\". Accepts the view by title or by numeric id (discover both with list_views); a title is matched case-insensitively against the agent's active views, and on no match the available titles are returned so you can retry in one step. Tickets come back with the same fields as list_tickets and are cursor-paginated. Each ticket an SLA policy covers also shows its next SLA breach: only the one metric closest to breaching, read from the view itself at no extra cost (use get_ticket or search_tickets for every metric). sort_by/sort_order override the view's order when you want a different cut.",
109549
110478
  inputSchema: object({
109550
110479
  view: union([string$2().min(1), number$2().int().positive()]).describe("The view to read: its exact title as shown in Zendesk (e.g. \"Unassigned tickets\") or its numeric id from list_views. A title is matched case-insensitively against your active views; on no match the tool returns the available titles instead of erroring, so you can retry with a correct one."),
109551
110480
  sort_by: string$2().optional().describe("Optional column to sort by, overriding the view's own sort. Must be one of the view's columns (e.g. \"status\", \"priority\", \"updated_at\", or a custom field id); \"subject\" and \"submitter\" are not sortable. Omit to keep the view's configured order."),
@@ -109582,9 +110511,10 @@ const createTicketTools = (ctx) => {
109582
110511
  }
109583
110512
  const ids = rows.map(extractRowTicketId).filter((id) => typeof id === "number");
109584
110513
  const tickets = await hydrateViewTickets(subdomain, token, ids);
110514
+ const nextBreach = nextBreachByTicket(rows);
109585
110515
  return { content: [{
109586
110516
  type: "text",
109587
- text: formatList(tickets, formatTicket, {
110517
+ text: formatList(tickets, (ticket) => formatTicket(ticket) + formatNextBreachBlock(nextBreach.get(ticket.id)), {
109588
110518
  ...meta,
109589
110519
  count: tickets.length
109590
110520
  })
@@ -109648,6 +110578,208 @@ const createTicketTools = (ctx) => {
109648
110578
  ];
109649
110579
  };
109650
110580
  //#endregion
110581
+ //#region src/tools/sla-alerts.ts
110582
+ const parseWithin = (within) => {
110583
+ if (within === void 0) return void 0;
110584
+ const minutes = parseDuration(within);
110585
+ if (minutes === void 0) throw new Error(`within: "${within}" is not a duration. ${DURATION_HINT}`);
110586
+ return minutes;
110587
+ };
110588
+ /**
110589
+ * The one form the tool may show — the scope when none was given, the
110590
+ * thresholds on a user's first call — and what came of it. Asking throws the
110591
+ * SDK's input-required signal; a client that cannot answer forms gets the
110592
+ * thresholds explained in the reply instead. Thresholds edited in the form
110593
+ * apply to this call only: keeping them is configure_sla_alerts' job. The one
110594
+ * write is the marker that the user was introduced, so it happens once.
110595
+ */
110596
+ const settleForm = async (sla, call, askScope) => {
110597
+ const prefs = sla.userPreferencesAllowed ? await sla.preferences() : void 0;
110598
+ const introduce = sla.userPreferencesAllowed && !prefs?.introduced;
110599
+ const notes = [];
110600
+ let picked;
110601
+ let edited;
110602
+ const form = buildAlertsElicitation({
110603
+ askScope,
110604
+ introduce,
110605
+ thresholds: await sla.thresholds()
110606
+ });
110607
+ const answered = call?.inputResponses?.[ALERTS_ELICITATION_KEY] !== void 0;
110608
+ if (form && call?.canElicit && !answered) throw inputRequiredError(inputRequired({ inputRequests: { [ALERTS_ELICITATION_KEY]: inputRequired.elicit(form) } }));
110609
+ const content = answered ? acceptedContent(call?.inputResponses, ALERTS_ELICITATION_KEY) : void 0;
110610
+ if (content) {
110611
+ const answer = readAlertsAnswer(content, await sla.thresholds());
110612
+ picked = answer.scope;
110613
+ notes.push(...answer.notes);
110614
+ if (!isEmptyChange(answer.change)) {
110615
+ const applied = applyPreferenceChange(sla.adminThresholds, prefs, answer.change);
110616
+ if ("preferences" in applied) {
110617
+ edited = effectiveThresholds(sla.adminThresholds, applied.preferences);
110618
+ notes.push(keepEditedNote(answer.change));
110619
+ } else notes.push(`Your edited thresholds were not applied: ${applied.error}`);
110620
+ }
110621
+ } else if (introduce && !answered) notes.push("", describeEffectiveThresholds(sla.adminThresholds, prefs, true), "");
110622
+ if (introduce) await sla.savePreferences({
110623
+ ...prefs,
110624
+ introduced: true
110625
+ });
110626
+ return {
110627
+ picked,
110628
+ notes,
110629
+ prefs,
110630
+ edited
110631
+ };
110632
+ };
110633
+ const keepEditedNote = (change) => {
110634
+ return `The thresholds you edited apply to this call only. To keep them, call configure_sla_alerts with ${Object.entries(change).filter(([, value]) => value !== void 0).map(([key, value]) => `${key}: "${value}"`).join(", ")}.`;
110635
+ };
110636
+ const createSlaAlertTools = (ctx) => {
110637
+ const { subdomain, getToken } = ctx;
110638
+ const resolveNamedScope = (params, picked, notes) => {
110639
+ if (params.query !== void 0) return {
110640
+ kind: "query",
110641
+ query: params.query
110642
+ };
110643
+ if (params.scope) return {
110644
+ kind: "scope",
110645
+ scope: params.scope
110646
+ };
110647
+ if (picked) return {
110648
+ kind: "scope",
110649
+ scope: picked
110650
+ };
110651
+ notes.push("No scope was given, so this checks your assigned tickets (scope \"mine\"). Pass scope, view or query for another set.");
110652
+ return {
110653
+ kind: "scope",
110654
+ scope: "mine"
110655
+ };
110656
+ };
110657
+ const resolveView = async (token, view) => {
110658
+ const resolved = await resolveViewId(subdomain, token, view);
110659
+ if ("id" in resolved) return {
110660
+ kind: "view",
110661
+ viewId: resolved.id
110662
+ };
110663
+ return resolved.available.length > 0 ? `No active view matches "${view}". Available views: ${resolved.available.join(", ")}.` : `No active view matches "${view}", and no active views were found for this agent.`;
110664
+ };
110665
+ return [{
110666
+ name: "list_sla_alerts",
110667
+ namespace: "tickets",
110668
+ readOnly: true,
110669
+ title: "List SLA Alerts",
110670
+ description: "List the SLA metrics that are breached or about to breach across a set of tickets, most urgent first. Pick the set with exactly one of scope (mine, my_groups or all unsolved tickets), view (a Zendesk view by title or id) or query (any Zendesk ticket search). Each running metric that is breached, or closer to its breach than its CRITICAL or WARNING threshold, is one line under BREACHED, CRITICAL or WARNING, soonest breach first, with the ticket id, metric, time left or overdue, subject, status and priority; paused and achieved metrics never appear. Thresholds are per metric family (reply metrics vs every other metric), the caller's own when set with configure_sla_alerts, else the server's; warning and critical override them for this call only. When no set is given the client may be asked which one, otherwise it defaults to the caller's own tickets and says so; on a user's first call the thresholds are shown too (in that form, or in the reply), and thresholds edited there apply to this call only. Never changes Zendesk; its one side effect is remembering, in this connector's own per-user state, that the user has been shown the thresholds. Results are cached per user for about a minute, and a set larger than a few hundred tickets is read in part, with a notice. For one ticket's full SLA state use get_ticket; for elapsed times use get_ticket_metrics.",
110671
+ inputSchema: object({
110672
+ scope: _enum([
110673
+ "mine",
110674
+ "my_groups",
110675
+ "all"
110676
+ ]).optional().describe("A named set of unsolved tickets: \"mine\" (assigned to you), \"my_groups\" (in any group you belong to) or \"all\" (the whole account, as far as your role sees it). Use instead of view or query."),
110677
+ view: union([string$2().min(1), number$2().int().positive()]).optional().describe("A Zendesk view to check, by its exact title (matched case-insensitively) or numeric id, as listed by list_views. Use instead of scope or query."),
110678
+ query: string$2().min(1).optional().describe("A Zendesk ticket search query selecting the tickets to check, e.g. \"status<solved priority:urgent\" or \"organization:acme status<solved\"; \"type:ticket\" is added automatically. Use instead of scope or view."),
110679
+ within: string$2().optional().describe(`Only list alerts breaching within this time from now (breached ones aside, see include_breached). ${DURATION_HINT} Omit to list everything inside the thresholds.`),
110680
+ include_breached: boolean$1().default(true).describe("Whether to list metrics already past their deadline (BREACHED). Defaults to true; set false to see only what can still be saved."),
110681
+ warning: string$2().optional().describe(`Override the WARNING threshold of every metric family for this call only: how much time left before a breach counts as a warning. Given alone, a family's critical threshold is lowered to it where longer. ${DURATION_HINT}`),
110682
+ critical: string$2().optional().describe(`Override the CRITICAL threshold of every metric family for this call only. Given alone, a family's warning threshold is raised to it where shorter; given with warning, it must not exceed it. ${DURATION_HINT}`)
110683
+ }),
110684
+ annotations: {
110685
+ readOnlyHint: true,
110686
+ destructiveHint: false,
110687
+ idempotentHint: true,
110688
+ openWorldHint: true
110689
+ },
110690
+ handler: async (rawParams, call) => {
110691
+ const params = rawParams;
110692
+ const sla = requireSla(ctx.sla);
110693
+ const given = [
110694
+ params.scope,
110695
+ params.view,
110696
+ params.query
110697
+ ].filter((v) => v !== void 0);
110698
+ if (given.length > 1) throw new Error("Pass only one of scope, view or query.");
110699
+ const withinMinutes = parseWithin(params.within);
110700
+ const overridden = params.warning !== void 0 || params.critical !== void 0;
110701
+ const checked = overrideThresholds(await sla.thresholds(), params.warning, params.critical);
110702
+ if ("error" in checked) throw new Error(checked.error);
110703
+ const token = await getToken();
110704
+ const view = params.view === void 0 ? void 0 : await resolveView(token, params.view);
110705
+ if (typeof view === "string") return { content: [{
110706
+ type: "text",
110707
+ text: view
110708
+ }] };
110709
+ const { picked, notes, prefs, edited } = await settleForm(sla, call, given.length === 0);
110710
+ const override = edited ? overrideThresholds(edited, params.warning, params.critical) : checked;
110711
+ if ("error" in override) throw new Error(override.error);
110712
+ const scope = view ?? resolveNamedScope(params, picked, notes);
110713
+ const { text } = await checkAlerts({
110714
+ sla,
110715
+ get: (path, query) => zendeskGet(subdomain, token, path, query),
110716
+ target: scope,
110717
+ thresholds: override.thresholds,
110718
+ source: thresholdsSource(sla, prefs, overridden || edited !== void 0),
110719
+ filter: {
110720
+ withinMinutes,
110721
+ includeBreached: params.include_breached
110722
+ },
110723
+ notes
110724
+ });
110725
+ return { content: [{
110726
+ type: "text",
110727
+ text
110728
+ }] };
110729
+ }
110730
+ }];
110731
+ };
110732
+ /**
110733
+ * Reads the alert resources (`zendesk-sla://alerts/...`) with the caller's
110734
+ * token and thresholds, rendered like list_sla_alerts with its defaults. The
110735
+ * poller asks it for the fingerprint only. A view title resolves once per
110736
+ * reader (that is, per connection, session or grant): resolving pages through
110737
+ * /views, which no scope cache would spare on every poll.
110738
+ */
110739
+ const createAlertsReader = (ctx) => {
110740
+ const viewIds = /* @__PURE__ */ new Map();
110741
+ const viewScope = async (view, token) => {
110742
+ const known = viewIds.get(view);
110743
+ if (known !== void 0) return {
110744
+ kind: "view",
110745
+ viewId: known
110746
+ };
110747
+ const resolved = await resolveViewId(ctx.subdomain, token, view);
110748
+ if ("available" in resolved) throw new Error(`No active view matches "${view}". Available views: ${resolved.available.join(", ") || "none"}.`);
110749
+ viewIds.set(view, resolved.id);
110750
+ return {
110751
+ kind: "view",
110752
+ viewId: resolved.id
110753
+ };
110754
+ };
110755
+ const evaluate = async (uri) => {
110756
+ const sla = requireSla(ctx.sla);
110757
+ const target = parseAlertsUri(uri);
110758
+ if (!target) throw new Error(`Not an SLA alert resource: ${uri}`);
110759
+ const token = await ctx.getToken();
110760
+ const scope = target.kind === "view" ? await viewScope(target.view, token) : target;
110761
+ const prefs = sla.userPreferencesAllowed ? await sla.preferences().catch(() => void 0) : void 0;
110762
+ try {
110763
+ return await checkAlerts({
110764
+ sla,
110765
+ get: (path, query) => zendeskGet(ctx.subdomain, token, path, query),
110766
+ target: scope,
110767
+ thresholds: await sla.thresholds(),
110768
+ source: thresholdsSource(sla, prefs, false),
110769
+ filter: { includeBreached: true },
110770
+ notes: []
110771
+ });
110772
+ } catch (err) {
110773
+ if (target.kind === "view") viewIds.delete(target.view);
110774
+ throw err;
110775
+ }
110776
+ };
110777
+ return {
110778
+ read: async (uri) => (await evaluate(uri)).text,
110779
+ fingerprint: async (uri) => (await evaluate(uri)).fingerprint
110780
+ };
110781
+ };
110782
+ //#endregion
109651
110783
  //#region src/tools/users.ts
109652
110784
  const createUserTools = (ctx) => {
109653
110785
  const { subdomain, getToken } = ctx;
@@ -109783,10 +110915,12 @@ const createUserTools = (ctx) => {
109783
110915
  //#region src/tools/index.ts
109784
110916
  const createAllTools = (ctx) => [
109785
110917
  ...createTicketTools(ctx),
110918
+ ...createSlaAlertTools(ctx),
109786
110919
  ...createRequestTools(ctx),
109787
110920
  ...createSearchTools(ctx),
109788
110921
  ...createHelpCenterTools(ctx),
109789
- ...createUserTools(ctx)
110922
+ ...createUserTools(ctx),
110923
+ ...createPreferenceTools(ctx)
109790
110924
  ];
109791
110925
  //#endregion
109792
110926
  //#region src/utils/logger.ts
@@ -109911,7 +111045,359 @@ const createRequestStateCodec = ({ key = randomBytes(32), principal = () => "" }
109911
111045
  bind: (ctx) => `${ctx.mcpReq.method}\0${principal()}`
109912
111046
  });
109913
111047
  //#endregion
111048
+ //#region src/utils/sign-in-required.ts
111049
+ const SIGN_IN_REQUIRED = "SignInRequiredError";
111050
+ /**
111051
+ * What a background caller (the SLA alert poller) gets instead of a browser
111052
+ * sign-in: only a user action may open one, so the next tool call does.
111053
+ */
111054
+ const signInRequiredError = () => Object.assign(/* @__PURE__ */ new Error("Not signed in to Zendesk: the next tool call starts the sign-in."), { name: SIGN_IN_REQUIRED });
111055
+ const isSignInRequired = (error) => error instanceof Error && error.name === SIGN_IN_REQUIRED;
111056
+ //#endregion
111057
+ //#region src/utils/sla-poller.ts
111058
+ /**
111059
+ * Polls the subscribed alert resources while there is at least one, and
111060
+ * notifies a URI only when its alert set changed: which tickets, metrics and
111061
+ * levels. The fingerprint at subscription time is the baseline, so the first
111062
+ * round never notifies on its own. The timer is unref'd and stops with the
111063
+ * last subscription.
111064
+ */
111065
+ const createAlertPoller = (options) => {
111066
+ const { intervalMs, check, notify, onError = () => void 0 } = options;
111067
+ const fingerprints = /* @__PURE__ */ new Map();
111068
+ let timer;
111069
+ let running = false;
111070
+ const refresh = async (uri) => {
111071
+ try {
111072
+ const next = await check(uri);
111073
+ const previous = fingerprints.get(uri);
111074
+ if (previous !== void 0 && previous !== next) await notify(uri);
111075
+ if (fingerprints.has(uri)) fingerprints.set(uri, next);
111076
+ } catch (error) {
111077
+ onError(uri, error);
111078
+ }
111079
+ };
111080
+ const tick = async () => {
111081
+ if (running) return;
111082
+ running = true;
111083
+ try {
111084
+ for (const uri of [...fingerprints.keys()]) await refresh(uri);
111085
+ } finally {
111086
+ running = false;
111087
+ }
111088
+ };
111089
+ const stopTimer = () => {
111090
+ clearInterval(timer);
111091
+ timer = void 0;
111092
+ };
111093
+ return {
111094
+ subscribe: (uri) => {
111095
+ if (fingerprints.has(uri)) return;
111096
+ fingerprints.set(uri, void 0);
111097
+ refresh(uri);
111098
+ if (!timer) {
111099
+ timer = setInterval(() => void tick(), intervalMs);
111100
+ timer.unref();
111101
+ }
111102
+ },
111103
+ unsubscribe: (uri) => {
111104
+ fingerprints.delete(uri);
111105
+ if (fingerprints.size === 0) stopTimer();
111106
+ },
111107
+ stop: () => {
111108
+ fingerprints.clear();
111109
+ stopTimer();
111110
+ },
111111
+ subscriptions: () => [...fingerprints.keys()],
111112
+ tick
111113
+ };
111114
+ };
111115
+ //#endregion
111116
+ //#region src/auth/token-persistence.ts
111117
+ const isWindows = process.platform === "win32";
111118
+ /**
111119
+ * Config-dir segments derived from the *scoped* package name
111120
+ * (`@fruggr/zendesk-mcp-server` → `fruggr` + `zendesk-mcp-server`) so the path is
111121
+ * vendor-namespaced and can't collide with another `zendesk-mcp-server`.
111122
+ */
111123
+ const SCOPED_PACKAGE_NAME = /^@([^/]+)\/(.+)/;
111124
+ const appDirSegments = () => {
111125
+ const { name } = readPackageInfo();
111126
+ const scoped = SCOPED_PACKAGE_NAME.exec(name);
111127
+ return scoped?.[1] && scoped[2] ? [scoped[1], scoped[2]] : [name];
111128
+ };
111129
+ const configDir = () => {
111130
+ const segments = appDirSegments();
111131
+ if (isWindows) {
111132
+ const base = process.env["APPDATA"] ?? join(homedir(), "AppData", "Roaming");
111133
+ return join(base, ...segments);
111134
+ }
111135
+ const base = process.env["XDG_CONFIG_HOME"] ?? join(homedir(), ".config");
111136
+ return join(base, ...segments);
111137
+ };
111138
+ const safeName = (part) => part.replace(/[^a-z0-9-]/gi, "_");
111139
+ const READABLE_BUDGET = 120;
111140
+ const keyDigest = (key) => createHash("sha256").update(JSON.stringify([
111141
+ key.subdomain,
111142
+ key.oauthClientId,
111143
+ key.scope
111144
+ ])).digest("hex").slice(0, 8);
111145
+ /**
111146
+ * Path to the token file for one credential. Keyed on the whole `TokenKey`, so
111147
+ * a read-only server, a read-write one and one on another OAuth client each get
111148
+ * their own record instead of clobbering a shared file. Why that triple, and
111149
+ * why no migration from the old subdomain-only layout:
111150
+ * `docs/decisions/token-file-keying.md`.
111151
+ * `OAUTH_TOKEN_FILE` overrides with an explicit path — the way to separate
111152
+ * two Zendesk accounts that share a subdomain, client and scope.
111153
+ */
111154
+ const resolveTokenPath = (key) => {
111155
+ const { name, value: override } = readEnv("OAUTH_TOKEN_FILE");
111156
+ if (override === "") throw new Error(`Empty ${name}. Set it to a value, or unset it entirely.`);
111157
+ if (override) return override;
111158
+ const readable = [
111159
+ key.subdomain,
111160
+ key.oauthClientId,
111161
+ key.scope
111162
+ ].map(safeName).join("--").slice(0, READABLE_BUDGET);
111163
+ return join(configDir(), `${readable}--${keyDigest(key)}.json`);
111164
+ };
111165
+ const writeFileAtomic = (path, record) => {
111166
+ const dir = dirname(path);
111167
+ mkdirSync(dir, { recursive: true });
111168
+ const tmp = `${path}.${process.pid}.tmp`;
111169
+ writeFileSync(tmp, JSON.stringify(record, null, 2), "utf8");
111170
+ if (!isWindows) chmodSync(tmp, 384);
111171
+ renameSync(tmp, path);
111172
+ if (!isWindows) try {
111173
+ chmodSync(dir, 448);
111174
+ } catch {}
111175
+ };
111176
+ const loadToken = (path) => {
111177
+ try {
111178
+ const raw = JSON.parse(readFileSync(path, "utf8"));
111179
+ if (typeof raw?.accessToken === "string") return raw;
111180
+ } catch {}
111181
+ };
111182
+ const saveToken = (path, record, logger = silentLogger) => {
111183
+ try {
111184
+ writeFileAtomic(path, record);
111185
+ logger.debug("token_persisted");
111186
+ } catch (err) {
111187
+ logger.warn("token_persist_failed", { error: err instanceof Error ? err.message : String(err) });
111188
+ }
111189
+ };
111190
+ const clearToken = (path, logger = silentLogger) => {
111191
+ try {
111192
+ rmSync(path, { force: true });
111193
+ logger.debug("token_cleared");
111194
+ } catch (err) {
111195
+ logger.warn("token_clear_failed", { error: err instanceof Error ? err.message : String(err) });
111196
+ }
111197
+ };
111198
+ //#endregion
111199
+ //#region src/utils/scope-cache.ts
111200
+ /**
111201
+ * Serves `load()` from `store` for `ttlMs`, and makes concurrent calls for the
111202
+ * same key share one in-flight load. Zendesk charges a conditional GET as a
111203
+ * full request (PR #387 spike 2), so this lifetime is the only quota saving.
111204
+ * A store that fails to save only costs the cache, never the answer.
111205
+ */
111206
+ const createScopeCache = (store, ttlMs) => {
111207
+ const inFlight = /* @__PURE__ */ new Map();
111208
+ return (key, load) => {
111209
+ const pending = inFlight.get(key);
111210
+ if (pending) return pending;
111211
+ const run = (async () => {
111212
+ const hit = await store.get(key).catch(() => void 0);
111213
+ if (hit !== void 0) return hit;
111214
+ const value = await load();
111215
+ await store.set(key, value, ttlMs).catch(() => void 0);
111216
+ return value;
111217
+ })().finally(() => inFlight.delete(key));
111218
+ inFlight.set(key, run);
111219
+ return run;
111220
+ };
111221
+ };
111222
+ //#endregion
111223
+ //#region src/utils/state-store.ts
111224
+ const isLive = (entry, now) => entry.expiresAt === void 0 || entry.expiresAt > now;
111225
+ const toEntry = (key, value, now, ttlMs) => ttlMs === void 0 ? {
111226
+ key,
111227
+ value
111228
+ } : {
111229
+ key,
111230
+ value,
111231
+ expiresAt: now + ttlMs
111232
+ };
111233
+ /** One process, nothing on disk: the default on HTTP, where sessions share the process. */
111234
+ const createMemoryStore = (now = Date.now) => {
111235
+ const entries = /* @__PURE__ */ new Map();
111236
+ return {
111237
+ get: async (key) => {
111238
+ const entry = entries.get(key);
111239
+ return entry && isLive(entry, now()) ? entry.value : void 0;
111240
+ },
111241
+ set: async (key, value, ttlMs) => {
111242
+ entries.set(key, toEntry(key, value, now(), ttlMs));
111243
+ },
111244
+ delete: async (key) => {
111245
+ entries.delete(key);
111246
+ },
111247
+ sweep: async () => {
111248
+ const at = now();
111249
+ for (const [key, entry] of entries) if (!isLive(entry, at)) entries.delete(key);
111250
+ return true;
111251
+ }
111252
+ };
111253
+ };
111254
+ const LOCK_FILE = "cleanup.lock";
111255
+ const errnoOf = (err) => err.code;
111256
+ const ignore = () => void 0;
111257
+ const remove = (path) => unlink(path).catch(ignore);
111258
+ const fileOf = (dir, key) => join(dir, `${createHash("sha256").update(key).digest("hex").slice(0, 32)}.json`);
111259
+ const readEntry = (path) => readFile(path, "utf8").then((text) => JSON.parse(text)).catch((err) => {
111260
+ if (err instanceof SyntaxError || errnoOf(err) === "ENOENT") return void 0;
111261
+ throw err;
111262
+ });
111263
+ /**
111264
+ * Entries as one file each under `dir` (0600, directory 0700), written to a
111265
+ * temp file then renamed so a reader never sees half an entry. Several stdio
111266
+ * processes we do not control share it; see `sweep` for how they clean up
111267
+ * without stepping on each other.
111268
+ */
111269
+ const createDiskStore = (dir, options = {}) => {
111270
+ const { now = Date.now, lockStaleMs = 12e4, tempStaleMs = 3e5 } = options;
111271
+ const ensureDir = async () => {
111272
+ await mkdir(dir, {
111273
+ recursive: true,
111274
+ mode: 448
111275
+ });
111276
+ await chmod(dir, 448);
111277
+ };
111278
+ const acquireLock = async (lock, mayTakeOver = true) => {
111279
+ try {
111280
+ const handle = await open(lock, "wx", 384);
111281
+ await handle.writeFile(JSON.stringify({
111282
+ pid: process.pid,
111283
+ at: now()
111284
+ }));
111285
+ await handle.close();
111286
+ return true;
111287
+ } catch (err) {
111288
+ const code = errnoOf(err);
111289
+ if (code === "ENOENT") return null;
111290
+ if (code !== "EEXIST" || !mayTakeOver) return false;
111291
+ }
111292
+ const held = await readEntry(lock).catch(ignore);
111293
+ if (typeof held?.at === "number" && now() - held.at < lockStaleMs) return false;
111294
+ await remove(lock);
111295
+ return acquireLock(lock, false);
111296
+ };
111297
+ const sweepFile = async (name, at) => {
111298
+ const path = join(dir, name);
111299
+ if (name.endsWith(".tmp")) {
111300
+ const info = await stat(path).catch(ignore);
111301
+ if (info && at - info.mtimeMs > tempStaleMs) await remove(path);
111302
+ return;
111303
+ }
111304
+ const entry = await readEntry(path).catch(ignore);
111305
+ if (entry && !isLive(entry, at)) await remove(path);
111306
+ };
111307
+ return {
111308
+ get: async (key) => {
111309
+ const entry = await readEntry(fileOf(dir, key));
111310
+ return entry?.key === key && isLive(entry, now()) ? entry.value : void 0;
111311
+ },
111312
+ set: async (key, value, ttlMs) => {
111313
+ await ensureDir();
111314
+ const path = fileOf(dir, key);
111315
+ const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
111316
+ await writeFile(temp, JSON.stringify(toEntry(key, value, now(), ttlMs)), { mode: 384 });
111317
+ await rename(temp, path);
111318
+ },
111319
+ delete: (key) => remove(fileOf(dir, key)),
111320
+ sweep: async () => {
111321
+ const lock = join(dir, LOCK_FILE);
111322
+ const acquired = await acquireLock(lock);
111323
+ if (acquired !== true) return acquired === null;
111324
+ try {
111325
+ const at = now();
111326
+ for (const name of await readdir(dir)) await sweepFile(name, at);
111327
+ } finally {
111328
+ await remove(lock);
111329
+ }
111330
+ return true;
111331
+ }
111332
+ };
111333
+ };
111334
+ /**
111335
+ * Sweeps `store` every `intervalMs`, never at startup. The timer is unref'd so
111336
+ * it never keeps a process alive; the returned function stops it.
111337
+ */
111338
+ const startSweeper = (store, intervalMs = 6e5) => {
111339
+ const timer = setInterval(() => {
111340
+ store.sweep().catch(() => void 0);
111341
+ }, intervalMs);
111342
+ timer.unref();
111343
+ return () => clearInterval(timer);
111344
+ };
111345
+ //#endregion
111346
+ //#region src/utils/sla-service.ts
111347
+ const createSlaRuntime = (settings, store = createMemoryStore()) => ({
111348
+ store,
111349
+ cache: createScopeCache(store, settings.slaCacheTtlSeconds * 1e3)
111350
+ });
111351
+ /**
111352
+ * The runtime the entry point builds once per process: the store the config
111353
+ * selects, next to the token files when on disk, swept in the background.
111354
+ * `stop` ends the sweeper.
111355
+ */
111356
+ const startSlaRuntime = (config) => {
111357
+ const store = slaStateStoreOf(config) === "disk" ? createDiskStore(join(configDir(), "sla-state")) : createMemoryStore();
111358
+ return {
111359
+ runtime: createSlaRuntime(config, store),
111360
+ stop: startSweeper(store)
111361
+ };
111362
+ };
111363
+ const MAX_REMEMBERED_TOKENS = 8;
111364
+ /**
111365
+ * Resolves the caller's Zendesk user id once per token (`/users/me`), so a
111366
+ * refreshed or another user's token can never read an entry cached under the
111367
+ * previous one. A failed lookup is forgotten, to be retried on the next call.
111368
+ */
111369
+ const createUserIdResolver = (subdomain, getToken) => {
111370
+ const byToken = /* @__PURE__ */ new Map();
111371
+ return async () => {
111372
+ const token = await getToken();
111373
+ const known = byToken.get(token);
111374
+ if (known) return known;
111375
+ const lookup = zendeskGet(subdomain, token, "/users/me").then(({ user }) => user.id);
111376
+ lookup.catch(() => byToken.delete(token));
111377
+ if (byToken.size >= MAX_REMEMBERED_TOKENS) byToken.delete(byToken.keys().next().value);
111378
+ byToken.set(token, lookup);
111379
+ return lookup;
111380
+ };
111381
+ };
111382
+ const createSlaService = (settings, runtime, userId) => {
111383
+ const userKey = async () => `${settings.subdomain}:${await userId()}`;
111384
+ const preferences = async () => await runtime.store.get(`prefs:${await userKey()}`);
111385
+ return {
111386
+ push: settings.slaPush,
111387
+ userPreferencesAllowed: settings.slaUserPreferences,
111388
+ adminThresholds: settings.slaThresholds,
111389
+ thresholds: async () => settings.slaUserPreferences ? preferences().then((prefs) => effectiveThresholds(settings.slaThresholds, prefs), () => settings.slaThresholds) : settings.slaThresholds,
111390
+ preferences,
111391
+ savePreferences: async (prefs) => runtime.store.set(`prefs:${await userKey()}`, prefs),
111392
+ userId,
111393
+ cached: async (scope, load) => runtime.cache(`cache:${await userKey()}:${scope}`, load)
111394
+ };
111395
+ };
111396
+ //#endregion
109914
111397
  //#region src/server.ts
111398
+ const noteUnauthorized = (err, onUnauthorized) => {
111399
+ if (onUnauthorized && err instanceof ZendeskApiError && err.status === 401) onUnauthorized();
111400
+ };
109915
111401
  /**
109916
111402
  * Invoke a tool handler, notifying `onUnauthorized` when Zendesk rejects the
109917
111403
  * token (401), so the OAuth store drops the dead token instead of replaying it.
@@ -109924,19 +111410,25 @@ const createRequestStateCodec = ({ key = randomBytes(32), principal = () => "" }
109924
111410
  * this rare — it fires only for a token revoked between the pre-call check and
109925
111411
  * the request.
109926
111412
  */
109927
- const runHandler = async (def, params, onUnauthorized) => {
111413
+ const runHandler = async (def, params, onUnauthorized, call) => {
109928
111414
  try {
109929
- return await def.handler(params);
111415
+ return await def.handler(params, call);
109930
111416
  } catch (err) {
109931
- if (onUnauthorized && err instanceof ZendeskApiError && err.status === 401) onUnauthorized();
111417
+ if (isInputRequiredError(err)) return err.inputRequired;
111418
+ noteUnauthorized(err, onUnauthorized);
109932
111419
  throw err;
109933
111420
  }
109934
111421
  };
111422
+ /** The parts of the SDK request context a tool handler may use. */
111423
+ const toCallContext = (server, ctx) => ({
111424
+ canElicit: supportsFormElicitation(ctx.mcpReq.envelope?.["io.modelcontextprotocol/clientCapabilities"] ?? server.server.getClientCapabilities()),
111425
+ inputResponses: ctx.mcpReq.inputResponses
111426
+ });
109935
111427
  const aggregateAnnotations = (tools) => ({
109936
111428
  readOnlyHint: tools.every((t) => t.annotations.readOnlyHint),
109937
111429
  destructiveHint: tools.some((t) => t.annotations.destructiveHint),
109938
111430
  idempotentHint: tools.every((t) => t.annotations.idempotentHint),
109939
- openWorldHint: true
111431
+ openWorldHint: tools.some((t) => t.annotations.openWorldHint)
109940
111432
  });
109941
111433
  const buildProxyDispatch = (tools, onUnauthorized) => {
109942
111434
  const operationNames = tools.map((t) => t.name);
@@ -109944,7 +111436,7 @@ const buildProxyDispatch = (tools, onUnauthorized) => {
109944
111436
  def: t,
109945
111437
  parseParams: createStrictParamsParser(t.inputSchema)
109946
111438
  }]));
109947
- return async (args) => {
111439
+ return async (args, call) => {
109948
111440
  const { operation, params } = args;
109949
111441
  const entry = localHandlers.get(operation);
109950
111442
  if (!entry) return { content: [{
@@ -109952,7 +111444,7 @@ const buildProxyDispatch = (tools, onUnauthorized) => {
109952
111444
  text: `Unknown operation "${operation}". Available: ${operationNames.join(", ")}`
109953
111445
  }] };
109954
111446
  const validated = entry.parseParams(params);
109955
- return runHandler(entry.def, validated, onUnauthorized);
111447
+ return runHandler(entry.def, validated, onUnauthorized, call);
109956
111448
  };
109957
111449
  };
109958
111450
  const registerProxyTool = (server, toolName, title, tools, readOnlyMode, onUnauthorized) => {
@@ -109963,14 +111455,25 @@ const registerProxyTool = (server, toolName, title, tools, readOnlyMode, onUnaut
109963
111455
  description: buildProxyDescription({
109964
111456
  title,
109965
111457
  tools,
109966
- readOnly: readOnlyMode
111458
+ readOnly: readOnlyMode && annotations.readOnlyHint
109967
111459
  }),
109968
111460
  inputSchema: object({
109969
111461
  operation: string$2().describe(buildOperationFieldDescription(tools)),
109970
111462
  params: record(string$2(), unknown$3()).default({}).describe(PARAMS_FIELD_DESCRIPTION)
109971
111463
  }),
109972
111464
  annotations
109973
- }, async (args) => dispatch(args));
111465
+ }, async (args, ctx) => dispatch(args, toCallContext(server, ctx)));
111466
+ };
111467
+ /**
111468
+ * `single` mode: one `zendesk` proxy for every Zendesk tool. The preferences
111469
+ * configure this server, not Zendesk: they keep their own proxy, so `zendesk`
111470
+ * stays `[RO]` and readOnlyHint under --read-only.
111471
+ */
111472
+ const registerSingleProxies = (server, tools, readOnlyMode, onUnauthorized) => {
111473
+ const preferences = tools.filter((tool) => tool.namespace === "preferences");
111474
+ const zendesk = tools.filter((tool) => tool.namespace !== "preferences");
111475
+ const label = NAMESPACE_LABELS.preferences;
111476
+ return [...zendesk.length > 0 || preferences.length === 0 ? [registerProxyTool(server, "zendesk", "Zendesk", zendesk, readOnlyMode, onUnauthorized)] : [], ...preferences.length > 0 ? [registerProxyTool(server, label.toolName, label.title, preferences, readOnlyMode, onUnauthorized)] : []];
109974
111477
  };
109975
111478
  /**
109976
111479
  * Build the bare `McpServer` — identity, `instructions` — with no tools or
@@ -109993,10 +111496,158 @@ const createServerCaches = (config, getToken, onUnauthorized) => {
109993
111496
  const resolveBrandSubdomain = createBrandSubdomainResolver(config.subdomain, getToken);
109994
111497
  return {
109995
111498
  resolveBrandSubdomain,
111499
+ resolveUserId: createUserIdResolver(config.subdomain, getToken),
109996
111500
  topology: helpCenterContextEnabled(config) ? createTopologyProvider(getToken, config.subdomain, onUnauthorized, config.brandIds, firstBrandSubdomainResolver(config, resolveBrandSubdomain)) : void 0,
109997
111501
  articles: articleResourceEnabled(config) ? createArticleResourcesProvider(getToken, config.subdomain, onUnauthorized, resourceBrandResolver(config, resolveBrandSubdomain), config.brandIds) : void 0
109998
111502
  };
109999
111503
  };
111504
+ const ALERTS_MIME = "text/markdown";
111505
+ const registerAlertResources = (server, reader, push) => {
111506
+ const read = async (uri) => ({ contents: [{
111507
+ uri: uri.toString(),
111508
+ mimeType: ALERTS_MIME,
111509
+ text: await reader.read(uri.toString())
111510
+ }] });
111511
+ const describe = `SLA metrics breached or about to breach, one line each, grouped BREACHED / CRITICAL / WARNING and soonest first, against your alert thresholds; the same report as list_sla_alerts.${push ? " Subscribe to be told when the set of alerts changes." : ""}`;
111512
+ return [
111513
+ server.registerResource("sla-alerts", new ResourceTemplate(SLA_ALERTS_TEMPLATES.named, { list: async () => ({ resources: NAMED_SCOPES.map((scope) => ({
111514
+ uri: namedAlertsUri(scope),
111515
+ name: `SLA alerts: ${scope}`,
111516
+ mimeType: ALERTS_MIME
111517
+ })) }) }), {
111518
+ title: "Zendesk SLA alerts",
111519
+ description: `${describe} {scope} is mine, my_groups or all (unsolved tickets).`,
111520
+ mimeType: ALERTS_MIME
111521
+ }, read),
111522
+ server.registerResource("sla-alerts-view", new ResourceTemplate(SLA_ALERTS_TEMPLATES.view, { list: void 0 }), {
111523
+ title: "Zendesk SLA alerts in a view",
111524
+ description: `${describe} {view} is a view title or id, as list_views shows it.`,
111525
+ mimeType: ALERTS_MIME
111526
+ }, read),
111527
+ server.registerResource("sla-alerts-query", new ResourceTemplate(SLA_ALERTS_TEMPLATES.query, { list: void 0 }), {
111528
+ title: "Zendesk SLA alerts for a search",
111529
+ description: `${describe} {query} is any Zendesk ticket search query.`,
111530
+ mimeType: ALERTS_MIME
111531
+ }, read)
111532
+ ];
111533
+ };
111534
+ /** Re-checks each subscribed URI every `slaPollSeconds`; notifies when its alert set changes. */
111535
+ const createAlertsPoller = (config, reader, notify, logger) => createAlertPoller({
111536
+ intervalMs: config.slaPollSeconds * 1e3,
111537
+ check: reader.fingerprint,
111538
+ notify,
111539
+ onError: (uri, error) => {
111540
+ if (isSignInRequired(error)) {
111541
+ logger.debug("sla_alerts_poll_skipped", { uri });
111542
+ return;
111543
+ }
111544
+ logger.warn("sla_alerts_poll_failed", {
111545
+ uri,
111546
+ error: error instanceof Error ? error.message : String(error)
111547
+ });
111548
+ }
111549
+ });
111550
+ /**
111551
+ * The SLA settings a server applies: personal thresholds only where
111552
+ * `configure_sla_alerts`, the one way to change them, is exposed.
111553
+ */
111554
+ const slaSettings = (config) => ({
111555
+ ...config,
111556
+ slaUserPreferences: slaPreferencesEnabled(config)
111557
+ });
111558
+ /** The context the tools and the alert readers share. */
111559
+ const toolContext = (config, getToken, resolveBrandSubdomain, sla) => ({
111560
+ subdomain: config.subdomain,
111561
+ brandIds: config.brandIds,
111562
+ resolveBrandSubdomain,
111563
+ getToken,
111564
+ sla
111565
+ });
111566
+ /** `reader`, with a Zendesk 401 reported like a tool's: the dead token is dropped, not replayed. */
111567
+ const reportingUnauthorized = (reader, onUnauthorized) => {
111568
+ const note = (err) => {
111569
+ noteUnauthorized(err, onUnauthorized);
111570
+ throw err;
111571
+ };
111572
+ return {
111573
+ read: (uri) => reader.read(uri).catch(note),
111574
+ fingerprint: (uri) => reader.fingerprint(uri).catch(note)
111575
+ };
111576
+ };
111577
+ /** The reader the SLA alert poller checks with. */
111578
+ const createAlertPollingReader = ({ config, getToken, resolveUserId, caches, slaRuntime, onUnauthorized }) => reportingUnauthorized(createAlertsReader(toolContext(config, getToken, caches.resolveBrandSubdomain, createSlaService(slaSettings(config), slaRuntime, resolveUserId))), onUnauthorized);
111579
+ /**
111580
+ * The token source and user-id lookup a poller runs with: a background one
111581
+ * when the transport has it (stdio, whose `getToken` may open a browser).
111582
+ */
111583
+ const pollingIdentity = (config, getToken, caches, getBackgroundToken) => getBackgroundToken ? {
111584
+ getToken: getBackgroundToken,
111585
+ resolveUserId: createUserIdResolver(config.subdomain, getBackgroundToken)
111586
+ } : {
111587
+ getToken,
111588
+ resolveUserId: caches.resolveUserId
111589
+ };
111590
+ /** Polls the SLA alert resources a 2026-07-28 client listens to; other URIs are not ours. */
111591
+ const pollListenedAlerts = (poller, listens) => listens.attach({
111592
+ add: (uri) => {
111593
+ if (parseAlertsUri(uri)) poller.subscribe(uri);
111594
+ },
111595
+ remove: (uri) => poller.unsubscribe(uri)
111596
+ });
111597
+ /**
111598
+ * The server pushing alert changes on its own, through `resources/subscribe`
111599
+ * for a 2025 client and the `listens` streams for a 2026-07-28 one when the
111600
+ * server lives as long as its connection (stdio). Installed once per server,
111601
+ * before it connects, and only with push on. Stops with the server.
111602
+ */
111603
+ const installAlertSubscriptions = (server, config, reader, logger = silentLogger, listens) => {
111604
+ const poller = createAlertsPoller(config, reader, (uri) => server.server.sendResourceUpdated({ uri }), logger);
111605
+ const subscribable = (uri) => {
111606
+ if (!parseAlertsUri(uri)) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Only SLA alert resources (${SLA_RESOURCE_SCHEME}://alerts/...) can be subscribed to: ${uri}`);
111607
+ return uri;
111608
+ };
111609
+ server.server.registerCapabilities({ resources: { subscribe: true } });
111610
+ server.server.setRequestHandler("resources/subscribe", (request) => {
111611
+ poller.subscribe(subscribable(request.params.uri));
111612
+ return {};
111613
+ });
111614
+ server.server.setRequestHandler("resources/unsubscribe", (request) => {
111615
+ poller.unsubscribe(request.params.uri);
111616
+ return {};
111617
+ });
111618
+ const detach = listens ? pollListenedAlerts(poller, listens) : void 0;
111619
+ const previous = server.server.onclose;
111620
+ server.server.onclose = () => {
111621
+ detach?.();
111622
+ poller.stop();
111623
+ previous?.();
111624
+ };
111625
+ return poller;
111626
+ };
111627
+ /**
111628
+ * The SLA alert polling of a 2026-07-28 HTTP grant: its servers last one
111629
+ * request each, so the poller lives with the grant, fed by the grant's listen
111630
+ * streams. Returns the stop; a no-op when the push is off.
111631
+ */
111632
+ const watchGrantAlerts = ({ config, getToken, caches, slaRuntime, logger, listens, notify, onUnauthorized }) => {
111633
+ if (!(config.slaPush && slaAlertsEnabled(config))) return () => void 0;
111634
+ const reader = createAlertPollingReader({
111635
+ config,
111636
+ getToken,
111637
+ resolveUserId: caches.resolveUserId,
111638
+ caches,
111639
+ slaRuntime,
111640
+ onUnauthorized
111641
+ });
111642
+ const poller = createAlertsPoller(config, reader, async (uri) => {
111643
+ notify(uri);
111644
+ }, logger);
111645
+ const detach = pollListenedAlerts(poller, listens);
111646
+ return () => {
111647
+ detach();
111648
+ poller.stop();
111649
+ };
111650
+ };
110000
111651
  /**
110001
111652
  * Lazily resolve the Help Center subdomain the topology resource pins to: the
110002
111653
  * FIRST allowed brand when a list is set (named in the header), else undefined
@@ -110032,8 +111683,9 @@ const resourceBrandResolver = (config, resolveBrandSubdomain) => {
110032
111683
  * without dropping the transport. `tools` is passed in (not built here) so the
110033
111684
  * reload path can hand over freshly re-imported definitions.
110034
111685
  */
110035
- const registerToolset = (server, { config, getToken, caches, onUnauthorized, logger = silentLogger }, tools) => {
110036
- const { topology, articles } = caches ?? createServerCaches(config, getToken, onUnauthorized);
111686
+ const registerToolset = (server, { config, getToken, caches, onUnauthorized, logger = silentLogger, sla }, tools) => {
111687
+ const shared = caches ?? createServerCaches(config, getToken, onUnauthorized);
111688
+ const { topology, articles } = shared;
110037
111689
  const registered = [];
110038
111690
  const dispose = () => {
110039
111691
  for (const handle of registered) handle.remove();
@@ -110051,7 +111703,7 @@ const registerToolset = (server, { config, getToken, caches, onUnauthorized, log
110051
111703
  description: tool.description,
110052
111704
  inputSchema: toStrictStandardSchema(tool.inputSchema),
110053
111705
  annotations: tool.annotations
110054
- }, async (params) => runHandler(tool, params, onUnauthorized)));
111706
+ }, async (params, ctx) => runHandler(tool, params, onUnauthorized, toCallContext(server, ctx))));
110055
111707
  break;
110056
111708
  case "namespace": {
110057
111709
  const grouped = groupByNamespace(filteredTools);
@@ -110062,7 +111714,7 @@ const registerToolset = (server, { config, getToken, caches, onUnauthorized, log
110062
111714
  break;
110063
111715
  }
110064
111716
  case "single":
110065
- registered.push(registerProxyTool(server, "zendesk", "Zendesk", filteredTools, config.readOnly, onUnauthorized));
111717
+ registered.push(...registerSingleProxies(server, filteredTools, config.readOnly, onUnauthorized));
110066
111718
  break;
110067
111719
  default: {
110068
111720
  const unhandled = config.mode;
@@ -110117,6 +111769,7 @@ const registerToolset = (server, { config, getToken, caches, onUnauthorized, log
110117
111769
  }] };
110118
111770
  }));
110119
111771
  }
111772
+ if (sla && slaAlertsEnabled(config)) registered.push(...registerAlertResources(server, reportingUnauthorized(createAlertsReader(toolContext(config, getToken, shared.resolveBrandSubdomain, sla)), onUnauthorized), config.slaPush));
110120
111773
  } catch (err) {
110121
111774
  dispose();
110122
111775
  throw err;
@@ -110130,25 +111783,38 @@ const registerToolset = (server, { config, getToken, caches, onUnauthorized, log
110130
111783
  dispose
110131
111784
  };
110132
111785
  };
110133
- const createMcpServer = (config, getToken, logger = silentLogger, onUnauthorized, { caches = createServerCaches(config, getToken, onUnauthorized), requestStateCodec } = {}) => {
111786
+ const createMcpServer = (config, getToken, logger = silentLogger, onUnauthorized, { caches = createServerCaches(config, getToken, onUnauthorized), requestStateCodec, slaRuntime = createSlaRuntime(config), listens, getBackgroundToken, alertPolling = "server" } = {}) => {
110134
111787
  const server = createServerShell(config, requestStateCodec);
110135
- const tools = createAllTools({
110136
- subdomain: config.subdomain,
110137
- brandIds: config.brandIds,
110138
- resolveBrandSubdomain: caches.resolveBrandSubdomain,
110139
- getToken
110140
- });
111788
+ const sla = createSlaService(slaSettings(config), slaRuntime, caches.resolveUserId);
110141
111789
  registerToolset(server, {
110142
111790
  config,
110143
111791
  getToken,
110144
111792
  caches,
110145
111793
  onUnauthorized,
110146
- logger
110147
- }, tools);
111794
+ logger,
111795
+ sla
111796
+ }, createAllTools(toolContext(config, getToken, caches.resolveBrandSubdomain, sla)));
111797
+ if (config.slaPush && slaAlertsEnabled(config)) {
111798
+ if (alertPolling === "grant") server.server.registerCapabilities({ resources: { subscribe: true } });
111799
+ else {
111800
+ const reader = createAlertPollingReader({
111801
+ config,
111802
+ ...pollingIdentity(config, getToken, caches, getBackgroundToken),
111803
+ caches,
111804
+ slaRuntime,
111805
+ onUnauthorized
111806
+ });
111807
+ installAlertSubscriptions(server, config, reader, logger, listens);
111808
+ }
111809
+ }
110148
111810
  return server;
110149
111811
  };
110150
- const defaultServerFactory = ({ config, getToken, logger, onUnauthorized, caches, requestStateCodec }) => createMcpServer(config, getToken, logger, onUnauthorized, {
111812
+ const defaultServerFactory = ({ config, getToken, logger, onUnauthorized, caches, requestStateCodec, slaRuntime, listens, getBackgroundToken, alertPolling }) => createMcpServer(config, getToken, logger, onUnauthorized, {
110151
111813
  ...caches ? { caches } : {},
111814
+ ...slaRuntime ? { slaRuntime } : {},
111815
+ ...listens ? { listens } : {},
111816
+ ...getBackgroundToken ? { getBackgroundToken } : {},
111817
+ ...alertPolling ? { alertPolling } : {},
110152
111818
  requestStateCodec
110153
111819
  });
110154
111820
  //#endregion
@@ -110731,8 +112397,119 @@ function toError(value) {
110731
112397
  return value instanceof Error ? value : new Error(String(value));
110732
112398
  }
110733
112399
  //#endregion
112400
+ //#region src/utils/listen-feed.ts
112401
+ const tell = (sink, change, uri) => {
112402
+ try {
112403
+ sink[change](uri);
112404
+ } catch {}
112405
+ };
112406
+ const createListenFeed = () => {
112407
+ const streams = /* @__PURE__ */ new Map();
112408
+ const listeners = /* @__PURE__ */ new Map();
112409
+ const sinks = /* @__PURE__ */ new Set();
112410
+ const close = (key) => {
112411
+ const uris = streams.get(key);
112412
+ if (!uris) return;
112413
+ streams.delete(key);
112414
+ for (const uri of uris) {
112415
+ const count = (listeners.get(uri) ?? 0) - 1;
112416
+ if (count > 0) {
112417
+ listeners.set(uri, count);
112418
+ continue;
112419
+ }
112420
+ listeners.delete(uri);
112421
+ for (const sink of sinks) tell(sink, "remove", uri);
112422
+ }
112423
+ };
112424
+ return {
112425
+ open: (key, uris) => {
112426
+ if (streams.has(key)) return;
112427
+ const unique = [...new Set(uris)];
112428
+ streams.set(key, unique);
112429
+ for (const uri of unique) {
112430
+ const count = (listeners.get(uri) ?? 0) + 1;
112431
+ listeners.set(uri, count);
112432
+ if (count === 1) for (const sink of sinks) tell(sink, "add", uri);
112433
+ }
112434
+ },
112435
+ close,
112436
+ closeAll: () => {
112437
+ for (const key of [...streams.keys()]) close(key);
112438
+ },
112439
+ attach: (sink) => {
112440
+ sinks.add(sink);
112441
+ for (const uri of listeners.keys()) tell(sink, "add", uri);
112442
+ return () => {
112443
+ sinks.delete(sink);
112444
+ };
112445
+ }
112446
+ };
112447
+ };
112448
+ const isRecord = (value) => typeof value === "object" && value !== null;
112449
+ /**
112450
+ * The resource URIs a `subscriptions/listen` request asks for, or `undefined`
112451
+ * when `message` is not one. A malformed filter reads as no URI: the SDK
112452
+ * refuses that request anyway.
112453
+ */
112454
+ const listenedUris = (message) => {
112455
+ if (!isRecord(message) || message["method"] !== "subscriptions/listen") return void 0;
112456
+ const params = isRecord(message["params"]) ? message["params"] : {};
112457
+ const uris = (isRecord(params["notifications"]) ? params["notifications"] : {})["resourceSubscriptions"];
112458
+ return Array.isArray(uris) ? uris.filter((uri) => typeof uri === "string") : [];
112459
+ };
112460
+ /**
112461
+ * Feeds `feed` from a JSON-RPC connection's traffic: a listen request opens a
112462
+ * stream under its id; the client cancelling it, or any response carrying that
112463
+ * id (the SDK's refusal or its closing result), ends it.
112464
+ */
112465
+ const createListenWireObserver = (feed) => ({
112466
+ inbound: (message) => {
112467
+ if (!isRecord(message)) return;
112468
+ const uris = listenedUris(message);
112469
+ if (uris && "id" in message) {
112470
+ feed.open(message["id"], uris);
112471
+ return;
112472
+ }
112473
+ if (message["method"] === "notifications/cancelled" && isRecord(message["params"])) feed.close(message["params"]["requestId"]);
112474
+ },
112475
+ outbound: (message) => {
112476
+ if (isRecord(message) && !("method" in message) && "id" in message) feed.close(message["id"]);
112477
+ }
112478
+ });
112479
+ //#endregion
110734
112480
  //#region src/transports/stdio.ts
110735
112481
  /**
112482
+ * `wire`, with its traffic reported to `feed`: `serveStdio` answers
112483
+ * `subscriptions/listen` before any server sees it.
112484
+ */
112485
+ const observeListens = (wire, feed) => {
112486
+ const observer = createListenWireObserver(feed);
112487
+ return {
112488
+ start: () => wire.start(),
112489
+ send: (message, options) => {
112490
+ observer.outbound(message);
112491
+ return wire.send(message, options);
112492
+ },
112493
+ close: () => wire.close(),
112494
+ setProtocolVersion: (version) => wire.setProtocolVersion?.(version),
112495
+ set onmessage(handler) {
112496
+ wire.onmessage = handler && ((message, extra) => {
112497
+ observer.inbound(message);
112498
+ handler(message, extra);
112499
+ });
112500
+ },
112501
+ set onclose(handler) {
112502
+ wire.onclose = () => {
112503
+ feed.closeAll();
112504
+ handler?.();
112505
+ };
112506
+ },
112507
+ set onerror(handler) {
112508
+ wire.onerror = handler;
112509
+ }
112510
+ };
112511
+ };
112512
+ /**
110736
112513
  * Serve MCP over stdio, both protocol eras: the SDK entry picks the era from the
110737
112514
  * client's opening exchange and pins one server from `build` per connection (a
110738
112515
  * `server/discover` probe that falls back to `initialize` builds one more, then
@@ -110740,17 +112517,19 @@ function toError(value) {
110740
112517
  */
110741
112518
  const startStdioTransport = (build, logger = silentLogger, options = {}) => {
110742
112519
  const requestStateCodec = createRequestStateCodec({});
112520
+ const listens = createListenFeed();
110743
112521
  const handle = serveStdio(() => {
110744
112522
  let server;
110745
112523
  server = build({
110746
112524
  requestStateCodec,
112525
+ listens,
110747
112526
  notifyResourceUpdated: (uri) => {
110748
112527
  server?.server.sendResourceUpdated({ uri }).catch(() => void 0);
110749
112528
  }
110750
112529
  });
110751
112530
  return server;
110752
112531
  }, {
110753
- transport: options.transport ?? new StdioServerTransport(void 0, void 0, { maxBufferSize: 10485760 }),
112532
+ transport: observeListens(options.transport ?? new StdioServerTransport(void 0, void 0, { maxBufferSize: 10485760 }), listens),
110754
112533
  onerror: (error) => logger.warn("stdio_transport_error", { error: error.message })
110755
112534
  });
110756
112535
  logger.info("stdio_transport_ready");
@@ -110765,6 +112544,10 @@ const TOOL_MODULES = [
110765
112544
  file: "tickets.ts",
110766
112545
  factory: "createTicketTools"
110767
112546
  },
112547
+ {
112548
+ file: "sla-alerts.ts",
112549
+ factory: "createSlaAlertTools"
112550
+ },
110768
112551
  {
110769
112552
  file: "requests.ts",
110770
112553
  factory: "createRequestTools"
@@ -110780,6 +112563,10 @@ const TOOL_MODULES = [
110780
112563
  {
110781
112564
  file: "users.ts",
110782
112565
  factory: "createUserTools"
112566
+ },
112567
+ {
112568
+ file: "preferences.ts",
112569
+ factory: "createPreferenceTools"
110783
112570
  }
110784
112571
  ];
110785
112572
  /**
@@ -110806,22 +112593,29 @@ const loadFreshTools = async (ctx) => {
110806
112593
  * only matters after an edit); `reload()` disposes the current generation and
110807
112594
  * registers a fresh one.
110808
112595
  */
110809
- const createReloadableServer = (config, getToken, logger = silentLogger, onUnauthorized, loadTools = loadFreshTools, requestStateCodec) => {
112596
+ const createReloadableServer = (config, getToken, logger = silentLogger, onUnauthorized, loadTools = loadFreshTools, requestStateCodec, slaRuntime = createSlaRuntime(config), listens, getBackgroundToken) => {
110810
112597
  const server = createServerShell(config, requestStateCodec);
110811
112598
  const caches = createServerCaches(config, getToken, onUnauthorized);
110812
- const ctx = {
110813
- subdomain: config.subdomain,
110814
- brandIds: config.brandIds,
110815
- resolveBrandSubdomain: caches.resolveBrandSubdomain,
110816
- getToken
110817
- };
112599
+ const sla = createSlaService(slaSettings(config), slaRuntime, caches.resolveUserId);
112600
+ const ctx = toolContext(config, getToken, caches.resolveBrandSubdomain, sla);
110818
112601
  const params = {
110819
112602
  config,
110820
112603
  getToken,
110821
112604
  caches,
110822
112605
  onUnauthorized,
110823
- logger
112606
+ logger,
112607
+ sla
110824
112608
  };
112609
+ if (config.slaPush && slaAlertsEnabled(config)) {
112610
+ const reader = createAlertPollingReader({
112611
+ config,
112612
+ ...pollingIdentity(config, getToken, caches, getBackgroundToken),
112613
+ caches,
112614
+ slaRuntime,
112615
+ onUnauthorized
112616
+ });
112617
+ installAlertSubscriptions(server, config, reader, logger, listens);
112618
+ }
110825
112619
  let currentTools = createAllTools(ctx);
110826
112620
  let current = registerToolset(server, params, currentTools);
110827
112621
  const reload = async () => {
@@ -110896,9 +112690,9 @@ const registerReloadTool = (server, reload, logger = silentLogger) => {
110896
112690
  */
110897
112691
  /* v8 ignore start -- runtime bootstrap: binds the reload tool to a real stdio
110898
112692
  transport; the reload machinery it wires up is covered by dev-reload.test.ts */
110899
- const startDevServer = (config, getToken, logger = silentLogger, onUnauthorized) => {
110900
- const handle = startStdioTransport(({ requestStateCodec }) => {
110901
- const { server, reload } = createReloadableServer(config, getToken, logger, onUnauthorized, loadFreshTools, requestStateCodec);
112693
+ const startDevServer = (config, getToken, logger = silentLogger, onUnauthorized, slaRuntime, getBackgroundToken) => {
112694
+ const handle = startStdioTransport(({ requestStateCodec, listens }) => {
112695
+ const { server, reload } = createReloadableServer(config, getToken, logger, onUnauthorized, loadFreshTools, requestStateCodec, slaRuntime, listens, getBackgroundToken);
110902
112696
  registerReloadTool(server, reload, logger);
110903
112697
  return server;
110904
112698
  }, logger);
@@ -111055,7 +112849,7 @@ const installShutdown = (options) => {
111055
112849
  //#endregion
111056
112850
  //#region src/index.ts
111057
112851
  const buildStdioTokenStore = async (config, logger) => {
111058
- const { createTokenStore } = await import("./token-store-DyO12sbo.js");
112852
+ const { createTokenStore } = await import("./token-store-CQx_KFe2.js");
111059
112853
  return createTokenStore({
111060
112854
  subdomain: config.subdomain,
111061
112855
  oauthClientId: config.oauthClientId,
@@ -111063,13 +112857,15 @@ const buildStdioTokenStore = async (config, logger) => {
111063
112857
  readOnly: config.readOnly
111064
112858
  }, logger);
111065
112859
  };
111066
- const connectStdio = (config, tokenStore, logger) => {
111067
- if (config.dev) return startDevServer(config, tokenStore.getToken, logger, tokenStore.invalidate);
112860
+ const connectStdio = (config, tokenStore, logger, slaRuntime) => {
112861
+ if (config.dev) return startDevServer(config, tokenStore.getToken, logger, tokenStore.invalidate, slaRuntime, tokenStore.getTokenSilently);
111068
112862
  return startStdioTransport((extras) => defaultServerFactory({
111069
112863
  config,
111070
112864
  getToken: tokenStore.getToken,
111071
112865
  logger,
111072
112866
  onUnauthorized: tokenStore.invalidate,
112867
+ getBackgroundToken: tokenStore.getTokenSilently,
112868
+ slaRuntime,
111073
112869
  ...extras
111074
112870
  }), logger);
111075
112871
  };
@@ -111088,11 +112884,13 @@ const main = async () => {
111088
112884
  const logger = createLogger(config.logLevel);
111089
112885
  if (config.transport === "stdio") {
111090
112886
  const tokenStore = await buildStdioTokenStore(config, logger);
111091
- const stdio = connectStdio(config, tokenStore, logger);
112887
+ const sla = startSlaRuntime(config);
112888
+ const stdio = connectStdio(config, tokenStore, logger, sla.runtime);
111092
112889
  installShutdown({
111093
112890
  watchStdin: true,
111094
112891
  logger,
111095
112892
  cleanup: async () => {
112893
+ sla.stop();
111096
112894
  await stdio.close();
111097
112895
  tokenStore.dispose();
111098
112896
  }
@@ -111100,7 +112898,7 @@ const main = async () => {
111100
112898
  return;
111101
112899
  }
111102
112900
  if (config.dev) logger.warn("dev_mode_ignored_http");
111103
- const { startHttpTransport } = await import("./http-DU1dSrPe.js");
112901
+ const { startHttpTransport } = await import("./http-DDdso3Av.js");
111104
112902
  const http = await startHttpTransport(config, logger);
111105
112903
  installShutdown({
111106
112904
  watchStdin: false,
@@ -111114,4 +112912,4 @@ main().catch((error) => {
111114
112912
  process.exit(1);
111115
112913
  });
111116
112914
  //#endregion
111117
- export { __exportAll$1 as C, __commonJSMin$1 as S, __toESM$1 as T, OAuthErrorCode as _, silentLogger as a, errnoCode as b, deadlineSignal as c, DEFAULT_MAX_REQUEST_BODY_SIZE as d, WebStandardStreamableHTTPServerTransport as f, OAuthError as g, verifyBearerToken as h, readPackageInfo as i, DEFAULT_CALLBACK_PORT as l, isLegacyRequest as m, defaultServerFactory as n, zendeskGet as o, createMcpHandler as p, createRequestStateCodec as r, REQUEST_TIMEOUT_MS as s, createServerCaches as t, getOAuthUrls as u, STARTUP_DOCS as v, __require as w, readEnv as x, createStartupError as y };
112915
+ export { __commonJSMin$1 as A, isLegacyRequest as C, STARTUP_DOCS as D, OAuthErrorCode as E, __require as M, __toESM$1 as N, createStartupError as O, createMcpHandler as S, OAuthError as T, deadlineSignal as _, watchGrantAlerts as a, DEFAULT_MAX_REQUEST_BODY_SIZE as b, configDir as c, saveToken as d, signInRequiredError as f, REQUEST_TIMEOUT_MS as g, zendeskGet as h, defaultServerFactory as i, __exportAll$1 as j, errnoCode as k, loadToken as l, silentLogger as m, listenedUris as n, startSlaRuntime as o, createRequestStateCodec as p, createServerCaches as r, clearToken as s, createListenFeed as t, resolveTokenPath as u, DEFAULT_CALLBACK_PORT as v, verifyBearerToken as w, WebStandardStreamableHTTPServerTransport as x, getOAuthUrls as y };