infinitecampus-mcp 2.8.2 → 2.8.3
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 +290 -159
- package/dist/index.js +57 -26
- package/package.json +5 -5
- 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.8.
|
|
10
|
+
"version": "2.8.3"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "Infinite Campus",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Infinite Campus (Campus Parent) MCP server for Claude — grades, attendance, assignments, messages, and documents via natural language",
|
|
18
|
-
"version": "2.8.
|
|
18
|
+
"version": "2.8.3",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinitecampus-mcp",
|
|
3
3
|
"displayName": "Infinite Campus",
|
|
4
|
-
"version": "2.8.
|
|
4
|
+
"version": "2.8.3",
|
|
5
5
|
"description": "Infinite Campus (Campus Parent) MCP server for Claude — grades, attendance, assignments, messages, and documents",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
package/dist/bundle.js
CHANGED
|
@@ -40097,6 +40097,8 @@ function validateFrame(raw) {
|
|
|
40097
40097
|
return validateEncrypted(raw);
|
|
40098
40098
|
if (t === "pair-pending")
|
|
40099
40099
|
return validatePairPending(raw);
|
|
40100
|
+
if (t === "hello-rejected")
|
|
40101
|
+
return validateHelloRejected(raw);
|
|
40100
40102
|
if (t === "extension-disconnected")
|
|
40101
40103
|
return { type: "extension-disconnected" };
|
|
40102
40104
|
throw new ProtocolError(`unknown frame type: ${String(t)}`);
|
|
@@ -40227,6 +40229,16 @@ function validatePairPending(raw) {
|
|
|
40227
40229
|
}
|
|
40228
40230
|
return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
|
|
40229
40231
|
}
|
|
40232
|
+
function validateHelloRejected(raw) {
|
|
40233
|
+
assertString(raw.mcpId, "hello-rejected.mcpId");
|
|
40234
|
+
if (!isValidMcpId(raw.mcpId))
|
|
40235
|
+
throw new ProtocolError("hello-rejected.mcpId: invalid format");
|
|
40236
|
+
assertString(raw.reason, "hello-rejected.reason");
|
|
40237
|
+
if (raw.reason.length > 200) {
|
|
40238
|
+
throw new ProtocolError("hello-rejected.reason: must be at most 200 characters");
|
|
40239
|
+
}
|
|
40240
|
+
return { type: "hello-rejected", mcpId: raw.mcpId, reason: raw.reason };
|
|
40241
|
+
}
|
|
40230
40242
|
function validateInnerFrame(raw) {
|
|
40231
40243
|
assertObject(raw, "inner");
|
|
40232
40244
|
const t = raw.type;
|
|
@@ -40261,6 +40273,11 @@ function validateInnerRequest(raw) {
|
|
|
40261
40273
|
if (raw.init.inPage !== void 0) {
|
|
40262
40274
|
assertBoolean(raw.init.inPage, "inner.init.inPage");
|
|
40263
40275
|
}
|
|
40276
|
+
if (raw.init.credentials !== void 0) {
|
|
40277
|
+
if (raw.init.credentials !== "include" && raw.init.credentials !== "omit") {
|
|
40278
|
+
throw new ProtocolError("inner.init.credentials: must be 'include' or 'omit'");
|
|
40279
|
+
}
|
|
40280
|
+
}
|
|
40264
40281
|
return raw;
|
|
40265
40282
|
}
|
|
40266
40283
|
if (raw.op === "read_cookies") {
|
|
@@ -41041,6 +41058,17 @@ var FetchproxySessionNotReadyError = class extends Error {
|
|
|
41041
41058
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
41042
41059
|
}
|
|
41043
41060
|
};
|
|
41061
|
+
var FetchproxyHelloRejectedError = class extends Error {
|
|
41062
|
+
mcpId;
|
|
41063
|
+
reason;
|
|
41064
|
+
constructor(info) {
|
|
41065
|
+
super(`fetchproxy: the extension refused the connection for "${info.mcpId}": ${info.reason}. This is the extension's own reason \u2014 it is not a timeout, and retrying unchanged will be refused the same way.`);
|
|
41066
|
+
this.name = "FetchproxyHelloRejectedError";
|
|
41067
|
+
this.mcpId = info.mcpId;
|
|
41068
|
+
this.reason = info.reason;
|
|
41069
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
41070
|
+
}
|
|
41071
|
+
};
|
|
41044
41072
|
async function awaitSessionReady(ready, opts) {
|
|
41045
41073
|
const ms = opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS;
|
|
41046
41074
|
if (ms <= 0)
|
|
@@ -41066,12 +41094,21 @@ import { join as join2 } from "node:path";
|
|
|
41066
41094
|
|
|
41067
41095
|
// node_modules/@fetchproxy/server/dist/identity.js
|
|
41068
41096
|
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
41069
|
-
import { join } from "node:path";
|
|
41097
|
+
import { isAbsolute, join } from "node:path";
|
|
41070
41098
|
import { homedir } from "node:os";
|
|
41071
41099
|
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
41072
41100
|
var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
41101
|
+
function envIdentityDir() {
|
|
41102
|
+
const raw = process.env.FETCHPROXY_IDENTITY_DIR;
|
|
41103
|
+
if (raw === void 0)
|
|
41104
|
+
return void 0;
|
|
41105
|
+
const dir = raw.trim();
|
|
41106
|
+
if (dir === "" || !isAbsolute(dir))
|
|
41107
|
+
return void 0;
|
|
41108
|
+
return dir;
|
|
41109
|
+
}
|
|
41073
41110
|
function defaultIdentityDir() {
|
|
41074
|
-
return join(homedir(), ".fetchproxy", "identity");
|
|
41111
|
+
return envIdentityDir() ?? join(homedir(), ".fetchproxy", "identity");
|
|
41075
41112
|
}
|
|
41076
41113
|
function safeIdentityFileBase(serverName) {
|
|
41077
41114
|
if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
|
|
@@ -41079,42 +41116,57 @@ function safeIdentityFileBase(serverName) {
|
|
|
41079
41116
|
}
|
|
41080
41117
|
return serverName.replace(/\//g, "_");
|
|
41081
41118
|
}
|
|
41082
|
-
|
|
41083
|
-
|
|
41084
|
-
|
|
41085
|
-
|
|
41086
|
-
try {
|
|
41087
|
-
const raw = await readFile(path, "utf8");
|
|
41088
|
-
const j2 = JSON.parse(raw);
|
|
41089
|
-
return {
|
|
41090
|
-
x25519Priv: fromB64(j2.x25519Priv),
|
|
41091
|
-
x25519Pub: fromB64(j2.x25519Pub),
|
|
41092
|
-
ed25519Priv: fromB64(j2.ed25519Priv),
|
|
41093
|
-
ed25519Pub: fromB64(j2.ed25519Pub),
|
|
41094
|
-
createdAt: j2.createdAt
|
|
41095
|
-
};
|
|
41096
|
-
} catch (e) {
|
|
41097
|
-
if (e.code !== "ENOENT")
|
|
41098
|
-
throw e;
|
|
41099
|
-
}
|
|
41119
|
+
function identityFilePath(dir, serverName) {
|
|
41120
|
+
return join(dir, `${safeIdentityFileBase(serverName)}.json`);
|
|
41121
|
+
}
|
|
41122
|
+
async function generateIdentity() {
|
|
41100
41123
|
const x = await generateX25519();
|
|
41101
41124
|
const ed = await generateEd25519();
|
|
41102
|
-
|
|
41125
|
+
return {
|
|
41103
41126
|
x25519Priv: x.privateKey,
|
|
41104
41127
|
x25519Pub: x.publicKey,
|
|
41105
41128
|
ed25519Priv: ed.privateKey,
|
|
41106
41129
|
ed25519Pub: ed.publicKey,
|
|
41107
41130
|
createdAt: Date.now()
|
|
41108
41131
|
};
|
|
41109
|
-
|
|
41132
|
+
}
|
|
41133
|
+
function serializeIdentity(id) {
|
|
41134
|
+
return JSON.stringify({
|
|
41110
41135
|
x25519Priv: toB64(id.x25519Priv),
|
|
41111
41136
|
x25519Pub: toB64(id.x25519Pub),
|
|
41112
41137
|
ed25519Priv: toB64(id.ed25519Priv),
|
|
41113
41138
|
ed25519Pub: toB64(id.ed25519Pub),
|
|
41114
41139
|
createdAt: id.createdAt
|
|
41140
|
+
}, null, 2);
|
|
41141
|
+
}
|
|
41142
|
+
function parseIdentity(text) {
|
|
41143
|
+
const j = JSON.parse(text);
|
|
41144
|
+
return {
|
|
41145
|
+
x25519Priv: fromB64(j.x25519Priv),
|
|
41146
|
+
x25519Pub: fromB64(j.x25519Pub),
|
|
41147
|
+
ed25519Priv: fromB64(j.ed25519Priv),
|
|
41148
|
+
ed25519Pub: fromB64(j.ed25519Pub),
|
|
41149
|
+
createdAt: j.createdAt
|
|
41115
41150
|
};
|
|
41116
|
-
|
|
41151
|
+
}
|
|
41152
|
+
async function writeIdentityFile(dir, serverName, id) {
|
|
41153
|
+
const path = identityFilePath(dir, serverName);
|
|
41154
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
41155
|
+
await writeFile(path, serializeIdentity(id), { mode: 384 });
|
|
41117
41156
|
await chmod(path, 384);
|
|
41157
|
+
return path;
|
|
41158
|
+
}
|
|
41159
|
+
async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
41160
|
+
const path = identityFilePath(dir, serverName);
|
|
41161
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
41162
|
+
try {
|
|
41163
|
+
return parseIdentity(await readFile(path, "utf8"));
|
|
41164
|
+
} catch (e) {
|
|
41165
|
+
if (e.code !== "ENOENT")
|
|
41166
|
+
throw e;
|
|
41167
|
+
}
|
|
41168
|
+
const id = await generateIdentity();
|
|
41169
|
+
await writeIdentityFile(dir, serverName, id);
|
|
41118
41170
|
return id;
|
|
41119
41171
|
}
|
|
41120
41172
|
|
|
@@ -41218,6 +41270,9 @@ async function startHost(opts) {
|
|
|
41218
41270
|
}
|
|
41219
41271
|
});
|
|
41220
41272
|
const ownHello = await buildServerHello({
|
|
41273
|
+
// 2.6.0: tell the extension it can say WHY it refused a hello, instead of
|
|
41274
|
+
// leaving us to time out and guess.
|
|
41275
|
+
accepts: ["hello-rejected"],
|
|
41221
41276
|
identity: opts.ownIdentity,
|
|
41222
41277
|
mcpId: opts.ownMcpId,
|
|
41223
41278
|
serverName: opts.ownServerName,
|
|
@@ -41429,6 +41484,16 @@ async function startHost(opts) {
|
|
|
41429
41484
|
extensionWs.send(JSON.stringify(frame));
|
|
41430
41485
|
}
|
|
41431
41486
|
}
|
|
41487
|
+
if (frame.type === "hello-rejected" && identified === "extension") {
|
|
41488
|
+
if (frame.mcpId === opts.ownMcpId) {
|
|
41489
|
+
rejectOwnSession(new FetchproxyHelloRejectedError({ mcpId: frame.mcpId, reason: frame.reason }));
|
|
41490
|
+
} else {
|
|
41491
|
+
const slot = peers.get(frame.mcpId);
|
|
41492
|
+
if (slot?.helloFrame.accepts?.includes("hello-rejected")) {
|
|
41493
|
+
slot.ws.send(JSON.stringify(frame));
|
|
41494
|
+
}
|
|
41495
|
+
}
|
|
41496
|
+
}
|
|
41432
41497
|
if (frame.type === "pair-pending" && identified === "extension") {
|
|
41433
41498
|
if (frame.mcpId === opts.ownMcpId) {
|
|
41434
41499
|
ownPendingPairCode = frame.pairCode;
|
|
@@ -41540,7 +41605,8 @@ async function startPeer(opts) {
|
|
|
41540
41605
|
graphqlOps: opts.graphqlOps,
|
|
41541
41606
|
// 2.5.0: let a host ≥2.5.0 tell us when the extension leaves, so
|
|
41542
41607
|
// `extensionConnected()` / `sessionLinked()` can go back to false.
|
|
41543
|
-
|
|
41608
|
+
// 2.6.0: `hello-rejected` for the same reason, from the extension.
|
|
41609
|
+
accepts: ["extension-disconnected", "hello-rejected"]
|
|
41544
41610
|
});
|
|
41545
41611
|
const sessionNonce = fromB64(hello.sessionNonce);
|
|
41546
41612
|
ws.send(JSON.stringify(hello));
|
|
@@ -41655,6 +41721,9 @@ async function startPeer(opts) {
|
|
|
41655
41721
|
}
|
|
41656
41722
|
return;
|
|
41657
41723
|
}
|
|
41724
|
+
if (frame.type === "hello-rejected" && frame.mcpId === opts.mcpId) {
|
|
41725
|
+
rejectFirstReady(new FetchproxyHelloRejectedError({ mcpId: frame.mcpId, reason: frame.reason }));
|
|
41726
|
+
}
|
|
41658
41727
|
if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
|
|
41659
41728
|
pendingPairCode = frame.pairCode;
|
|
41660
41729
|
pendingPairListeners.forEach((cb) => cb(frame.pairCode));
|
|
@@ -41752,6 +41821,8 @@ function classifyFetchError(error61) {
|
|
|
41752
41821
|
|
|
41753
41822
|
// node_modules/@fetchproxy/server/dist/classify-bridge-error.js
|
|
41754
41823
|
function classifyBridgeError(err) {
|
|
41824
|
+
if (err instanceof FetchproxyHelloRejectedError)
|
|
41825
|
+
return "hello_rejected";
|
|
41755
41826
|
if (err instanceof FetchproxySessionNotReadyError)
|
|
41756
41827
|
return "session_not_ready";
|
|
41757
41828
|
if (err instanceof FetchproxyTimeoutError)
|
|
@@ -41852,11 +41923,20 @@ var FetchproxyNoTabError = class extends FetchproxyHintedError {
|
|
|
41852
41923
|
this.name = "FetchproxyNoTabError";
|
|
41853
41924
|
}
|
|
41854
41925
|
};
|
|
41926
|
+
var FetchproxyTabOpeningError = class extends FetchproxyHintedError {
|
|
41927
|
+
constructor(originalError) {
|
|
41928
|
+
super(originalError, "the extension is opening that tab now \u2014 retry in a few seconds. You do not need to open one yourself, and this is not a version problem.");
|
|
41929
|
+
this.name = "FetchproxyTabOpeningError";
|
|
41930
|
+
}
|
|
41931
|
+
};
|
|
41855
41932
|
var SCOPE_REJECTION = /not in declared/;
|
|
41856
|
-
var NO_TAB_REJECTION = /no tab matching (?!.*content script loaded)/;
|
|
41933
|
+
var NO_TAB_REJECTION = /no tab matching (?!.*(content script loaded|one is still opening))/;
|
|
41934
|
+
var TAB_OPENING_REJECTION = /no tab matching .*one is still opening/;
|
|
41857
41935
|
function protocolErrorFrom(error61) {
|
|
41858
41936
|
if (SCOPE_REJECTION.test(error61))
|
|
41859
41937
|
return new FetchproxyScopeError(error61);
|
|
41938
|
+
if (TAB_OPENING_REJECTION.test(error61))
|
|
41939
|
+
return new FetchproxyTabOpeningError(error61);
|
|
41860
41940
|
if (NO_TAB_REJECTION.test(error61))
|
|
41861
41941
|
return new FetchproxyNoTabError(error61);
|
|
41862
41942
|
return new FetchproxyProtocolError(error61);
|
|
@@ -42476,9 +42556,9 @@ var FetchproxyServer = class {
|
|
|
42476
42556
|
async _fetchOnceWithTimeout(init) {
|
|
42477
42557
|
const id = this.nextRequestId++;
|
|
42478
42558
|
const inner = { type: "request", id, op: "fetch", init };
|
|
42479
|
-
const pending = new Promise((resolve2) => {
|
|
42559
|
+
const pending = this.guardPending(new Promise((resolve2) => {
|
|
42480
42560
|
this.pending.set(id, resolve2);
|
|
42481
|
-
});
|
|
42561
|
+
}));
|
|
42482
42562
|
await this.sendInnerFrame(inner);
|
|
42483
42563
|
const timeoutMs = this.opts.fetchTimeoutMs;
|
|
42484
42564
|
if (timeoutMs === void 0 || timeoutMs <= 0)
|
|
@@ -42525,6 +42605,34 @@ var FetchproxyServer = class {
|
|
|
42525
42605
|
* already surface for fetch timeouts). `0`/unset opts out (unbounded),
|
|
42526
42606
|
* matching the fetch path.
|
|
42527
42607
|
*/
|
|
42608
|
+
/**
|
|
42609
|
+
* Register a verb's pending promise and keep an early rejection from being
|
|
42610
|
+
* fatal.
|
|
42611
|
+
*
|
|
42612
|
+
* Every verb stores its resolver, then AWAITS `sendInnerFrame` before the
|
|
42613
|
+
* caller attaches a handler — and that send does real work (sealing, a
|
|
42614
|
+
* socket write), so it yields across event-loop turns. Anything that drains
|
|
42615
|
+
* the pending maps inside that window rejects a promise nothing is
|
|
42616
|
+
* listening to yet: a refused pairing (`rejectAllPending(pairingErrorMessage
|
|
42617
|
+
* (code))`), an extension disconnect, a `close()`. Under Node's default
|
|
42618
|
+
* `--unhandled-rejections=throw` that ends the PROCESS, so an MCP that
|
|
42619
|
+
* should have reported "approve pair code NNN-NN" dies instead, and each
|
|
42620
|
+
* respawn mints a fresh code nobody can approve (#329).
|
|
42621
|
+
*
|
|
42622
|
+
* The no-op catch is a listener, not a swallow. `.catch()` derives a NEW
|
|
42623
|
+
* promise and leaves this one's rejection intact, so `_withVerbTimeout` and
|
|
42624
|
+
* the caller still see it — the only thing that changes is that the
|
|
42625
|
+
* rejection is never unhandled.
|
|
42626
|
+
*
|
|
42627
|
+
* This is the same hazard `_withVerbTimeout`'s timer already guards against
|
|
42628
|
+
* from the other side, where it drops the resolver "so a late bridge
|
|
42629
|
+
* response doesn't become an unhandled promise that crashes the host".
|
|
42630
|
+
*/
|
|
42631
|
+
guardPending(pending) {
|
|
42632
|
+
pending.catch(() => {
|
|
42633
|
+
});
|
|
42634
|
+
return pending;
|
|
42635
|
+
}
|
|
42528
42636
|
async _withVerbTimeout(pending, pendingMap, id, url2, requestedTimeoutMs) {
|
|
42529
42637
|
const timeoutMs = this.opts.fetchTimeoutMs;
|
|
42530
42638
|
if (timeoutMs === void 0 || timeoutMs <= 0)
|
|
@@ -42604,9 +42712,9 @@ var FetchproxyServer = class {
|
|
|
42604
42712
|
if (opts.subdomain !== void 0)
|
|
42605
42713
|
assertSubdomainLabel(opts.subdomain);
|
|
42606
42714
|
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
42607
|
-
const
|
|
42715
|
+
const isAbsolute2 = path.startsWith("http://") || path.startsWith("https://");
|
|
42608
42716
|
let host;
|
|
42609
|
-
if (
|
|
42717
|
+
if (isAbsolute2) {
|
|
42610
42718
|
try {
|
|
42611
42719
|
host = new URL(path).host;
|
|
42612
42720
|
} catch {
|
|
@@ -42615,7 +42723,7 @@ var FetchproxyServer = class {
|
|
|
42615
42723
|
} else {
|
|
42616
42724
|
host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
|
|
42617
42725
|
}
|
|
42618
|
-
const url2 =
|
|
42726
|
+
const url2 = isAbsolute2 ? path : `https://${host}${path}`;
|
|
42619
42727
|
assertUrlInDomains("request url", url2, this.opts.domains);
|
|
42620
42728
|
let tabUrl = `https://${host}/`;
|
|
42621
42729
|
if (opts.viaTab !== void 0) {
|
|
@@ -42636,7 +42744,11 @@ var FetchproxyServer = class {
|
|
|
42636
42744
|
// Spread rather than `inPage: opts.inPage`: the wire's validator treats
|
|
42637
42745
|
// the key's PRESENCE as the request, and a literal `undefined` would
|
|
42638
42746
|
// both serialize away inconsistently and read as "asked for" here.
|
|
42639
|
-
...opts.inPage === true ? { inPage: true } : {}
|
|
42747
|
+
...opts.inPage === true ? { inPage: true } : {},
|
|
42748
|
+
// Spread for the reason `inPage` is: only a deliberate 'omit' goes on
|
|
42749
|
+
// the wire, so an untouched request serialises byte-identically to
|
|
42750
|
+
// every version before this option existed.
|
|
42751
|
+
...opts.credentials === "omit" ? { credentials: "omit" } : {}
|
|
42640
42752
|
};
|
|
42641
42753
|
const result = await this.fetch(init);
|
|
42642
42754
|
if (!result.ok) {
|
|
@@ -42864,9 +42976,9 @@ var FetchproxyServer = class {
|
|
|
42864
42976
|
const tabUrl = `https://${host}/`;
|
|
42865
42977
|
inner = { type: "request", id, op: "read_cookies", init: { tabUrl } };
|
|
42866
42978
|
}
|
|
42867
|
-
const pending = new Promise((resolve2) => {
|
|
42979
|
+
const pending = this.guardPending(new Promise((resolve2) => {
|
|
42868
42980
|
this.pendingReadCookies.set(id, resolve2);
|
|
42869
|
-
});
|
|
42981
|
+
}));
|
|
42870
42982
|
await this.sendInnerFrame(inner);
|
|
42871
42983
|
const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
|
|
42872
42984
|
if (!result.ok) {
|
|
@@ -42923,9 +43035,9 @@ var FetchproxyServer = class {
|
|
|
42923
43035
|
...cookiePath !== void 0 ? { path: cookiePath } : {}
|
|
42924
43036
|
}
|
|
42925
43037
|
};
|
|
42926
|
-
const pending = new Promise((resolve2, reject) => {
|
|
43038
|
+
const pending = this.guardPending(new Promise((resolve2, reject) => {
|
|
42927
43039
|
this.pendingWriteCookies.set(id, { resolve: resolve2, reject });
|
|
42928
|
-
});
|
|
43040
|
+
}));
|
|
42929
43041
|
await this.sendInnerFrame(inner);
|
|
42930
43042
|
return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
|
|
42931
43043
|
}
|
|
@@ -42986,9 +43098,9 @@ var FetchproxyServer = class {
|
|
|
42986
43098
|
...opts.pointers ? { pointers: { ...opts.pointers } } : {}
|
|
42987
43099
|
}
|
|
42988
43100
|
};
|
|
42989
|
-
const pending = new Promise((resolve2, reject) => {
|
|
43101
|
+
const pending = this.guardPending(new Promise((resolve2, reject) => {
|
|
42990
43102
|
this.pendingStorage.set(id, { resolve: resolve2, reject });
|
|
42991
|
-
});
|
|
43103
|
+
}));
|
|
42992
43104
|
await this.sendInnerFrame(inner);
|
|
42993
43105
|
return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
|
|
42994
43106
|
}
|
|
@@ -43091,9 +43203,9 @@ var FetchproxyServer = class {
|
|
|
43091
43203
|
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
43092
43204
|
}
|
|
43093
43205
|
};
|
|
43094
|
-
const pending = new Promise((resolve2, reject) => {
|
|
43206
|
+
const pending = this.guardPending(new Promise((resolve2, reject) => {
|
|
43095
43207
|
this.pendingCapture.set(id, { resolve: resolve2, reject });
|
|
43096
|
-
});
|
|
43208
|
+
}));
|
|
43097
43209
|
await this.sendInnerFrame(inner);
|
|
43098
43210
|
return this._withVerbTimeout(
|
|
43099
43211
|
pending,
|
|
@@ -43184,9 +43296,9 @@ var FetchproxyServer = class {
|
|
|
43184
43296
|
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
43185
43297
|
}
|
|
43186
43298
|
};
|
|
43187
|
-
const pending = new Promise((resolve2, reject) => {
|
|
43299
|
+
const pending = this.guardPending(new Promise((resolve2, reject) => {
|
|
43188
43300
|
this.pendingRedirect.set(id, { resolve: resolve2, reject });
|
|
43189
|
-
});
|
|
43301
|
+
}));
|
|
43190
43302
|
await this.sendInnerFrame(inner);
|
|
43191
43303
|
return this._withVerbTimeout(
|
|
43192
43304
|
pending,
|
|
@@ -43274,9 +43386,9 @@ var FetchproxyServer = class {
|
|
|
43274
43386
|
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
43275
43387
|
}
|
|
43276
43388
|
};
|
|
43277
|
-
const pending = new Promise((resolve2, reject) => {
|
|
43389
|
+
const pending = this.guardPending(new Promise((resolve2, reject) => {
|
|
43278
43390
|
this.pendingDownload.set(id, { resolve: resolve2, reject });
|
|
43279
|
-
});
|
|
43391
|
+
}));
|
|
43280
43392
|
await this.sendInnerFrame(inner);
|
|
43281
43393
|
return this._withVerbTimeout(
|
|
43282
43394
|
pending,
|
|
@@ -43332,9 +43444,9 @@ var FetchproxyServer = class {
|
|
|
43332
43444
|
keys: [...opts.keys]
|
|
43333
43445
|
}
|
|
43334
43446
|
};
|
|
43335
|
-
const pending = new Promise((resolve2, reject) => {
|
|
43447
|
+
const pending = this.guardPending(new Promise((resolve2, reject) => {
|
|
43336
43448
|
this.pendingIdb.set(id, { resolve: resolve2, reject });
|
|
43337
|
-
});
|
|
43449
|
+
}));
|
|
43338
43450
|
await this.sendInnerFrame(inner);
|
|
43339
43451
|
return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
|
|
43340
43452
|
}
|
|
@@ -43372,9 +43484,9 @@ var FetchproxyServer = class {
|
|
|
43372
43484
|
op: "read_dom",
|
|
43373
43485
|
init: { origin, names: [...opts.names] }
|
|
43374
43486
|
};
|
|
43375
|
-
const pending = new Promise((resolve2, reject) => {
|
|
43487
|
+
const pending = this.guardPending(new Promise((resolve2, reject) => {
|
|
43376
43488
|
this.pendingStorage.set(id, { resolve: resolve2, reject });
|
|
43377
|
-
});
|
|
43489
|
+
}));
|
|
43378
43490
|
await this.sendInnerFrame(inner);
|
|
43379
43491
|
return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
|
|
43380
43492
|
}
|
|
@@ -43418,9 +43530,9 @@ var FetchproxyServer = class {
|
|
|
43418
43530
|
...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
|
|
43419
43531
|
}
|
|
43420
43532
|
};
|
|
43421
|
-
const pending = new Promise((resolve2, reject) => {
|
|
43533
|
+
const pending = this.guardPending(new Promise((resolve2, reject) => {
|
|
43422
43534
|
this.pendingGraphql.set(id, { resolve: resolve2, reject });
|
|
43423
|
-
});
|
|
43535
|
+
}));
|
|
43424
43536
|
await this.sendInnerFrame(inner);
|
|
43425
43537
|
return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
|
|
43426
43538
|
}
|
|
@@ -43898,6 +44010,8 @@ var CREDENTIAL_ARMS = /* @__PURE__ */ new Set([
|
|
|
43898
44010
|
"ok",
|
|
43899
44011
|
"no_credential",
|
|
43900
44012
|
"credential_rejected",
|
|
44013
|
+
"session_expired",
|
|
44014
|
+
"verification_pending",
|
|
43901
44015
|
"timeout",
|
|
43902
44016
|
"http",
|
|
43903
44017
|
"transport",
|
|
@@ -43914,6 +44028,10 @@ function credentialHint(arm, prefix, hostLabel, source2) {
|
|
|
43914
44028
|
return `No credential resolved. Nothing was available to authenticate with \u2014 sign in and reconnect the connector so ${prefix} receives a token, or set the documented environment variable.`;
|
|
43915
44029
|
case "credential_rejected":
|
|
43916
44030
|
return `${hostLabel} rejected the credential from '${source2}'. It is present but no longer valid \u2014 most often expired or revoked upstream. Re-authenticate and reconnect; retrying will not fix it.`;
|
|
44031
|
+
case "session_expired":
|
|
44032
|
+
return `The credential from '${source2}' is configured, but no session is live \u2014 ${hostLabel} served a sign-in page rather than the data. Sign in again; a cookie-session portal expires these on its own, so this recurs between uses.`;
|
|
44033
|
+
case "verification_pending":
|
|
44034
|
+
return `${hostLabel} is holding the sign-in on a second factor rather than refusing it. Supply the verification code the ACCOUNT HOLDER received \u2014 the credential itself is not the problem, so changing it will not help.`;
|
|
43917
44035
|
case "timeout":
|
|
43918
44036
|
return `The credential from '${source2}' resolved, but ${hostLabel} did not answer in time. Usually transient \u2014 retry. If it persists, ${hostLabel} is slow or unreachable from here.`;
|
|
43919
44037
|
case "http":
|
|
@@ -43924,12 +44042,102 @@ function credentialHint(arm, prefix, hostLabel, source2) {
|
|
|
43924
44042
|
return `Unexpected failure \u2014 see error.message.`;
|
|
43925
44043
|
}
|
|
43926
44044
|
}
|
|
44045
|
+
function credentialHealthcheckDescription(hostLabel) {
|
|
44046
|
+
return `Resolves the credential the way real tools do, then makes one authenticated request to ${hostLabel}. Reports which source supplied the credential, whether ${hostLabel} accepted it, the round-trip time, and a plain-English hint distinguishing 'no credential' from 'credential rejected' from 'a ${hostLabel}-side problem'. Read-only; never returns the credential itself.`;
|
|
44047
|
+
}
|
|
44048
|
+
async function runCredentialHealthcheck(args) {
|
|
44049
|
+
const { prefix, hostLabel, probePath, resolveCredential, probeFn, classifyThrown, hints } = args;
|
|
44050
|
+
const probeUrl = probePath ? `https://${hostLabel}${probePath.startsWith("/") ? "" : "/"}${probePath}` : void 0;
|
|
44051
|
+
let probeStarted = 0;
|
|
44052
|
+
let state;
|
|
44053
|
+
try {
|
|
44054
|
+
state = await resolveCredential();
|
|
44055
|
+
} catch (e) {
|
|
44056
|
+
const classified = classifyThrown?.(e);
|
|
44057
|
+
const result2 = {
|
|
44058
|
+
ok: false,
|
|
44059
|
+
// Still false, and still no source: a classification explains WHY
|
|
44060
|
+
// nothing resolved, it does not invent a credential that did.
|
|
44061
|
+
credential: { source: null, resolved: false },
|
|
44062
|
+
// No `url`: nothing was probed, and naming one implies it was tried.
|
|
44063
|
+
probe: { elapsed_ms: 0 },
|
|
44064
|
+
error: {
|
|
44065
|
+
kind: classified?.kind ?? "no_credential",
|
|
44066
|
+
message: truncateErrorMessage(messageOf(e)),
|
|
44067
|
+
...classified?.detail !== void 0 ? { detail: classified.detail } : {}
|
|
44068
|
+
},
|
|
44069
|
+
// The hint must follow the KIND beside it. Falling back to
|
|
44070
|
+
// `no_credential`'s copy under a classified kind would state a cause
|
|
44071
|
+
// the kind contradicts — the same disagreement this path exists to
|
|
44072
|
+
// remove. So: an inline hint wins; else the classified arm's own
|
|
44073
|
+
// copy (consumer override first); else, for a kind this module has
|
|
44074
|
+
// no copy for, the neutral `unknown` text rather than one that
|
|
44075
|
+
// asserts a cause; else the unclassified `no_credential` default.
|
|
44076
|
+
hint: classified?.hint ?? (isArm(classified?.kind) ? hints?.[classified.kind] ?? credentialHint(classified.kind, prefix, hostLabel, null) : classified !== void 0 ? credentialHint("unknown", prefix, hostLabel, null) : hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null))
|
|
44077
|
+
};
|
|
44078
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
44079
|
+
}
|
|
44080
|
+
const credential = {
|
|
44081
|
+
source: state.source,
|
|
44082
|
+
resolved: state.source !== null,
|
|
44083
|
+
...state.detail !== void 0 ? { detail: state.detail } : {}
|
|
44084
|
+
};
|
|
44085
|
+
if (!credential.resolved) {
|
|
44086
|
+
const result2 = {
|
|
44087
|
+
ok: false,
|
|
44088
|
+
credential,
|
|
44089
|
+
probe: { elapsed_ms: 0 },
|
|
44090
|
+
error: { kind: "no_credential", message: "no credential source resolved" },
|
|
44091
|
+
hint: hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null)
|
|
44092
|
+
};
|
|
44093
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
44094
|
+
}
|
|
44095
|
+
let arm = "ok";
|
|
44096
|
+
let error61;
|
|
44097
|
+
let status;
|
|
44098
|
+
let customHint;
|
|
44099
|
+
probeStarted = Date.now();
|
|
44100
|
+
try {
|
|
44101
|
+
await probeFn();
|
|
44102
|
+
} catch (e) {
|
|
44103
|
+
status = statusOf(e);
|
|
44104
|
+
const aborted2 = e instanceof Error && e.name === "AbortError";
|
|
44105
|
+
arm = status === 401 || status === 403 ? "credential_rejected" : status !== void 0 ? "http" : aborted2 || /timeout|timed out|ETIMEDOUT/i.test(messageOf(e)) ? "timeout" : /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|network/i.test(messageOf(e)) ? "transport" : "unknown";
|
|
44106
|
+
let kind = arm;
|
|
44107
|
+
let detail;
|
|
44108
|
+
const custom2 = classifyThrown?.(e);
|
|
44109
|
+
if (custom2) {
|
|
44110
|
+
kind = custom2.kind;
|
|
44111
|
+
customHint = custom2.hint;
|
|
44112
|
+
detail = custom2.detail;
|
|
44113
|
+
arm = isArm(kind) ? kind : "unknown";
|
|
44114
|
+
}
|
|
44115
|
+
error61 = {
|
|
44116
|
+
kind,
|
|
44117
|
+
// Redacted AND bounded before it reaches the result: an upstream
|
|
44118
|
+
// failure routinely quotes what it was sent, and a healthcheck is
|
|
44119
|
+
// the tool people paste into a chat when something is broken.
|
|
44120
|
+
message: truncateErrorMessage(messageOf(e)),
|
|
44121
|
+
...detail !== void 0 ? { detail } : {}
|
|
44122
|
+
};
|
|
44123
|
+
}
|
|
44124
|
+
const result = {
|
|
44125
|
+
ok: error61 === void 0,
|
|
44126
|
+
credential,
|
|
44127
|
+
probe: {
|
|
44128
|
+
...probeUrl ? { url: probeUrl } : {},
|
|
44129
|
+
elapsed_ms: Date.now() - probeStarted,
|
|
44130
|
+
...status !== void 0 ? { status } : {}
|
|
44131
|
+
},
|
|
44132
|
+
...error61 ? { error: error61 } : {},
|
|
44133
|
+
hint: customHint ?? hints?.[arm] ?? credentialHint(arm, prefix, hostLabel, state.source)
|
|
44134
|
+
};
|
|
44135
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
44136
|
+
}
|
|
43927
44137
|
function registerCredentialHealthcheckTool(args) {
|
|
43928
|
-
|
|
43929
|
-
const probeUrl = probePath ? `https://${hostLabel}${probePath}` : void 0;
|
|
43930
|
-
server.registerTool(`${prefix}_healthcheck`, {
|
|
44138
|
+
args.server.registerTool(`${args.prefix}_healthcheck`, {
|
|
43931
44139
|
title: "Verify credentials and upstream reachability",
|
|
43932
|
-
description:
|
|
44140
|
+
description: `${credentialHealthcheckDescription(args.hostLabel)} Call this when a real tool fails and you want to know which hop broke.`,
|
|
43933
44141
|
annotations: {
|
|
43934
44142
|
title: "Verify credentials and upstream reachability",
|
|
43935
44143
|
readOnlyHint: true,
|
|
@@ -43937,92 +44145,7 @@ function registerCredentialHealthcheckTool(args) {
|
|
|
43937
44145
|
openWorldHint: true
|
|
43938
44146
|
},
|
|
43939
44147
|
inputSchema: {}
|
|
43940
|
-
}, async () =>
|
|
43941
|
-
let probeStarted = 0;
|
|
43942
|
-
let state;
|
|
43943
|
-
try {
|
|
43944
|
-
state = await resolveCredential();
|
|
43945
|
-
} catch (e) {
|
|
43946
|
-
const classified = classifyThrown?.(e);
|
|
43947
|
-
const result2 = {
|
|
43948
|
-
ok: false,
|
|
43949
|
-
// Still false, and still no source: a classification explains WHY
|
|
43950
|
-
// nothing resolved, it does not invent a credential that did.
|
|
43951
|
-
credential: { source: null, resolved: false },
|
|
43952
|
-
// No `url`: nothing was probed, and naming one implies it was tried.
|
|
43953
|
-
probe: { elapsed_ms: 0 },
|
|
43954
|
-
error: {
|
|
43955
|
-
kind: classified?.kind ?? "no_credential",
|
|
43956
|
-
message: truncateErrorMessage(messageOf(e)),
|
|
43957
|
-
...classified?.detail !== void 0 ? { detail: classified.detail } : {}
|
|
43958
|
-
},
|
|
43959
|
-
// The hint must follow the KIND beside it. Falling back to
|
|
43960
|
-
// `no_credential`'s copy under a classified kind would state a cause
|
|
43961
|
-
// the kind contradicts — the same disagreement this path exists to
|
|
43962
|
-
// remove. So: an inline hint wins; else the classified arm's own
|
|
43963
|
-
// copy (consumer override first); else, for a kind this module has
|
|
43964
|
-
// no copy for, the neutral `unknown` text rather than one that
|
|
43965
|
-
// asserts a cause; else the unclassified `no_credential` default.
|
|
43966
|
-
hint: classified?.hint ?? (isArm(classified?.kind) ? hints?.[classified.kind] ?? credentialHint(classified.kind, prefix, hostLabel, null) : classified !== void 0 ? credentialHint("unknown", prefix, hostLabel, null) : hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null))
|
|
43967
|
-
};
|
|
43968
|
-
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
43969
|
-
}
|
|
43970
|
-
const credential = {
|
|
43971
|
-
source: state.source,
|
|
43972
|
-
resolved: state.source !== null,
|
|
43973
|
-
...state.detail !== void 0 ? { detail: state.detail } : {}
|
|
43974
|
-
};
|
|
43975
|
-
if (!credential.resolved) {
|
|
43976
|
-
const result2 = {
|
|
43977
|
-
ok: false,
|
|
43978
|
-
credential,
|
|
43979
|
-
probe: { elapsed_ms: 0 },
|
|
43980
|
-
error: { kind: "no_credential", message: "no credential source resolved" },
|
|
43981
|
-
hint: hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null)
|
|
43982
|
-
};
|
|
43983
|
-
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
43984
|
-
}
|
|
43985
|
-
let arm = "ok";
|
|
43986
|
-
let error61;
|
|
43987
|
-
let status;
|
|
43988
|
-
let customHint;
|
|
43989
|
-
probeStarted = Date.now();
|
|
43990
|
-
try {
|
|
43991
|
-
await probeFn();
|
|
43992
|
-
} catch (e) {
|
|
43993
|
-
status = statusOf(e);
|
|
43994
|
-
const aborted2 = e instanceof Error && e.name === "AbortError";
|
|
43995
|
-
arm = status === 401 || status === 403 ? "credential_rejected" : status !== void 0 ? "http" : aborted2 || /timeout|timed out|ETIMEDOUT/i.test(messageOf(e)) ? "timeout" : /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|network/i.test(messageOf(e)) ? "transport" : "unknown";
|
|
43996
|
-
let kind = arm;
|
|
43997
|
-
let detail;
|
|
43998
|
-
const custom2 = classifyThrown?.(e);
|
|
43999
|
-
if (custom2) {
|
|
44000
|
-
kind = custom2.kind;
|
|
44001
|
-
customHint = custom2.hint;
|
|
44002
|
-
detail = custom2.detail;
|
|
44003
|
-
}
|
|
44004
|
-
error61 = {
|
|
44005
|
-
kind,
|
|
44006
|
-
// Redacted AND bounded before it reaches the result: an upstream
|
|
44007
|
-
// failure routinely quotes what it was sent, and a healthcheck is
|
|
44008
|
-
// the tool people paste into a chat when something is broken.
|
|
44009
|
-
message: truncateErrorMessage(messageOf(e)),
|
|
44010
|
-
...detail !== void 0 ? { detail } : {}
|
|
44011
|
-
};
|
|
44012
|
-
}
|
|
44013
|
-
const result = {
|
|
44014
|
-
ok: error61 === void 0,
|
|
44015
|
-
credential,
|
|
44016
|
-
probe: {
|
|
44017
|
-
...probeUrl ? { url: probeUrl } : {},
|
|
44018
|
-
elapsed_ms: Date.now() - probeStarted,
|
|
44019
|
-
...status !== void 0 ? { status } : {}
|
|
44020
|
-
},
|
|
44021
|
-
...error61 ? { error: error61 } : {},
|
|
44022
|
-
hint: customHint ?? hints?.[arm] ?? credentialHint(arm, prefix, hostLabel, state.source)
|
|
44023
|
-
};
|
|
44024
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
44025
|
-
});
|
|
44148
|
+
}, async () => runCredentialHealthcheck(args));
|
|
44026
44149
|
}
|
|
44027
44150
|
|
|
44028
44151
|
// src/config.ts
|
|
@@ -44066,7 +44189,7 @@ function loadAccount(env = process.env) {
|
|
|
44066
44189
|
// package.json
|
|
44067
44190
|
var package_default = {
|
|
44068
44191
|
name: "infinitecampus-mcp",
|
|
44069
|
-
version: "2.8.
|
|
44192
|
+
version: "2.8.3",
|
|
44070
44193
|
mcpName: "io.github.chrischall/infinitecampus-mcp",
|
|
44071
44194
|
description: "Infinite Campus (Campus Parent) MCP server \u2014 multi-district read + message/document write",
|
|
44072
44195
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -44111,13 +44234,13 @@ var package_default = {
|
|
|
44111
44234
|
typecheck: "tsc -p tsconfig.json --noEmit"
|
|
44112
44235
|
},
|
|
44113
44236
|
dependencies: {
|
|
44114
|
-
"@chrischall/mcp-utils": "^0.
|
|
44237
|
+
"@chrischall/mcp-utils": "^0.26.1",
|
|
44115
44238
|
"@fetchproxy/bootstrap": "^2.2.0",
|
|
44116
|
-
"@fetchproxy/server": "^2.
|
|
44117
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
44239
|
+
"@fetchproxy/server": "^2.10.0",
|
|
44240
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
44118
44241
|
dotenv: "^17.4.2",
|
|
44119
44242
|
"node-html-parser": "^9.0.0",
|
|
44120
|
-
zod: "^4.4
|
|
44243
|
+
zod: "^4.5.4"
|
|
44121
44244
|
},
|
|
44122
44245
|
devDependencies: {
|
|
44123
44246
|
"@types/node": "^26.0.0",
|
|
@@ -46084,13 +46207,29 @@ try {
|
|
|
46084
46207
|
}
|
|
46085
46208
|
var COMMON = {
|
|
46086
46209
|
name: "infinitecampus",
|
|
46087
|
-
version: "2.8.
|
|
46210
|
+
version: "2.8.3"
|
|
46088
46211
|
// x-release-please-version
|
|
46089
46212
|
};
|
|
46090
46213
|
var healthState = { account, source, configError, client: null };
|
|
46091
|
-
|
|
46092
|
-
const
|
|
46093
|
-
|
|
46214
|
+
function unconfiguredClient(reason) {
|
|
46215
|
+
const fail = () => {
|
|
46216
|
+
throw new McpToolError(
|
|
46217
|
+
`infinitecampus-mcp is not configured: ${reason?.message ?? "unknown error"}`,
|
|
46218
|
+
{
|
|
46219
|
+
hint: "Set IC_BASE_URL (your portal URL) and IC_DISTRICT (the app-name path segment), plus IC_USERNAME and IC_PASSWORD together if you are not signing in through the browser. Call ic_healthcheck for the resolved state."
|
|
46220
|
+
}
|
|
46221
|
+
);
|
|
46222
|
+
};
|
|
46223
|
+
return new Proxy({}, {
|
|
46224
|
+
get(_target, prop) {
|
|
46225
|
+
if (typeof prop === "symbol" || prop === "then") return void 0;
|
|
46226
|
+
return fail;
|
|
46227
|
+
}
|
|
46228
|
+
});
|
|
46229
|
+
}
|
|
46230
|
+
{
|
|
46231
|
+
const client = account ? new ICClient(account, { refreshSession }) : unconfiguredClient(configError);
|
|
46232
|
+
if (account) healthState.client = client;
|
|
46094
46233
|
const tools = [
|
|
46095
46234
|
(server) => registerHealthcheckTools(server, healthState),
|
|
46096
46235
|
registerDistrictTools,
|
|
@@ -46112,16 +46251,8 @@ if (account) {
|
|
|
46112
46251
|
registerFeaturesTools
|
|
46113
46252
|
];
|
|
46114
46253
|
const suffix = source === "fetchproxy" ? " [via fetchproxy]" : "";
|
|
46115
|
-
const
|
|
46116
|
-
|
|
46254
|
+
const banner = account ? `[infinitecampus-mcp] District: ${account.name} (${account.baseUrl})${suffix}` : `[infinitecampus-mcp] Not configured: ${configError?.message ?? "unknown error"}
|
|
46255
|
+
[infinitecampus-mcp] Every tool is registered and will report this until the required env vars are set. Call ic_healthcheck for the resolved state.`;
|
|
46256
|
+
await runMcp({ ...COMMON, deps: client, tools, banner: `${banner}
|
|
46117
46257
|
${AI_NOTICE}` });
|
|
46118
|
-
} else {
|
|
46119
|
-
const notConfigured = `[infinitecampus-mcp] Not configured: ${configError?.message ?? "unknown error"}
|
|
46120
|
-
[infinitecampus-mcp] Server is running with no tools registered. Set the required env vars and reinstall.`;
|
|
46121
|
-
await runMcp({
|
|
46122
|
-
...COMMON,
|
|
46123
|
-
tools: [(server) => registerHealthcheckTools(server, healthState)],
|
|
46124
|
-
banner: `${notConfigured}
|
|
46125
|
-
${AI_NOTICE}`
|
|
46126
|
-
});
|
|
46127
46258
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { dirname, join } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
import { loadDotenvSafely, runMcp } from '@chrischall/mcp-utils';
|
|
4
|
+
import { loadDotenvSafely, McpToolError, runMcp } from '@chrischall/mcp-utils';
|
|
5
5
|
// quiet load of the local .env (no-throw, silent when the file is absent —
|
|
6
6
|
// e.g. inside an mcpb bundle). Path is resolved next to dist/ so the same
|
|
7
7
|
// `..`/.env that the previous inline dotenv call used still applies.
|
|
@@ -35,7 +35,7 @@ const AI_NOTICE = '[infinitecampus-mcp] Developed and maintained by AI (Claude).
|
|
|
35
35
|
// Defer config errors so the server can still start cleanly when env vars
|
|
36
36
|
// aren't set (e.g. during the host's install-time smoke test, before the
|
|
37
37
|
// user has filled in user_config OR the user hasn't yet signed into their
|
|
38
|
-
// IC portal in the browser). When not configured
|
|
38
|
+
// IC portal in the browser). When not configured every tool still registers and
|
|
39
39
|
// log a clear stderr message — far better than the previous crash loop.
|
|
40
40
|
let account = null;
|
|
41
41
|
let refreshSession;
|
|
@@ -51,21 +51,61 @@ catch (e) {
|
|
|
51
51
|
configError = e;
|
|
52
52
|
}
|
|
53
53
|
// Hand off to mcp-utils' runMcp for the connect + stdio transport + graceful
|
|
54
|
-
// shutdown.
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// the host's install-time tools/list still succeeds and the user gets an
|
|
58
|
-
// actionable stderr message (banner) instead of a crash loop.
|
|
54
|
+
// shutdown. The registrar list is the SAME either way; only the client and the
|
|
55
|
+
// banner differ. A tool surface that follows the environment is what this
|
|
56
|
+
// avoids — see `unconfiguredClient`.
|
|
59
57
|
const COMMON = {
|
|
60
58
|
name: 'infinitecampus',
|
|
61
|
-
version: '2.8.
|
|
59
|
+
version: '2.8.3', // x-release-please-version
|
|
62
60
|
};
|
|
63
61
|
// Shared with the healthcheck so it can report the SAME state the tools see,
|
|
64
62
|
// including on the unconfigured path where there are no tools at all.
|
|
65
63
|
const healthState = { account, source, configError, client: null };
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Stands in for the client when configuration is missing, so that every tool
|
|
66
|
+
* registers either way and the failure surfaces WHERE IT IS ACTIONABLE — in the
|
|
67
|
+
* answer to the tool somebody called, not in a tool list they never saw.
|
|
68
|
+
*
|
|
69
|
+
* The surface must not follow the environment. This server is hosted with
|
|
70
|
+
* `perUserChild` + `authFields`, so mcp-host spawns a principal-less child with
|
|
71
|
+
* none of the per-user config for prewarm, auto-update, restart and every
|
|
72
|
+
* admin-portal tool listing — and THAT child's list is what the portal
|
|
73
|
+
* publishes as the connector's surface. It advertised one tool for a connector
|
|
74
|
+
* that has twenty.
|
|
75
|
+
*
|
|
76
|
+
* This continues the reasoning that put the healthcheck on this path rather
|
|
77
|
+
* than reversing it: a server that can explain itself beats one that cannot,
|
|
78
|
+
* and nineteen tools that each name the missing variables explain it better
|
|
79
|
+
* than nineteen absences do. The healthcheck still reports the same state, and
|
|
80
|
+
* `healthState.client` deliberately stays null so it reports "unconfigured"
|
|
81
|
+
* rather than mistaking this stand-in for a live client.
|
|
82
|
+
*/
|
|
83
|
+
function unconfiguredClient(reason) {
|
|
84
|
+
const fail = () => {
|
|
85
|
+
throw new McpToolError(`infinitecampus-mcp is not configured: ${reason?.message ?? 'unknown error'}`, {
|
|
86
|
+
hint: 'Set IC_BASE_URL (your portal URL) and IC_DISTRICT (the app-name path segment), ' +
|
|
87
|
+
'plus IC_USERNAME and IC_PASSWORD together if you are not signing in through the ' +
|
|
88
|
+
'browser. Call ic_healthcheck for the resolved state.',
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
return new Proxy({}, {
|
|
92
|
+
get(_target, prop) {
|
|
93
|
+
// `then` must answer undefined or this object is thenable, and any
|
|
94
|
+
// `await client` would resolve to something other than the client.
|
|
95
|
+
// Symbols (toStringTag, node's inspect) are asked for by the runtime
|
|
96
|
+
// rather than by tool code, so they get the same treatment.
|
|
97
|
+
if (typeof prop === 'symbol' || prop === 'then')
|
|
98
|
+
return undefined;
|
|
99
|
+
return fail;
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
{
|
|
104
|
+
const client = account
|
|
105
|
+
? new ICClient(account, { refreshSession })
|
|
106
|
+
: unconfiguredClient(configError);
|
|
107
|
+
if (account)
|
|
108
|
+
healthState.client = client;
|
|
69
109
|
const tools = [
|
|
70
110
|
(server) => registerHealthcheckTools(server, healthState),
|
|
71
111
|
registerDistrictTools,
|
|
@@ -87,19 +127,10 @@ if (account) {
|
|
|
87
127
|
registerFeaturesTools,
|
|
88
128
|
];
|
|
89
129
|
const suffix = source === 'fetchproxy' ? ' [via fetchproxy]' : '';
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// NOT `tools: []` any more. A server that registers nothing explains itself
|
|
97
|
-
// only on stderr, which a hosted connector never sees — so a misconfigured
|
|
98
|
-
// server was indistinguishable from a broken one. The healthcheck is the one
|
|
99
|
-
// tool that must survive this path, because this is the path that needs it.
|
|
100
|
-
await runMcp({
|
|
101
|
-
...COMMON,
|
|
102
|
-
tools: [(server) => registerHealthcheckTools(server, healthState)],
|
|
103
|
-
banner: `${notConfigured}\n${AI_NOTICE}`,
|
|
104
|
-
});
|
|
130
|
+
const banner = account
|
|
131
|
+
? `[infinitecampus-mcp] District: ${account.name} (${account.baseUrl})${suffix}`
|
|
132
|
+
: `[infinitecampus-mcp] Not configured: ${configError?.message ?? 'unknown error'}\n` +
|
|
133
|
+
'[infinitecampus-mcp] Every tool is registered and will report this until the ' +
|
|
134
|
+
'required env vars are set. Call ic_healthcheck for the resolved state.';
|
|
135
|
+
await runMcp({ ...COMMON, deps: client, tools, banner: `${banner}\n${AI_NOTICE}` });
|
|
105
136
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinitecampus-mcp",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.3",
|
|
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>",
|
|
@@ -45,13 +45,13 @@
|
|
|
45
45
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@chrischall/mcp-utils": "^0.
|
|
48
|
+
"@chrischall/mcp-utils": "^0.26.1",
|
|
49
49
|
"@fetchproxy/bootstrap": "^2.2.0",
|
|
50
|
-
"@fetchproxy/server": "^2.
|
|
51
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
50
|
+
"@fetchproxy/server": "^2.10.0",
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
52
52
|
"dotenv": "^17.4.2",
|
|
53
53
|
"node-html-parser": "^9.0.0",
|
|
54
|
-
"zod": "^4.4
|
|
54
|
+
"zod": "^4.5.4"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/node": "^26.0.0",
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/infinitecampus-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.8.
|
|
9
|
+
"version": "2.8.3",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "infinitecampus-mcp",
|
|
14
|
-
"version": "2.8.
|
|
14
|
+
"version": "2.8.3",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|