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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/LICENSE +21 -0
- package/README.md +4 -0
- package/dist/bundle.js +251 -63
- package/dist/index.js +1 -1
- package/package.json +4 -4
- package/server.json +2 -2
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
+
[](https://github.com/chrischall/infinitecampus-mcp/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/infinitecampus-mcp)
|
|
5
|
+
[](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
|
-
|
|
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
|
|
34741
|
-
var
|
|
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 =
|
|
34783
|
+
const entries = setCookieEntries(setCookieHeaders);
|
|
34744
34784
|
const jar = /* @__PURE__ */ new Map();
|
|
34745
34785
|
for (const entry of entries) {
|
|
34746
|
-
if (
|
|
34786
|
+
if (isDeletionCookie(entry))
|
|
34747
34787
|
continue;
|
|
34748
|
-
const
|
|
34749
|
-
|
|
34750
|
-
if (eqIdx < 1)
|
|
34788
|
+
const pair = parseNameValue(entry);
|
|
34789
|
+
if (!pair || !pair.value)
|
|
34751
34790
|
continue;
|
|
34752
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
37156
|
-
|
|
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
|
-
|
|
37227
|
-
|
|
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
|
-
|
|
37336
|
-
|
|
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
|
-
|
|
37425
|
-
|
|
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
|
-
|
|
37511
|
-
|
|
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
|
-
|
|
37563
|
-
|
|
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.
|
|
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.
|
|
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": "^
|
|
38238
|
+
"node-html-parser": "^8.0.2",
|
|
38051
38239
|
zod: "^4.4.3"
|
|
38052
38240
|
},
|
|
38053
38241
|
devDependencies: {
|
|
38054
|
-
"@types/node": "^
|
|
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.
|
|
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.
|
|
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
|
+
"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.
|
|
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": "^
|
|
50
|
+
"node-html-parser": "^8.0.2",
|
|
51
51
|
"zod": "^4.4.3"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@types/node": "^
|
|
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.
|
|
9
|
+
"version": "2.3.5",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "infinitecampus-mcp",
|
|
14
|
-
"version": "2.3.
|
|
14
|
+
"version": "2.3.5",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|