infinitecampus-mcp 2.4.3 → 2.4.4

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.4.3"
10
+ "version": "2.4.4"
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.4.3",
18
+ "version": "2.4.4",
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.4.3",
4
+ "version": "2.4.4",
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/auth.js CHANGED
@@ -115,6 +115,18 @@ function lifterFor(host) {
115
115
  // <district>.infinitecampus.org, etc.). Declare just this host
116
116
  // — each district is its own root, no wildcard needed.
117
117
  domains: [host],
118
+ // IC is a Tomcat app deployed at /campus, and Tomcat scopes JSESSIONID
119
+ // to the servlet context by default:
120
+ //
121
+ // JSESSIONID=...; Path=/campus; Secure; HttpOnly; SameSite=Lax
122
+ //
123
+ // chrome.cookies.get matches that Path against the URL it is given, so a
124
+ // read aimed at the origin root silently returns nothing — which is
125
+ // indistinguishable from "the user is signed out", and is why this path
126
+ // never worked on any tenant. Verified live: without this the lift found
127
+ // no cookies while a fetch through the same tab returned authenticated
128
+ // data; with it, JSESSIONID + XSRF-TOKEN both come back.
129
+ storagePath: '/campus',
118
130
  declare: {
119
131
  // JSESSIONID is HttpOnly (chrome.cookies.get sees it; the
120
132
  // security gate is the declared key list). XSRF-TOKEN is
package/dist/bundle.js CHANGED
@@ -34977,6 +34977,15 @@ function assertHttpUrl(x, label) {
34977
34977
  throw new ProtocolError(`${label}: must be http(s), got ${u.protocol}`);
34978
34978
  }
34979
34979
  }
34980
+ function assertCookiePath(x, label) {
34981
+ assertString(x, label);
34982
+ if (!x.startsWith("/") || x.startsWith("//")) {
34983
+ throw new ProtocolError(`${label}: must be an absolute path like "/campus"`);
34984
+ }
34985
+ if (x.includes("?") || x.includes("#") || x.includes("\\")) {
34986
+ throw new ProtocolError(`${label}: must not contain a query, fragment, or backslash`);
34987
+ }
34988
+ }
34980
34989
  function assertHttpsOriginOnly(x, label) {
34981
34990
  assertString(x, label);
34982
34991
  let u;
@@ -35421,8 +35430,10 @@ function validateInnerRequest(raw) {
35421
35430
  }
35422
35431
  assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
35423
35432
  assertNonEmptyKeyArray(raw.init.keys, "inner.init.keys");
35433
+ if (raw.init.path !== void 0)
35434
+ assertCookiePath(raw.init.path, "inner.init.path");
35424
35435
  for (const k of Object.keys(raw.init)) {
35425
- if (k !== "origin" && k !== "keys") {
35436
+ if (k !== "origin" && k !== "keys" && k !== "path") {
35426
35437
  throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on read_cookies`);
35427
35438
  }
35428
35439
  }
@@ -36653,6 +36664,22 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
36653
36664
  this.hint = hint;
36654
36665
  }
36655
36666
  };
36667
+ var FetchproxyScopeError = class extends FetchproxyProtocolError {
36668
+ /** The extension's raw rejection, unmodified. */
36669
+ originalError;
36670
+ hint;
36671
+ constructor(originalError) {
36672
+ const hint = "the declared scope changed since you paired, so the extension is refusing the request. Revoke this MCP in the Transporter extension popup, then re-run \u2014 you will be asked to approve the new scope. This is not a version problem and does not need an update.";
36673
+ super(`${originalError} \u2014 ${hint}`);
36674
+ this.name = "FetchproxyScopeError";
36675
+ this.originalError = originalError;
36676
+ this.hint = hint;
36677
+ }
36678
+ };
36679
+ var SCOPE_REJECTION = /not in declared/;
36680
+ function protocolErrorFrom(error51) {
36681
+ return SCOPE_REJECTION.test(error51) ? new FetchproxyScopeError(error51) : new FetchproxyProtocolError(error51);
36682
+ }
36656
36683
  var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
36657
36684
  url;
36658
36685
  timeoutMs;
@@ -36682,6 +36709,17 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
36682
36709
  }
36683
36710
  };
36684
36711
  var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
36712
+ function normalizeCookiePath(path) {
36713
+ if (path === void 0 || path === "")
36714
+ return void 0;
36715
+ const trimmed = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
36716
+ try {
36717
+ assertCookiePath(trimmed, "path");
36718
+ } catch (e) {
36719
+ throw new Error(`FetchproxyServer: ${e instanceof Error ? e.message : String(e)} (got ${JSON.stringify(path)})`);
36720
+ }
36721
+ return trimmed;
36722
+ }
36685
36723
  function assertSubdomainLabel(label) {
36686
36724
  if (!SUBDOMAIN_LABEL_RE.test(label)) {
36687
36725
  throw new Error(`FetchproxyServer: subdomain must be a DNS label like "www" or "api" (or dot-separated like "auth.api"), got ${JSON.stringify(label)}`);
@@ -37323,7 +37361,7 @@ var FetchproxyServer = class {
37323
37361
  port: this.opts.port
37324
37362
  });
37325
37363
  }
37326
- return new FetchproxyProtocolError(result.error);
37364
+ return protocolErrorFrom(result.error);
37327
37365
  }
37328
37366
  /**
37329
37367
  * Convenience wrapper around `fetch()`. Builds the URL from a path
@@ -37574,9 +37612,14 @@ var FetchproxyServer = class {
37574
37612
  let inner;
37575
37613
  if (opts.keys !== void 0) {
37576
37614
  this.assertScopeSubset(opts.keys, this.opts.cookieKeys, "cookieKeys");
37615
+ const cookiePath = normalizeCookiePath(opts.path);
37577
37616
  const initV3 = {
37617
+ // Origin stays BARE. The path travels as its own validated field —
37618
+ // `assertHttpsOriginOnly` deliberately refuses a path here so one
37619
+ // cannot be used to re-point the read past the domain gate.
37578
37620
  origin: `https://${host}`,
37579
- keys: [...opts.keys]
37621
+ keys: [...opts.keys],
37622
+ ...cookiePath !== void 0 ? { path: cookiePath } : {}
37580
37623
  };
37581
37624
  inner = { type: "request", id, op: "read_cookies", init: initV3 };
37582
37625
  } else {
@@ -37589,7 +37632,7 @@ var FetchproxyServer = class {
37589
37632
  await this.sendInnerFrame(inner);
37590
37633
  const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
37591
37634
  if (!result.ok) {
37592
- throw new FetchproxyProtocolError(result.error);
37635
+ throw protocolErrorFrom(result.error);
37593
37636
  }
37594
37637
  return result.cookies;
37595
37638
  }
@@ -38139,7 +38182,7 @@ var FetchproxyServer = class {
38139
38182
  storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
38140
38183
  }
38141
38184
  } else {
38142
- storageCb.reject(new FetchproxyProtocolError(inner.error));
38185
+ storageCb.reject(protocolErrorFrom(inner.error));
38143
38186
  }
38144
38187
  return;
38145
38188
  }
@@ -38153,7 +38196,7 @@ var FetchproxyServer = class {
38153
38196
  captureCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture awaiter`));
38154
38197
  }
38155
38198
  } else {
38156
- captureCb.reject(new FetchproxyProtocolError(inner.error));
38199
+ captureCb.reject(protocolErrorFrom(inner.error));
38157
38200
  }
38158
38201
  return;
38159
38202
  }
@@ -38167,7 +38210,7 @@ var FetchproxyServer = class {
38167
38210
  redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
38168
38211
  }
38169
38212
  } else {
38170
- redirectCb.reject(new FetchproxyProtocolError(inner.error));
38213
+ redirectCb.reject(protocolErrorFrom(inner.error));
38171
38214
  }
38172
38215
  return;
38173
38216
  }
@@ -38181,7 +38224,7 @@ var FetchproxyServer = class {
38181
38224
  idbCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on read_indexed_db awaiter`));
38182
38225
  }
