infinitecampus-mcp 2.8.4 → 2.8.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.8.4"
10
+ "version": "2.8.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.8.4",
18
+ "version": "2.8.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.8.4",
4
+ "version": "2.8.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/dist/bundle.js CHANGED
@@ -9444,7 +9444,7 @@ var require_websocket = __commonJS({
9444
9444
  var http = __require("http");
9445
9445
  var net = __require("net");
9446
9446
  var tls = __require("tls");
9447
- var { randomBytes: randomBytes2, createHash } = __require("crypto");
9447
+ var { randomBytes: randomBytes3, createHash } = __require("crypto");
9448
9448
  var { Duplex, Readable } = __require("stream");
9449
9449
  var { URL: URL2 } = __require("url");
9450
9450
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -9982,7 +9982,7 @@ var require_websocket = __commonJS({
9982
9982
  }
9983
9983
  }
9984
9984
  const defaultPort = isSecure ? 443 : 80;
9985
- const key = randomBytes2(16).toString("base64");
9985
+ const key = randomBytes3(16).toString("base64");
9986
9986
  const request = isSecure ? https.request : http.request;
9987
9987
  const protocolSet = /* @__PURE__ */ new Set();
9988
9988
  let perMessageDeflate;
@@ -42072,9 +42072,29 @@ var SessionState = class {
42072
42072
  outboundSeq = 0;
42073
42073
  lastInboundSeq = 0;
42074
42074
  inflightInbound = /* @__PURE__ */ new Set();
42075
+ saturationWarned = false;
42075
42076
  constructor(sessionKey) {
42076
42077
  this.sessionKey = sessionKey;
42077
42078
  }
42079
+ /**
42080
+ * Whether the caller holding this claim verdict should log saturation now.
42081
+ * True only on the TRANSITION into it: a line per dropped frame turned a
42082
+ * flood — or a set that never drains — into a log flood amplifying the very
42083
+ * condition it reported (#376). An `'ok'` claim re-arms the latch, since it
42084
+ * is the one signal every caller sees that the set has drained below the
42085
+ * bound; a replay needs no capacity and leaves it alone. Latched per
42086
+ * session, so a renegotiated one starts armed. A set hovering at the bound
42087
+ * can still alternate ok/saturated, but that logs at the rate frames finish
42088
+ * opening, not the rate they arrive.
42089
+ */
42090
+ saturationWarningDue(claim2) {
42091
+ if (claim2 === "ok")
42092
+ this.saturationWarned = false;
42093
+ if (claim2 !== "saturated" || this.saturationWarned)
42094
+ return false;
42095
+ this.saturationWarned = true;
42096
+ return true;
42097
+ }
42078
42098
  nextOutboundSeq() {
42079
42099
  this.outboundSeq += 1;
42080
42100
  return this.outboundSeq;
@@ -42085,19 +42105,21 @@ var SessionState = class {
42085
42105
  * the first `await` of the receive path, or a duplicate frame read in the
42086
42106
  * same pass will be judged against a counter neither frame has moved yet.
42087
42107
  *
42088
- * Answering false means "not this frame's to process" — replayed, or a
42089
- * duplicate of one still in flight. Every true MUST be answered by exactly
42090
- * one {@link commitInboundSeq} or {@link releaseInboundSeq}.
42108
+ * Anything but `'ok'` means "not this frame's to process": `'replay'` for
42109
+ * a spent seq or a duplicate of one still in flight, `'saturated'` when the
42110
+ * in-flight bound is full. Replay is checked first, since refusing a stale
42111
+ * seq needs no capacity. Every `'ok'` MUST be answered by exactly one
42112
+ * {@link commitInboundSeq} or {@link releaseInboundSeq}.
42091
42113
  */
42092
42114
  claimInboundSeq(seq) {
42093
42115
  if (seq <= this.lastInboundSeq)
42094
- return false;
42116
+ return "replay";
42095
42117
  if (this.inflightInbound.has(seq))
42096
- return false;
42118
+ return "replay";
42097
42119
  if (this.inflightInbound.size >= MAX_INFLIGHT_INBOUND_SEQS)
42098
- return false;
42120
+ return "saturated";
42099
42121
  this.inflightInbound.add(seq);
42100
- return true;
42122
+ return "ok";
42101
42123
  }
42102
42124
  /**
42103
42125
  * Give a claim back without spending the seq — for a frame that did not
@@ -42192,11 +42214,12 @@ async function awaitSessionReady(ready, opts) {
42192
42214
  }
42193
42215
 
42194
42216
  // node_modules/@fetchproxy/server/dist/extension-trust.js
42195
- import { readFile as readFile2, writeFile as writeFile2, rename, unlink, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
42217
+ import { readFile as readFile2, writeFile as writeFile2, rename as rename2, unlink as unlink2, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
42196
42218
  import { isAbsolute as isAbsolute2, join as join2 } from "node:path";
42197
42219
 
42198
42220
  // node_modules/@fetchproxy/server/dist/identity.js
42199
- import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
42221
+ import { readFile, readdir, writeFile, mkdir, chmod, rename, rm, unlink } from "node:fs/promises";
42222
+ import { randomBytes } from "node:crypto";
42200
42223
  import { isAbsolute, join } from "node:path";
42201
42224
  import { homedir } from "node:os";
42202
42225
  var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
@@ -42252,16 +42275,46 @@ function parseIdentity(text) {
42252
42275
  createdAt: j.createdAt
42253
42276
  };
42254
42277
  }
42278
+ async function openIdentityDir(dir) {
42279
+ await mkdir(dir, { recursive: true, mode: 448 });
42280
+ try {
42281
+ await chmod(dir, 448);
42282
+ } catch (e) {
42283
+ const code = e.code;
42284
+ if (code !== "EROFS" && code !== "EPERM")
42285
+ throw e;
42286
+ }
42287
+ }
42288
+ var STAGING_BYTES = 6;
42289
+ async function sweepStagingFiles(dir, serverName) {
42290
+ const base = safeIdentityFileBase(serverName).replace(/[.]/g, "\\.");
42291
+ const staging = new RegExp(`^${base}\\.json\\.[0-9a-f]{${STAGING_BYTES * 2}}\\.tmp$`);
42292
+ let names;
42293
+ try {
42294
+ names = await readdir(dir);
42295
+ } catch {
42296
+ return;
42297
+ }
42298
+ await Promise.all(names.filter((name) => staging.test(name)).map((name) => unlink(join(dir, name)).catch(() => void 0)));
42299
+ }
42255
42300
  async function writeIdentityFile(dir, serverName, id) {
42256
42301
  const path = identityFilePath(dir, serverName);
42257
- await mkdir(dir, { recursive: true, mode: 448 });
42258
- await writeFile(path, serializeIdentity(id), { mode: 384 });
42259
- await chmod(path, 384);
42302
+ await openIdentityDir(dir);
42303
+ await sweepStagingFiles(dir, serverName);
42304
+ const tmp = `${path}.${randomBytes(STAGING_BYTES).toString("hex")}.tmp`;
42305
+ try {
42306
+ await writeFile(tmp, serializeIdentity(id), { mode: 384, flag: "wx" });
42307
+ await chmod(tmp, 384);
42308
+ await rename(tmp, path);
42309
+ } catch (e) {
42310
+ await rm(tmp, { force: true });
42311
+ throw e;
42312
+ }
42260
42313
  return path;
42261
42314
  }
42262
42315
  async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
42263
42316
  const path = identityFilePath(dir, serverName);
42264
- await mkdir(dir, { recursive: true, mode: 448 });
42317
+ await openIdentityDir(dir);
42265
42318
  try {
42266
42319
  return parseIdentity(await readFile(path, "utf8"));
42267
42320
  } catch (e) {
@@ -42374,7 +42427,7 @@ async function writeExtensionPin(serverName, pin, dir = defaultTrustDir()) {
42374
42427
  const tmp = `${path}.tmp`;
42375
42428
  await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384 });
42376
42429
  await chmod2(tmp, 384);
42377
- await rename(tmp, path);
42430
+ await rename2(tmp, path);
42378
42431
  }
42379
42432
 
42380
42433
  // node_modules/@fetchproxy/server/dist/host.js
@@ -42729,7 +42782,11 @@ async function startHost(opts) {
42729
42782
  const session = ownSession;
42730
42783
  if (!session)
42731
42784
  return;
42732
- if (!session.claimInboundSeq(frame.seq))
42785
+ const claim2 = session.claimInboundSeq(frame.seq);
42786
+ if (session.saturationWarningDue(claim2)) {
42787
+ console.warn(`[fetchproxy] ${opts.ownServerName}: dropped an inbound frame (seq ${frame.seq}) unread \u2014 too many frames from the extension are still being opened (inbound claims saturated). Not a replay. Further drops are not logged until the in-flight set drains.`);
42788
+ }
42789
+ if (claim2 !== "ok")
42733
42790
  return;
42734
42791
  let inner;
42735
42792
  try {
@@ -42858,7 +42915,9 @@ async function startHost(opts) {
42858
42915
  // node_modules/@fetchproxy/server/dist/peer.js
42859
42916
  var enc5 = new TextEncoder();
42860
42917
  async function startPeer(opts) {
42861
- const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}`);
42918
+ const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}`, {
42919
+ maxPayload: opts.maxPayloadBytes ?? MAX_PAYLOAD_BYTES
42920
+ });
42862
42921
  await new Promise((resolve2, reject) => {
42863
42922
  const onOpen = () => {
42864
42923
  cleanup();
@@ -43096,7 +43155,11 @@ async function startPeer(opts) {
43096
43155
  const inboundSession = session;
43097
43156
  if (!inboundSession)
43098
43157
  return;
43099
- if (!inboundSession.claimInboundSeq(frame.seq))
43158
+ const claim2 = inboundSession.claimInboundSeq(frame.seq);
43159
+ if (inboundSession.saturationWarningDue(claim2)) {
43160
+ console.warn(`[fetchproxy] ${opts.serverName}: dropped an inbound frame (seq ${frame.seq}) unread \u2014 too many frames from the extension are still being opened (inbound claims saturated). Not a replay. Further drops are not logged until the in-flight set drains.`);
43161
+ }
43162
+ if (claim2 !== "ok")
43100
43163
  return;
43101
43164
  let result;
43102
43165
  try {
@@ -45652,7 +45715,7 @@ function loadAccount(env = process.env) {
45652
45715
  // package.json
45653
45716
  var package_default = {
45654
45717
  name: "infinitecampus-mcp",
45655
- version: "2.8.4",
45718
+ version: "2.8.5",
45656
45719
  mcpName: "io.github.chrischall/infinitecampus-mcp",
45657
45720
  description: "Infinite Campus (Campus Parent) MCP server \u2014 multi-district read + message/document write",
45658
45721
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -45698,8 +45761,8 @@ var package_default = {
45698
45761
  },
45699
45762
  dependencies: {
45700
45763
  "@chrischall/mcp-utils": "^0.26.1",
45701
- "@fetchproxy/bootstrap": "^3.0.0",
45702
- "@fetchproxy/server": "^3.0.0",
45764
+ "@fetchproxy/bootstrap": "^3.0.1",
45765
+ "@fetchproxy/server": "^3.0.1",
45703
45766
  "@modelcontextprotocol/sdk": "^1.30.0",
45704
45767
  dotenv: "^17.4.2",
45705
45768
  "node-html-parser": "^9.0.0",
@@ -45814,7 +45877,7 @@ import { dirname as dirname2 } from "path";
45814
45877
  import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
45815
45878
  import { dirname, join as join3, resolve } from "node:path";
45816
45879
  import { homedir as homedir2 } from "node:os";
45817
- import { randomBytes, createHmac } from "node:crypto";
45880
+ import { randomBytes as randomBytes2, createHmac } from "node:crypto";
45818
45881
  function bindingDigest(secret, salt) {
45819
45882
  return createHmac("sha256", salt).update(secret).digest("hex").slice(0, 32);
45820
45883
  }
@@ -45866,7 +45929,7 @@ function createFileStatePersistence(opts) {
45866
45929
  },
45867
45930
  save(state) {
45868
45931
  const dir = dirname(filePath);
45869
- const tmp = `${filePath}.tmp-${randomBytes(6).toString("hex")}`;
45932
+ const tmp = `${filePath}.tmp-${randomBytes2(6).toString("hex")}`;
45870
45933
  const dirExisted = existsSync(dir);
45871
45934
  try {
45872
45935
  mkdirSync(dir, { recursive: true, mode: 448 });
@@ -45874,7 +45937,7 @@ function createFileStatePersistence(opts) {
45874
45937
  chmodSync(dir, 448);
45875
45938
  let boundTo;
45876
45939
  if (secret !== void 0) {
45877
- const salt = randomBytes(16).toString("hex");
45940
+ const salt = randomBytes2(16).toString("hex");
45878
45941
  boundTo = { salt, digest: bindingDigest(secret, salt) };
45879
45942
  }
45880
45943
  const envelope = { v: 1, ...boundTo !== void 0 ? { boundTo } : {}, state };
@@ -47670,7 +47733,7 @@ try {
47670
47733
  }
47671
47734
  var COMMON = {
47672
47735
  name: "infinitecampus",
47673
- version: "2.8.4"
47736
+ version: "2.8.5"
47674
47737
  // x-release-please-version
47675
47738
  };
47676
47739
  var healthState = { account, source, configError, client: null };
package/dist/index.js CHANGED
@@ -56,7 +56,7 @@ catch (e) {
56
56
  // avoids — see `unconfiguredClient`.
57
57
  const COMMON = {
58
58
  name: 'infinitecampus',
59
- version: '2.8.4', // x-release-please-version
59
+ version: '2.8.5', // x-release-please-version
60
60
  };
61
61
  // Shared with the healthcheck so it can report the SAME state the tools see,
62
62
  // including on the unconfigured path where there are no tools at all.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinitecampus-mcp",
3
- "version": "2.8.4",
3
+ "version": "2.8.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>",
@@ -46,8 +46,8 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@chrischall/mcp-utils": "^0.26.1",
49
- "@fetchproxy/bootstrap": "^3.0.0",
50
- "@fetchproxy/server": "^3.0.0",
49
+ "@fetchproxy/bootstrap": "^3.0.1",
50
+ "@fetchproxy/server": "^3.0.1",
51
51
  "@modelcontextprotocol/sdk": "^1.30.0",
52
52
  "dotenv": "^17.4.2",
53
53
  "node-html-parser": "^9.0.0",
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.8.4",
9
+ "version": "2.8.5",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "infinitecampus-mcp",
14
- "version": "2.8.4",
14
+ "version": "2.8.5",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },