infinitecampus-mcp 2.3.3 → 2.3.5

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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "MCP server for Infinite Campus (Campus Parent) — grades, attendance, assignments, messages, documents",
10
- "version": "2.3.3"
10
+ "version": "2.3.5"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "Infinite Campus",
16
16
  "source": "./",
17
17
  "description": "Infinite Campus (Campus Parent) MCP server for Claude — grades, attendance, assignments, messages, and documents via natural language",
18
- "version": "2.3.3",
18
+ "version": "2.3.5",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "infinitecampus-mcp",
3
3
  "displayName": "Infinite Campus",
4
- "version": "2.3.3",
4
+ "version": "2.3.5",
5
5
  "description": "Infinite Campus (Campus Parent) MCP server for Claude — grades, attendance, assignments, messages, and documents",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chris Hall
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # infinitecampus-mcp
2
2
 
3
+ [![CI](https://github.com/chrischall/infinitecampus-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/chrischall/infinitecampus-mcp/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/infinitecampus-mcp)](https://www.npmjs.com/package/infinitecampus-mcp)
5
+ [![license](https://img.shields.io/npm/l/infinitecampus-mcp)](LICENSE)
6
+
3
7
  MCP server for Infinite Campus (Campus Parent portal). Single-account config — linked districts are auto-discovered via CUPS SSO after login.
4
8
 
5
9
  ## Tools
package/dist/bundle.js CHANGED
@@ -13,7 +13,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
13
13
  throw Error('Dynamic require of "' + x + '" is not supported');
14
14
  });
15
15
  var __commonJS = (cb, mod) => function __require2() {
16
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
16
+ try {
17
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
18
+ } catch (e) {
19
+ throw mod = 0, e;
20
+ }
17
21
  };
18
22
  var __export = (target, all) => {
19
23
  for (var name in all)
@@ -34685,6 +34689,22 @@ async function runMcp(opts) {
34685
34689
  return server;
34686
34690
  }
34687
34691
 
34692
+ // node_modules/@chrischall/mcp-utils/dist/errors/index.js
34693
+ var API_KEY_RE = new RegExp([
34694
+ "sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
34695
+ // OpenAI / Anthropic (incl. sk-ant-…)
34696
+ "gh[pousr]_[A-Za-z0-9]{36,}\\b",
34697
+ // GitHub ghp_/gho_/ghu_/ghs_/ghr_
34698
+ "xox[baprs]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])",
34699
+ // Slack
34700
+ "AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])",
34701
+ // Google API key (39 chars total)
34702
+ "AKIA[0-9A-Z]{16}\\b",
34703
+ // AWS access key id (20 chars total)
34704
+ "whsec_[A-Za-z0-9]{16,}\\b"
34705
+ // webhook signing secret (Stripe-style)
34706
+ ].map((p) => `\\b${p}`).join("|"), "g");
34707
+
34688
34708
  // node_modules/@chrischall/mcp-utils/dist/response/index.js
34689
34709
  function textResult(data) {
34690
34710
  return {
@@ -34737,23 +34757,38 @@ async function loadDotenvSafely(opts = {}) {
34737
34757
  }
34738
34758
 
34739
34759
  // node_modules/@chrischall/mcp-utils/dist/http/index.js
34740
- var MAX_AGE_ZERO_RE = /(?:^|;)\s*Max-Age\s*=\s*0\s*(?:;|$)/i;
34741
- var EXPIRES_EPOCH_RE = /(?:^|;)\s*Expires\s*=\s*Thu,\s*01\s*Jan\s*1970/i;
34760
+ var MAX_AGE_RE = /(?:^|;)\s*Max-Age\s*=\s*(-?\d+)\s*(?:;|$)/i;
34761
+ var EXPIRES_RE = /(?:^|;)\s*Expires\s*=\s*([^;]+)/i;
34762
+ var EXPIRES_DELETION_YEAR = 2e3;
34763
+ function isDeletionCookie(entry) {
34764
+ const maxAge = MAX_AGE_RE.exec(entry);
34765
+ if (maxAge)
34766
+ return Number(maxAge[1]) <= 0;
34767
+ const expires = EXPIRES_RE.exec(entry);
34768
+ if (expires && expires[1]) {
34769
+ const when = new Date(expires[1].trim());
34770
+ if (!Number.isNaN(when.getTime()))
34771
+ return when.getUTCFullYear() < EXPIRES_DELETION_YEAR;
34772
+ }
34773
+ return false;
34774
+ }
34775
+ function parseNameValue(entry) {
34776
+ const nameValue = (entry.split(";")[0] ?? "").trim();
34777
+ const eqIdx = nameValue.indexOf("=");
34778
+ if (eqIdx < 1)
34779
+ return null;
34780
+ return { name: nameValue.slice(0, eqIdx).trim(), value: nameValue.slice(eqIdx + 1).trim() };
34781
+ }
34742
34782
  function parseCookieJar(setCookieHeaders) {
34743
- const entries = setCookieHeaders == null ? [] : Array.isArray(setCookieHeaders) ? setCookieHeaders : splitSetCookie(setCookieHeaders);
34783
+ const entries = setCookieEntries(setCookieHeaders);
34744
34784
  const jar = /* @__PURE__ */ new Map();
34745
34785
  for (const entry of entries) {
34746
- if (MAX_AGE_ZERO_RE.test(entry) || EXPIRES_EPOCH_RE.test(entry))
34786
+ if (isDeletionCookie(entry))
34747
34787
  continue;
34748
- const nameValue = (entry.split(";")[0] ?? "").trim();
34749
- const eqIdx = nameValue.indexOf("=");
34750
- if (eqIdx < 1)
34788
+ const pair = parseNameValue(entry);
34789
+ if (!pair || !pair.value)
34751
34790
  continue;
34752
- const name = nameValue.slice(0, eqIdx).trim();
34753
- const value = nameValue.slice(eqIdx + 1).trim();
34754
- if (!value)
34755
- continue;
34756
- jar.set(name, value);
34791
+ jar.set(pair.name, pair.value);
34757
34792
  }
34758
34793
  const cookies = {};
34759
34794
  for (const [k, v] of jar)
@@ -34761,6 +34796,18 @@ function parseCookieJar(setCookieHeaders) {
34761
34796
  const cookieHeader = [...jar.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
34762
34797
  return { cookies, cookieHeader };
34763
34798
  }
34799
+ function setCookieEntries(source2) {
34800
+ if (source2 == null)
34801
+ return [];
34802
+ if (Array.isArray(source2))
34803
+ return source2;
34804
+ if (typeof source2 === "string")
34805
+ return splitSetCookie(source2);
34806
+ if (typeof source2.getSetCookie === "function")
34807
+ return source2.getSetCookie();
34808
+ const joined = source2.get("set-cookie");
34809
+ return joined ? splitSetCookie(joined) : [];
34810
+ }
34764
34811
  function splitSetCookie(header) {
34765
34812
  return header.split(/,(?=\s*[^;,\s]+\s*=)/).map((s) => s.trim()).filter((s) => s.length > 0);
34766
34813
  }
@@ -34771,6 +34818,8 @@ var NonNegInt = external_exports.number().int().nonnegative();
34771
34818
  var NonEmptyString = external_exports.string().min(1);
34772
34819
  var IsoDate = external_exports.iso.date();
34773
34820
  var IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
34821
+ var NumericIdString = external_exports.string().regex(/^\d+$/, "must be a numeric id (digits only)").describe("A numeric id string (digits only), safe to interpolate into a URL path.");
34822
+ var SafePathSegment = external_exports.string().min(1).regex(/^[^/?#\s]+$/, 'must not contain "/", "?", "#", or whitespace').refine((v) => !v.includes(".."), 'must not contain ".."').describe('A single URL path segment, safe to interpolate: no "/", "..", "?", "#", or whitespace.');
34774
34823
  var schemaOrigin = external_exports.string().optional().describe("Portal origin (e.g. https://<vendor>.example.co) selecting which active session to use. Optional when only one session is active.");
34775
34824
  var schemaConfirm = external_exports.boolean().optional().describe("Must be true to proceed. Without this, the tool returns a preview.");
34776
34825
  var paginationSchema = {
@@ -35738,10 +35787,20 @@ async function openEncryptedFrame(sessionKey, frame) {
35738
35787
 
35739
35788
  // node_modules/@fetchproxy/server/dist/election.js
35740
35789
  import { createServer, Server as HttpServer } from "node:http";
35790
+ var DEFAULT_BIND_TIMEOUT_MS = 5e3;
35741
35791
  async function electRole(opts) {
35742
35792
  const server = createServer();
35793
+ const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
35743
35794
  return new Promise((resolve, reject) => {
35795
+ let timer;
35796
+ const clearBindTimer = () => {
35797
+ if (timer) {
35798
+ clearTimeout(timer);
35799
+ timer = void 0;
35800
+ }
35801
+ };
35744
35802
  const onError = (e) => {
35803
+ clearBindTimer();
35745
35804
  server.removeListener("listening", onListening);
35746
35805
  if (e.code === "EADDRINUSE") {
35747
35806
  try {
@@ -35754,11 +35813,25 @@ async function electRole(opts) {
35754
35813
  }
35755
35814
  };
35756
35815
  const onListening = () => {
35816
+ clearBindTimer();
35757
35817
  server.removeListener("error", onError);
35758
35818
  resolve({ role: "host", server });
35759
35819
  };
35760
35820
  server.once("error", onError);
35761
35821
  server.once("listening", onListening);
35822
+ if (bindTimeoutMs > 0) {
35823
+ timer = setTimeout(() => {
35824
+ server.removeListener("error", onError);
35825
+ server.removeListener("listening", onListening);
35826
+ try {
35827
+ server.close();
35828
+ } catch {
35829
+ }
35830
+ reject(new Error(`fetchproxy: bind to ${opts.host}:${opts.port} timed out after ${bindTimeoutMs}ms (port may be in a bad state)`));
35831
+ }, bindTimeoutMs);
35832
+ if (typeof timer.unref === "function")
35833
+ timer.unref();
35834
+ }
35762
35835
  server.listen(opts.port, opts.host);
35763
35836
  });
35764
35837
  }
@@ -35851,6 +35924,44 @@ var SessionState = class {
35851
35924
  }
35852
35925
  };
35853
35926
 
35927
+ // node_modules/@fetchproxy/server/dist/session-ready.js
35928
+ var SESSION_READY_TIMEOUT_MS = 3e4;
35929
+ var FetchproxySessionNotReadyError = class extends Error {
35930
+ reason;
35931
+ pairCode;
35932
+ mcpId;
35933
+ hint;
35934
+ constructor(info) {
35935
+ const pairing = info.pairCode !== null && info.pairCode !== "";
35936
+ const hint = pairing ? `Open the Transporter extension popup and approve pair code ${info.pairCode} for "${info.mcpId}", then retry.` : `The extension is connected but hasn't confirmed a session for "${info.mcpId}" \u2014 sign in to the target site in that browser (and approve the requested scope if it changed), then retry.`;
35937
+ super(`fetchproxy: ${pairing ? "pairing not yet approved" : "no confirmed browser session"} for "${info.mcpId}". ${hint}`);
35938
+ this.name = "FetchproxySessionNotReadyError";
35939
+ this.reason = pairing ? "pair-required" : "not-ready";
35940
+ this.pairCode = pairing ? info.pairCode : null;
35941
+ this.mcpId = info.mcpId;
35942
+ this.hint = hint;
35943
+ Object.setPrototypeOf(this, new.target.prototype);
35944
+ }
35945
+ };
35946
+ async function awaitSessionReady(ready, opts) {
35947
+ const ms = opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS;
35948
+ if (ms <= 0)
35949
+ return ready;
35950
+ let timer;
35951
+ const timeout = new Promise((_, reject) => {
35952
+ timer = setTimeout(() => {
35953
+ reject(new FetchproxySessionNotReadyError({ mcpId: opts.mcpId, pairCode: opts.pendingPairCode() }));
35954
+ }, ms);
35955
+ timer.unref?.();
35956
+ });
35957
+ try {
35958
+ return await Promise.race([ready, timeout]);
35959
+ } finally {
35960
+ if (timer)
35961
+ clearTimeout(timer);
35962
+ }
35963
+ }
35964
+
35854
35965
  // node_modules/@fetchproxy/server/dist/host.js
35855
35966
  var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
35856
35967
  var enc2 = new TextEncoder();
@@ -35938,6 +36049,29 @@ async function startHost(opts) {
35938
36049
  return;
35939
36050
  }
35940
36051
  if (frame.type === "hello" && frame.role === "server") {
36052
+ const peerEdPub = fromB64(frame.identityEd25519Pub);
36053
+ const peerSigMsg = concatBytes(enc2.encode(frame.mcpId), fromB64(frame.sessionNonce));
36054
+ const peerSig = fromB64(frame.sessionSig);
36055
+ let peerSigOk = false;
36056
+ try {
36057
+ peerSigOk = await ed25519Verify(peerEdPub, peerSigMsg, peerSig);
36058
+ } catch {
36059
+ peerSigOk = false;
36060
+ }
36061
+ if (!peerSigOk) {
36062
+ console.warn("[fetchproxy] peer hello signature invalid \u2014 refusing registration (possible squatter)");
36063
+ ws.close(1008, "peer hello signature invalid");
36064
+ return;
36065
+ }
36066
+ const existing = peers.get(frame.mcpId);
36067
+ if (existing && existing.ws !== ws) {
36068
+ const existingEdPub = existing.helloFrame.identityEd25519Pub;
36069
+ if (existingEdPub !== frame.identityEd25519Pub) {
36070
+ console.warn("[fetchproxy] peer mcpId already mapped to a different identity \u2014 refusing (mcpId squatting)");
36071
+ ws.close(1008, "mcpId already registered to another identity");
36072
+ return;
36073
+ }
36074
+ }
35941
36075
  identified = "peer";
35942
36076
  peerMcpId = frame.mcpId;
35943
36077
  peers.set(frame.mcpId, { ws, helloFrame: frame });
@@ -36030,8 +36164,10 @@ async function startHost(opts) {
36030
36164
  resetSessionPromise();
36031
36165
  disconnectListeners.forEach((cb) => cb());
36032
36166
  }
36033
- if (identified === "peer" && peerMcpId)
36034
- peers.delete(peerMcpId);
36167
+ if (identified === "peer" && peerMcpId) {
36168
+ if (peers.get(peerMcpId)?.ws === ws)
36169
+ peers.delete(peerMcpId);
36170
+ }
36035
36171
  });
36036
36172
  });
36037
36173
  return {
@@ -36047,7 +36183,10 @@ async function startHost(opts) {
36047
36183
  });
36048
36184
  }),
36049
36185
  sendOwnInner: async (inner) => {
36050
- const session = await ownSessionReady;
36186
+ const session = await awaitSessionReady(ownSessionReady, {
36187
+ mcpId: opts.ownMcpId,
36188
+ pendingPairCode: () => ownPendingPairCode
36189
+ });
36051
36190
  if (!extensionWs)
36052
36191
  throw new Error("host: no extension connected");
36053
36192
  const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), inner);
@@ -36152,7 +36291,10 @@ async function startPeer(opts) {
36152
36291
  ws,
36153
36292
  session: sessionPromise,
36154
36293
  sendInner: async (inner) => {
36155
- await sessionPromise;
36294
+ await awaitSessionReady(sessionPromise, {
36295
+ mcpId: opts.mcpId,
36296
+ pendingPairCode: () => pendingPairCode
36297
+ });
36156
36298
  const s = session;
36157
36299
  const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
36158
36300
  ws.send(JSON.stringify(sealed));
@@ -36818,6 +36960,35 @@ var FetchproxyServer = class {
36818
36960
  this.keepAliveTimer = null;
36819
36961
  }
36820
36962
  }
36963
+ /**
36964
+ * Send an inner request frame via whichever bridge handle is active. If the
36965
+ * send throws (e.g. `FetchproxySessionNotReadyError` — the session never
36966
+ * confirmed), the frame never reached the bridge, so no reply will arrive:
36967
+ * drop the just-registered pending resolver for this id (it lives in exactly
36968
+ * one of the op maps — request ids are unique) so it doesn't leak until the
36969
+ * server closes, then rethrow.
36970
+ */
36971
+ async sendInnerFrame(inner) {
36972
+ try {
36973
+ if (this.hostHandle) {
36974
+ await this.hostHandle.sendOwnInner(inner);
36975
+ } else if (this.peerHandle) {
36976
+ await this.peerHandle.sendInner(inner);
36977
+ }
36978
+ } catch (err) {
36979
+ if ("id" in inner && typeof inner.id === "number") {
36980
+ const { id } = inner;
36981
+ this.pending.delete(id);
36982
+ this.pendingReadCookies.delete(id);
36983
+ this.pendingStorage.delete(id);
36984
+ this.pendingCapture.delete(id);
36985
+ this.pendingRedirect.delete(id);
36986
+ this.pendingDownload.delete(id);
36987
+ this.pendingIdb.delete(id);
36988
+ }
36989
+ throw err;
36990
+ }
36991
+ }
36821
36992
  /**
36822
36993
  * Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
36823
36994
  * On timeout returns the `{ok:false, kind:'timeout'}` envelope —
@@ -36829,11 +37000,7 @@ var FetchproxyServer = class {
36829
37000
  const pending = new Promise((resolve) => {
36830
37001
  this.pending.set(id, resolve);
36831
37002
  });
36832
- if (this.hostHandle) {
36833
- await this.hostHandle.sendOwnInner(inner);
36834
- } else if (this.peerHandle) {
36835
- await this.peerHandle.sendInner(inner);
36836
- }
37003
+ await this.sendInnerFrame(inner);
36837
37004
  const timeoutMs = this.opts.fetchTimeoutMs;
36838
37005
  if (timeoutMs === void 0 || timeoutMs <= 0)
36839
37006
  return pending;
@@ -36862,6 +37029,51 @@ var FetchproxyServer = class {
36862
37029
  clearTimeout(timer);
36863
37030
  }
36864
37031
  }
37032
+ /**
37033
+ * FP-B2: bound a non-`fetch` verb's reply wait by `fetchTimeoutMs`.
37034
+ *
37035
+ * Before this, only `fetch()` raced its `pending` reply against a timer
37036
+ * (`_fetchOnceWithTimeout`); `readCookies`, the storage reads,
37037
+ * `capture_request_header`, `capture_redirect`, `download`, and
37038
+ * `read_indexed_db` awaited their `pending` promise with no race, so a
37039
+ * wedged extension hung the tool call indefinitely.
37040
+ *
37041
+ * `pending` is the already-registered reply promise. `pendingMap`/`id`
37042
+ * point at the op-specific map entry so we can drop it on expiry exactly
37043
+ * as `_fetchOnceWithTimeout` does — otherwise a late bridge reply would
37044
+ * resolve into a stale resolver / leak. On timeout we reject with a
37045
+ * `FetchproxyTimeoutError` (the same throwable the convenience methods
37046
+ * already surface for fetch timeouts). `0`/unset opts out (unbounded),
37047
+ * matching the fetch path.
37048
+ */
37049
+ async _withVerbTimeout(pending, pendingMap, id, url2) {
37050
+ const timeoutMs = this.opts.fetchTimeoutMs;
37051
+ if (timeoutMs === void 0 || timeoutMs <= 0)
37052
+ return pending;
37053
+ let timer;
37054
+ const start = Date.now();
37055
+ try {
37056
+ return await Promise.race([
37057
+ pending,
37058
+ new Promise((_resolve, reject) => {
37059
+ timer = setTimeout(() => {
37060
+ pendingMap.delete(id);
37061
+ reject(new FetchproxyTimeoutError({
37062
+ url: url2,
37063
+ timeoutMs,
37064
+ role: this.role,
37065
+ port: this.opts.port,
37066
+ elapsedMs: Date.now() - start,
37067
+ retryAttempted: false
37068
+ }));
37069
+ }, timeoutMs);
37070
+ })
37071
+ ]);
37072
+ } finally {
37073
+ if (timer)
37074
+ clearTimeout(timer);
37075
+ }
37076
+ }
36865
37077
  /**
36866
37078
  * Map an `ok:false` fetch result to its typed throwable. Centralizes
36867
37079
  * the kind-to-error-class switch so `request()` and (via the same
@@ -37152,12 +37364,8 @@ var FetchproxyServer = class {
37152
37364
  const pending = new Promise((resolve) => {
37153
37365
  this.pendingReadCookies.set(id, resolve);
37154
37366
  });
37155
- if (this.hostHandle) {
37156
- await this.hostHandle.sendOwnInner(inner);
37157
- } else if (this.peerHandle) {
37158
- await this.peerHandle.sendInner(inner);
37159
- }
37160
- const result = await pending;
37367
+ await this.sendInnerFrame(inner);
37368
+ const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
37161
37369
  if (!result.ok) {
37162
37370
  throw new FetchproxyProtocolError(result.error);
37163
37371
  }
@@ -37223,12 +37431,8 @@ var FetchproxyServer = class {
37223
37431
  const pending = new Promise((resolve, reject) => {
37224
37432
  this.pendingStorage.set(id, { resolve, reject });
37225
37433
  });
37226
- if (this.hostHandle) {
37227
- await this.hostHandle.sendOwnInner(inner);
37228
- } else if (this.peerHandle) {
37229
- await this.peerHandle.sendInner(inner);
37230
- }
37231
- return pending;
37434
+ await this.sendInnerFrame(inner);
37435
+ return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
37232
37436
  }
37233
37437
  /**
37234
37438
  * 0.3.0+: snapshot the next outgoing request's named header. Single-
@@ -37332,12 +37536,8 @@ var FetchproxyServer = class {
37332
37536
  const pending = new Promise((resolve, reject) => {
37333
37537
  this.pendingCapture.set(id, { resolve, reject });
37334
37538
  });
37335
- if (this.hostHandle) {
37336
- await this.hostHandle.sendOwnInner(inner);
37337
- } else if (this.peerHandle) {
37338
- await this.peerHandle.sendInner(inner);
37339
- }
37340
- return pending;
37539
+ await this.sendInnerFrame(inner);
37540
+ return this._withVerbTimeout(pending, this.pendingCapture, id, `https://${opts.host}${opts.path ?? "/*"}`);
37341
37541
  }
37342
37542
  /**
37343
37543
  * Snapshot the redirect target URL of the next request the browser
@@ -37421,12 +37621,8 @@ var FetchproxyServer = class {
37421
37621
  const pending = new Promise((resolve, reject) => {
37422
37622
  this.pendingRedirect.set(id, { resolve, reject });
37423
37623
  });
37424
- if (this.hostHandle) {
37425
- await this.hostHandle.sendOwnInner(inner);
37426
- } else if (this.peerHandle) {
37427
- await this.peerHandle.sendInner(inner);
37428
- }
37429
- return pending;
37624
+ await this.sendInnerFrame(inner);
37625
+ return this._withVerbTimeout(pending, this.pendingRedirect, id, `https://${opts.host}${opts.path ?? "/*"}`);
37430
37626
  }
37431
37627
  /**
37432
37628
  * Download `url` through the BROWSER's own network stack via
@@ -37507,12 +37703,8 @@ var FetchproxyServer = class {
37507
37703
  const pending = new Promise((resolve, reject) => {
37508
37704
  this.pendingDownload.set(id, { resolve, reject });
37509
37705
  });
37510
- if (this.hostHandle) {
37511
- await this.hostHandle.sendOwnInner(inner);
37512
- } else if (this.peerHandle) {
37513
- await this.peerHandle.sendInner(inner);
37514
- }
37515
- return pending;
37706
+ await this.sendInnerFrame(inner);
37707
+ return this._withVerbTimeout(pending, this.pendingDownload, id, opts.url);
37516
37708
  }
37517
37709
  /**
37518
37710
  * 0.4.0+: read declared IndexedDB keys from the user's signed-in
@@ -37559,12 +37751,8 @@ var FetchproxyServer = class {
37559
37751
  const pending = new Promise((resolve, reject) => {
37560
37752
  this.pendingIdb.set(id, { resolve, reject });
37561
37753
  });
37562
- if (this.hostHandle) {
37563
- await this.hostHandle.sendOwnInner(inner);
37564
- } else if (this.peerHandle) {
37565
- await this.peerHandle.sendInner(inner);
37566
- }
37567
- return pending;
37754
+ await this.sendInnerFrame(inner);
37755
+ return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
37568
37756
  }
37569
37757
  assertScopeSubset(requested, declared, label) {
37570
37758
  const undeclared = undeclaredKeys(requested, declared);
@@ -38000,7 +38188,7 @@ function loadAccount(env = process.env) {
38000
38188
  // package.json
38001
38189
  var package_default = {
38002
38190
  name: "infinitecampus-mcp",
38003
- version: "2.3.3",
38191
+ version: "2.3.5",
38004
38192
  mcpName: "io.github.chrischall/infinitecampus-mcp",
38005
38193
  description: "Infinite Campus (Campus Parent) MCP server \u2014 multi-district read + message/document write",
38006
38194
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -38042,16 +38230,16 @@ var package_default = {
38042
38230
  "test:watch": "vitest"
38043
38231
  },
38044
38232
  dependencies: {
38045
- "@chrischall/mcp-utils": "^0.5.0",
38233
+ "@chrischall/mcp-utils": "^0.10.3",
38046
38234
  "@fetchproxy/bootstrap": "^1.0.0",
38047
38235
  "@fetchproxy/server": "^1.0.0",
38048
38236
  "@modelcontextprotocol/sdk": "^1.29.0",
38049
38237
  dotenv: "^17.4.2",
38050
- "node-html-parser": "^7.1.0",
38238
+ "node-html-parser": "^8.0.2",
38051
38239
  zod: "^4.4.3"
38052
38240
  },
38053
38241
  devDependencies: {
38054
- "@types/node": "^25.9.1",
38242
+ "@types/node": "^26.0.0",
38055
38243
  "@vitest/coverage-v8": "^4.1.7",
38056
38244
  esbuild: "^0.28.0",
38057
38245
  typescript: "^6.0.3",
@@ -39403,7 +39591,7 @@ try {
39403
39591
  }
39404
39592
  var COMMON = {
39405
39593
  name: "infinitecampus",
39406
- version: "2.3.3"
39594
+ version: "2.3.5"
39407
39595
  // x-release-please-version
39408
39596
  };
39409
39597
  if (account) {
package/dist/index.js CHANGED
@@ -57,7 +57,7 @@ catch (e) {
57
57
  // actionable stderr message (banner) instead of a crash loop.
58
58
  const COMMON = {
59
59
  name: 'infinitecampus',
60
- version: '2.3.3', // x-release-please-version
60
+ version: '2.3.5', // x-release-please-version
61
61
  };
62
62
  if (account) {
63
63
  const client = new ICClient(account, { preloaded });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinitecampus-mcp",
3
- "version": "2.3.3",
3
+ "version": "2.3.5",
4
4
  "mcpName": "io.github.chrischall/infinitecampus-mcp",
5
5
  "description": "Infinite Campus (Campus Parent) MCP server — multi-district read + message/document write",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -42,16 +42,16 @@
42
42
  "test:watch": "vitest"
43
43
  },
44
44
  "dependencies": {
45
- "@chrischall/mcp-utils": "^0.5.0",
45
+ "@chrischall/mcp-utils": "^0.10.3",
46
46
  "@fetchproxy/bootstrap": "^1.0.0",
47
47
  "@fetchproxy/server": "^1.0.0",
48
48
  "@modelcontextprotocol/sdk": "^1.29.0",
49
49
  "dotenv": "^17.4.2",
50
- "node-html-parser": "^7.1.0",
50
+ "node-html-parser": "^8.0.2",
51
51
  "zod": "^4.4.3"
52
52
  },
53
53
  "devDependencies": {
54
- "@types/node": "^25.9.1",
54
+ "@types/node": "^26.0.0",
55
55
  "@vitest/coverage-v8": "^4.1.7",
56
56
  "esbuild": "^0.28.0",
57
57
  "typescript": "^6.0.3",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/infinitecampus-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "2.3.3",
9
+ "version": "2.3.5",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "infinitecampus-mcp",
14
- "version": "2.3.3",
14
+ "version": "2.3.5",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },