infinitecampus-mcp 3.1.9 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bundle.js CHANGED
@@ -91,6 +91,7 @@ var require_util = __commonJS({
91
91
  exports.explicitlyAborted = explicitlyAborted2;
92
92
  exports.prefixIssues = prefixIssues2;
93
93
  exports.unwrapMessage = unwrapMessage2;
94
+ exports.sizeCaps = sizeCaps2;
94
95
  exports.attachSchema = attachSchema2;
95
96
  exports.finalizeIssue = finalizeIssue2;
96
97
  exports.getSizableOrigin = getSizableOrigin2;
@@ -625,6 +626,18 @@ var require_util = __commonJS({
625
626
  function unwrapMessage2(message) {
626
627
  return typeof message === "string" ? message : message?.message;
627
628
  }
629
+ function sizeCaps2(defChecks) {
630
+ const caps = [];
631
+ for (const ch of defChecks ?? []) {
632
+ const d = ch._zod.def;
633
+ if (!/_(length|size)$|_equals$/.test(d.check))
634
+ break;
635
+ const max = d.maximum ?? d.length ?? d.size;
636
+ if (max >= 0)
637
+ caps.push([max, ch]);
638
+ }
639
+ return caps;
640
+ }
628
641
  function attachSchema2(issues, start, inst) {
629
642
  var _a3;
630
643
  for (let i = start; i < issues.length; i++) {
@@ -2289,7 +2302,7 @@ var require_versions = __commonJS({
2289
2302
  exports.version = {
2290
2303
  major: 4,
2291
2304
  minor: 6,
2292
- patch: 5
2305
+ patch: 7
2293
2306
  };
2294
2307
  (function() {
2295
2308
  var keys = Object.getOwnPropertyNames(exports);
@@ -3123,6 +3136,31 @@ var require_schemas = __commonJS({
3123
3136
  return payload;
3124
3137
  };
3125
3138
  });
3139
+ function capSize2(inst, size) {
3140
+ const caps = util.sizeCaps(inst._zod.def.checks);
3141
+ if (!caps.length)
3142
+ return;
3143
+ const cap = Math.min(...caps.map((c) => c[0]));
3144
+ const parse3 = inst._zod.parse;
3145
+ inst._zod.parse = (payload, ctx) => {
3146
+ const n = size(payload.value);
3147
+ if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
3148
+ for (const [max, ch] of util.sizeCaps(inst._zod.def.checks)) {
3149
+ if (n > max && ch._zod.def.when(payload)) {
3150
+ const start = payload.issues.length;
3151
+ ch._zod.check(payload);
3152
+ payload.issues[start].continue = false;
3153
+ util.attachSchema(payload.issues, start, inst);
3154
+ return payload;
3155
+ }
3156
+ }
3157
+ }
3158
+ return parse3(payload, ctx);
3159
+ };
3160
+ }
3161
+ var arraySize2 = (input2) => Array.isArray(input2) ? input2.length : -1;
3162
+ var mapSize2 = (input2) => input2 instanceof Map ? input2.size : -1;
3163
+ var setSize2 = (input2) => input2 instanceof Set ? input2.size : -1;
3126
3164
  function handleArrayResult2(result, final, index) {
3127
3165
  if (result.issues.length) {
3128
3166
  final.issues.push(...util.prefixIssues(index, result.issues));
@@ -3166,6 +3204,7 @@ var require_schemas = __commonJS({
3166
3204
  }
3167
3205
  return payload;
3168
3206
  };
3207
+ capSize2(inst, arraySize2);
3169
3208
  });
3170
3209
  function handlePropertyResult2(result, final, key, input2, optin, optout) {
3171
3210
  const isPresent = key in input2;
@@ -4122,6 +4161,7 @@ var require_schemas = __commonJS({
4122
4161
  return Promise.all(proms).then(() => payload);
4123
4162
  return payload;
4124
4163
  };
4164
+ capSize2(inst, mapSize2);
4125
4165
  });
4126
4166
  function handleMapResult2(keyResult, valueResult, final, key, input2, inst, ctx) {
4127
4167
  if (keyResult.issues.length) {
@@ -4188,6 +4228,7 @@ var require_schemas = __commonJS({
4188
4228
  return Promise.all(proms).then(() => payload);
4189
4229
  return payload;
4190
4230
  };
4231
+ capSize2(inst, setSize2);
4191
4232
  });
4192
4233
  function handleSetResult2(result, final) {
4193
4234
  if (result.issues.length) {
@@ -16292,9 +16333,15 @@ ${code}
16292
16333
  doc.write(`}`);
16293
16334
  return outputVar;
16294
16335
  }
16336
+ function generateSizeGate2(doc, schema, size) {
16337
+ for (const [max, check2] of util.sizeCaps(schema._zod.def.checks)) {
16338
+ doc.write(`if (${size} > ${numericOperand2(max, check2._zod.def.check)}) return INVALID;`);
16339
+ }
16340
+ }
16295
16341
  function generateArrayCheck2(doc, ctx, schema, accessor, buildsValue = true) {
16296
16342
  const def = schema._zod.def;
16297
16343
  doc.write(`if (!Array.isArray(${accessor})) return INVALID;`);
16344
+ generateSizeGate2(doc, schema, `${accessor}.length`);
16298
16345
  const outputVar = buildsValue ? newVar2(ctx) : null;
16299
16346
  const iVar = newVar2(ctx);
16300
16347
  const elemVar = newVar2(ctx);
@@ -16694,6 +16741,7 @@ ${code}
16694
16741
  function generateMapCheck2(doc, ctx, schema, accessor) {
16695
16742
  const def = schema._zod.def;
16696
16743
  doc.write(`if (!(${accessor} instanceof Map)) return INVALID;`);
16744
+ generateSizeGate2(doc, schema, `${accessor}.size`);
16697
16745
  const outputVar = newVar2(ctx);
16698
16746
  const kVar = newVar2(ctx);
16699
16747
  const valVar = newVar2(ctx);
@@ -16710,6 +16758,7 @@ ${code}
16710
16758
  function generateSetCheck2(doc, ctx, schema, accessor) {
16711
16759
  const def = schema._zod.def;
16712
16760
  doc.write(`if (!(${accessor} instanceof Set)) return INVALID;`);
16761
+ generateSizeGate2(doc, schema, `${accessor}.size`);
16713
16762
  const outputVar = newVar2(ctx);
16714
16763
  const valVar = newVar2(ctx);
16715
16764
  doc.write(`const ${outputVar} = new Set();`);
@@ -27078,7 +27127,7 @@ function declaredDialect(schema, remedy) {
27078
27127
  throw new Error(`JSON Schema declares an unsupported dialect ("$schema": "${schema.$schema.slice(0, 200)}"). The default validator supports JSON Schema 2020-12, 2019-09, draft-07, and draft-06; ${remedy}`);
27079
27128
  }
27080
27129
 
27081
- // node_modules/@modelcontextprotocol/core/dist/auth-BNDyLTqp.mjs
27130
+ // node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/dist/auth-BNDyLTqp.mjs
27082
27131
  var z = __toESM(require_v4(), 1);
27083
27132
  var LATEST_PROTOCOL_VERSION = "2025-11-25";
27084
27133
  var SUPPORTED_PROTOCOL_VERSIONS = [
@@ -41871,7 +41920,7 @@ var API_KEY_RE = new RegExp([
41871
41920
  // webhook signing secret (Stripe-style)
41872
41921
  ].map((p) => `\\b${p}`).join("|"), "g");
41873
41922
  var GOOGLE_OAUTH_TOKEN_RE = /(?<![A-Za-z0-9+/])(?:ya29\.|1\/\/)[A-Za-z0-9._-]{8,}/g;
41874
- var QUERY_SECRET_RE = new RegExp(`([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig|(?:code[_-]?)?verifier|(?:php|j)?sess(?:ion)?[_-]?id|sid|x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN})=)[^&#\\s"'<>\`]+`, "gi");
41923
+ var QUERY_SECRET_RE = new RegExp(`([?&#;](?:(?:access|refresh|id|auth|session|csrf|xsrf|api)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|secret|token|auth|key|sig|(?:code[_-]?)?verifier|(?:php|j)?sess(?:ion)?[_-]?id|sid|x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN})=)[^&#\\s"'<>\`]+`, "gi");
41875
41924
  var OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
41876
41925
  var FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
41877
41926
  var HEADER_SECRET_RE = new RegExp(`(\\b(?:x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN}|api[-_]?key)\\s*:\\s*)[^\\s,;"'<>\`]+`, "gi");
@@ -41882,6 +41931,8 @@ var JSON_SECRET_KEYS = [
41882
41931
  "api[_-]?secret",
41883
41932
  "(?:x[_-])?api[_-]?key",
41884
41933
  "private[_-]?key",
41934
+ "aws[_-]?secret[_-]?access[_-]?key",
41935
+ // fleet-audit#1297
41885
41936
  "code[_-]?verifier",
41886
41937
  // PKCE; a bare `verifier` key is too generic for JSON
41887
41938
  `x[_-]${HEADER_NAME_RUN}?(?:api[_-]?key|token|secret|auth)${HEADER_NAME_RUN}`,
@@ -42579,6 +42630,7 @@ __export(util_exports, {
42579
42630
  required: () => required,
42580
42631
  safeExtend: () => safeExtend,
42581
42632
  shallowClone: () => shallowClone,
42633
+ sizeCaps: () => sizeCaps,
42582
42634
  slugify: () => slugify,
42583
42635
  stringifyPrimitive: () => stringifyPrimitive,
42584
42636
  toZod: () => toZod,
@@ -43098,6 +43150,18 @@ function prefixIssues(path, issues) {
43098
43150
  function unwrapMessage(message) {
43099
43151
  return typeof message === "string" ? message : message?.message;
43100
43152
  }
43153
+ function sizeCaps(defChecks) {
43154
+ const caps = [];
43155
+ for (const ch of defChecks ?? []) {
43156
+ const d = ch._zod.def;
43157
+ if (!/_(length|size)$|_equals$/.test(d.check))
43158
+ break;
43159
+ const max = d.maximum ?? d.length ?? d.size;
43160
+ if (max >= 0)
43161
+ caps.push([max, ch]);
43162
+ }
43163
+ return caps;
43164
+ }
43101
43165
  function attachSchema(issues, start, inst) {
43102
43166
  var _a3;
43103
43167
  for (let i = start; i < issues.length; i++) {
@@ -44518,7 +44582,7 @@ ${content.join("\n")}
44518
44582
  var version = {
44519
44583
  major: 4,
44520
44584
  minor: 6,
44521
- patch: 5
44585
+ patch: 7
44522
44586
  };
44523
44587
 
44524
44588
  // node_modules/zod/v4/core/schemas.js
@@ -45274,6 +45338,31 @@ var $ZodDate = /* @__PURE__ */ $constructor("$ZodDate", (inst, def) => {
45274
45338
  return payload;
45275
45339
  };
45276
45340
  });
45341
+ function capSize(inst, size) {
45342
+ const caps = sizeCaps(inst._zod.def.checks);
45343
+ if (!caps.length)
45344
+ return;
45345
+ const cap = Math.min(...caps.map((c) => c[0]));
45346
+ const parse3 = inst._zod.parse;
45347
+ inst._zod.parse = (payload, ctx) => {
45348
+ const n = size(payload.value);
45349
+ if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
45350
+ for (const [max, ch] of sizeCaps(inst._zod.def.checks)) {
45351
+ if (n > max && ch._zod.def.when(payload)) {
45352
+ const start = payload.issues.length;
45353
+ ch._zod.check(payload);
45354
+ payload.issues[start].continue = false;
45355
+ attachSchema(payload.issues, start, inst);
45356
+ return payload;
45357
+ }
45358
+ }
45359
+ }
45360
+ return parse3(payload, ctx);
45361
+ };
45362
+ }
45363
+ var arraySize = (input2) => Array.isArray(input2) ? input2.length : -1;
45364
+ var mapSize = (input2) => input2 instanceof Map ? input2.size : -1;
45365
+ var setSize = (input2) => input2 instanceof Set ? input2.size : -1;
45277
45366
  function handleArrayResult(result, final, index) {
45278
45367
  if (result.issues.length) {
45279
45368
  final.issues.push(...prefixIssues(index, result.issues));
@@ -45317,6 +45406,7 @@ var $ZodArray = /* @__PURE__ */ $constructor("$ZodArray", (inst, def) => {
45317
45406
  }
45318
45407
  return payload;
45319
45408
  };
45409
+ capSize(inst, arraySize);
45320
45410
  });
45321
45411
  function handlePropertyResult(result, final, key, input2, optin, optout) {
45322
45412
  const isPresent = key in input2;
@@ -46273,6 +46363,7 @@ var $ZodMap = /* @__PURE__ */ $constructor("$ZodMap", (inst, def) => {
46273
46363
  return Promise.all(proms).then(() => payload);
46274
46364
  return payload;
46275
46365
  };
46366
+ capSize(inst, mapSize);
46276
46367
  });
46277
46368
  function handleMapResult(keyResult, valueResult, final, key, input2, inst, ctx) {
46278
46369
  if (keyResult.issues.length) {
@@ -46339,6 +46430,7 @@ var $ZodSet = /* @__PURE__ */ $constructor("$ZodSet", (inst, def) => {
46339
46430
  return Promise.all(proms).then(() => payload);
46340
46431
  return payload;
46341
46432
  };
46433
+ capSize(inst, setSize);
46342
46434
  });
46343
46435
  function handleSetResult(result, final) {
46344
46436
  if (result.issues.length) {
@@ -55869,9 +55961,15 @@ function generateNullableCheck(doc, ctx, schema, accessor, buildsValue = true) {
55869
55961
  doc.write(`}`);
55870
55962
  return outputVar;
55871
55963
  }
55964
+ function generateSizeGate(doc, schema, size) {
55965
+ for (const [max, check2] of sizeCaps(schema._zod.def.checks)) {
55966
+ doc.write(`if (${size} > ${numericOperand(max, check2._zod.def.check)}) return INVALID;`);
55967
+ }
55968
+ }
55872
55969
  function generateArrayCheck(doc, ctx, schema, accessor, buildsValue = true) {
55873
55970
  const def = schema._zod.def;
55874
55971
  doc.write(`if (!Array.isArray(${accessor})) return INVALID;`);
55972
+ generateSizeGate(doc, schema, `${accessor}.length`);
55875
55973
  const outputVar = buildsValue ? newVar(ctx) : null;
55876
55974
  const iVar = newVar(ctx);
55877
55975
  const elemVar = newVar(ctx);
@@ -56271,6 +56369,7 @@ function literalPropertyKey(ctx, key) {
56271
56369
  function generateMapCheck(doc, ctx, schema, accessor) {
56272
56370
  const def = schema._zod.def;
56273
56371
  doc.write(`if (!(${accessor} instanceof Map)) return INVALID;`);
56372
+ generateSizeGate(doc, schema, `${accessor}.size`);
56274
56373
  const outputVar = newVar(ctx);
56275
56374
  const kVar = newVar(ctx);
56276
56375
  const valVar = newVar(ctx);
@@ -56287,6 +56386,7 @@ function generateMapCheck(doc, ctx, schema, accessor) {
56287
56386
  function generateSetCheck(doc, ctx, schema, accessor) {
56288
56387
  const def = schema._zod.def;
56289
56388
  doc.write(`if (!(${accessor} instanceof Set)) return INVALID;`);
56389
+ generateSizeGate(doc, schema, `${accessor}.size`);
56290
56390
  const outputVar = newVar(ctx);
56291
56391
  const valVar = newVar(ctx);
56292
56392
  doc.write(`const ${outputVar} = new Set();`);
@@ -62094,10 +62194,63 @@ function codePointOr(code, raw) {
62094
62194
  return String.fromCodePoint(code);
62095
62195
  }
62096
62196
 
62197
+ // node_modules/@chrischall/mcp-utils/dist/internal/tags.js
62198
+ function replaceTags(html, replacement) {
62199
+ let out = "";
62200
+ let i = 0;
62201
+ for (; ; ) {
62202
+ const lt = html.indexOf("<", i);
62203
+ if (lt < 0)
62204
+ break;
62205
+ const gt = html.indexOf(">", lt + 1);
62206
+ if (gt < 0)
62207
+ break;
62208
+ if (gt === lt + 1) {
62209
+ out += html.slice(i, lt + 1);
62210
+ i = lt + 1;
62211
+ continue;
62212
+ }
62213
+ out += html.slice(i, lt) + replacement;
62214
+ i = gt + 1;
62215
+ }
62216
+ return out + html.slice(i);
62217
+ }
62218
+
62097
62219
  // node_modules/@chrischall/mcp-utils/dist/scrape/index.js
62220
+ var NAMED_ENTITIES = {
62221
+ nbsp: " ",
62222
+ lt: "<",
62223
+ gt: ">",
62224
+ quot: '"',
62225
+ apos: "'"
62226
+ };
62227
+ function decodeHtmlEntities(text) {
62228
+ return text.replace(/&#(\d+);/g, (whole, d) => codePointOr(Number(d), whole)).replace(/&#x([0-9a-fA-F]+);/g, (whole, h) => codePointOr(parseInt(h, 16), whole)).replace(/&(nbsp|lt|gt|quot|apos);/gi, (whole, name) => {
62229
+ return NAMED_ENTITIES[name.toLowerCase()] ?? whole;
62230
+ }).replace(/&amp;/gi, "&");
62231
+ }
62098
62232
  var NEXT_DATA_MAX_CHARS = 16 * 1024 * 1024;
62099
62233
  function isCloudflareChallenge(html) {
62100
- return /_cf_chl_opt/.test(html) || /<title[^>]*>\s*Just a moment/i.test(html);
62234
+ return html.includes("_cf_chl_opt") || hasJustAMomentTitle(html);
62235
+ }
62236
+ var JUST_A_MOMENT = "just a moment";
62237
+ function hasJustAMomentTitle(html) {
62238
+ const lower = asciiLower(html);
62239
+ let i = 0;
62240
+ for (; ; ) {
62241
+ const open3 = lower.indexOf("<title", i);
62242
+ if (open3 < 0)
62243
+ return false;
62244
+ const gt = lower.indexOf(">", open3 + 6);
62245
+ if (gt < 0)
62246
+ return false;
62247
+ let j = gt + 1;
62248
+ while (j < lower.length && /\s/.test(lower[j]))
62249
+ j++;
62250
+ if (lower.startsWith(JUST_A_MOMENT, j))
62251
+ return true;
62252
+ i = gt + 1;
62253
+ }
62101
62254
  }
62102
62255
 
62103
62256
  // node_modules/@chrischall/mcp-utils/dist/http/index.js
@@ -62231,6 +62384,55 @@ function splitSetCookie(header) {
62231
62384
  return header.split(/,(?=\s*[^;,\s]+\s*=)/).map((s) => s.trim()).filter((s) => s.length > 0);
62232
62385
  }
62233
62386
 
62387
+ // node_modules/@chrischall/mcp-utils/dist/dates/index.js
62388
+ var ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
62389
+ var pad2 = (n) => String(n).padStart(2, "0");
62390
+ var USER_TIME_ZONE_ENV = "MCP_USER_TZ";
62391
+ function isValidTimeZone(timeZone) {
62392
+ if (!timeZone)
62393
+ return false;
62394
+ try {
62395
+ new Intl.DateTimeFormat("en-US", { timeZone });
62396
+ return true;
62397
+ } catch {
62398
+ return false;
62399
+ }
62400
+ }
62401
+ function resolveUserTimeZone(opts = {}) {
62402
+ if (opts.timeZone !== void 0)
62403
+ return opts.timeZone;
62404
+ const fromEnv = readEnvVar(USER_TIME_ZONE_ENV, { env: opts.env });
62405
+ return fromEnv !== void 0 && isValidTimeZone(fromEnv) ? fromEnv : void 0;
62406
+ }
62407
+ function todayIso(nowOrOpts) {
62408
+ const opts = nowOrOpts instanceof Date ? { now: nowOrOpts } : nowOrOpts ?? {};
62409
+ const now = opts.now ?? /* @__PURE__ */ new Date();
62410
+ const timeZone = resolveUserTimeZone(opts);
62411
+ if (timeZone === void 0) {
62412
+ return `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-${pad2(now.getDate())}`;
62413
+ }
62414
+ if (!isValidTimeZone(timeZone))
62415
+ throw new RangeError(`Invalid IANA time zone: ${timeZone}`);
62416
+ const parts = new Intl.DateTimeFormat("en-CA", {
62417
+ timeZone,
62418
+ year: "numeric",
62419
+ month: "2-digit",
62420
+ day: "2-digit"
62421
+ }).formatToParts(now);
62422
+ const part = (type) => parts.find((p) => p.type === type)?.value ?? "";
62423
+ return `${part("year")}-${part("month")}-${part("day")}`;
62424
+ }
62425
+ function toIsoDateUtc(d) {
62426
+ return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
62427
+ }
62428
+ function shiftIsoDate(date5, days) {
62429
+ const m = ISO_DATE.exec(date5.trim());
62430
+ if (!m)
62431
+ return date5.trim();
62432
+ const shifted = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]) + days));
62433
+ return toIsoDateUtc(shifted);
62434
+ }
62435
+
62234
62436
  // node_modules/@chrischall/mcp-utils/dist/zod/index.js
62235
62437
  var PositiveInt = external_exports.number().int().positive();
62236
62438
  var NonNegInt = external_exports.number().int().nonnegative();
@@ -68700,7 +68902,7 @@ function resolveDownloadDir(env = process.env) {
68700
68902
  // package.json
68701
68903
  var package_default = {
68702
68904
  name: "infinitecampus-mcp",
68703
- version: "3.1.9",
68905
+ version: "3.2.1",
68704
68906
  mcpName: "io.github.chrischall/infinitecampus-mcp",
68705
68907
  description: "Infinite Campus (Campus Parent) MCP server \u2014 multi-district, read-only (plus local document download)",
68706
68908
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -68745,7 +68947,7 @@ var package_default = {
68745
68947
  typecheck: "tsc -p tsconfig.json --noEmit"
68746
68948
  },
68747
68949
  dependencies: {
68748
- "@chrischall/mcp-utils": "^3.0.0",
68950
+ "@chrischall/mcp-utils": "^3.0.3",
68749
68951
  "@fetchproxy/bootstrap": "^3.6.0",
68750
68952
  "@fetchproxy/server": "^3.6.0",
68751
68953
  "@modelcontextprotocol/server": "^2.3.0",
@@ -69427,6 +69629,10 @@ var ICClient = class {
69427
69629
  // linkedDistrictName → primaryDistrictName
69428
69630
  primaryName;
69429
69631
  featuresCache = /* @__PURE__ */ new Map();
69632
+ /** Campus version per district, read once. A failed read is dropped, to be retried after VERSION_RETRY_MS. */
69633
+ versions = /* @__PURE__ */ new Map();
69634
+ /** When each district's last version read failed — see VERSION_RETRY_MS. */
69635
+ versionFailedAt = /* @__PURE__ */ new Map();
69430
69636
  /**
69431
69637
  * True when the primary account has empty creds (fetchproxy mode). The
69432
69638
  * client uses pre-loaded cookies in place of the `verify.jsp` POST and
@@ -69597,7 +69803,37 @@ var ICClient = class {
69597
69803
  async request(district, path, opts = {}) {
69598
69804
  const account2 = await this.resolveAccount(district);
69599
69805
  await this.managers.get(account2.name).ensure();
69600
- return this.doRequest(account2, path, opts);
69806
+ try {
69807
+ return await this.doRequest(account2, path, opts);
69808
+ } catch (e) {
69809
+ if (e instanceof PortalUnreachableError) e.setCampusVersion(await this.campusVersion(account2.name));
69810
+ throw e;
69811
+ }
69812
+ }
69813
+ /**
69814
+ * The district's Campus version (e.g. "2635.16"), or null when it cannot be
69815
+ * read. DIAGNOSTIC ONLY — nothing chooses behaviour by it. The only place IC
69816
+ * states it is the parent login page footer ("Version:Campus-2635.16"),
69817
+ * which needs no session, so this works in fetchproxy mode too and never
69818
+ * touches a district's cookies.
69819
+ */
69820
+ campusVersion(district) {
69821
+ const account2 = this.accounts.get(district);
69822
+ if (!account2) return Promise.resolve(null);
69823
+ const failedAt = this.versionFailedAt.get(account2.name);
69824
+ if (failedAt !== void 0 && Date.now() - failedAt < VERSION_RETRY_MS) return Promise.resolve(null);
69825
+ let pending = this.versions.get(account2.name);
69826
+ if (!pending) {
69827
+ pending = fetchCampusVersion(account2).then((v) => {
69828
+ if (v === null) {
69829
+ this.versions.delete(account2.name);
69830
+ this.versionFailedAt.set(account2.name, Date.now());
69831
+ }
69832
+ return v;
69833
+ });
69834
+ this.versions.set(account2.name, pending);
69835
+ }
69836
+ return pending;
69601
69837
  }
69602
69838
  /**
69603
69839
  * The account for `district`, running CUPS discovery once on a miss.
@@ -69973,7 +70209,30 @@ var PortalUnreachableError = class extends Error {
69973
70209
  }
69974
70210
  district;
69975
70211
  status;
70212
+ /** The district's Campus version, when it could be read — for bug reports. */
70213
+ campusVersion;
70214
+ setCampusVersion(version2) {
70215
+ if (version2 === null) return;
70216
+ this.campusVersion = version2;
70217
+ this.message += ` [Campus ${version2}]`;
70218
+ }
69976
70219
  };
70220
+ function parseCampusVersion(html) {
70221
+ return /Version:\s*Campus-([0-9][\w.-]*)/.exec(html)?.[1] ?? null;
70222
+ }
70223
+ async function fetchCampusVersion(account2) {
70224
+ try {
70225
+ const res = await icFetch(
70226
+ `${account2.baseUrl}/campus/portal/parents/${encodeURIComponent(account2.district)}.jsp`,
70227
+ {},
70228
+ VERSION_TIMEOUT_MS
70229
+ );
70230
+ if (!res.ok) return null;
70231
+ return parseCampusVersion(await res.text());
70232
+ } catch {
70233
+ return null;
70234
+ }
70235
+ }
69977
70236
  var SessionExpiredError = class extends Error {
69978
70237
  constructor(district) {
69979
70238
  super(`Session expired for district '${district}' after re-login retry`);
@@ -69995,6 +70254,8 @@ var DocumentOriginNotAllowedError = class extends Error {
69995
70254
  allowedOrigin;
69996
70255
  };
69997
70256
  var REQUEST_TIMEOUT_MS = 3e4;
70257
+ var VERSION_TIMEOUT_MS = 5e3;
70258
+ var VERSION_RETRY_MS = 5 * 60 * 1e3;
69998
70259
  var DOWNLOAD_TIMEOUT_MS = 12e4;
69999
70260
  var MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024;
70000
70261
  function icFetch(url4, init = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
@@ -70119,12 +70380,19 @@ function registerHealthcheckTools(server, state) {
70119
70380
  const detail = {
70120
70381
  base_url: state.account.baseUrl,
70121
70382
  configured_district: state.account.district,
70122
- auth_source: state.source ?? "unknown"
70383
+ auth_source: state.source ?? "unknown",
70384
+ // Read off the login page, so it needs no session and is reported
70385
+ // even when discovery cannot run. Diagnostic only (#253).
70386
+ campus_version: await state.client.campusVersion(state.account.name)
70123
70387
  };
70124
70388
  try {
70125
70389
  await state.client.ensureDiscovery();
70126
70390
  const districts = state.client.listDistricts();
70127
- detail.districts = districts.map((d) => ({ name: d.name, linked: d.linked }));
70391
+ detail.districts = await Promise.all(districts.map(async (d) => ({
70392
+ name: d.name,
70393
+ linked: d.linked,
70394
+ campus_version: await state.client.campusVersion(d.name)
70395
+ })));
70128
70396
  const linked = districts.filter((d) => d.linked).map((d) => d.name);
70129
70397
  detail.configured_district_is_linked = districts.some(
70130
70398
  (d) => d.name === state.account.district && d.linked
@@ -70189,8 +70457,8 @@ var argsSchema3 = external_exports.object({
70189
70457
  district: external_exports.string(),
70190
70458
  studentId: external_exports.string(),
70191
70459
  courseId: external_exports.string().describe("sectionID (optional, from ic_get_schedule). The endpoint supports server-side filtering by sectionID only.").optional(),
70192
- since: external_exports.string().describe("YYYY-MM-DD; filters dueDate >= since (client-side)").optional(),
70193
- until: external_exports.string().describe("YYYY-MM-DD; filters dueDate <= until (client-side)").optional(),
70460
+ since: IsoDate.describe("YYYY-MM-DD; filters dueDate >= since (client-side)").optional(),
70461
+ until: IsoDate.describe("YYYY-MM-DD; filters dueDate <= until (client-side)").optional(),
70194
70462
  missingOnly: external_exports.boolean().describe("Only return assignments flagged missing by the teacher").optional()
70195
70463
  });
70196
70464
  function registerAssignmentTools(server, client) {
@@ -70277,8 +70545,8 @@ function toArray(value) {
70277
70545
  var argsSchema5 = external_exports.object({
70278
70546
  district: external_exports.string(),
70279
70547
  studentId: external_exports.string(),
70280
- since: external_exports.string().describe("YYYY-MM-DD").optional(),
70281
- until: external_exports.string().describe("YYYY-MM-DD").optional()
70548
+ since: IsoDate.describe("YYYY-MM-DD").optional(),
70549
+ until: IsoDate.describe("YYYY-MM-DD").optional()
70282
70550
  });
70283
70551
  function inRange(date5, since, until) {
70284
70552
  if (typeof date5 !== "string") return true;
@@ -70349,8 +70617,8 @@ function registerAttendanceTools(server, client) {
70349
70617
  var argsSchema6 = external_exports.object({
70350
70618
  district: external_exports.string(),
70351
70619
  studentId: external_exports.string(),
70352
- since: external_exports.string().optional(),
70353
- until: external_exports.string().optional()
70620
+ since: IsoDate.describe("YYYY-MM-DD").optional(),
70621
+ until: IsoDate.describe("YYYY-MM-DD").optional()
70354
70622
  });
70355
70623
  function registerBehaviorTools(server, client) {
70356
70624
  server.registerTool("ic_list_behavior", {
@@ -70380,8 +70648,8 @@ function registerBehaviorTools(server, client) {
70380
70648
  var argsSchema7 = external_exports.object({
70381
70649
  district: external_exports.string(),
70382
70650
  studentId: external_exports.string(),
70383
- since: external_exports.string().optional(),
70384
- until: external_exports.string().optional()
70651
+ since: IsoDate.describe("YYYY-MM-DD").optional(),
70652
+ until: IsoDate.describe("YYYY-MM-DD").optional()
70385
70653
  });
70386
70654
  var EMPTY_FOOD = { balance: null, transactions: [] };
70387
70655
  function registerFoodServiceTools(server, client) {
@@ -70417,25 +70685,11 @@ function registerFoodServiceTools(server, client) {
70417
70685
  }
70418
70686
 
70419
70687
  // node_modules/@chrischall/mcp-utils/dist/html/index.js
70420
- var NAMED_ENTITIES = {
70421
- nbsp: " ",
70422
- amp: "&",
70423
- lt: "<",
70424
- gt: ">",
70425
- quot: '"',
70426
- apos: "'"
70427
- };
70428
70688
  function extractPlainTextFromHtml(html) {
70429
70689
  if (!html)
70430
70690
  return "";
70431
70691
  let text = stripElementContent(stripElementContent(html, "script"), "style");
70432
- text = text.replace(/<[^>]+>/g, " ");
70433
- text = text.replace(/&#(\d+);/g, (whole, d) => codePointOr(Number(d), whole));
70434
- text = text.replace(/&#x([0-9a-fA-F]+);/g, (whole, h) => codePointOr(parseInt(h, 16), whole));
70435
- text = text.replace(/&([a-zA-Z]+);/g, (whole, name) => {
70436
- const decoded = NAMED_ENTITIES[name.toLowerCase()];
70437
- return decoded ?? whole;
70438
- });
70692
+ text = decodeHtmlEntities(replaceTags(text, " "));
70439
70693
  return text.replace(/\s+/g, " ").trim();
70440
70694
  }
70441
70695
  function stripElementContent(html, tag) {
@@ -70637,15 +70891,70 @@ function registerDocumentTools(server, client) {
70637
70891
  }
70638
70892
 
70639
70893
  // src/tools/calendar.ts
70894
+ function isRouteFailure(e) {
70895
+ return e instanceof PortalUnreachableError || is404(e);
70896
+ }
70897
+ var preferDayRoute = /* @__PURE__ */ new WeakMap();
70898
+ async function fetchDayRoute(client, district, calendarID, route) {
70899
+ if (route === "instructionalDay") {
70900
+ return toArray(await client.request(
70901
+ district,
70902
+ `/campus/resources/calendar/instructionalDay?calendarID=${calendarID}`
70903
+ ));
70904
+ }
70905
+ const all = toArray(await client.request(
70906
+ district,
70907
+ `/campus/resources/calendar/day?calendarID=${calendarID}`
70908
+ ));
70909
+ return all.filter((d) => d.isInstructionalDay);
70910
+ }
70911
+ async function fetchInstructionalDays(client, district, calendarID) {
70912
+ let preferred = preferDayRoute.get(client);
70913
+ if (!preferred) preferDayRoute.set(client, preferred = /* @__PURE__ */ new Set());
70914
+ const routes = preferred.has(district) ? ["day", "instructionalDay"] : ["instructionalDay", "day"];
70915
+ for (const route of routes) {
70916
+ try {
70917
+ const days = await fetchDayRoute(client, district, calendarID, route);
70918
+ if (route === "day") preferred.add(district);
70919
+ else preferred.delete(district);
70920
+ return days;
70921
+ } catch (e) {
70922
+ if (!isRouteFailure(e)) throw e;
70923
+ }
70924
+ }
70925
+ return null;
70926
+ }
70927
+ async function fetchDayEvents(client, district, personID) {
70928
+ let groups;
70929
+ try {
70930
+ groups = toArray(await client.request(
70931
+ district,
70932
+ `/campus/resources/portal/dayEvent/byEnrollment?personID=${encodeURIComponent(personID)}`
70933
+ ));
70934
+ } catch (e) {
70935
+ if (!isRouteFailure(e)) throw e;
70936
+ groups = [];
70937
+ }
70938
+ const byCalendar = /* @__PURE__ */ new Map();
70939
+ for (const ev of groups.flatMap((g) => toArray(g.events))) {
70940
+ let byDate = byCalendar.get(ev.calendarID);
70941
+ if (!byDate) byCalendar.set(ev.calendarID, byDate = /* @__PURE__ */ new Map());
70942
+ byDate.set(ev.date, [...byDate.get(ev.date) ?? [], ev]);
70943
+ }
70944
+ return byCalendar;
70945
+ }
70946
+ function joinLabels(labels) {
70947
+ return [...new Set(labels.filter((l) => !!l))].join("; ");
70948
+ }
70640
70949
  var argsSchema8 = external_exports.object({
70641
70950
  district: external_exports.string(),
70642
70951
  studentId: external_exports.string().describe("Student personID from ic_list_students"),
70643
- since: external_exports.string().describe("YYYY-MM-DD; include only days on or after this date").optional(),
70644
- until: external_exports.string().describe("YYYY-MM-DD; include only days on or before this date").optional()
70952
+ since: IsoDate.describe("YYYY-MM-DD; include only days on or after this date").optional(),
70953
+ until: IsoDate.describe("YYYY-MM-DD; include only days on or before this date").optional()
70645
70954
  });
70646
70955
  function registerCalendarTools(server, client) {
70647
70956
  server.registerTool("ic_list_school_days", {
70648
- description: "List a student's school days (instructional calendar) grouped by term. Returns one entry per enrollment, with term boundaries (Q1-Q4 start/end dates) and the school days inside each term \u2014 including comments like 'Teacher Workday' or 'Spring Break'. Use since/until to narrow the range.",
70957
+ description: "List a student's school days (instructional calendar) grouped by term. Returns one entry per enrollment, with term boundaries (Q1-Q4 start/end dates) and the school days inside each term \u2014 a day carries comments when the calendar labels it (e.g. 'Early Release Day'). Each enrollment also lists its labelled daysOff (e.g. 'Holiday', 'Teacher Work Day', 'Spring Break'), including breaks that fall between terms. Use since/until to narrow the range. An enrollment whose day list the district won't serve comes back with warning 'SchoolDaysUnavailable' (plus campusVersion when known), its terms and days off, and no school days.",
70649
70958
  annotations: { readOnlyHint: true, openWorldHint: true },
70650
70959
  inputSchema: external_exports.object({ ...argsSchema8.shape, view: viewArg() })
70651
70960
  }, async (rawArgs) => {
@@ -70653,18 +70962,18 @@ function registerCalendarTools(server, client) {
70653
70962
  const student = await findStudent(client, args.district, args.studentId);
70654
70963
  if (!student) return studentNotFound(args.studentId);
70655
70964
  const result = [];
70656
- for (const enr of student.enrollments ?? []) {
70657
- const [termsRaw, daysRaw] = await Promise.all([
70965
+ const enrollments = student.enrollments ?? [];
70966
+ const events = enrollments.length > 0 ? await fetchDayEvents(client, args.district, args.studentId) : /* @__PURE__ */ new Map();
70967
+ for (const enr of enrollments) {
70968
+ const [termsRaw, days] = await Promise.all([
70658
70969
  client.request(args.district, `/campus/resources/term?structureID=${enr.structureID}`),
70659
- client.request(args.district, `/campus/resources/calendar/instructionalDay?calendarID=${enr.calendarID}`)
70970
+ fetchInstructionalDays(client, args.district, enr.calendarID)
70660
70971
  ]);
70661
70972
  const terms = toArray(termsRaw);
70662
- const days = toArray(daysRaw);
70663
- const filteredDays = days.filter((d) => {
70664
- if (args.since && d.date < args.since) return false;
70665
- if (args.until && d.date > args.until) return false;
70666
- return true;
70667
- });
70973
+ const inRange2 = (date5) => !(args.since && date5 < args.since) && !(args.until && date5 > args.until);
70974
+ const filteredDays = (days ?? []).filter((d) => inRange2(d.date));
70975
+ const calendarEvents = events.get(enr.calendarID) ?? /* @__PURE__ */ new Map();
70976
+ const daysOff = [...calendarEvents.entries()].filter(([date5, evs]) => inRange2(date5) && evs.some((e) => e.instruction === false)).map(([date5, evs]) => ({ date: date5, comments: joinLabels(evs.map((e) => e.name)) })).sort((a, b) => a.date.localeCompare(b.date));
70668
70977
  const enrollmentTerms = terms.filter((t) => t.structureID === enr.structureID).sort((a, b) => a.seq - b.seq);
70669
70978
  const trimmedTerms = enrollmentTerms.map((t) => ({
70670
70979
  termID: t.termID,
@@ -70673,16 +70982,21 @@ function registerCalendarTools(server, client) {
70673
70982
  endDate: t.endDate,
70674
70983
  days: filteredDays.filter((d) => d.date >= t.startDate && d.date <= t.endDate).map((d) => {
70675
70984
  const out = { date: d.date, requiresAttendance: d.requiresAttendance };
70676
- if (d.comments) out.comments = d.comments;
70985
+ const comments = joinLabels([d.comments, ...(calendarEvents.get(d.date) ?? []).map((e) => e.name)]);
70986
+ if (comments) out.comments = comments;
70677
70987
  return out;
70678
70988
  })
70679
- })).filter((t) => t.days.length > 0 || !args.since && !args.until);
70989
+ })).filter((t) => days === null ? !(args.since && t.endDate < args.since) && !(args.until && t.startDate > args.until) : t.days.length > 0 || !args.since && !args.until);
70990
+ const campusVersion = days === null ? await client.campusVersion(args.district) : null;
70680
70991
  result.push({
70681
70992
  enrollmentID: enr.enrollmentID,
70682
70993
  calendarID: enr.calendarID,
70683
70994
  structureID: enr.structureID,
70684
70995
  calendarName: enr.calendarName,
70685
- terms: trimmedTerms
70996
+ ...days === null ? { warning: "SchoolDaysUnavailable" } : {},
70997
+ ...campusVersion ? { campusVersion } : {},
70998
+ terms: trimmedTerms,
70999
+ ...daysOff.length > 0 ? { daysOff } : {}
70686
71000
  });
70687
71001
  }
70688
71002
  return viewResponse(rawArgs.view, result);
@@ -70693,8 +71007,8 @@ function registerCalendarTools(server, client) {
70693
71007
  var argsSchema9 = external_exports.object({
70694
71008
  district: external_exports.string(),
70695
71009
  studentId: external_exports.string().describe("Student personID from ic_list_students"),
70696
- since: external_exports.string().describe("YYYY-MM-DD; include only events on or after this date").optional(),
70697
- until: external_exports.string().describe("YYYY-MM-DD; include only events on or before this date").optional(),
71010
+ since: IsoDate.describe("YYYY-MM-DD; include only events on or after this date").optional(),
71011
+ until: IsoDate.describe("YYYY-MM-DD; include only events on or before this date").optional(),
70698
71012
  excusedOnly: external_exports.boolean().describe("Only include events with excuse=E").optional()
70699
71013
  });
70700
71014
  var EVENT_KEYS = [
@@ -70788,7 +71102,7 @@ function registerAttendanceEventsTools(server, client) {
70788
71102
  var argsSchema10 = external_exports.object({
70789
71103
  district: external_exports.string(),
70790
71104
  studentId: external_exports.string(),
70791
- since: external_exports.string().describe("YYYY-MM-DD; defaults to 14 days ago. Passed to the endpoint as an ISO timestamp (modifiedDate filter).").optional()
71105
+ since: IsoDate.describe("YYYY-MM-DD; defaults to 14 days ago. Passed to the endpoint as an ISO timestamp (modifiedDate filter).").optional()
70792
71106
  });
70793
71107
  var KEYS = [
70794
71108
  "assignmentName",
@@ -70806,10 +71120,8 @@ var KEYS = [
70806
71120
  "feedback",
70807
71121
  "comments"
70808
71122
  ];
70809
- function defaultSinceDate(now) {
70810
- const d = new Date(now);
70811
- d.setUTCDate(d.getUTCDate() - 14);
70812
- return d.toISOString().substring(0, 10);
71123
+ function defaultSinceDate() {
71124
+ return shiftIsoDate(todayIso(), -14);
70813
71125
  }
70814
71126
  function registerRecentGradesTools(server, client) {
70815
71127
  server.registerTool("ic_list_recent_grades", {
@@ -70818,7 +71130,7 @@ function registerRecentGradesTools(server, client) {
70818
71130
  inputSchema: external_exports.object({ ...argsSchema10.shape, view: viewArg() })
70819
71131
  }, async (rawArgs) => {
70820
71132
  const args = argsSchema10.parse(rawArgs);
70821
- const sinceDate = args.since ?? defaultSinceDate(/* @__PURE__ */ new Date());
71133
+ const sinceDate = args.since ?? defaultSinceDate();
70822
71134
  const modifiedDate = `${sinceDate}T00:00:00`;
70823
71135
  const raw = await client.request(
70824
71136
  args.district,
@@ -71034,7 +71346,7 @@ try {
71034
71346
  }
71035
71347
  var COMMON = {
71036
71348
  name: "infinitecampus",
71037
- version: "3.1.9"
71349
+ version: "3.2.1"
71038
71350
  // x-release-please-version
71039
71351
  };
71040
71352
  var healthState = { account, source, configError, client: null };