38183
38226
  } else {
38184
- idbCb.reject(new FetchproxyProtocolError(inner.error));
38227
+ idbCb.reject(protocolErrorFrom(inner.error));
38185
38228
  }
38186
38229
  return;
38187
38230
  }
@@ -38195,7 +38238,7 @@ var FetchproxyServer = class {
38195
38238
  downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
38196
38239
  }
38197
38240
  } else {
38198
- downloadCb.reject(new FetchproxyProtocolError(inner.error));
38241
+ downloadCb.reject(protocolErrorFrom(inner.error));
38199
38242
  }
38200
38243
  return;
38201
38244
  }
@@ -38209,7 +38252,7 @@ var FetchproxyServer = class {
38209
38252
  graphqlCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on graphql_query awaiter`));
38210
38253
  }
38211
38254
  } else {
38212
- graphqlCb.reject(new FetchproxyProtocolError(inner.error));
38255
+ graphqlCb.reject(protocolErrorFrom(inner.error));
38213
38256
  }
38214
38257
  return;
38215
38258
  }
@@ -38402,7 +38445,9 @@ async function runOneLift(opts) {
38402
38445
  if (opts.declare.cookies.length > 0) {
38403
38446
  const joined = await server.readCookies({
38404
38447
  keys: opts.declare.cookies,
38405
- ...storageDomainOpts
38448
+ ...storageDomainOpts,
38449
+ // Cookie-only: the other buckets are origin-scoped and ignore path.
38450
+ ...opts.storagePath !== void 0 ? { path: opts.storagePath } : {}
38406
38451
  });
38407
38452
  for (const piece of joined.split("; ")) {
38408
38453
  if (!piece)
@@ -38532,7 +38577,7 @@ function loadAccount(env = process.env) {
38532
38577
  // package.json
38533
38578
  var package_default = {
38534
38579
  name: "infinitecampus-mcp",
38535
- version: "2.4.3",
38580
+ version: "2.4.4",
38536
38581
  mcpName: "io.github.chrischall/infinitecampus-mcp",
38537
38582
  description: "Infinite Campus (Campus Parent) MCP server \u2014 multi-district read + message/document write",
38538
38583
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -38575,7 +38620,7 @@ var package_default = {
38575
38620
  },
38576
38621
  dependencies: {
38577
38622
  "@chrischall/mcp-utils": "^0.14.0",
38578
- "@fetchproxy/bootstrap": "^1.9.1",
38623
+ "@fetchproxy/bootstrap": "^1.11.0",
38579
38624
  "@fetchproxy/server": "^1.7.0",
38580
38625
  "@modelcontextprotocol/sdk": "^1.29.0",
38581
38626
  dotenv: "^17.4.2",
@@ -38622,6 +38667,18 @@ function lifterFor(host) {
38622
38667
  // <district>.infinitecampus.org, etc.). Declare just this host
38623
38668
  // — each district is its own root, no wildcard needed.
38624
38669
  domains: [host],
38670
+ // IC is a Tomcat app deployed at /campus, and Tomcat scopes JSESSIONID
38671
+ // to the servlet context by default:
38672
+ //
38673
+ // JSESSIONID=...; Path=/campus; Secure; HttpOnly; SameSite=Lax
38674
+ //
38675
+ // chrome.cookies.get matches that Path against the URL it is given, so a
38676
+ // read aimed at the origin root silently returns nothing — which is
38677
+ // indistinguishable from "the user is signed out", and is why this path
38678
+ // never worked on any tenant. Verified live: without this the lift found
38679
+ // no cookies while a fetch through the same tab returned authenticated
38680
+ // data; with it, JSESSIONID + XSRF-TOKEN both come back.
38681
+ storagePath: "/campus",
38625
38682
  declare: {
38626
38683
  // JSESSIONID is HttpOnly (chrome.cookies.get sees it; the
38627
38684
  // security gate is the declared key list). XSRF-TOKEN is
@@ -40158,7 +40215,7 @@ try {
40158
40215
  }
40159
40216
  var COMMON = {
40160
40217
  name: "infinitecampus",
40161
- version: "2.4.3"
40218
+ version: "2.4.4"
40162
40219
  // x-release-please-version
40163
40220
  };
40164
40221
  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.4.3', // x-release-please-version
60
+ version: '2.4.4', // x-release-please-version
61
61
  };
62
62
  if (account) {
63
63
  const client = new ICClient(account, { refreshSession });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinitecampus-mcp",
3
- "version": "2.4.3",
3
+ "version": "2.4.4",
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>",
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@chrischall/mcp-utils": "^0.14.0",
46
- "@fetchproxy/bootstrap": "^1.9.1",
46
+ "@fetchproxy/bootstrap": "^1.11.0",
47
47
  "@fetchproxy/server": "^1.7.0",
48
48
  "@modelcontextprotocol/sdk": "^1.29.0",
49
49
  "dotenv": "^17.4.2",
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.4.3",
9
+ "version": "2.4.4",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "infinitecampus-mcp",
14
- "version": "2.4.3",
14
+ "version": "2.4.4",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },