infinitecampus-mcp 2.3.4 → 2.3.6
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/dist/bundle.js +609 -205
- package/dist/client.js +161 -114
- package/dist/index.js +1 -1
- package/dist/tools/_shared.js +2 -11
- package/dist/tools/assessments.js +3 -2
- package/dist/tools/assignments.js +2 -2
- package/dist/tools/attendance.js +3 -2
- package/dist/tools/attendance_events.js +3 -2
- package/dist/tools/behavior.js +3 -2
- package/dist/tools/calendar.js +3 -2
- package/dist/tools/districts.js +2 -2
- package/dist/tools/documents.js +5 -4
- package/dist/tools/features.js +3 -2
- package/dist/tools/fees.js +3 -2
- package/dist/tools/foodservice.js +3 -2
- package/dist/tools/grades.js +2 -2
- package/dist/tools/messages.js +4 -3
- package/dist/tools/recent_grades.js +2 -2
- package/dist/tools/schedule.js +2 -2
- package/dist/tools/students.js +2 -2
- package/dist/tools/teachers.js +3 -2
- package/package.json +4 -4
- package/server.json +2 -2
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,25 @@ 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
|
+
var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
|
|
34708
|
+
var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
|
|
34709
|
+
var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
|
|
34710
|
+
|
|
34688
34711
|
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
34689
34712
|
function textResult(data) {
|
|
34690
34713
|
return {
|
|
@@ -34737,23 +34760,38 @@ async function loadDotenvSafely(opts = {}) {
|
|
|
34737
34760
|
}
|
|
34738
34761
|
|
|
34739
34762
|
// node_modules/@chrischall/mcp-utils/dist/http/index.js
|
|
34740
|
-
var
|
|
34741
|
-
var
|
|
34763
|
+
var MAX_AGE_RE = /(?:^|;)\s*Max-Age\s*=\s*(-?\d+)\s*(?:;|$)/i;
|
|
34764
|
+
var EXPIRES_RE = /(?:^|;)\s*Expires\s*=\s*([^;]+)/i;
|
|
34765
|
+
var EXPIRES_DELETION_YEAR = 2e3;
|
|
34766
|
+
function isDeletionCookie(entry) {
|
|
34767
|
+
const maxAge = MAX_AGE_RE.exec(entry);
|
|
34768
|
+
if (maxAge)
|
|
34769
|
+
return Number(maxAge[1]) <= 0;
|
|
34770
|
+
const expires = EXPIRES_RE.exec(entry);
|
|
34771
|
+
if (expires && expires[1]) {
|
|
34772
|
+
const when = new Date(expires[1].trim());
|
|
34773
|
+
if (!Number.isNaN(when.getTime()))
|
|
34774
|
+
return when.getUTCFullYear() < EXPIRES_DELETION_YEAR;
|
|
34775
|
+
}
|
|
34776
|
+
return false;
|
|
34777
|
+
}
|
|
34778
|
+
function parseNameValue(entry) {
|
|
34779
|
+
const nameValue = (entry.split(";")[0] ?? "").trim();
|
|
34780
|
+
const eqIdx = nameValue.indexOf("=");
|
|
34781
|
+
if (eqIdx < 1)
|
|
34782
|
+
return null;
|
|
34783
|
+
return { name: nameValue.slice(0, eqIdx).trim(), value: nameValue.slice(eqIdx + 1).trim() };
|
|
34784
|
+
}
|
|
34742
34785
|
function parseCookieJar(setCookieHeaders) {
|
|
34743
|
-
const entries =
|
|
34786
|
+
const entries = setCookieEntries(setCookieHeaders);
|
|
34744
34787
|
const jar = /* @__PURE__ */ new Map();
|
|
34745
34788
|
for (const entry of entries) {
|
|
34746
|
-
if (
|
|
34747
|
-
continue;
|
|
34748
|
-
const nameValue = (entry.split(";")[0] ?? "").trim();
|
|
34749
|
-
const eqIdx = nameValue.indexOf("=");
|
|
34750
|
-
if (eqIdx < 1)
|
|
34789
|
+
if (isDeletionCookie(entry))
|
|
34751
34790
|
continue;
|
|
34752
|
-
const
|
|
34753
|
-
|
|
34754
|
-
if (!value)
|
|
34791
|
+
const pair = parseNameValue(entry);
|
|
34792
|
+
if (!pair || !pair.value)
|
|
34755
34793
|
continue;
|
|
34756
|
-
jar.set(name, value);
|
|
34794
|
+
jar.set(pair.name, pair.value);
|
|
34757
34795
|
}
|
|
34758
34796
|
const cookies = {};
|
|
34759
34797
|
for (const [k, v] of jar)
|
|
@@ -34761,6 +34799,18 @@ function parseCookieJar(setCookieHeaders) {
|
|
|
34761
34799
|
const cookieHeader = [...jar.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
|
|
34762
34800
|
return { cookies, cookieHeader };
|
|
34763
34801
|
}
|
|
34802
|
+
function setCookieEntries(source2) {
|
|
34803
|
+
if (source2 == null)
|
|
34804
|
+
return [];
|
|
34805
|
+
if (Array.isArray(source2))
|
|
34806
|
+
return source2;
|
|
34807
|
+
if (typeof source2 === "string")
|
|
34808
|
+
return splitSetCookie(source2);
|
|
34809
|
+
if (typeof source2.getSetCookie === "function")
|
|
34810
|
+
return source2.getSetCookie();
|
|
34811
|
+
const joined = source2.get("set-cookie");
|
|
34812
|
+
return joined ? splitSetCookie(joined) : [];
|
|
34813
|
+
}
|
|
34764
34814
|
function splitSetCookie(header) {
|
|
34765
34815
|
return header.split(/,(?=\s*[^;,\s]+\s*=)/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
34766
34816
|
}
|
|
@@ -34771,6 +34821,8 @@ var NonNegInt = external_exports.number().int().nonnegative();
|
|
|
34771
34821
|
var NonEmptyString = external_exports.string().min(1);
|
|
34772
34822
|
var IsoDate = external_exports.iso.date();
|
|
34773
34823
|
var IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
|
|
34824
|
+
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.");
|
|
34825
|
+
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
34826
|
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
34827
|
var schemaConfirm = external_exports.boolean().optional().describe("Must be true to proceed. Without this, the tool returns a preview.");
|
|
34776
34828
|
var paginationSchema = {
|
|
@@ -35738,10 +35790,20 @@ async function openEncryptedFrame(sessionKey, frame) {
|
|
|
35738
35790
|
|
|
35739
35791
|
// node_modules/@fetchproxy/server/dist/election.js
|
|
35740
35792
|
import { createServer, Server as HttpServer } from "node:http";
|
|
35793
|
+
var DEFAULT_BIND_TIMEOUT_MS = 5e3;
|
|
35741
35794
|
async function electRole(opts) {
|
|
35742
35795
|
const server = createServer();
|
|
35796
|
+
const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
|
|
35743
35797
|
return new Promise((resolve, reject) => {
|
|
35798
|
+
let timer;
|
|
35799
|
+
const clearBindTimer = () => {
|
|
35800
|
+
if (timer) {
|
|
35801
|
+
clearTimeout(timer);
|
|
35802
|
+
timer = void 0;
|
|
35803
|
+
}
|
|
35804
|
+
};
|
|
35744
35805
|
const onError = (e) => {
|
|
35806
|
+
clearBindTimer();
|
|
35745
35807
|
server.removeListener("listening", onListening);
|
|
35746
35808
|
if (e.code === "EADDRINUSE") {
|
|
35747
35809
|
try {
|
|
@@ -35754,11 +35816,25 @@ async function electRole(opts) {
|
|
|
35754
35816
|
}
|
|
35755
35817
|
};
|
|
35756
35818
|
const onListening = () => {
|
|
35819
|
+
clearBindTimer();
|
|
35757
35820
|
server.removeListener("error", onError);
|
|
35758
35821
|
resolve({ role: "host", server });
|
|
35759
35822
|
};
|
|
35760
35823
|
server.once("error", onError);
|
|
35761
35824
|
server.once("listening", onListening);
|
|
35825
|
+
if (bindTimeoutMs > 0) {
|
|
35826
|
+
timer = setTimeout(() => {
|
|
35827
|
+
server.removeListener("error", onError);
|
|
35828
|
+
server.removeListener("listening", onListening);
|
|
35829
|
+
try {
|
|
35830
|
+
server.close();
|
|
35831
|
+
} catch {
|
|
35832
|
+
}
|
|
35833
|
+
reject(new Error(`fetchproxy: bind to ${opts.host}:${opts.port} timed out after ${bindTimeoutMs}ms (port may be in a bad state)`));
|
|
35834
|
+
}, bindTimeoutMs);
|
|
35835
|
+
if (typeof timer.unref === "function")
|
|
35836
|
+
timer.unref();
|
|
35837
|
+
}
|
|
35762
35838
|
server.listen(opts.port, opts.host);
|
|
35763
35839
|
});
|
|
35764
35840
|
}
|
|
@@ -35851,6 +35927,44 @@ var SessionState = class {
|
|
|
35851
35927
|
}
|
|
35852
35928
|
};
|
|
35853
35929
|
|
|
35930
|
+
// node_modules/@fetchproxy/server/dist/session-ready.js
|
|
35931
|
+
var SESSION_READY_TIMEOUT_MS = 3e4;
|
|
35932
|
+
var FetchproxySessionNotReadyError = class extends Error {
|
|
35933
|
+
reason;
|
|
35934
|
+
pairCode;
|
|
35935
|
+
mcpId;
|
|
35936
|
+
hint;
|
|
35937
|
+
constructor(info) {
|
|
35938
|
+
const pairing = info.pairCode !== null && info.pairCode !== "";
|
|
35939
|
+
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.`;
|
|
35940
|
+
super(`fetchproxy: ${pairing ? "pairing not yet approved" : "no confirmed browser session"} for "${info.mcpId}". ${hint}`);
|
|
35941
|
+
this.name = "FetchproxySessionNotReadyError";
|
|
35942
|
+
this.reason = pairing ? "pair-required" : "not-ready";
|
|
35943
|
+
this.pairCode = pairing ? info.pairCode : null;
|
|
35944
|
+
this.mcpId = info.mcpId;
|
|
35945
|
+
this.hint = hint;
|
|
35946
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
35947
|
+
}
|
|
35948
|
+
};
|
|
35949
|
+
async function awaitSessionReady(ready, opts) {
|
|
35950
|
+
const ms = opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS;
|
|
35951
|
+
if (ms <= 0)
|
|
35952
|
+
return ready;
|
|
35953
|
+
let timer;
|
|
35954
|
+
const timeout = new Promise((_, reject) => {
|
|
35955
|
+
timer = setTimeout(() => {
|
|
35956
|
+
reject(new FetchproxySessionNotReadyError({ mcpId: opts.mcpId, pairCode: opts.pendingPairCode() }));
|
|
35957
|
+
}, ms);
|
|
35958
|
+
timer.unref?.();
|
|
35959
|
+
});
|
|
35960
|
+
try {
|
|
35961
|
+
return await Promise.race([ready, timeout]);
|
|
35962
|
+
} finally {
|
|
35963
|
+
if (timer)
|
|
35964
|
+
clearTimeout(timer);
|
|
35965
|
+
}
|
|
35966
|
+
}
|
|
35967
|
+
|
|
35854
35968
|
// node_modules/@fetchproxy/server/dist/host.js
|
|
35855
35969
|
var PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
|
|
35856
35970
|
var enc2 = new TextEncoder();
|
|
@@ -35938,6 +36052,29 @@ async function startHost(opts) {
|
|
|
35938
36052
|
return;
|
|
35939
36053
|
}
|
|
35940
36054
|
if (frame.type === "hello" && frame.role === "server") {
|
|
36055
|
+
const peerEdPub = fromB64(frame.identityEd25519Pub);
|
|
36056
|
+
const peerSigMsg = concatBytes(enc2.encode(frame.mcpId), fromB64(frame.sessionNonce));
|
|
36057
|
+
const peerSig = fromB64(frame.sessionSig);
|
|
36058
|
+
let peerSigOk = false;
|
|
36059
|
+
try {
|
|
36060
|
+
peerSigOk = await ed25519Verify(peerEdPub, peerSigMsg, peerSig);
|
|
36061
|
+
} catch {
|
|
36062
|
+
peerSigOk = false;
|
|
36063
|
+
}
|
|
36064
|
+
if (!peerSigOk) {
|
|
36065
|
+
console.warn("[fetchproxy] peer hello signature invalid \u2014 refusing registration (possible squatter)");
|
|
36066
|
+
ws.close(1008, "peer hello signature invalid");
|
|
36067
|
+
return;
|
|
36068
|
+
}
|
|
36069
|
+
const existing = peers.get(frame.mcpId);
|
|
36070
|
+
if (existing && existing.ws !== ws) {
|
|
36071
|
+
const existingEdPub = existing.helloFrame.identityEd25519Pub;
|
|
36072
|
+
if (existingEdPub !== frame.identityEd25519Pub) {
|
|
36073
|
+
console.warn("[fetchproxy] peer mcpId already mapped to a different identity \u2014 refusing (mcpId squatting)");
|
|
36074
|
+
ws.close(1008, "mcpId already registered to another identity");
|
|
36075
|
+
return;
|
|
36076
|
+
}
|
|
36077
|
+
}
|
|
35941
36078
|
identified = "peer";
|
|
35942
36079
|
peerMcpId = frame.mcpId;
|
|
35943
36080
|
peers.set(frame.mcpId, { ws, helloFrame: frame });
|
|
@@ -36030,8 +36167,10 @@ async function startHost(opts) {
|
|
|
36030
36167
|
resetSessionPromise();
|
|
36031
36168
|
disconnectListeners.forEach((cb) => cb());
|
|
36032
36169
|
}
|
|
36033
|
-
if (identified === "peer" && peerMcpId)
|
|
36034
|
-
peers.
|
|
36170
|
+
if (identified === "peer" && peerMcpId) {
|
|
36171
|
+
if (peers.get(peerMcpId)?.ws === ws)
|
|
36172
|
+
peers.delete(peerMcpId);
|
|
36173
|
+
}
|
|
36035
36174
|
});
|
|
36036
36175
|
});
|
|
36037
36176
|
return {
|
|
@@ -36047,7 +36186,10 @@ async function startHost(opts) {
|
|
|
36047
36186
|
});
|
|
36048
36187
|
}),
|
|
36049
36188
|
sendOwnInner: async (inner) => {
|
|
36050
|
-
const session = await ownSessionReady
|
|
36189
|
+
const session = await awaitSessionReady(ownSessionReady, {
|
|
36190
|
+
mcpId: opts.ownMcpId,
|
|
36191
|
+
pendingPairCode: () => ownPendingPairCode
|
|
36192
|
+
});
|
|
36051
36193
|
if (!extensionWs)
|
|
36052
36194
|
throw new Error("host: no extension connected");
|
|
36053
36195
|
const sealed = await sealInnerFrame(session.sessionKey, opts.ownMcpId, session.nextOutboundSeq(), inner);
|
|
@@ -36152,7 +36294,10 @@ async function startPeer(opts) {
|
|
|
36152
36294
|
ws,
|
|
36153
36295
|
session: sessionPromise,
|
|
36154
36296
|
sendInner: async (inner) => {
|
|
36155
|
-
await sessionPromise
|
|
36297
|
+
await awaitSessionReady(sessionPromise, {
|
|
36298
|
+
mcpId: opts.mcpId,
|
|
36299
|
+
pendingPairCode: () => pendingPairCode
|
|
36300
|
+
});
|
|
36156
36301
|
const s = session;
|
|
36157
36302
|
const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
|
|
36158
36303
|
ws.send(JSON.stringify(sealed));
|
|
@@ -36818,6 +36963,35 @@ var FetchproxyServer = class {
|
|
|
36818
36963
|
this.keepAliveTimer = null;
|
|
36819
36964
|
}
|
|
36820
36965
|
}
|
|
36966
|
+
/**
|
|
36967
|
+
* Send an inner request frame via whichever bridge handle is active. If the
|
|
36968
|
+
* send throws (e.g. `FetchproxySessionNotReadyError` — the session never
|
|
36969
|
+
* confirmed), the frame never reached the bridge, so no reply will arrive:
|
|
36970
|
+
* drop the just-registered pending resolver for this id (it lives in exactly
|
|
36971
|
+
* one of the op maps — request ids are unique) so it doesn't leak until the
|
|
36972
|
+
* server closes, then rethrow.
|
|
36973
|
+
*/
|
|
36974
|
+
async sendInnerFrame(inner) {
|
|
36975
|
+
try {
|
|
36976
|
+
if (this.hostHandle) {
|
|
36977
|
+
await this.hostHandle.sendOwnInner(inner);
|
|
36978
|
+
} else if (this.peerHandle) {
|
|
36979
|
+
await this.peerHandle.sendInner(inner);
|
|
36980
|
+
}
|
|
36981
|
+
} catch (err) {
|
|
36982
|
+
if ("id" in inner && typeof inner.id === "number") {
|
|
36983
|
+
const { id } = inner;
|
|
36984
|
+
this.pending.delete(id);
|
|
36985
|
+
this.pendingReadCookies.delete(id);
|
|
36986
|
+
this.pendingStorage.delete(id);
|
|
36987
|
+
this.pendingCapture.delete(id);
|
|
36988
|
+
this.pendingRedirect.delete(id);
|
|
36989
|
+
this.pendingDownload.delete(id);
|
|
36990
|
+
this.pendingIdb.delete(id);
|
|
36991
|
+
}
|
|
36992
|
+
throw err;
|
|
36993
|
+
}
|
|
36994
|
+
}
|
|
36821
36995
|
/**
|
|
36822
36996
|
* Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
|
|
36823
36997
|
* On timeout returns the `{ok:false, kind:'timeout'}` envelope —
|
|
@@ -36829,11 +37003,7 @@ var FetchproxyServer = class {
|
|
|
36829
37003
|
const pending = new Promise((resolve) => {
|
|
36830
37004
|
this.pending.set(id, resolve);
|
|
36831
37005
|
});
|
|
36832
|
-
|
|
36833
|
-
await this.hostHandle.sendOwnInner(inner);
|
|
36834
|
-
} else if (this.peerHandle) {
|
|
36835
|
-
await this.peerHandle.sendInner(inner);
|
|
36836
|
-
}
|
|
37006
|
+
await this.sendInnerFrame(inner);
|
|
36837
37007
|
const timeoutMs = this.opts.fetchTimeoutMs;
|
|
36838
37008
|
if (timeoutMs === void 0 || timeoutMs <= 0)
|
|
36839
37009
|
return pending;
|
|
@@ -36862,6 +37032,51 @@ var FetchproxyServer = class {
|
|
|
36862
37032
|
clearTimeout(timer);
|
|
36863
37033
|
}
|
|
36864
37034
|
}
|
|
37035
|
+
/**
|
|
37036
|
+
* FP-B2: bound a non-`fetch` verb's reply wait by `fetchTimeoutMs`.
|
|
37037
|
+
*
|
|
37038
|
+
* Before this, only `fetch()` raced its `pending` reply against a timer
|
|
37039
|
+
* (`_fetchOnceWithTimeout`); `readCookies`, the storage reads,
|
|
37040
|
+
* `capture_request_header`, `capture_redirect`, `download`, and
|
|
37041
|
+
* `read_indexed_db` awaited their `pending` promise with no race, so a
|
|
37042
|
+
* wedged extension hung the tool call indefinitely.
|
|
37043
|
+
*
|
|
37044
|
+
* `pending` is the already-registered reply promise. `pendingMap`/`id`
|
|
37045
|
+
* point at the op-specific map entry so we can drop it on expiry exactly
|
|
37046
|
+
* as `_fetchOnceWithTimeout` does — otherwise a late bridge reply would
|
|
37047
|
+
* resolve into a stale resolver / leak. On timeout we reject with a
|
|
37048
|
+
* `FetchproxyTimeoutError` (the same throwable the convenience methods
|
|
37049
|
+
* already surface for fetch timeouts). `0`/unset opts out (unbounded),
|
|
37050
|
+
* matching the fetch path.
|
|
37051
|
+
*/
|
|
37052
|
+
async _withVerbTimeout(pending, pendingMap, id, url2) {
|
|
37053
|
+
const timeoutMs = this.opts.fetchTimeoutMs;
|
|
37054
|
+
if (timeoutMs === void 0 || timeoutMs <= 0)
|
|
37055
|
+
return pending;
|
|
37056
|
+
let timer;
|
|
37057
|
+
const start = Date.now();
|
|
37058
|
+
try {
|
|
37059
|
+
return await Promise.race([
|
|
37060
|
+
pending,
|
|
37061
|
+
new Promise((_resolve, reject) => {
|
|
37062
|
+
timer = setTimeout(() => {
|
|
37063
|
+
pendingMap.delete(id);
|
|
37064
|
+
reject(new FetchproxyTimeoutError({
|
|
37065
|
+
url: url2,
|
|
37066
|
+
timeoutMs,
|
|
37067
|
+
role: this.role,
|
|
37068
|
+
port: this.opts.port,
|
|
37069
|
+
elapsedMs: Date.now() - start,
|
|
37070
|
+
retryAttempted: false
|
|
37071
|
+
}));
|
|
37072
|
+
}, timeoutMs);
|
|
37073
|
+
})
|
|
37074
|
+
]);
|
|
37075
|
+
} finally {
|
|
37076
|
+
if (timer)
|
|
37077
|
+
clearTimeout(timer);
|
|
37078
|
+
}
|
|
37079
|
+
}
|
|
36865
37080
|
/**
|
|
36866
37081
|
* Map an `ok:false` fetch result to its typed throwable. Centralizes
|
|
36867
37082
|
* the kind-to-error-class switch so `request()` and (via the same
|
|
@@ -37152,12 +37367,8 @@ var FetchproxyServer = class {
|
|
|
37152
37367
|
const pending = new Promise((resolve) => {
|
|
37153
37368
|
this.pendingReadCookies.set(id, resolve);
|
|
37154
37369
|
});
|
|
37155
|
-
|
|
37156
|
-
|
|
37157
|
-
} else if (this.peerHandle) {
|
|
37158
|
-
await this.peerHandle.sendInner(inner);
|
|
37159
|
-
}
|
|
37160
|
-
const result = await pending;
|
|
37370
|
+
await this.sendInnerFrame(inner);
|
|
37371
|
+
const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
|
|
37161
37372
|
if (!result.ok) {
|
|
37162
37373
|
throw new FetchproxyProtocolError(result.error);
|
|
37163
37374
|
}
|
|
@@ -37223,12 +37434,8 @@ var FetchproxyServer = class {
|
|
|
37223
37434
|
const pending = new Promise((resolve, reject) => {
|
|
37224
37435
|
this.pendingStorage.set(id, { resolve, reject });
|
|
37225
37436
|
});
|
|
37226
|
-
|
|
37227
|
-
|
|
37228
|
-
} else if (this.peerHandle) {
|
|
37229
|
-
await this.peerHandle.sendInner(inner);
|
|
37230
|
-
}
|
|
37231
|
-
return pending;
|
|
37437
|
+
await this.sendInnerFrame(inner);
|
|
37438
|
+
return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
|
|
37232
37439
|
}
|
|
37233
37440
|
/**
|
|
37234
37441
|
* 0.3.0+: snapshot the next outgoing request's named header. Single-
|
|
@@ -37332,12 +37539,8 @@ var FetchproxyServer = class {
|
|
|
37332
37539
|
const pending = new Promise((resolve, reject) => {
|
|
37333
37540
|
this.pendingCapture.set(id, { resolve, reject });
|
|
37334
37541
|
});
|
|
37335
|
-
|
|
37336
|
-
|
|
37337
|
-
} else if (this.peerHandle) {
|
|
37338
|
-
await this.peerHandle.sendInner(inner);
|
|
37339
|
-
}
|
|
37340
|
-
return pending;
|
|
37542
|
+
await this.sendInnerFrame(inner);
|
|
37543
|
+
return this._withVerbTimeout(pending, this.pendingCapture, id, `https://${opts.host}${opts.path ?? "/*"}`);
|
|
37341
37544
|
}
|
|
37342
37545
|
/**
|
|
37343
37546
|
* Snapshot the redirect target URL of the next request the browser
|
|
@@ -37421,12 +37624,8 @@ var FetchproxyServer = class {
|
|
|
37421
37624
|
const pending = new Promise((resolve, reject) => {
|
|
37422
37625
|
this.pendingRedirect.set(id, { resolve, reject });
|
|
37423
37626
|
});
|
|
37424
|
-
|
|
37425
|
-
|
|
37426
|
-
} else if (this.peerHandle) {
|
|
37427
|
-
await this.peerHandle.sendInner(inner);
|
|
37428
|
-
}
|
|
37429
|
-
return pending;
|
|
37627
|
+
await this.sendInnerFrame(inner);
|
|
37628
|
+
return this._withVerbTimeout(pending, this.pendingRedirect, id, `https://${opts.host}${opts.path ?? "/*"}`);
|
|
37430
37629
|
}
|
|
37431
37630
|
/**
|
|
37432
37631
|
* Download `url` through the BROWSER's own network stack via
|
|
@@ -37507,12 +37706,8 @@ var FetchproxyServer = class {
|
|
|
37507
37706
|
const pending = new Promise((resolve, reject) => {
|
|
37508
37707
|
this.pendingDownload.set(id, { resolve, reject });
|
|
37509
37708
|
});
|
|
37510
|
-
|
|
37511
|
-
|
|
37512
|
-
} else if (this.peerHandle) {
|
|
37513
|
-
await this.peerHandle.sendInner(inner);
|
|
37514
|
-
}
|
|
37515
|
-
return pending;
|
|
37709
|
+
await this.sendInnerFrame(inner);
|
|
37710
|
+
return this._withVerbTimeout(pending, this.pendingDownload, id, opts.url);
|
|
37516
37711
|
}
|
|
37517
37712
|
/**
|
|
37518
37713
|
* 0.4.0+: read declared IndexedDB keys from the user's signed-in
|
|
@@ -37559,12 +37754,8 @@ var FetchproxyServer = class {
|
|
|
37559
37754
|
const pending = new Promise((resolve, reject) => {
|
|
37560
37755
|
this.pendingIdb.set(id, { resolve, reject });
|
|
37561
37756
|
});
|
|
37562
|
-
|
|
37563
|
-
|
|
37564
|
-
} else if (this.peerHandle) {
|
|
37565
|
-
await this.peerHandle.sendInner(inner);
|
|
37566
|
-
}
|
|
37567
|
-
return pending;
|
|
37757
|
+
await this.sendInnerFrame(inner);
|
|
37758
|
+
return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
|
|
37568
37759
|
}
|
|
37569
37760
|
assertScopeSubset(requested, declared, label) {
|
|
37570
37761
|
const undeclared = undeclaredKeys(requested, declared);
|
|
@@ -38000,7 +38191,7 @@ function loadAccount(env = process.env) {
|
|
|
38000
38191
|
// package.json
|
|
38001
38192
|
var package_default = {
|
|
38002
38193
|
name: "infinitecampus-mcp",
|
|
38003
|
-
version: "2.3.
|
|
38194
|
+
version: "2.3.6",
|
|
38004
38195
|
mcpName: "io.github.chrischall/infinitecampus-mcp",
|
|
38005
38196
|
description: "Infinite Campus (Campus Parent) MCP server \u2014 multi-district read + message/document write",
|
|
38006
38197
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -38042,16 +38233,16 @@ var package_default = {
|
|
|
38042
38233
|
"test:watch": "vitest"
|
|
38043
38234
|
},
|
|
38044
38235
|
dependencies: {
|
|
38045
|
-
"@chrischall/mcp-utils": "^0.
|
|
38236
|
+
"@chrischall/mcp-utils": "^0.12.0",
|
|
38046
38237
|
"@fetchproxy/bootstrap": "^1.0.0",
|
|
38047
38238
|
"@fetchproxy/server": "^1.0.0",
|
|
38048
38239
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38049
38240
|
dotenv: "^17.4.2",
|
|
38050
|
-
"node-html-parser": "^
|
|
38241
|
+
"node-html-parser": "^8.0.2",
|
|
38051
38242
|
zod: "^4.4.3"
|
|
38052
38243
|
},
|
|
38053
38244
|
devDependencies: {
|
|
38054
|
-
"@types/node": "^
|
|
38245
|
+
"@types/node": "^26.0.0",
|
|
38055
38246
|
"@vitest/coverage-v8": "^4.1.7",
|
|
38056
38247
|
esbuild: "^0.28.0",
|
|
38057
38248
|
typescript: "^6.0.3",
|
|
@@ -38130,10 +38321,163 @@ async function resolveAuth() {
|
|
|
38130
38321
|
// src/client.ts
|
|
38131
38322
|
import { writeFile as writeFile2, stat } from "fs/promises";
|
|
38132
38323
|
import { dirname } from "path";
|
|
38324
|
+
|
|
38325
|
+
// node_modules/@chrischall/mcp-utils/dist/session/index.js
|
|
38326
|
+
var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
|
|
38327
|
+
var CookieSessionManager = class {
|
|
38328
|
+
session;
|
|
38329
|
+
inFlight;
|
|
38330
|
+
/** A cached *permanent* config error: once set, every `ensure` rethrows it. */
|
|
38331
|
+
permanentError;
|
|
38332
|
+
/** When the current session was minted (login) or installed (seed) — for maxAgeMs. */
|
|
38333
|
+
sessionAt = 0;
|
|
38334
|
+
loginFn;
|
|
38335
|
+
isExpiredFn;
|
|
38336
|
+
isPermanentErrorFn;
|
|
38337
|
+
maxAgeMs;
|
|
38338
|
+
now;
|
|
38339
|
+
onReplayLoginErrorFn;
|
|
38340
|
+
constructor(opts) {
|
|
38341
|
+
this.loginFn = opts.login;
|
|
38342
|
+
this.isExpiredFn = opts.isExpired ?? (() => false);
|
|
38343
|
+
this.isPermanentErrorFn = opts.isPermanentError ?? (() => false);
|
|
38344
|
+
this.maxAgeMs = opts.maxAgeMs;
|
|
38345
|
+
this.now = opts.now ?? Date.now;
|
|
38346
|
+
this.onReplayLoginErrorFn = opts.onReplayLoginError;
|
|
38347
|
+
}
|
|
38348
|
+
/** The current session, or `undefined` before the first successful login. */
|
|
38349
|
+
get current() {
|
|
38350
|
+
return this.session;
|
|
38351
|
+
}
|
|
38352
|
+
/** True when {@link CookieSessionManagerOptions.maxAgeMs} says the session is too old. */
|
|
38353
|
+
isStale() {
|
|
38354
|
+
return this.maxAgeMs !== void 0 && this.now() - this.sessionAt >= this.maxAgeMs;
|
|
38355
|
+
}
|
|
38356
|
+
/**
|
|
38357
|
+
* Return the current session, or single-flight a login if there is none.
|
|
38358
|
+
*
|
|
38359
|
+
* Concurrent callers share one in-flight login; the in-flight promise is
|
|
38360
|
+
* cleared on settle (success OR failure) so a rejected login never sticks and
|
|
38361
|
+
* the next call retries. A login error classified permanent by
|
|
38362
|
+
* {@link CookieSessionManagerOptions.isPermanentError} is cached and rethrown
|
|
38363
|
+
* on every later call; a transient error is not cached (next call retries).
|
|
38364
|
+
* With {@link CookieSessionManagerOptions.maxAgeMs} set, a session past its
|
|
38365
|
+
* TTL is invalidated first, so the call falls through to a (single-flight)
|
|
38366
|
+
* re-login.
|
|
38367
|
+
*/
|
|
38368
|
+
async ensure() {
|
|
38369
|
+
if (this.session !== void 0) {
|
|
38370
|
+
if (!this.isStale())
|
|
38371
|
+
return this.session;
|
|
38372
|
+
this.invalidate();
|
|
38373
|
+
}
|
|
38374
|
+
if (this.permanentError !== void 0)
|
|
38375
|
+
throw this.permanentError;
|
|
38376
|
+
if (this.inFlight === void 0) {
|
|
38377
|
+
this.inFlight = this.runLogin();
|
|
38378
|
+
}
|
|
38379
|
+
return this.inFlight;
|
|
38380
|
+
}
|
|
38381
|
+
/**
|
|
38382
|
+
* Install an externally-minted session (e.g. infinitecampus's CUPS linked-
|
|
38383
|
+
* district discovery, which mints sessions outside {@link
|
|
38384
|
+
* CookieSessionManagerOptions.login}). The seed becomes the current session
|
|
38385
|
+
* with a fresh {@link CookieSessionManagerOptions.maxAgeMs} clock, and any
|
|
38386
|
+
* in-flight login is DETACHED: its waiters still receive its result, but a
|
|
38387
|
+
* late resolution will not overwrite the seed. A cached permanent config
|
|
38388
|
+
* error is left intact — the login path is still misconfigured, and the next
|
|
38389
|
+
* post-seed re-login should keep saying so.
|
|
38390
|
+
*/
|
|
38391
|
+
seed(session) {
|
|
38392
|
+
this.session = session;
|
|
38393
|
+
this.sessionAt = this.now();
|
|
38394
|
+
this.inFlight = void 0;
|
|
38395
|
+
}
|
|
38396
|
+
/**
|
|
38397
|
+
* One login attempt. Self-clears `inFlight` on settle so a rejected login
|
|
38398
|
+
* never sticks, and stamps the resulting session only while it's still the
|
|
38399
|
+
* live attempt (an `invalidate()` mid-flight cleared `inFlight` — a stale
|
|
38400
|
+
* resolution must not re-stamp the dropped session over the new state).
|
|
38401
|
+
*/
|
|
38402
|
+
runLogin() {
|
|
38403
|
+
const holder = {};
|
|
38404
|
+
holder.p = (async () => {
|
|
38405
|
+
try {
|
|
38406
|
+
const session = await this.loginFn();
|
|
38407
|
+
if (this.inFlight === holder.p) {
|
|
38408
|
+
this.session = session;
|
|
38409
|
+
this.sessionAt = this.now();
|
|
38410
|
+
}
|
|
38411
|
+
return session;
|
|
38412
|
+
} catch (err) {
|
|
38413
|
+
if (this.isPermanentErrorFn(err))
|
|
38414
|
+
this.permanentError = err;
|
|
38415
|
+
throw err;
|
|
38416
|
+
} finally {
|
|
38417
|
+
if (this.inFlight === holder.p)
|
|
38418
|
+
this.inFlight = void 0;
|
|
38419
|
+
}
|
|
38420
|
+
})();
|
|
38421
|
+
return holder.p;
|
|
38422
|
+
}
|
|
38423
|
+
/**
|
|
38424
|
+
* Drop the current session (and any in-flight login) so the next
|
|
38425
|
+
* {@link ensure} re-runs {@link CookieSessionManagerOptions.login}. Does NOT
|
|
38426
|
+
* clear a cached permanent config error — that only clears on a fresh process
|
|
38427
|
+
* (new config). Used to recover from a detected session expiry.
|
|
38428
|
+
*/
|
|
38429
|
+
invalidate() {
|
|
38430
|
+
this.session = void 0;
|
|
38431
|
+
this.inFlight = void 0;
|
|
38432
|
+
}
|
|
38433
|
+
/**
|
|
38434
|
+
* Run an authenticated `call` with the current session and reactive
|
|
38435
|
+
* expiry-replay. `call` receives the session and returns a `Response`. If
|
|
38436
|
+
* {@link CookieSessionManagerOptions.isExpired} flags the response, the
|
|
38437
|
+
* session is invalidated, a single-flight re-login runs, and `call` is
|
|
38438
|
+
* replayed EXACTLY once. A persistent expiry surfaces the (second) response
|
|
38439
|
+
* rather than looping. If the re-login itself fails, the original
|
|
38440
|
+
* (expired-looking) response is returned so the caller can surface a clean
|
|
38441
|
+
* sign-in error rather than the login failure.
|
|
38442
|
+
*
|
|
38443
|
+
* `call` resolves to `R` (the generic response type, default {@link Response});
|
|
38444
|
+
* the manager passes it untouched to {@link CookieSessionManagerOptions.isExpired}
|
|
38445
|
+
* and returns it untouched, so a custom transport type flows through cleanly.
|
|
38446
|
+
*/
|
|
38447
|
+
async withSession(call) {
|
|
38448
|
+
const session = await this.ensure();
|
|
38449
|
+
const res = await call(session);
|
|
38450
|
+
if (!await this.isExpiredFn(res))
|
|
38451
|
+
return res;
|
|
38452
|
+
if (this.current === session)
|
|
38453
|
+
this.invalidate();
|
|
38454
|
+
let fresh;
|
|
38455
|
+
try {
|
|
38456
|
+
fresh = await this.ensure();
|
|
38457
|
+
} catch (err) {
|
|
38458
|
+
this.onReplayLoginErrorFn?.(err);
|
|
38459
|
+
return res;
|
|
38460
|
+
}
|
|
38461
|
+
return call(fresh);
|
|
38462
|
+
}
|
|
38463
|
+
};
|
|
38464
|
+
function createCookieSessionManager(opts) {
|
|
38465
|
+
return new CookieSessionManager(opts);
|
|
38466
|
+
}
|
|
38467
|
+
|
|
38468
|
+
// src/client.ts
|
|
38133
38469
|
var SESSION_TTL_MS = 5 * 60 * 60 * 1e3;
|
|
38134
38470
|
var ICClient = class {
|
|
38135
38471
|
accounts = /* @__PURE__ */ new Map();
|
|
38136
|
-
|
|
38472
|
+
/**
|
|
38473
|
+
* One CookieSessionManager per district (primary + each discovered linked
|
|
38474
|
+
* district). The manager owns the session lifecycle: single-flight login,
|
|
38475
|
+
* `withSession` exactly-one 401 replay, `invalidate()`, permanent-error
|
|
38476
|
+
* caching, proactive `maxAgeMs` (SESSION_TTL_MS) expiry, and — for linked
|
|
38477
|
+
* districts — the CUPS-minted session installed via `seed()` during
|
|
38478
|
+
* discovery.
|
|
38479
|
+
*/
|
|
38480
|
+
managers = /* @__PURE__ */ new Map();
|
|
38137
38481
|
linkedTo = /* @__PURE__ */ new Map();
|
|
38138
38482
|
// linkedDistrictName → primaryDistrictName
|
|
38139
38483
|
primaryName;
|
|
@@ -38147,6 +38491,8 @@ var ICClient = class {
|
|
|
38147
38491
|
*/
|
|
38148
38492
|
fetchproxyMode = false;
|
|
38149
38493
|
fetchproxyDiscoveryRan = false;
|
|
38494
|
+
/** fetchproxy cookies, consumed by the primary manager's first login. */
|
|
38495
|
+
preloaded = null;
|
|
38150
38496
|
/**
|
|
38151
38497
|
* `preloaded` is the fetchproxy escape hatch: when set, the client treats
|
|
38152
38498
|
* the supplied cookies as a freshly-completed login on the primary account.
|
|
@@ -38159,21 +38505,66 @@ var ICClient = class {
|
|
|
38159
38505
|
constructor(account2, opts = {}) {
|
|
38160
38506
|
this.accounts.set(account2.name, account2);
|
|
38161
38507
|
this.primaryName = account2.name;
|
|
38508
|
+
this.managers.set(account2.name, this.makeManager(account2));
|
|
38162
38509
|
if (opts.preloaded) {
|
|
38163
|
-
this.
|
|
38164
|
-
cookie: opts.preloaded.cookieHeader,
|
|
38165
|
-
xsrfToken: opts.preloaded.xsrfToken,
|
|
38166
|
-
loggedInAt: Date.now(),
|
|
38167
|
-
loginInFlight: null
|
|
38168
|
-
});
|
|
38510
|
+
this.preloaded = opts.preloaded;
|
|
38169
38511
|
this.fetchproxyMode = true;
|
|
38170
38512
|
}
|
|
38171
38513
|
}
|
|
38514
|
+
/**
|
|
38515
|
+
* Build the per-district CookieSessionManager.
|
|
38516
|
+
*
|
|
38517
|
+
* - `maxAgeMs: SESSION_TTL_MS` makes `ensure()` proactively invalidate and
|
|
38518
|
+
* re-login a session older than 5h (single-flight), replacing IC's old
|
|
38519
|
+
* hand-rolled `ensureFresh` wrapper. `now` reads `Date.now` at call time so
|
|
38520
|
+
* the clock stays mockable in tests.
|
|
38521
|
+
* - `isPermanentError` caches the one config error the process can never
|
|
38522
|
+
* recover from (fetchproxy no-creds) so `ensure()` fails fast instead of
|
|
38523
|
+
* pointlessly retrying a login that can never succeed.
|
|
38524
|
+
* - `onReplayLoginError` rethrows the re-login failure from `withSession`'s
|
|
38525
|
+
* expiry-replay path. By default that failure is swallowed and the stale
|
|
38526
|
+
* 401 returned; rethrowing surfaces the actionable error (e.g. fetchproxy's
|
|
38527
|
+
* "sign back into your IC portal in the browser") to the caller instead of
|
|
38528
|
+
* a generic SessionExpiredError — the behavior IC's `lastLoginError`
|
|
38529
|
+
* bookkeeping used to provide.
|
|
38530
|
+
*/
|
|
38531
|
+
makeManager(account2) {
|
|
38532
|
+
return createCookieSessionManager({
|
|
38533
|
+
login: () => this.login(account2),
|
|
38534
|
+
isExpired: (res) => this.detectExpiredSession(account2.name, res),
|
|
38535
|
+
isPermanentError: (e) => e instanceof AuthFailedError && e.permanent,
|
|
38536
|
+
maxAgeMs: SESSION_TTL_MS,
|
|
38537
|
+
now: () => Date.now(),
|
|
38538
|
+
onReplayLoginError: (err) => {
|
|
38539
|
+
throw err;
|
|
38540
|
+
}
|
|
38541
|
+
});
|
|
38542
|
+
}
|
|
38543
|
+
/**
|
|
38544
|
+
* `isExpired` predicate for `withSession`: a 401 means the session died.
|
|
38545
|
+
* Side effect for linked districts: the shared CUPS SSO umbrella died with
|
|
38546
|
+
* it, so drop the primary and every *sibling* linked session too — the
|
|
38547
|
+
* linked district's re-login (via `login()`'s linked branch) then re-logs
|
|
38548
|
+
* the primary in, whose discovery re-establishes all of them. `withSession`
|
|
38549
|
+
* invalidates the requesting district itself. Mirrors the old hand-rolled
|
|
38550
|
+
* doRequest 401 handling.
|
|
38551
|
+
*/
|
|
38552
|
+
detectExpiredSession(district, res) {
|
|
38553
|
+
if (res.status !== 401) return false;
|
|
38554
|
+
const primaryName = this.linkedTo.get(district);
|
|
38555
|
+
if (primaryName) {
|
|
38556
|
+
this.managers.get(primaryName).invalidate();
|
|
38557
|
+
for (const linked of this.linkedTo.keys()) {
|
|
38558
|
+
if (linked !== district) this.managers.get(linked).invalidate();
|
|
38559
|
+
}
|
|
38560
|
+
}
|
|
38561
|
+
return true;
|
|
38562
|
+
}
|
|
38172
38563
|
async ensureDiscovery() {
|
|
38173
|
-
await this.
|
|
38564
|
+
const session = await this.managers.get(this.primaryName).ensure();
|
|
38174
38565
|
if (this.fetchproxyMode && !this.fetchproxyDiscoveryRan) {
|
|
38175
38566
|
this.fetchproxyDiscoveryRan = true;
|
|
38176
|
-
await this.discoverLinkedDistricts(this.accounts.get(this.primaryName));
|
|
38567
|
+
await this.discoverLinkedDistricts(this.accounts.get(this.primaryName), session);
|
|
38177
38568
|
}
|
|
38178
38569
|
}
|
|
38179
38570
|
listDistricts() {
|
|
@@ -38206,50 +38597,37 @@ var ICClient = class {
|
|
|
38206
38597
|
account2 = this.accounts.get(district);
|
|
38207
38598
|
if (!account2) throw new UnknownDistrictError(district, [...this.accounts.keys()]);
|
|
38208
38599
|
}
|
|
38209
|
-
await this.
|
|
38600
|
+
const session = await this.managers.get(account2.name).ensure();
|
|
38210
38601
|
if (this.fetchproxyMode && !this.fetchproxyDiscoveryRan && account2.name === this.primaryName) {
|
|
38211
38602
|
this.fetchproxyDiscoveryRan = true;
|
|
38212
|
-
await this.discoverLinkedDistricts(account2);
|
|
38213
|
-
}
|
|
38214
|
-
return this.doRequest(account2, path, opts, false);
|
|
38215
|
-
}
|
|
38216
|
-
async ensureSession(account2) {
|
|
38217
|
-
let s = this.sessions.get(account2.name);
|
|
38218
|
-
if (s && Date.now() - s.loggedInAt < SESSION_TTL_MS) return;
|
|
38219
|
-
if (s?.loginInFlight) {
|
|
38220
|
-
await s.loginInFlight;
|
|
38221
|
-
return;
|
|
38222
|
-
}
|
|
38223
|
-
if (!s) {
|
|
38224
|
-
s = { cookie: "", xsrfToken: "", loggedInAt: 0, loginInFlight: null };
|
|
38225
|
-
this.sessions.set(account2.name, s);
|
|
38226
|
-
}
|
|
38227
|
-
s.loginInFlight = this.login(account2);
|
|
38228
|
-
try {
|
|
38229
|
-
await s.loginInFlight;
|
|
38230
|
-
} finally {
|
|
38231
|
-
s.loginInFlight = null;
|
|
38603
|
+
await this.discoverLinkedDistricts(account2, session);
|
|
38232
38604
|
}
|
|
38605
|
+
return this.doRequest(account2, path, opts);
|
|
38233
38606
|
}
|
|
38234
38607
|
async login(account2) {
|
|
38235
38608
|
const primaryName = this.linkedTo.get(account2.name);
|
|
38236
38609
|
if (primaryName) {
|
|
38237
|
-
this.
|
|
38238
|
-
const
|
|
38239
|
-
|
|
38240
|
-
if (!this.sessions.has(account2.name)) {
|
|
38610
|
+
await this.managers.get(primaryName).ensure();
|
|
38611
|
+
const restored = this.managers.get(account2.name).current;
|
|
38612
|
+
if (!restored) {
|
|
38241
38613
|
throw new AuthFailedError(
|
|
38242
38614
|
account2.name,
|
|
38243
38615
|
"linked-district session could not be re-established via the primary district",
|
|
38244
38616
|
{ credentialHint: false }
|
|
38245
38617
|
);
|
|
38246
38618
|
}
|
|
38247
|
-
return;
|
|
38619
|
+
return restored;
|
|
38620
|
+
}
|
|
38621
|
+
if (this.preloaded) {
|
|
38622
|
+
const { cookieHeader, xsrfToken } = this.preloaded;
|
|
38623
|
+
this.preloaded = null;
|
|
38624
|
+
return { cookieHeader, xsrfToken };
|
|
38248
38625
|
}
|
|
38249
38626
|
if (!account2.username || !account2.password) {
|
|
38250
38627
|
throw new AuthFailedError(
|
|
38251
38628
|
account2.name,
|
|
38252
|
-
"session expired and no IC_USERNAME/IC_PASSWORD set \u2014 sign back into your IC portal in the browser and restart the MCP"
|
|
38629
|
+
"session expired and no IC_USERNAME/IC_PASSWORD set \u2014 sign back into your IC portal in the browser and restart the MCP",
|
|
38630
|
+
{ permanent: true }
|
|
38253
38631
|
);
|
|
38254
38632
|
}
|
|
38255
38633
|
const postRes = await fetch(
|
|
@@ -38282,17 +38660,17 @@ var ICClient = class {
|
|
|
38282
38660
|
if (!cookies.cookieHeader) {
|
|
38283
38661
|
throw new AuthFailedError(account2.name, "login response missing session cookies");
|
|
38284
38662
|
}
|
|
38285
|
-
const session =
|
|
38286
|
-
|
|
38287
|
-
|
|
38288
|
-
|
|
38289
|
-
await this.discoverLinkedDistricts(account2);
|
|
38663
|
+
const session = {
|
|
38664
|
+
cookieHeader: cookies.cookieHeader,
|
|
38665
|
+
xsrfToken: cookies.xsrfToken
|
|
38666
|
+
};
|
|
38667
|
+
await this.discoverLinkedDistricts(account2, session);
|
|
38668
|
+
return session;
|
|
38290
38669
|
}
|
|
38291
|
-
async discoverLinkedDistricts(account2) {
|
|
38670
|
+
async discoverLinkedDistricts(account2, session) {
|
|
38292
38671
|
try {
|
|
38293
|
-
const session = this.sessions.get(account2.name);
|
|
38294
38672
|
const baseHeaders = {
|
|
38295
|
-
Cookie: session.
|
|
38673
|
+
Cookie: session.cookieHeader,
|
|
38296
38674
|
Accept: "application/json",
|
|
38297
38675
|
"Content-Type": "application/json",
|
|
38298
38676
|
Referer: `${account2.baseUrl}/campus/nav-wrapper/`,
|
|
@@ -38360,13 +38738,14 @@ var ICClient = class {
|
|
|
38360
38738
|
password: "(linked)"
|
|
38361
38739
|
};
|
|
38362
38740
|
this.accounts.set(linked.districtName, syntheticAccount);
|
|
38363
|
-
this.sessions.set(linked.districtName, {
|
|
38364
|
-
cookie: switchCookies.cookieHeader,
|
|
38365
|
-
xsrfToken: switchCookies.xsrfToken,
|
|
38366
|
-
loggedInAt: Date.now(),
|
|
38367
|
-
loginInFlight: null
|
|
38368
|
-
});
|
|
38369
38741
|
this.linkedTo.set(linked.districtName, account2.name);
|
|
38742
|
+
if (!this.managers.has(linked.districtName)) {
|
|
38743
|
+
this.managers.set(linked.districtName, this.makeManager(syntheticAccount));
|
|
38744
|
+
}
|
|
38745
|
+
this.managers.get(linked.districtName).seed({
|
|
38746
|
+
cookieHeader: switchCookies.cookieHeader,
|
|
38747
|
+
xsrfToken: switchCookies.xsrfToken
|
|
38748
|
+
});
|
|
38370
38749
|
console.error(`[ic] Linked district discovered: ${linked.districtName} (${linked.appName})`);
|
|
38371
38750
|
} catch (e) {
|
|
38372
38751
|
console.error(`[ic] CUPS flow failed for ${linked.districtName}: ${e instanceof Error ? e.message : e}`);
|
|
@@ -38392,12 +38771,11 @@ var ICClient = class {
|
|
|
38392
38771
|
}
|
|
38393
38772
|
const account2 = this.accounts.get(district);
|
|
38394
38773
|
if (!account2) throw new UnknownDistrictError(district, [...this.accounts.keys()]);
|
|
38395
|
-
await this.
|
|
38396
|
-
const session = this.sessions.get(account2.name);
|
|
38774
|
+
const session = await this.managers.get(account2.name).ensure();
|
|
38397
38775
|
const url2 = /^https?:\/\//i.test(path) ? path : `${account2.baseUrl}${path}`;
|
|
38398
38776
|
const res = await fetch(url2, {
|
|
38399
38777
|
headers: {
|
|
38400
|
-
Cookie: session.
|
|
38778
|
+
Cookie: session.cookieHeader,
|
|
38401
38779
|
...session.xsrfToken ? { "X-XSRF-TOKEN": session.xsrfToken } : {}
|
|
38402
38780
|
}
|
|
38403
38781
|
});
|
|
@@ -38410,34 +38788,23 @@ var ICClient = class {
|
|
|
38410
38788
|
contentType: res.headers.get("content-type") ?? "application/octet-stream"
|
|
38411
38789
|
};
|
|
38412
38790
|
}
|
|
38413
|
-
async doRequest(account2, path, opts
|
|
38414
|
-
const
|
|
38791
|
+
async doRequest(account2, path, opts) {
|
|
38792
|
+
const mgr = this.managers.get(account2.name);
|
|
38415
38793
|
const accept = opts.responseType === "text" ? "text/html, text/plain, */*" : "application/json";
|
|
38416
|
-
const res = await
|
|
38417
|
-
|
|
38418
|
-
|
|
38419
|
-
|
|
38420
|
-
|
|
38421
|
-
|
|
38422
|
-
|
|
38423
|
-
|
|
38424
|
-
|
|
38425
|
-
|
|
38794
|
+
const res = await mgr.withSession(
|
|
38795
|
+
async (session) => fetch(`${account2.baseUrl}${path}`, {
|
|
38796
|
+
method: opts.method ?? "GET",
|
|
38797
|
+
headers: {
|
|
38798
|
+
Cookie: session.cookieHeader,
|
|
38799
|
+
Accept: accept,
|
|
38800
|
+
...session.xsrfToken ? { "X-XSRF-TOKEN": session.xsrfToken } : {},
|
|
38801
|
+
...opts.headers ?? {}
|
|
38802
|
+
},
|
|
38803
|
+
body: opts.body
|
|
38804
|
+
})
|
|
38805
|
+
);
|
|
38426
38806
|
if (res.status === 401) {
|
|
38427
|
-
|
|
38428
|
-
this.sessions.delete(account2.name);
|
|
38429
|
-
const primaryName = this.linkedTo.get(account2.name);
|
|
38430
|
-
if (primaryName) {
|
|
38431
|
-
this.sessions.delete(primaryName);
|
|
38432
|
-
for (const [linked] of this.linkedTo) {
|
|
38433
|
-
this.sessions.delete(linked);
|
|
38434
|
-
}
|
|
38435
|
-
const primaryAccount = this.accounts.get(primaryName);
|
|
38436
|
-
await this.ensureSession(primaryAccount);
|
|
38437
|
-
} else {
|
|
38438
|
-
await this.ensureSession(account2);
|
|
38439
|
-
}
|
|
38440
|
-
return this.doRequest(account2, path, opts, true);
|
|
38807
|
+
throw new SessionExpiredError(account2.name);
|
|
38441
38808
|
}
|
|
38442
38809
|
if (res.status >= 500) throw new PortalUnreachableError(account2.name, res.status);
|
|
38443
38810
|
if (!res.ok) throw new Error(`IC ${res.status} ${res.statusText} for ${path}`);
|
|
@@ -38450,14 +38817,11 @@ var ICClient = class {
|
|
|
38450
38817
|
};
|
|
38451
38818
|
function parseSetCookies(headers) {
|
|
38452
38819
|
const raw = headers.getSetCookie?.() ?? [];
|
|
38453
|
-
const
|
|
38454
|
-
|
|
38820
|
+
const { cookies, cookieHeader } = parseCookieJar(
|
|
38821
|
+
raw.length > 0 ? raw : headers.get("set-cookie")
|
|
38822
|
+
);
|
|
38455
38823
|
return { cookieHeader, xsrfToken: cookies["XSRF-TOKEN"] ?? "" };
|
|
38456
38824
|
}
|
|
38457
|
-
function splitFallback(header) {
|
|
38458
|
-
if (!header) return [];
|
|
38459
|
-
return header.split(",").map((s) => s.trim());
|
|
38460
|
-
}
|
|
38461
38825
|
var UnknownDistrictError = class extends Error {
|
|
38462
38826
|
constructor(district, available) {
|
|
38463
38827
|
super(`Unknown district '${district}'. Configured: [${available.join(", ")}]`);
|
|
@@ -38474,6 +38838,7 @@ var AuthFailedError = class extends Error {
|
|
|
38474
38838
|
* "Check IC_USERNAME and IC_PASSWORD" suffix — used for failures where the
|
|
38475
38839
|
* credentials are known-good (e.g. a linked-district CUPS/SSO re-discovery
|
|
38476
38840
|
* failure) and pointing the user at their creds would be misleading.
|
|
38841
|
+
* @param opts.permanent See {@link AuthFailedError.permanent}.
|
|
38477
38842
|
*/
|
|
38478
38843
|
constructor(district, reason, opts) {
|
|
38479
38844
|
const detail = reason ? ` (${reason})` : "";
|
|
@@ -38482,9 +38847,17 @@ var AuthFailedError = class extends Error {
|
|
|
38482
38847
|
this.district = district;
|
|
38483
38848
|
this.reason = reason;
|
|
38484
38849
|
this.name = "AuthFailedError";
|
|
38850
|
+
this.permanent = opts?.permanent ?? false;
|
|
38485
38851
|
}
|
|
38486
38852
|
district;
|
|
38487
38853
|
reason;
|
|
38854
|
+
/**
|
|
38855
|
+
* A *permanent* configuration failure: retrying the login inside this
|
|
38856
|
+
* process can never succeed (e.g. fetchproxy mode with no creds — the user
|
|
38857
|
+
* must re-sign-in in the browser and restart). CookieSessionManager caches
|
|
38858
|
+
* and rethrows permanent errors instead of retrying.
|
|
38859
|
+
*/
|
|
38860
|
+
permanent;
|
|
38488
38861
|
};
|
|
38489
38862
|
var PortalUnreachableError = class extends Error {
|
|
38490
38863
|
constructor(district, status) {
|
|
@@ -38529,40 +38902,6 @@ var FileExistsError = class extends Error {
|
|
|
38529
38902
|
path;
|
|
38530
38903
|
};
|
|
38531
38904
|
|
|
38532
|
-
// src/tools/_shared.ts
|
|
38533
|
-
function textContent(data) {
|
|
38534
|
-
return textResult(data);
|
|
38535
|
-
}
|
|
38536
|
-
function is404(e) {
|
|
38537
|
-
return e instanceof Error && e.message.startsWith("IC 404 ");
|
|
38538
|
-
}
|
|
38539
|
-
function featureDisabled(feature, district, data = []) {
|
|
38540
|
-
return textContent({ warning: "FeatureDisabled", feature, district, data });
|
|
38541
|
-
}
|
|
38542
|
-
async function findStudent(client, district, studentId) {
|
|
38543
|
-
const students = await client.request(district, "/campus/api/portal/students");
|
|
38544
|
-
return students.find((s) => String(s.personID) === studentId) ?? null;
|
|
38545
|
-
}
|
|
38546
|
-
function studentNotFound(studentId) {
|
|
38547
|
-
return textContent({ error: "StudentNotFound", studentId });
|
|
38548
|
-
}
|
|
38549
|
-
async function checkFeatureDisabled(client, district, studentId, student, flag, toolName, emptyData = []) {
|
|
38550
|
-
const structureID = student.enrollments?.[0]?.structureID;
|
|
38551
|
-
if (!structureID) return null;
|
|
38552
|
-
try {
|
|
38553
|
-
const features = await client.getFeatures(district, structureID, studentId);
|
|
38554
|
-
if (features[flag] === false) return featureDisabled(toolName, district, emptyData);
|
|
38555
|
-
return null;
|
|
38556
|
-
} catch (e) {
|
|
38557
|
-
console.error(`[ic] displayOptions check failed for ${district}/${flag}: ${e instanceof Error ? e.message : e}`);
|
|
38558
|
-
return null;
|
|
38559
|
-
}
|
|
38560
|
-
}
|
|
38561
|
-
function toArray(value) {
|
|
38562
|
-
if (value === null || value === void 0) return [];
|
|
38563
|
-
return Array.isArray(value) ? value : [value];
|
|
38564
|
-
}
|
|
38565
|
-
|
|
38566
38905
|
// src/tools/districts.ts
|
|
38567
38906
|
function registerDistrictTools(server, client) {
|
|
38568
38907
|
server.registerTool("ic_list_districts", {
|
|
@@ -38571,7 +38910,7 @@ function registerDistrictTools(server, client) {
|
|
|
38571
38910
|
}, async () => {
|
|
38572
38911
|
await client.ensureDiscovery();
|
|
38573
38912
|
const data = client.listDistricts();
|
|
38574
|
-
return
|
|
38913
|
+
return textResult(data);
|
|
38575
38914
|
});
|
|
38576
38915
|
}
|
|
38577
38916
|
|
|
@@ -38587,7 +38926,7 @@ function registerStudentTools(server, client) {
|
|
|
38587
38926
|
}, async (rawArgs) => {
|
|
38588
38927
|
const args = argsSchema.parse(rawArgs);
|
|
38589
38928
|
const data = await client.request(args.district, "/campus/api/portal/students");
|
|
38590
|
-
return
|
|
38929
|
+
return textResult(data);
|
|
38591
38930
|
});
|
|
38592
38931
|
}
|
|
38593
38932
|
|
|
@@ -38607,7 +38946,7 @@ function registerScheduleTools(server, client) {
|
|
|
38607
38946
|
const args = argsSchema2.parse(rawArgs);
|
|
38608
38947
|
const params = new URLSearchParams({ personID: args.studentId });
|
|
38609
38948
|
const data = await client.request(args.district, `/campus/resources/portal/roster?${params}`);
|
|
38610
|
-
return
|
|
38949
|
+
return textResult(data);
|
|
38611
38950
|
});
|
|
38612
38951
|
}
|
|
38613
38952
|
|
|
@@ -38645,7 +38984,7 @@ function registerAssignmentTools(server, client) {
|
|
|
38645
38984
|
if (args.missingOnly) {
|
|
38646
38985
|
data = data.filter((a) => a.missing);
|
|
38647
38986
|
}
|
|
38648
|
-
return
|
|
38987
|
+
return textResult(data);
|
|
38649
38988
|
});
|
|
38650
38989
|
}
|
|
38651
38990
|
|
|
@@ -38665,10 +39004,41 @@ function registerGradeTools(server, client) {
|
|
|
38665
39004
|
const params = new URLSearchParams({ personID: args.studentId });
|
|
38666
39005
|
if (args.termId) params.set("termID", args.termId);
|
|
38667
39006
|
const data = await client.request(args.district, `/campus/resources/portal/grades?${params}`);
|
|
38668
|
-
return
|
|
39007
|
+
return textResult(data);
|
|
38669
39008
|
});
|
|
38670
39009
|
}
|
|
38671
39010
|
|
|
39011
|
+
// src/tools/_shared.ts
|
|
39012
|
+
function is404(e) {
|
|
39013
|
+
return e instanceof Error && e.message.startsWith("IC 404 ");
|
|
39014
|
+
}
|
|
39015
|
+
function featureDisabled(feature, district, data = []) {
|
|
39016
|
+
return textResult({ warning: "FeatureDisabled", feature, district, data });
|
|
39017
|
+
}
|
|
39018
|
+
async function findStudent(client, district, studentId) {
|
|
39019
|
+
const students = await client.request(district, "/campus/api/portal/students");
|
|
39020
|
+
return students.find((s) => String(s.personID) === studentId) ?? null;
|
|
39021
|
+
}
|
|
39022
|
+
function studentNotFound(studentId) {
|
|
39023
|
+
return textResult({ error: "StudentNotFound", studentId });
|
|
39024
|
+
}
|
|
39025
|
+
async function checkFeatureDisabled(client, district, studentId, student, flag, toolName, emptyData = []) {
|
|
39026
|
+
const structureID = student.enrollments?.[0]?.structureID;
|
|
39027
|
+
if (!structureID) return null;
|
|
39028
|
+
try {
|
|
39029
|
+
const features = await client.getFeatures(district, structureID, studentId);
|
|
39030
|
+
if (features[flag] === false) return featureDisabled(toolName, district, emptyData);
|
|
39031
|
+
return null;
|
|
39032
|
+
} catch (e) {
|
|
39033
|
+
console.error(`[ic] displayOptions check failed for ${district}/${flag}: ${e instanceof Error ? e.message : e}`);
|
|
39034
|
+
return null;
|
|
39035
|
+
}
|
|
39036
|
+
}
|
|
39037
|
+
function toArray(value) {
|
|
39038
|
+
if (value === null || value === void 0) return [];
|
|
39039
|
+
return Array.isArray(value) ? value : [value];
|
|
39040
|
+
}
|
|
39041
|
+
|
|
38672
39042
|
// src/tools/attendance.ts
|
|
38673
39043
|
var argsSchema5 = external_exports.object({
|
|
38674
39044
|
district: external_exports.string(),
|
|
@@ -38733,7 +39103,7 @@ function registerAttendanceTools(server, client) {
|
|
|
38733
39103
|
results.push({ ...entry, terms: trimmedTerms });
|
|
38734
39104
|
}
|
|
38735
39105
|
}
|
|
38736
|
-
return
|
|
39106
|
+
return textResult(results);
|
|
38737
39107
|
} catch (e) {
|
|
38738
39108
|
if (is404(e)) return featureDisabled("attendance", args.district);
|
|
38739
39109
|
throw e;
|
|
@@ -38764,7 +39134,7 @@ function registerBehaviorTools(server, client) {
|
|
|
38764
39134
|
if (args.until) params.set("endDate", args.until);
|
|
38765
39135
|
try {
|
|
38766
39136
|
const data = await client.request(args.district, `/campus/resources/portal/behavior?${params}`);
|
|
38767
|
-
return
|
|
39137
|
+
return textResult(data);
|
|
38768
39138
|
} catch (e) {
|
|
38769
39139
|
if (is404(e)) return featureDisabled("behavior", args.district);
|
|
38770
39140
|
throw e;
|
|
@@ -38804,7 +39174,7 @@ function registerFoodServiceTools(server, client) {
|
|
|
38804
39174
|
if (args.until) params.set("endDate", args.until);
|
|
38805
39175
|
try {
|
|
38806
39176
|
const data = await client.request(args.district, `/campus/resources/portal/foodService?${params}`);
|
|
38807
|
-
return
|
|
39177
|
+
return textResult(data);
|
|
38808
39178
|
} catch (e) {
|
|
38809
39179
|
if (is404(e)) return featureDisabled("foodService", args.district, EMPTY_FOOD);
|
|
38810
39180
|
throw e;
|
|
@@ -38824,16 +39194,50 @@ var NAMED_ENTITIES = {
|
|
|
38824
39194
|
function extractPlainTextFromHtml(html) {
|
|
38825
39195
|
if (!html)
|
|
38826
39196
|
return "";
|
|
38827
|
-
let text = html
|
|
39197
|
+
let text = stripElementContent(stripElementContent(html, "script"), "style");
|
|
38828
39198
|
text = text.replace(/<[^>]+>/g, " ");
|
|
38829
|
-
text = text.replace(/&#(\d+);/g, (
|
|
38830
|
-
text = text.replace(/&#x([0-9a-fA-F]+);/g, (
|
|
39199
|
+
text = text.replace(/&#(\d+);/g, (whole, d) => codePointOr(Number(d), whole));
|
|
39200
|
+
text = text.replace(/&#x([0-9a-fA-F]+);/g, (whole, h) => codePointOr(parseInt(h, 16), whole));
|
|
38831
39201
|
text = text.replace(/&([a-zA-Z]+);/g, (whole, name) => {
|
|
38832
39202
|
const decoded = NAMED_ENTITIES[name.toLowerCase()];
|
|
38833
39203
|
return decoded ?? whole;
|
|
38834
39204
|
});
|
|
38835
39205
|
return text.replace(/\s+/g, " ").trim();
|
|
38836
39206
|
}
|
|
39207
|
+
function stripElementContent(html, tag) {
|
|
39208
|
+
const lower = html.toLowerCase();
|
|
39209
|
+
const open = `<${tag}`;
|
|
39210
|
+
const close = `</${tag}>`;
|
|
39211
|
+
let out = "";
|
|
39212
|
+
let i = 0;
|
|
39213
|
+
for (; ; ) {
|
|
39214
|
+
const start = lower.indexOf(open, i);
|
|
39215
|
+
if (start < 0) {
|
|
39216
|
+
out += html.slice(i);
|
|
39217
|
+
return out;
|
|
39218
|
+
}
|
|
39219
|
+
const after = lower[start + open.length];
|
|
39220
|
+
if (after !== void 0 && after !== ">" && after !== " " && after !== " " && after !== "\n" && after !== "\r" && after !== "\f" && after !== "/") {
|
|
39221
|
+
out += html.slice(i, start + open.length);
|
|
39222
|
+
i = start + open.length;
|
|
39223
|
+
continue;
|
|
39224
|
+
}
|
|
39225
|
+
out += html.slice(i, start) + " ";
|
|
39226
|
+
const closeAt = lower.indexOf(close, start + open.length);
|
|
39227
|
+
if (closeAt < 0)
|
|
39228
|
+
return out;
|
|
39229
|
+
i = closeAt + close.length;
|
|
39230
|
+
}
|
|
39231
|
+
}
|
|
39232
|
+
function codePointOr(code, raw) {
|
|
39233
|
+
if (!Number.isInteger(code) || code < 0 || code > 1114111)
|
|
39234
|
+
return raw;
|
|
39235
|
+
try {
|
|
39236
|
+
return String.fromCodePoint(code);
|
|
39237
|
+
} catch {
|
|
39238
|
+
return raw;
|
|
39239
|
+
}
|
|
39240
|
+
}
|
|
38837
39241
|
|
|
38838
39242
|
// src/tools/messages.ts
|
|
38839
39243
|
var listArgs = external_exports.object({
|
|
@@ -38928,7 +39332,7 @@ function registerMessageTools(server, client) {
|
|
|
38928
39332
|
return { count: 0, items: [], error: e instanceof Error ? e.message : String(e) };
|
|
38929
39333
|
});
|
|
38930
39334
|
const [notifications, inbox, announcements] = await Promise.all([prismPromise, inboxPromise, noticePromise]);
|
|
38931
|
-
return
|
|
39335
|
+
return textResult({ notifications, inbox, announcements });
|
|
38932
39336
|
});
|
|
38933
39337
|
server.registerTool("ic_get_message", {
|
|
38934
39338
|
description: "Fetch the HTML body of an inbox message and return it parsed into { subject, date, body, url }. Takes a `messageUrl` which is the `url` field from an item returned by ic_list_messages' inbox section (e.g. 'portal/messageView.xsl?x=messenger.MessengerEngine-getMessageRecipientView&messageID=...'). Relative and /campus/-prefixed URLs are both accepted. Note: fetching the HTML body may mark the message as read on some district configurations; probe against an empty inbox could not confirm the side effect.",
|
|
@@ -38939,7 +39343,7 @@ function registerMessageTools(server, client) {
|
|
|
38939
39343
|
const path = normalizeMessageUrl(args.messageUrl);
|
|
38940
39344
|
const html = await client.request(args.district, path, { responseType: "text" });
|
|
38941
39345
|
const parsed = parseMessageHtml(html ?? "", path);
|
|
38942
|
-
return
|
|
39346
|
+
return textResult(parsed);
|
|
38943
39347
|
});
|
|
38944
39348
|
}
|
|
38945
39349
|
|
|
@@ -38979,7 +39383,7 @@ function registerDocumentTools(server, client) {
|
|
|
38979
39383
|
if (d.endYear !== void 0) out.endYear = d.endYear;
|
|
38980
39384
|
return out;
|
|
38981
39385
|
});
|
|
38982
|
-
return
|
|
39386
|
+
return textResult(trimmed);
|
|
38983
39387
|
} catch (e) {
|
|
38984
39388
|
if (is404(e)) return featureDisabled("documents", args.district);
|
|
38985
39389
|
throw e;
|
|
@@ -38995,10 +39399,10 @@ function registerDocumentTools(server, client) {
|
|
|
38995
39399
|
const meta3 = await client.download(args.district, args.documentId, args.destinationPath, {
|
|
38996
39400
|
overwrite: args.overwrite ?? false
|
|
38997
39401
|
});
|
|
38998
|
-
return
|
|
39402
|
+
return textResult(meta3);
|
|
38999
39403
|
} catch (e) {
|
|
39000
39404
|
if (e instanceof Error && e.message.startsWith("IC download 404")) {
|
|
39001
|
-
return
|
|
39405
|
+
return textResult({ warning: "FeatureDisabled", feature: "documents", district: args.district });
|
|
39002
39406
|
}
|
|
39003
39407
|
throw e;
|
|
39004
39408
|
}
|
|
@@ -39054,7 +39458,7 @@ function registerCalendarTools(server, client) {
|
|
|
39054
39458
|
terms: trimmedTerms
|
|
39055
39459
|
});
|
|
39056
39460
|
}
|
|
39057
|
-
return
|
|
39461
|
+
return textResult(result);
|
|
39058
39462
|
});
|
|
39059
39463
|
}
|
|
39060
39464
|
|
|
@@ -39145,7 +39549,7 @@ function registerAttendanceEventsTools(server, client) {
|
|
|
39145
39549
|
results.push(trimmed);
|
|
39146
39550
|
}
|
|
39147
39551
|
}
|
|
39148
|
-
return
|
|
39552
|
+
return textResult(results);
|
|
39149
39553
|
} catch (e) {
|
|
39150
39554
|
if (is404(e)) return featureDisabled("attendance_events", args.district);
|
|
39151
39555
|
throw e;
|
|
@@ -39201,7 +39605,7 @@ function registerRecentGradesTools(server, client) {
|
|
|
39201
39605
|
}
|
|
39202
39606
|
return out;
|
|
39203
39607
|
});
|
|
39204
|
-
return
|
|
39608
|
+
return textResult(trimmed);
|
|
39205
39609
|
});
|
|
39206
39610
|
}
|
|
39207
39611
|
|
|
@@ -39256,7 +39660,7 @@ function registerTeacherTools(server, client) {
|
|
|
39256
39660
|
const [teachersRaw, counselorsRaw] = await Promise.all([teachersPromise, counselorsPromise]);
|
|
39257
39661
|
const teachers = toArray(teachersRaw).map((t) => trimRecord(t));
|
|
39258
39662
|
const counselors = toArray(counselorsRaw).map((c) => trimRecord(c));
|
|
39259
|
-
return
|
|
39663
|
+
return textResult({ counselors, teachers });
|
|
39260
39664
|
});
|
|
39261
39665
|
}
|
|
39262
39666
|
|
|
@@ -39307,7 +39711,7 @@ function registerAssessmentTools(server, client) {
|
|
|
39307
39711
|
if (feature404 && result.length === 0) {
|
|
39308
39712
|
return featureDisabled("assessments", args.district);
|
|
39309
39713
|
}
|
|
39310
|
-
return
|
|
39714
|
+
return textResult(result);
|
|
39311
39715
|
});
|
|
39312
39716
|
}
|
|
39313
39717
|
|
|
@@ -39351,7 +39755,7 @@ function registerFeeTools(server, client) {
|
|
|
39351
39755
|
feeAssignments: assignments.ok ? assignments.value : [],
|
|
39352
39756
|
...issues.length > 0 ? { issues } : {}
|
|
39353
39757
|
};
|
|
39354
|
-
return
|
|
39758
|
+
return textResult(response);
|
|
39355
39759
|
});
|
|
39356
39760
|
}
|
|
39357
39761
|
|
|
@@ -39379,7 +39783,7 @@ function registerFeaturesTools(server, client) {
|
|
|
39379
39783
|
features
|
|
39380
39784
|
});
|
|
39381
39785
|
}
|
|
39382
|
-
return
|
|
39786
|
+
return textResult(result);
|
|
39383
39787
|
});
|
|
39384
39788
|
}
|
|
39385
39789
|
|
|
@@ -39403,7 +39807,7 @@ try {
|
|
|
39403
39807
|
}
|
|
39404
39808
|
var COMMON = {
|
|
39405
39809
|
name: "infinitecampus",
|
|
39406
|
-
version: "2.3.
|
|
39810
|
+
version: "2.3.6"
|
|
39407
39811
|
// x-release-please-version
|
|
39408
39812
|
};
|
|
39409
39813
|
if (account) {
|