infinitecampus-mcp 2.3.6 → 2.4.2
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 +2 -2
- package/dist/bundle.js +352 -52
- package/dist/index.js +1 -1
- package/package.json +6 -6
- package/server.json +2 -2
- package/skills/infinitecampus-api/SKILL.md +142 -0
- package/skills/infinitecampus-api/references/endpoints.md +225 -0
|
@@ -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.
|
|
10
|
+
"version": "2.4.2"
|
|
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.
|
|
18
|
+
"version": "2.4.2",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinitecampus-mcp",
|
|
3
3
|
"displayName": "Infinite Campus",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.4.2",
|
|
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",
|
|
@@ -18,6 +18,6 @@
|
|
|
18
18
|
"k12",
|
|
19
19
|
"mcp"
|
|
20
20
|
],
|
|
21
|
-
"skills": "./
|
|
21
|
+
"skills": "./skills/",
|
|
22
22
|
"mcp": "./.mcp.json"
|
|
23
23
|
}
|
package/dist/bundle.js
CHANGED
|
@@ -3776,6 +3776,7 @@ var require_fast_uri = __commonJS({
|
|
|
3776
3776
|
return uriTokens.join("");
|
|
3777
3777
|
}
|
|
3778
3778
|
var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
|
|
3779
|
+
var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
|
|
3779
3780
|
function getParseError(parsed, matches) {
|
|
3780
3781
|
if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
|
|
3781
3782
|
return 'URI path must start with "/" when authority is present.';
|
|
@@ -3805,6 +3806,11 @@ var require_fast_uri = __commonJS({
|
|
|
3805
3806
|
uri = "//" + uri;
|
|
3806
3807
|
}
|
|
3807
3808
|
}
|
|
3809
|
+
const authorityMatch = uri.match(AUTHORITY_PREFIX);
|
|
3810
|
+
if (authorityMatch !== null && authorityMatch[1].indexOf("\\") !== -1) {
|
|
3811
|
+
parsed.error = "URI authority must not contain a literal backslash.";
|
|
3812
|
+
malformedAuthorityOrPort = true;
|
|
3813
|
+
}
|
|
3808
3814
|
const matches = uri.match(URI_PARSE);
|
|
3809
3815
|
if (matches) {
|
|
3810
3816
|
parsed.scheme = matches[1];
|
|
@@ -3848,7 +3854,7 @@ var require_fast_uri = __commonJS({
|
|
|
3848
3854
|
if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) {
|
|
3849
3855
|
if (parsed.host && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
|
|
3850
3856
|
try {
|
|
3851
|
-
parsed.host = URL
|
|
3857
|
+
parsed.host = new URL("http://" + parsed.host).hostname;
|
|
3852
3858
|
} catch (e) {
|
|
3853
3859
|
parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
|
|
3854
3860
|
}
|
|
@@ -7693,6 +7699,7 @@ var require_receiver = __commonJS({
|
|
|
7693
7699
|
this._opcode = 0;
|
|
7694
7700
|
this._totalPayloadLength = 0;
|
|
7695
7701
|
this._messageLength = 0;
|
|
7702
|
+
this._numFragments = 0;
|
|
7696
7703
|
this._fragments = [];
|
|
7697
7704
|
this._errored = false;
|
|
7698
7705
|
this._loop = false;
|
|
@@ -8043,23 +8050,23 @@ var require_receiver = __commonJS({
|
|
|
8043
8050
|
this.controlMessage(data, cb);
|
|
8044
8051
|
return;
|
|
8045
8052
|
}
|
|
8053
|
+
if (this._maxFragments > 0 && ++this._numFragments > this._maxFragments) {
|
|
8054
|
+
const error51 = this.createError(
|
|
8055
|
+
RangeError,
|
|
8056
|
+
"Too many message fragments",
|
|
8057
|
+
false,
|
|
8058
|
+
1008,
|
|
8059
|
+
"WS_ERR_TOO_MANY_BUFFERED_PARTS"
|
|
8060
|
+
);
|
|
8061
|
+
cb(error51);
|
|
8062
|
+
return;
|
|
8063
|
+
}
|
|
8046
8064
|
if (this._compressed) {
|
|
8047
8065
|
this._state = INFLATING;
|
|
8048
8066
|
this.decompress(data, cb);
|
|
8049
8067
|
return;
|
|
8050
8068
|
}
|
|
8051
8069
|
if (data.length) {
|
|
8052
|
-
if (this._maxFragments > 0 && this._fragments.length >= this._maxFragments) {
|
|
8053
|
-
const error51 = this.createError(
|
|
8054
|
-
RangeError,
|
|
8055
|
-
"Too many message fragments",
|
|
8056
|
-
false,
|
|
8057
|
-
1008,
|
|
8058
|
-
"WS_ERR_TOO_MANY_BUFFERED_PARTS"
|
|
8059
|
-
);
|
|
8060
|
-
cb(error51);
|
|
8061
|
-
return;
|
|
8062
|
-
}
|
|
8063
8070
|
this._messageLength = this._totalPayloadLength;
|
|
8064
8071
|
this._fragments.push(data);
|
|
8065
8072
|
}
|
|
@@ -8089,17 +8096,6 @@ var require_receiver = __commonJS({
|
|
|
8089
8096
|
cb(error51);
|
|
8090
8097
|
return;
|
|
8091
8098
|
}
|
|
8092
|
-
if (this._maxFragments > 0 && this._fragments.length >= this._maxFragments) {
|
|
8093
|
-
const error51 = this.createError(
|
|
8094
|
-
RangeError,
|
|
8095
|
-
"Too many message fragments",
|
|
8096
|
-
false,
|
|
8097
|
-
1008,
|
|
8098
|
-
"WS_ERR_TOO_MANY_BUFFERED_PARTS"
|
|
8099
|
-
);
|
|
8100
|
-
cb(error51);
|
|
8101
|
-
return;
|
|
8102
|
-
}
|
|
8103
8099
|
this._fragments.push(buf);
|
|
8104
8100
|
}
|
|
8105
8101
|
this.dataMessage(cb);
|
|
@@ -8122,6 +8118,7 @@ var require_receiver = __commonJS({
|
|
|
8122
8118
|
this._totalPayloadLength = 0;
|
|
8123
8119
|
this._messageLength = 0;
|
|
8124
8120
|
this._fragmented = 0;
|
|
8121
|
+
this._numFragments = 0;
|
|
8125
8122
|
this._fragments = [];
|
|
8126
8123
|
if (this._opcode === 2) {
|
|
8127
8124
|
let data;
|
|
@@ -9622,8 +9619,8 @@ var require_websocket = __commonJS({
|
|
|
9622
9619
|
autoPong: true,
|
|
9623
9620
|
closeTimeout: CLOSE_TIMEOUT,
|
|
9624
9621
|
protocolVersion: protocolVersions[1],
|
|
9625
|
-
maxBufferedChunks:
|
|
9626
|
-
maxFragments:
|
|
9622
|
+
maxBufferedChunks: 256 * 1024,
|
|
9623
|
+
maxFragments: 16 * 1024,
|
|
9627
9624
|
maxPayload: 100 * 1024 * 1024,
|
|
9628
9625
|
skipUTF8Validation: false,
|
|
9629
9626
|
perMessageDeflate: true,
|
|
@@ -10210,9 +10207,9 @@ var require_websocket_server = __commonJS({
|
|
|
10210
10207
|
* called
|
|
10211
10208
|
* @param {Function} [options.handleProtocols] A hook to handle protocols
|
|
10212
10209
|
* @param {String} [options.host] The hostname where to bind the server
|
|
10213
|
-
* @param {Number} [options.maxBufferedChunks=
|
|
10210
|
+
* @param {Number} [options.maxBufferedChunks=262144] The maximum number of
|
|
10214
10211
|
* buffered data chunks
|
|
10215
|
-
* @param {Number} [options.maxFragments=
|
|
10212
|
+
* @param {Number} [options.maxFragments=16384] The maximum number of message
|
|
10216
10213
|
* fragments
|
|
10217
10214
|
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
|
|
10218
10215
|
* size
|
|
@@ -10235,8 +10232,8 @@ var require_websocket_server = __commonJS({
|
|
|
10235
10232
|
options = {
|
|
10236
10233
|
allowSynchronousEvents: true,
|
|
10237
10234
|
autoPong: true,
|
|
10238
|
-
maxBufferedChunks:
|
|
10239
|
-
maxFragments:
|
|
10235
|
+
maxBufferedChunks: 256 * 1024,
|
|
10236
|
+
maxFragments: 16 * 1024,
|
|
10240
10237
|
maxPayload: 100 * 1024 * 1024,
|
|
10241
10238
|
skipUTF8Validation: false,
|
|
10242
10239
|
perMessageDeflate: false,
|
|
@@ -34845,7 +34842,9 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
34845
34842
|
"capture_request_header",
|
|
34846
34843
|
"capture_redirect",
|
|
34847
34844
|
"read_indexed_db",
|
|
34848
|
-
"
|
|
34845
|
+
"read_dom",
|
|
34846
|
+
"download",
|
|
34847
|
+
"graphql"
|
|
34849
34848
|
]);
|
|
34850
34849
|
|
|
34851
34850
|
// node_modules/@fetchproxy/protocol/dist/mcp-id.js
|
|
@@ -35146,6 +35145,76 @@ function assertIndexedDbScopesArray(value, label) {
|
|
|
35146
35145
|
}
|
|
35147
35146
|
}
|
|
35148
35147
|
}
|
|
35148
|
+
var DOM_SELECTOR_RE = /^[^-]{1,512}$/;
|
|
35149
|
+
var DOM_ATTRIBUTE_RE = /^[A-Za-z_:][A-Za-z0-9_:.\-]{0,127}$/;
|
|
35150
|
+
var GRAPHQL_OP_NAME_RE = /^[_A-Za-z][_0-9A-Za-z]{0,127}$/;
|
|
35151
|
+
function assertDomSelectorsArray(value, label) {
|
|
35152
|
+
if (!Array.isArray(value)) {
|
|
35153
|
+
throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
|
|
35154
|
+
}
|
|
35155
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35156
|
+
for (let i = 0; i < value.length; i++) {
|
|
35157
|
+
const entry = value[i];
|
|
35158
|
+
assertObject(entry, `${label}[${i}]`);
|
|
35159
|
+
if (entry.name === void 0) {
|
|
35160
|
+
throw new ProtocolError(`${label}[${i}].name: missing`);
|
|
35161
|
+
}
|
|
35162
|
+
if (entry.selector === void 0) {
|
|
35163
|
+
throw new ProtocolError(`${label}[${i}].selector: missing`);
|
|
35164
|
+
}
|
|
35165
|
+
if (typeof entry.name !== "string" || !SCOPE_KEY_RE.test(entry.name)) {
|
|
35166
|
+
throw new ProtocolError(`${label}[${i}].name: invalid ${JSON.stringify(entry.name)}`);
|
|
35167
|
+
}
|
|
35168
|
+
if (typeof entry.selector !== "string" || !DOM_SELECTOR_RE.test(entry.selector)) {
|
|
35169
|
+
throw new ProtocolError(`${label}[${i}].selector: invalid ${JSON.stringify(entry.selector)}`);
|
|
35170
|
+
}
|
|
35171
|
+
if (entry.attribute !== void 0) {
|
|
35172
|
+
if (typeof entry.attribute !== "string" || !DOM_ATTRIBUTE_RE.test(entry.attribute)) {
|
|
35173
|
+
throw new ProtocolError(`${label}[${i}].attribute: invalid ${JSON.stringify(entry.attribute)}`);
|
|
35174
|
+
}
|
|
35175
|
+
}
|
|
35176
|
+
if (seen.has(entry.name)) {
|
|
35177
|
+
throw new ProtocolError(`${label}: duplicate name ${JSON.stringify(entry.name)}`);
|
|
35178
|
+
}
|
|
35179
|
+
seen.add(entry.name);
|
|
35180
|
+
for (const k of Object.keys(entry)) {
|
|
35181
|
+
if (k !== "name" && k !== "selector" && k !== "attribute") {
|
|
35182
|
+
throw new ProtocolError(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
|
|
35183
|
+
}
|
|
35184
|
+
}
|
|
35185
|
+
}
|
|
35186
|
+
}
|
|
35187
|
+
function assertGraphqlOpsArray(value, label) {
|
|
35188
|
+
if (!Array.isArray(value)) {
|
|
35189
|
+
throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
|
|
35190
|
+
}
|
|
35191
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35192
|
+
for (let i = 0; i < value.length; i++) {
|
|
35193
|
+
const entry = value[i];
|
|
35194
|
+
assertObject(entry, `${label}[${i}]`);
|
|
35195
|
+
if (entry.name === void 0) {
|
|
35196
|
+
throw new ProtocolError(`${label}[${i}].name: missing`);
|
|
35197
|
+
}
|
|
35198
|
+
if (entry.operationName === void 0) {
|
|
35199
|
+
throw new ProtocolError(`${label}[${i}].operationName: missing`);
|
|
35200
|
+
}
|
|
35201
|
+
if (typeof entry.name !== "string" || !SCOPE_KEY_RE.test(entry.name)) {
|
|
35202
|
+
throw new ProtocolError(`${label}[${i}].name: invalid ${JSON.stringify(entry.name)}`);
|
|
35203
|
+
}
|
|
35204
|
+
if (typeof entry.operationName !== "string" || !GRAPHQL_OP_NAME_RE.test(entry.operationName)) {
|
|
35205
|
+
throw new ProtocolError(`${label}[${i}].operationName: invalid ${JSON.stringify(entry.operationName)}`);
|
|
35206
|
+
}
|
|
35207
|
+
if (seen.has(entry.name)) {
|
|
35208
|
+
throw new ProtocolError(`${label}: duplicate name ${JSON.stringify(entry.name)}`);
|
|
35209
|
+
}
|
|
35210
|
+
seen.add(entry.name);
|
|
35211
|
+
for (const k of Object.keys(entry)) {
|
|
35212
|
+
if (k !== "name" && k !== "operationName") {
|
|
35213
|
+
throw new ProtocolError(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
|
|
35214
|
+
}
|
|
35215
|
+
}
|
|
35216
|
+
}
|
|
35217
|
+
}
|
|
35149
35218
|
function validateFrame(raw) {
|
|
35150
35219
|
assertObject(raw, "frame");
|
|
35151
35220
|
const t = raw.type;
|
|
@@ -35221,6 +35290,12 @@ function validateHello(raw) {
|
|
|
35221
35290
|
if (raw.sessionStoragePointers !== void 0) {
|
|
35222
35291
|
assertStoragePointersArray(raw.sessionStoragePointers, "hello.sessionStoragePointers", raw.sessionStorageKeys);
|
|
35223
35292
|
}
|
|
35293
|
+
if (raw.domSelectors !== void 0) {
|
|
35294
|
+
assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
|
|
35295
|
+
}
|
|
35296
|
+
if (raw.graphqlOps !== void 0) {
|
|
35297
|
+
assertGraphqlOpsArray(raw.graphqlOps, "hello.graphqlOps");
|
|
35298
|
+
}
|
|
35224
35299
|
assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
|
|
35225
35300
|
assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
|
|
35226
35301
|
assertBase64(raw.sessionNonce, "hello.sessionNonce");
|
|
@@ -35455,6 +35530,43 @@ function validateInnerRequest(raw) {
|
|
|
35455
35530
|
}
|
|
35456
35531
|
return raw;
|
|
35457
35532
|
}
|
|
35533
|
+
if (raw.op === "read_dom") {
|
|
35534
|
+
assertObject(raw.init, "inner.init");
|
|
35535
|
+
if (raw.init.origin === void 0)
|
|
35536
|
+
throw new ProtocolError("inner.init.origin: missing");
|
|
35537
|
+
if (raw.init.names === void 0)
|
|
35538
|
+
throw new ProtocolError("inner.init.names: missing");
|
|
35539
|
+
assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
|
|
35540
|
+
assertNonEmptyKeyArray(raw.init.names, "inner.init.names");
|
|
35541
|
+
for (const k of Object.keys(raw.init)) {
|
|
35542
|
+
if (k !== "origin" && k !== "names") {
|
|
35543
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on read_dom`);
|
|
35544
|
+
}
|
|
35545
|
+
}
|
|
35546
|
+
return raw;
|
|
35547
|
+
}
|
|
35548
|
+
if (raw.op === "graphql_query") {
|
|
35549
|
+
assertObject(raw.init, "inner.init");
|
|
35550
|
+
if (raw.init.name === void 0)
|
|
35551
|
+
throw new ProtocolError("inner.init.name: missing");
|
|
35552
|
+
if (raw.init.variables === void 0) {
|
|
35553
|
+
throw new ProtocolError("inner.init.variables: missing");
|
|
35554
|
+
}
|
|
35555
|
+
assertString(raw.init.name, "inner.init.name");
|
|
35556
|
+
if (raw.init.name.length === 0) {
|
|
35557
|
+
throw new ProtocolError("inner.init.name: must be non-empty");
|
|
35558
|
+
}
|
|
35559
|
+
assertObject(raw.init.variables, "inner.init.variables");
|
|
35560
|
+
if (raw.init.tabUrl !== void 0) {
|
|
35561
|
+
assertString(raw.init.tabUrl, "inner.init.tabUrl");
|
|
35562
|
+
}
|
|
35563
|
+
for (const k of Object.keys(raw.init)) {
|
|
35564
|
+
if (k !== "name" && k !== "variables" && k !== "tabUrl") {
|
|
35565
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on graphql_query`);
|
|
35566
|
+
}
|
|
35567
|
+
}
|
|
35568
|
+
return raw;
|
|
35569
|
+
}
|
|
35458
35570
|
if (raw.op === "download") {
|
|
35459
35571
|
assertObject(raw.init, "inner.init");
|
|
35460
35572
|
if (raw.init.url === void 0) {
|
|
@@ -35480,7 +35592,7 @@ function validateInnerRequest(raw) {
|
|
|
35480
35592
|
}
|
|
35481
35593
|
return raw;
|
|
35482
35594
|
}
|
|
35483
|
-
throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "download"; got ${JSON.stringify(raw.op)}`);
|
|
35595
|
+
throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "read_dom", "download", "graphql_query"; got ${JSON.stringify(raw.op)}`);
|
|
35484
35596
|
}
|
|
35485
35597
|
function assertNonEmptyKeyArray(value, label) {
|
|
35486
35598
|
if (!Array.isArray(value)) {
|
|
@@ -35511,6 +35623,10 @@ function assertStringMap(value, label) {
|
|
|
35511
35623
|
}
|
|
35512
35624
|
}
|
|
35513
35625
|
}
|
|
35626
|
+
var KNOWN_RESPONSE_OPS = /* @__PURE__ */ new Set([
|
|
35627
|
+
...KNOWN_CAPABILITIES,
|
|
35628
|
+
"graphql_query"
|
|
35629
|
+
]);
|
|
35514
35630
|
function validateInnerResponse(raw) {
|
|
35515
35631
|
assertPositiveInt(raw.id, "inner.id");
|
|
35516
35632
|
if (raw.ok === true) {
|
|
@@ -35565,6 +35681,20 @@ function validateInnerResponse(raw) {
|
|
|
35565
35681
|
assertObject(raw.values, "inner.values");
|
|
35566
35682
|
return raw;
|
|
35567
35683
|
}
|
|
35684
|
+
if (op === "read_dom") {
|
|
35685
|
+
if (raw.values === void 0) {
|
|
35686
|
+
throw new ProtocolError("inner.values: missing on read_dom response");
|
|
35687
|
+
}
|
|
35688
|
+
assertStringMap(raw.values, "inner.values");
|
|
35689
|
+
return raw;
|
|
35690
|
+
}
|
|
35691
|
+
if (op === "graphql_query") {
|
|
35692
|
+
if (raw.data === void 0) {
|
|
35693
|
+
throw new ProtocolError("inner.data: missing on graphql_query response");
|
|
35694
|
+
}
|
|
35695
|
+
assertObject(raw.data, "inner.data");
|
|
35696
|
+
return raw;
|
|
35697
|
+
}
|
|
35568
35698
|
if (op === "download") {
|
|
35569
35699
|
assertObject(raw.value, "inner.value");
|
|
35570
35700
|
assertString(raw.value.path, "inner.value.path");
|
|
@@ -35589,7 +35719,7 @@ function validateInnerResponse(raw) {
|
|
|
35589
35719
|
if (raw.ok === false) {
|
|
35590
35720
|
assertString(raw.error, "inner.error");
|
|
35591
35721
|
if (raw.op !== void 0) {
|
|
35592
|
-
if (typeof raw.op !== "string" || !
|
|
35722
|
+
if (typeof raw.op !== "string" || !KNOWN_RESPONSE_OPS.has(raw.op)) {
|
|
35593
35723
|
throw new ProtocolError(`inner.op: unknown response op ${JSON.stringify(raw.op)}`);
|
|
35594
35724
|
}
|
|
35595
35725
|
}
|
|
@@ -35781,11 +35911,33 @@ async function sealInnerFrame(sessionKey, mcpId, seq, inner) {
|
|
|
35781
35911
|
};
|
|
35782
35912
|
}
|
|
35783
35913
|
async function openEncryptedFrame(sessionKey, frame) {
|
|
35784
|
-
const
|
|
35785
|
-
|
|
35786
|
-
|
|
35787
|
-
|
|
35788
|
-
|
|
35914
|
+
const result = await openEncryptedFrameDetailed(sessionKey, frame);
|
|
35915
|
+
if (result.stage === "ok")
|
|
35916
|
+
return result.inner;
|
|
35917
|
+
throw result.error instanceof Error ? result.error : new Error(String(result.error));
|
|
35918
|
+
}
|
|
35919
|
+
async function openEncryptedFrameDetailed(sessionKey, frame) {
|
|
35920
|
+
let pt;
|
|
35921
|
+
try {
|
|
35922
|
+
const iv = fromB64(frame.iv);
|
|
35923
|
+
const ct = fromB64(frame.ciphertext);
|
|
35924
|
+
pt = await aesGcmOpen(sessionKey, iv, ct);
|
|
35925
|
+
} catch (error51) {
|
|
35926
|
+
return { stage: "decrypt-failed", error: error51 };
|
|
35927
|
+
}
|
|
35928
|
+
let parsed;
|
|
35929
|
+
try {
|
|
35930
|
+
parsed = JSON.parse(dec.decode(pt));
|
|
35931
|
+
} catch (error51) {
|
|
35932
|
+
return { stage: "validation-failed", error: error51, recoveredId: void 0 };
|
|
35933
|
+
}
|
|
35934
|
+
try {
|
|
35935
|
+
const inner = validateInnerFrame(parsed);
|
|
35936
|
+
return { stage: "ok", inner };
|
|
35937
|
+
} catch (error51) {
|
|
35938
|
+
const recoveredId = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && typeof parsed.id === "number" && Number.isInteger(parsed.id) && parsed.id > 0 ? parsed.id : void 0;
|
|
35939
|
+
return { stage: "validation-failed", error: error51, recoveredId };
|
|
35940
|
+
}
|
|
35789
35941
|
}
|
|
35790
35942
|
|
|
35791
35943
|
// node_modules/@fetchproxy/server/dist/election.js
|
|
@@ -35904,6 +36056,19 @@ async function buildServerHello(opts) {
|
|
|
35904
36056
|
jsonPointer: d.jsonPointer
|
|
35905
36057
|
}));
|
|
35906
36058
|
}
|
|
36059
|
+
if (opts.domSelectors && opts.domSelectors.length > 0) {
|
|
36060
|
+
hello.domSelectors = opts.domSelectors.map((d) => ({
|
|
36061
|
+
name: d.name,
|
|
36062
|
+
selector: d.selector,
|
|
36063
|
+
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
36064
|
+
}));
|
|
36065
|
+
}
|
|
36066
|
+
if (opts.graphqlOps && opts.graphqlOps.length > 0) {
|
|
36067
|
+
hello.graphqlOps = opts.graphqlOps.map((d) => ({
|
|
36068
|
+
name: d.name,
|
|
36069
|
+
operationName: d.operationName
|
|
36070
|
+
}));
|
|
36071
|
+
}
|
|
35907
36072
|
return hello;
|
|
35908
36073
|
}
|
|
35909
36074
|
|
|
@@ -35993,7 +36158,9 @@ async function startHost(opts) {
|
|
|
35993
36158
|
captureHeaders: opts.ownCaptureHeaders,
|
|
35994
36159
|
indexedDbScopes: opts.ownIndexedDbScopes,
|
|
35995
36160
|
localStoragePointers: opts.ownLocalStoragePointers,
|
|
35996
|
-
sessionStoragePointers: opts.ownSessionStoragePointers
|
|
36161
|
+
sessionStoragePointers: opts.ownSessionStoragePointers,
|
|
36162
|
+
domSelectors: opts.ownDomSelectors,
|
|
36163
|
+
graphqlOps: opts.ownGraphqlOps
|
|
35997
36164
|
});
|
|
35998
36165
|
const ownSessionNonce = fromB64(ownHello.sessionNonce);
|
|
35999
36166
|
let extensionWs = null;
|
|
@@ -36228,8 +36395,10 @@ async function startPeer(opts) {
|
|
|
36228
36395
|
sessionStorageKeys: opts.sessionStorageKeys,
|
|
36229
36396
|
captureHeaders: opts.captureHeaders,
|
|
36230
36397
|
indexedDbScopes: opts.indexedDbScopes,
|
|
36398
|
+
domSelectors: opts.domSelectors,
|
|
36231
36399
|
localStoragePointers: opts.localStoragePointers,
|
|
36232
|
-
sessionStoragePointers: opts.sessionStoragePointers
|
|
36400
|
+
sessionStoragePointers: opts.sessionStoragePointers,
|
|
36401
|
+
graphqlOps: opts.graphqlOps
|
|
36233
36402
|
});
|
|
36234
36403
|
const sessionNonce = fromB64(hello.sessionNonce);
|
|
36235
36404
|
ws.send(JSON.stringify(hello));
|
|
@@ -36273,10 +36442,20 @@ async function startPeer(opts) {
|
|
|
36273
36442
|
return;
|
|
36274
36443
|
if (!session.acceptInboundSeq(frame.seq))
|
|
36275
36444
|
return;
|
|
36276
|
-
|
|
36277
|
-
|
|
36278
|
-
innerListeners.forEach((cb) => cb(inner));
|
|
36279
|
-
}
|
|
36445
|
+
const result = await openEncryptedFrameDetailed(session.sessionKey, frame);
|
|
36446
|
+
if (result.stage === "ok") {
|
|
36447
|
+
innerListeners.forEach((cb) => cb(result.inner));
|
|
36448
|
+
} else if (result.stage === "decrypt-failed") {
|
|
36449
|
+
} else {
|
|
36450
|
+
console.error("[fetchproxy] peer: received a frame that decrypted OK but failed validation:", result.error);
|
|
36451
|
+
if (result.recoveredId !== void 0) {
|
|
36452
|
+
innerListeners.forEach((cb) => cb({
|
|
36453
|
+
type: "response",
|
|
36454
|
+
id: result.recoveredId,
|
|
36455
|
+
ok: false,
|
|
36456
|
+
error: `malformed response failed protocol validation: ${String(result.error)}`
|
|
36457
|
+
}));
|
|
36458
|
+
}
|
|
36280
36459
|
}
|
|
36281
36460
|
}
|
|
36282
36461
|
} catch (e) {
|
|
@@ -36553,6 +36732,10 @@ var FetchproxyServer = class {
|
|
|
36553
36732
|
pendingIdb = /* @__PURE__ */ new Map();
|
|
36554
36733
|
// download awaiters resolve the saved-file metadata (path + size + mime).
|
|
36555
36734
|
pendingDownload = /* @__PURE__ */ new Map();
|
|
36735
|
+
// 1.x+: graphql_query awaiters resolve the GraphQL `data` object. Its
|
|
36736
|
+
// shape is operation-specific, so the awaiter resolves `unknown` and the
|
|
36737
|
+
// caller narrows.
|
|
36738
|
+
pendingGraphql = /* @__PURE__ */ new Map();
|
|
36556
36739
|
mcpId = null;
|
|
36557
36740
|
identity = null;
|
|
36558
36741
|
// 0.5.3+: in-flight role-election / handle-start promise. Set the
|
|
@@ -36635,6 +36818,15 @@ var FetchproxyServer = class {
|
|
|
36635
36818
|
key: d.key,
|
|
36636
36819
|
jsonPointer: d.jsonPointer
|
|
36637
36820
|
})),
|
|
36821
|
+
domSelectors: (opts.domSelectors ?? []).map((d) => ({
|
|
36822
|
+
name: d.name,
|
|
36823
|
+
selector: d.selector,
|
|
36824
|
+
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
36825
|
+
})),
|
|
36826
|
+
graphqlOps: (opts.graphqlOps ?? []).map((d) => ({
|
|
36827
|
+
name: d.name,
|
|
36828
|
+
operationName: d.operationName
|
|
36829
|
+
})),
|
|
36638
36830
|
// 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
|
|
36639
36831
|
// adapter was about to set these to the same numbers anyway; the
|
|
36640
36832
|
// back-door is `0` (explicit opt-out) if a caller genuinely wants
|
|
@@ -36755,6 +36947,8 @@ var FetchproxyServer = class {
|
|
|
36755
36947
|
ownIndexedDbScopes: this.opts.indexedDbScopes,
|
|
36756
36948
|
ownLocalStoragePointers: this.opts.localStoragePointers,
|
|
36757
36949
|
ownSessionStoragePointers: this.opts.sessionStoragePointers,
|
|
36950
|
+
ownDomSelectors: this.opts.domSelectors,
|
|
36951
|
+
ownGraphqlOps: this.opts.graphqlOps,
|
|
36758
36952
|
onPairCode: this.opts.onPairCode
|
|
36759
36953
|
});
|
|
36760
36954
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
@@ -36782,7 +36976,9 @@ var FetchproxyServer = class {
|
|
|
36782
36976
|
captureHeaders: this.opts.captureHeaders,
|
|
36783
36977
|
indexedDbScopes: this.opts.indexedDbScopes,
|
|
36784
36978
|
localStoragePointers: this.opts.localStoragePointers,
|
|
36785
|
-
sessionStoragePointers: this.opts.sessionStoragePointers
|
|
36979
|
+
sessionStoragePointers: this.opts.sessionStoragePointers,
|
|
36980
|
+
domSelectors: this.opts.domSelectors,
|
|
36981
|
+
graphqlOps: this.opts.graphqlOps
|
|
36786
36982
|
});
|
|
36787
36983
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
36788
36984
|
this.peerHandle.onRenegotiate(() => {
|
|
@@ -36988,6 +37184,7 @@ var FetchproxyServer = class {
|
|
|
36988
37184
|
this.pendingRedirect.delete(id);
|
|
36989
37185
|
this.pendingDownload.delete(id);
|
|
36990
37186
|
this.pendingIdb.delete(id);
|
|
37187
|
+
this.pendingGraphql.delete(id);
|
|
36991
37188
|
}
|
|
36992
37189
|
throw err;
|
|
36993
37190
|
}
|
|
@@ -37757,6 +37954,92 @@ var FetchproxyServer = class {
|
|
|
37757
37954
|
await this.sendInnerFrame(inner);
|
|
37758
37955
|
return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
|
|
37759
37956
|
}
|
|
37957
|
+
/**
|
|
37958
|
+
* 1.4.0+: read declared DOM values from the user's signed-in tab.
|
|
37959
|
+
* Requires `'read_dom'` in capabilities AND every requested `name` to
|
|
37960
|
+
* match a declared `domSelectors` entry. The extension reads each
|
|
37961
|
+
* declared selector from the matched tab's DOM (isolated-world
|
|
37962
|
+
* `querySelector`, value or attribute) — no page-JS execution.
|
|
37963
|
+
*
|
|
37964
|
+
* Returns a `Record<string, string>` of `name → value`, with names
|
|
37965
|
+
* whose element (or attribute) was absent omitted. Throws
|
|
37966
|
+
* `FetchproxyProtocolError` on bridge failures and a plain `Error` on
|
|
37967
|
+
* developer mistakes (undeclared capability, undeclared name).
|
|
37968
|
+
*/
|
|
37969
|
+
async readDom(opts) {
|
|
37970
|
+
if (!this.opts.capabilities.includes("read_dom")) {
|
|
37971
|
+
throw new Error('FetchproxyServer.readDom(): MCP did not declare "read_dom" in capabilities');
|
|
37972
|
+
}
|
|
37973
|
+
await this.ensureConnected();
|
|
37974
|
+
this.throwIfPendingPair();
|
|
37975
|
+
if (!Array.isArray(opts.names) || opts.names.length === 0) {
|
|
37976
|
+
throw new Error("FetchproxyServer.readDom: opts.names must be a non-empty array");
|
|
37977
|
+
}
|
|
37978
|
+
this.assertScopeSubset(opts.names, this.opts.domSelectors.map((d) => d.name), "domSelectors");
|
|
37979
|
+
if (opts.subdomain !== void 0)
|
|
37980
|
+
assertSubdomainLabel(opts.subdomain);
|
|
37981
|
+
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
37982
|
+
const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
|
|
37983
|
+
const origin = `https://${host}`;
|
|
37984
|
+
const id = this.nextRequestId++;
|
|
37985
|
+
const inner = {
|
|
37986
|
+
type: "request",
|
|
37987
|
+
id,
|
|
37988
|
+
op: "read_dom",
|
|
37989
|
+
init: { origin, names: [...opts.names] }
|
|
37990
|
+
};
|
|
37991
|
+
const pending = new Promise((resolve, reject) => {
|
|
37992
|
+
this.pendingStorage.set(id, { resolve, reject });
|
|
37993
|
+
});
|
|
37994
|
+
await this.sendInnerFrame(inner);
|
|
37995
|
+
return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
|
|
37996
|
+
}
|
|
37997
|
+
/**
|
|
37998
|
+
* 1.x+: run a declared GraphQL operation through the page's own Apollo
|
|
37999
|
+
* client (`window.__APOLLO_CLIENT__`) in the signed-in tab's MAIN world.
|
|
38000
|
+
* Requires `'graphql'` in capabilities AND `name` to match a declared
|
|
38001
|
+
* `graphqlOps` entry. The extension resolves `name` → `operationName` →
|
|
38002
|
+
* the live DocumentNode the page already observed, then invokes
|
|
38003
|
+
* `client.query({ query, variables })` — the site's own request path, so
|
|
38004
|
+
* per-request bot telemetry (Akamai etc.) runs automatically.
|
|
38005
|
+
*
|
|
38006
|
+
* Returns the GraphQL `data` object on success (shape is
|
|
38007
|
+
* operation-specific; the caller narrows). Throws a plain `Error` on
|
|
38008
|
+
* developer mistakes (undeclared capability, undeclared name) and a
|
|
38009
|
+
* descriptive `Error` on the `ok:false` bridge path — which includes the
|
|
38010
|
+
* typed "operation not yet observed on this tab" case (open the site's
|
|
38011
|
+
* page and retry).
|
|
38012
|
+
*/
|
|
38013
|
+
async graphqlQuery(opts) {
|
|
38014
|
+
if (!this.opts.capabilities.includes("graphql")) {
|
|
38015
|
+
throw new Error('FetchproxyServer.graphqlQuery(): MCP did not declare "graphql" in capabilities');
|
|
38016
|
+
}
|
|
38017
|
+
if (typeof opts.name !== "string" || opts.name.length === 0) {
|
|
38018
|
+
throw new Error("FetchproxyServer.graphqlQuery: opts.name must be a non-empty string");
|
|
38019
|
+
}
|
|
38020
|
+
const declaredNames = this.opts.graphqlOps.map((d) => d.name);
|
|
38021
|
+
if (!declaredNames.includes(opts.name)) {
|
|
38022
|
+
throw new Error(`FetchproxyServer.graphqlQuery: operation ${JSON.stringify(opts.name)} not in declared graphqlOps [${declaredNames.map((n) => JSON.stringify(n)).join(", ")}]`);
|
|
38023
|
+
}
|
|
38024
|
+
await this.ensureConnected();
|
|
38025
|
+
this.throwIfPendingPair();
|
|
38026
|
+
const id = this.nextRequestId++;
|
|
38027
|
+
const inner = {
|
|
38028
|
+
type: "request",
|
|
38029
|
+
id,
|
|
38030
|
+
op: "graphql_query",
|
|
38031
|
+
init: {
|
|
38032
|
+
name: opts.name,
|
|
38033
|
+
variables: opts.variables,
|
|
38034
|
+
...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
|
|
38035
|
+
}
|
|
38036
|
+
};
|
|
38037
|
+
const pending = new Promise((resolve, reject) => {
|
|
38038
|
+
this.pendingGraphql.set(id, { resolve, reject });
|
|
38039
|
+
});
|
|
38040
|
+
await this.sendInnerFrame(inner);
|
|
38041
|
+
return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
|
|
38042
|
+
}
|
|
37760
38043
|
assertScopeSubset(requested, declared, label) {
|
|
37761
38044
|
const undeclared = undeclaredKeys(requested, declared);
|
|
37762
38045
|
if (undeclared.length > 0) {
|
|
@@ -37828,7 +38111,7 @@ var FetchproxyServer = class {
|
|
|
37828
38111
|
if (storageCb) {
|
|
37829
38112
|
this.pendingStorage.delete(inner.id);
|
|
37830
38113
|
if (inner.ok) {
|
|
37831
|
-
if ((inner.op === "read_local_storage" || inner.op === "read_session_storage") && inner.values) {
|
|
38114
|
+
if ((inner.op === "read_local_storage" || inner.op === "read_session_storage" || inner.op === "read_dom") && inner.values) {
|
|
37832
38115
|
storageCb.resolve({ ...inner.values });
|
|
37833
38116
|
} else {
|
|
37834
38117
|
storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
|
|
@@ -37894,6 +38177,20 @@ var FetchproxyServer = class {
|
|
|
37894
38177
|
}
|
|
37895
38178
|
return;
|
|
37896
38179
|
}
|
|
38180
|
+
const graphqlCb = this.pendingGraphql.get(inner.id);
|
|
38181
|
+
if (graphqlCb) {
|
|
38182
|
+
this.pendingGraphql.delete(inner.id);
|
|
38183
|
+
if (inner.ok) {
|
|
38184
|
+
if (inner.op === "graphql_query") {
|
|
38185
|
+
graphqlCb.resolve(inner.data);
|
|
38186
|
+
} else {
|
|
38187
|
+
graphqlCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on graphql_query awaiter`));
|
|
38188
|
+
}
|
|
38189
|
+
} else {
|
|
38190
|
+
graphqlCb.reject(new FetchproxyProtocolError(inner.error));
|
|
38191
|
+
}
|
|
38192
|
+
return;
|
|
38193
|
+
}
|
|
37897
38194
|
const cookiesCb = this.pendingReadCookies.get(inner.id);
|
|
37898
38195
|
if (cookiesCb) {
|
|
37899
38196
|
this.pendingReadCookies.delete(inner.id);
|
|
@@ -37945,6 +38242,9 @@ var FetchproxyServer = class {
|
|
|
37945
38242
|
for (const { reject } of this.pendingDownload.values())
|
|
37946
38243
|
reject(err);
|
|
37947
38244
|
this.pendingDownload.clear();
|
|
38245
|
+
for (const { reject } of this.pendingGraphql.values())
|
|
38246
|
+
reject(err);
|
|
38247
|
+
this.pendingGraphql.clear();
|
|
37948
38248
|
}
|
|
37949
38249
|
/**
|
|
37950
38250
|
* 0.5.2+: read the current pair-pending pair code from whichever handle
|
|
@@ -38191,7 +38491,7 @@ function loadAccount(env = process.env) {
|
|
|
38191
38491
|
// package.json
|
|
38192
38492
|
var package_default = {
|
|
38193
38493
|
name: "infinitecampus-mcp",
|
|
38194
|
-
version: "2.
|
|
38494
|
+
version: "2.4.2",
|
|
38195
38495
|
mcpName: "io.github.chrischall/infinitecampus-mcp",
|
|
38196
38496
|
description: "Infinite Campus (Campus Parent) MCP server \u2014 multi-district read + message/document write",
|
|
38197
38497
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -38233,19 +38533,19 @@ var package_default = {
|
|
|
38233
38533
|
"test:watch": "vitest"
|
|
38234
38534
|
},
|
|
38235
38535
|
dependencies: {
|
|
38236
|
-
"@chrischall/mcp-utils": "^0.
|
|
38237
|
-
"@fetchproxy/bootstrap": "^1.
|
|
38238
|
-
"@fetchproxy/server": "^1.
|
|
38536
|
+
"@chrischall/mcp-utils": "^0.14.0",
|
|
38537
|
+
"@fetchproxy/bootstrap": "^1.7.0",
|
|
38538
|
+
"@fetchproxy/server": "^1.7.0",
|
|
38239
38539
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38240
38540
|
dotenv: "^17.4.2",
|
|
38241
|
-
"node-html-parser": "^
|
|
38541
|
+
"node-html-parser": "^9.0.0",
|
|
38242
38542
|
zod: "^4.4.3"
|
|
38243
38543
|
},
|
|
38244
38544
|
devDependencies: {
|
|
38245
38545
|
"@types/node": "^26.0.0",
|
|
38246
38546
|
"@vitest/coverage-v8": "^4.1.7",
|
|
38247
38547
|
esbuild: "^0.28.0",
|
|
38248
|
-
typescript: "^
|
|
38548
|
+
typescript: "^7.0.2",
|
|
38249
38549
|
vitest: "^4.1.7"
|
|
38250
38550
|
}
|
|
38251
38551
|
};
|
|
@@ -39807,7 +40107,7 @@ try {
|
|
|
39807
40107
|
}
|
|
39808
40108
|
var COMMON = {
|
|
39809
40109
|
name: "infinitecampus",
|
|
39810
|
-
version: "2.
|
|
40110
|
+
version: "2.4.2"
|
|
39811
40111
|
// x-release-please-version
|
|
39812
40112
|
};
|
|
39813
40113
|
if (account) {
|
package/dist/index.js
CHANGED
|
@@ -57,7 +57,7 @@ catch (e) {
|
|
|
57
57
|
// actionable stderr message (banner) instead of a crash loop.
|
|
58
58
|
const COMMON = {
|
|
59
59
|
name: 'infinitecampus',
|
|
60
|
-
version: '2.
|
|
60
|
+
version: '2.4.2', // x-release-please-version
|
|
61
61
|
};
|
|
62
62
|
if (account) {
|
|
63
63
|
const client = new ICClient(account, { preloaded });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinitecampus-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"mcpName": "io.github.chrischall/infinitecampus-mcp",
|
|
5
5
|
"description": "Infinite Campus (Campus Parent) MCP server — multi-district read + message/document write",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -42,19 +42,19 @@
|
|
|
42
42
|
"test:watch": "vitest"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@chrischall/mcp-utils": "^0.
|
|
46
|
-
"@fetchproxy/bootstrap": "^1.
|
|
47
|
-
"@fetchproxy/server": "^1.
|
|
45
|
+
"@chrischall/mcp-utils": "^0.14.0",
|
|
46
|
+
"@fetchproxy/bootstrap": "^1.7.0",
|
|
47
|
+
"@fetchproxy/server": "^1.7.0",
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
49
|
"dotenv": "^17.4.2",
|
|
50
|
-
"node-html-parser": "^
|
|
50
|
+
"node-html-parser": "^9.0.0",
|
|
51
51
|
"zod": "^4.4.3"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "^26.0.0",
|
|
55
55
|
"@vitest/coverage-v8": "^4.1.7",
|
|
56
56
|
"esbuild": "^0.28.0",
|
|
57
|
-
"typescript": "^
|
|
57
|
+
"typescript": "^7.0.2",
|
|
58
58
|
"vitest": "^4.1.7"
|
|
59
59
|
}
|
|
60
60
|
}
|
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.
|
|
9
|
+
"version": "2.4.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "infinitecampus-mcp",
|
|
14
|
-
"version": "2.
|
|
14
|
+
"version": "2.4.2",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: infinitecampus-api
|
|
3
|
+
description: >-
|
|
4
|
+
Query an Infinite Campus (Campus Parent portal) district directly with curl
|
|
5
|
+
instead of running the infinitecampus-mcp server — log in with a real
|
|
6
|
+
username/password, capture the session cookie + XSRF token, and curl grades,
|
|
7
|
+
attendance, assignments, schedule, messages, documents, and fees. Use when
|
|
8
|
+
you want IC data without the MCP, in a script, or on a machine where the MCP
|
|
9
|
+
isn't installed. Infinite Campus is per-district: the base URL and district
|
|
10
|
+
app name are configurable, not hardcoded.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Infinite Campus via curl (no MCP)
|
|
14
|
+
|
|
15
|
+
Infinite Campus's Campus Parent portal is a classic server-rendered site
|
|
16
|
+
(no public API, no bot wall). A plain form POST to `verify.jsp` logs a
|
|
17
|
+
parent in and hands back a session cookie — no browser bridge needed. This
|
|
18
|
+
is the same login `infinitecampus-mcp`'s `src/client.ts`/`src/auth.ts` do;
|
|
19
|
+
this skill reproduces it with `curl` + a cookie jar.
|
|
20
|
+
|
|
21
|
+
**Infinite Campus is per-district.** Every parent's portal lives at their
|
|
22
|
+
own district's host (`https://campus.<district>.k12.example.us` or similar)
|
|
23
|
+
under an `appName` path segment. There is no shared IC endpoint — carry the
|
|
24
|
+
base URL and district from the same config the MCP uses:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
: "${IC_BASE_URL:?set to your district's portal URL, e.g. https://campus.yourdistrict.org}"
|
|
28
|
+
: "${IC_DISTRICT:?set to the appName path segment, e.g. yourdistrict}"
|
|
29
|
+
: "${IC_USERNAME:?parent portal username}"
|
|
30
|
+
: "${IC_PASSWORD:?parent portal password}"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
(Same 4 env vars the MCP reads — reuse the repo's `.env` or the host's
|
|
34
|
+
secret store; don't hardcode a district.)
|
|
35
|
+
|
|
36
|
+
## One-time setup
|
|
37
|
+
|
|
38
|
+
None — `curl` and `jq` are the only tools needed (`brew install jq` if
|
|
39
|
+
missing). No profile/pairing step, no extension.
|
|
40
|
+
|
|
41
|
+
## Login (capture cookies + XSRF token)
|
|
42
|
+
|
|
43
|
+
`verify.jsp` accepts a urlencoded form POST and returns 200 with an
|
|
44
|
+
`<AUTHENTICATION>state</AUTHENTICATION>` marker in the body — **not**
|
|
45
|
+
via HTTP status — plus ~20 `Set-Cookie` headers. Use a cookie jar so curl
|
|
46
|
+
does the Set-Cookie parsing/deduping for you:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
JAR=$(mktemp)
|
|
50
|
+
BODY=$(curl -sS -c "$JAR" \
|
|
51
|
+
"$IC_BASE_URL/campus/verify.jsp?nonBrowser=true" \
|
|
52
|
+
--data-urlencode "username=$IC_USERNAME" \
|
|
53
|
+
--data-urlencode "password=$IC_PASSWORD" \
|
|
54
|
+
--data-urlencode "appName=$IC_DISTRICT" \
|
|
55
|
+
--data-urlencode "portalLoginPage=parents")
|
|
56
|
+
|
|
57
|
+
echo "$BODY" | grep -o '<AUTHENTICATION>[^<]*' # AUTHENTICATIONsuccess = good
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`password-error` / `account-locked` / any non-`success` state means the
|
|
61
|
+
login failed — check that string before doing anything else. Credentials
|
|
62
|
+
go in the form body (never the query string — it lands in access logs).
|
|
63
|
+
|
|
64
|
+
Pull the XSRF token out of the jar (IC's JS reads it back and echoes it on
|
|
65
|
+
every request as `X-XSRF-TOKEN`; `JSESSIONID` alone is not enough):
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
XSRF=$(awk -F'\t' '$6=="XSRF-TOKEN"{print $7}' "$JAR")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Core call pattern
|
|
72
|
+
|
|
73
|
+
Every subsequent request rides the jar + the XSRF header:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
ic_get() { # ic_get <path-with-query>
|
|
77
|
+
curl -sS -b "$JAR" -H "X-XSRF-TOKEN: $XSRF" -H 'Accept: application/json' \
|
|
78
|
+
"$IC_BASE_URL$1"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
ic_get '/campus/api/portal/students' | jq .
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A **401** means the session expired (IC sessions last ~5-6h) — re-run the
|
|
85
|
+
Login step to get a fresh `$JAR`/`$XSRF` and retry once. There is no retry
|
|
86
|
+
budget beyond that in this shell version (the MCP's `CookieSessionManager`
|
|
87
|
+
does this automatically; here just redo the login block).
|
|
88
|
+
|
|
89
|
+
## The one rule: resolve student + enrollment first
|
|
90
|
+
|
|
91
|
+
Almost every read is scoped to a `personID` (student), and several need the
|
|
92
|
+
student's **enrollment** (`enrollmentID` / `calendarID` / `structureID`).
|
|
93
|
+
Always start with:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
ic_get '/campus/api/portal/students' | jq '.[] | {personID, firstName, lastName, enrollments}'
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Take `personID` for `studentId`/`personID` params, and the first (or
|
|
100
|
+
relevant) `enrollments[]` entry's `enrollmentID`/`calendarID`/`structureID`
|
|
101
|
+
for the endpoints that need them (attendance, school days, assessments,
|
|
102
|
+
features). There is no name→id search — `ic_list_students` (i.e. this call)
|
|
103
|
+
*is* the resolve step.
|
|
104
|
+
|
|
105
|
+
## Endpoints
|
|
106
|
+
|
|
107
|
+
All 17 read endpoints (grades, attendance, assignments, schedule, messages,
|
|
108
|
+
documents, fees, teachers, assessments, feature flags) with ready-to-run
|
|
109
|
+
`curl`+`jq` recipes are in `references/endpoints.md`. Two (`behavior`,
|
|
110
|
+
`food_service`) have real paths that were never confirmed live against a
|
|
111
|
+
district with that module enabled — try them, expect a 404 if the module
|
|
112
|
+
is off or the district's shape differs.
|
|
113
|
+
|
|
114
|
+
## Downloads
|
|
115
|
+
|
|
116
|
+
`ic_download_document`'s `url` field (from the documents list) may be a
|
|
117
|
+
relative `/campus/...` path or an absolute URL. Same jar + header pattern,
|
|
118
|
+
just write to a file instead of piping to `jq`:
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
curl -sS -b "$JAR" -H "X-XSRF-TOKEN: $XSRF" -o report-card.pdf "$IC_BASE_URL$DOC_URL"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## FeatureDisabled districts
|
|
125
|
+
|
|
126
|
+
Some districts turn whole modules off (behavior, food service, assessments,
|
|
127
|
+
documents, attendance). The MCP checks `displayOptions` first and falls
|
|
128
|
+
back to treating a 404 as "disabled." Doing the same by hand: fetch
|
|
129
|
+
`/campus/api/portal/displayOptions/{structureID}?personID=X` (see
|
|
130
|
+
references) and check the flag before assuming a 404 is a bug.
|
|
131
|
+
|
|
132
|
+
## Notes
|
|
133
|
+
|
|
134
|
+
- Read-only in practice: every recipe here is a GET. `ic_get_message`
|
|
135
|
+
fetching a message body *may* mark it read on some district configs — the
|
|
136
|
+
MCP's own probe couldn't confirm the side effect either way.
|
|
137
|
+
- No CUPS linked-district switching in this skill (a parent with kids in
|
|
138
|
+
2+ IC instances under shared SSO) — that's a multi-step token-exchange
|
|
139
|
+
flow (`src/client.ts`'s `discoverLinkedDistricts`); out of scope here.
|
|
140
|
+
Run the login flow again against the linked district's own
|
|
141
|
+
`IC_BASE_URL`/`IC_DISTRICT` if you need it.
|
|
142
|
+
- This project is developed and maintained by AI (Claude).
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Infinite Campus endpoints for curl
|
|
2
|
+
|
|
3
|
+
Assumes the Login block from `../SKILL.md` has already run (`$JAR`, `$XSRF`,
|
|
4
|
+
`$IC_BASE_URL` set) and the `ic_get` helper is defined:
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
ic_get() { curl -sS -b "$JAR" -H "X-XSRF-TOKEN: $XSRF" -H 'Accept: application/json' "$IC_BASE_URL$1"; }
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
All paths are transcribed from `infinitecampus-mcp`'s `src/tools/*.ts` and
|
|
11
|
+
`docs/endpoints.md` — the same requests the `ic_*` MCP tools make. `$PID` =
|
|
12
|
+
a student's `personID` from the resolve-first step.
|
|
13
|
+
|
|
14
|
+
Some IC/prism responses serialize a 1-item collection as a bare object
|
|
15
|
+
instead of a 1-element array — pipe through `jq 'if type=="array" then . else [.] end'`
|
|
16
|
+
if a recipe below assumes an array and you get an object back.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Students (resolve-first — always start here)
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
ic_get '/campus/api/portal/students' \
|
|
24
|
+
| jq '.[] | {personID, firstName, lastName, enrollments}'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Each `enrollments[]` entry carries `enrollmentID`, `calendarID`,
|
|
28
|
+
`structureID`, `schoolName`, `endDate` — needed by several endpoints below.
|
|
29
|
+
|
|
30
|
+
## Schedule
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
ic_get "/campus/resources/portal/roster?personID=$PID" | jq .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
(Only `personID` is honored server-side — no working date/term filter, even
|
|
37
|
+
though the MCP tool's schema accepts `date`/`termFilter` args.)
|
|
38
|
+
|
|
39
|
+
## Assignments
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
# all assignments (full term history, ~hundreds of rows)
|
|
43
|
+
ic_get "/campus/api/portal/assignment/listView?personID=$PID" | jq .
|
|
44
|
+
|
|
45
|
+
# scoped to one course (sectionID from the schedule call above)
|
|
46
|
+
ic_get "/campus/api/portal/assignment/listView?personID=$PID§ionID=$SECTION_ID" | jq .
|
|
47
|
+
|
|
48
|
+
# client-side filters (server ignores date/missing params entirely):
|
|
49
|
+
ic_get "/campus/api/portal/assignment/listView?personID=$PID" \
|
|
50
|
+
| jq --arg since 2026-01-01 '[.[] | select(.dueDate >= $since)]'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Recent (recently-graded) assignments
|
|
54
|
+
|
|
55
|
+
Server-side filtered by `modifiedDate` (defaults to 14 days ago in the MCP;
|
|
56
|
+
reproduce with `date -u -v-14d '+%Y-%m-%dT00:00:00'` on macOS):
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
SINCE=$(date -u -v-14d '+%Y-%m-%dT00:00:00')
|
|
60
|
+
ic_get "/campus/api/portal/assignment/recentlyScored?modifiedDate=$SINCE&personID=$PID" | jq .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Grades
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
ic_get "/campus/resources/portal/grades?personID=$PID" | jq .
|
|
67
|
+
# scoped to one term:
|
|
68
|
+
ic_get "/campus/resources/portal/grades?personID=$PID&termID=$TERM_ID" | jq .
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Attendance (per-course summary, loop over each enrollment)
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
ic_get "/campus/resources/portal/attendance/$ENROLLMENT_ID?courseSummary=true&personID=$PID" \
|
|
75
|
+
| jq '.terms[]?.courses[]? | {absentList, tardyList, presentList, earlyReleaseList}'
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Feature-gated: check `displayOptions.attendance` first (see Feature flags,
|
|
79
|
+
below) — a 404 here on a district with attendance disabled is expected, not
|
|
80
|
+
a bug.
|
|
81
|
+
|
|
82
|
+
## Attendance events (individual absences/tardies, loop over each enrollment)
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
ic_get "/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID" \
|
|
86
|
+
| jq '.events[]? | {date: .localDate, code, description, excuse, comments}'
|
|
87
|
+
|
|
88
|
+
# excused-only, since a date (client-side filter):
|
|
89
|
+
ic_get "/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID" \
|
|
90
|
+
| jq --arg since 2026-01-01 '[.events[]? | select((.localDate[0:10] >= $since) and (.excuse == "E"))]'
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## School days / calendar (loop over each enrollment)
|
|
94
|
+
|
|
95
|
+
Two calls per enrollment, then join term windows to days client-side:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
ic_get "/campus/resources/term?structureID=$STRUCTURE_ID" | jq . # term boundaries
|
|
99
|
+
ic_get "/campus/resources/calendar/instructionalDay?calendarID=$CALENDAR_ID" | jq . # day list
|
|
100
|
+
|
|
101
|
+
# days within term N's [startDate,endDate]:
|
|
102
|
+
jq --slurpfile terms terms.json --arg t "$TERM_ID" \
|
|
103
|
+
'[.[] | select(.date >= $terms[0][0].startDate and .date <= $terms[0][0].endDate)]' days.json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Assessments (standardized tests, loop over each enrollment)
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
ic_get "/campus/resources/prism/portal/assessments?personID=$PID&calendarID=$CALENDAR_ID" \
|
|
110
|
+
| jq '{stateTests, nationalTests, districtTests}'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Feature-gated on `displayOptions.assessment`; test-item shape varies by
|
|
114
|
+
district/test type — pass through unchanged, don't assume field names
|
|
115
|
+
beyond `stateTests`/`nationalTests`/`districtTests`.
|
|
116
|
+
|
|
117
|
+
## Behavior *(path unconfirmed — never verified live against a district with the module on)*
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
ic_get "/campus/resources/portal/behavior?personID=$PID" | jq .
|
|
121
|
+
# with a date range:
|
|
122
|
+
ic_get "/campus/resources/portal/behavior?personID=$PID&startDate=2026-01-01&endDate=2026-06-01" | jq .
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Feature-gated on `displayOptions.behavior` — a 404 is the expected result
|
|
126
|
+
on most districts (few enable this module for parent portal).
|
|
127
|
+
|
|
128
|
+
## Food service *(path unconfirmed — same caveat as behavior)*
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
ic_get "/campus/resources/portal/foodService?personID=$PID" | jq .
|
|
132
|
+
ic_get "/campus/resources/portal/foodService?personID=$PID&startDate=2026-01-01&endDate=2026-06-01" | jq .
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Feature-gated on `displayOptions.foodService`.
|
|
136
|
+
|
|
137
|
+
## Documents + download
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
ic_get "/campus/resources/portal/report/all?personID=$PID" \
|
|
141
|
+
| jq '.[] | {name, type, url, moduleLabel, endYear}'
|
|
142
|
+
|
|
143
|
+
# download (url may be relative /campus/... or an absolute URL — handle both):
|
|
144
|
+
DOC_URL='/campus/resources/portal/report/...' # from the .url field above
|
|
145
|
+
curl -sS -b "$JAR" -H "X-XSRF-TOKEN: $XSRF" -o report-card.pdf \
|
|
146
|
+
"$( [[ "$DOC_URL" == http* ]] && echo "$DOC_URL" || echo "$IC_BASE_URL$DOC_URL" )"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Feature-gated on `displayOptions.documents`.
|
|
150
|
+
|
|
151
|
+
## Teachers + counselors
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
ic_get "/campus/resources/portal/section/contacts?personID=$PID" | jq .
|
|
155
|
+
ic_get "/campus/resources/portal/studentCounselor/byUser?personID=$PID" | jq .
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Either endpoint 404ing is treated as "no results" (not FeatureDisabled) by
|
|
159
|
+
the MCP — do the same.
|
|
160
|
+
|
|
161
|
+
## Messages (three independent sources — combine client-side)
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
# 1. prism notifications (assignment/grade/attendance alerts)
|
|
165
|
+
ic_get '/campus/prism?x=notifications.Notification-retrieve&limitCount=20' \
|
|
166
|
+
| jq '.data.NotificationList.Notification'
|
|
167
|
+
|
|
168
|
+
# 2. Messenger 2.0 inbox (teacher messages, district announcements)
|
|
169
|
+
ic_get '/campus/api/portal/process-message' | jq .
|
|
170
|
+
|
|
171
|
+
# 3. portal userNotice announcements
|
|
172
|
+
ic_get '/campus/resources/portal/userNotice' | jq .
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Each inbox item's `url` field feeds `ic_get_message` below (normalize a bare
|
|
176
|
+
or `portal/...`-relative URL to `/campus/portal/...` first).
|
|
177
|
+
|
|
178
|
+
## Get one message body (HTML → text)
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
# normalize: bare "portal/..." → "/campus/portal/..."; already-absolute /campus/... stays as-is
|
|
182
|
+
MSG_PATH='/campus/portal/messageView.xsl?x=messenger.MessengerEngine-getMessageRecipientView&messageID=...&messageRecipientID=...'
|
|
183
|
+
curl -sS -b "$JAR" -H "X-XSRF-TOKEN: $XSRF" -H 'Accept: text/html' "$IC_BASE_URL$MSG_PATH"
|
|
184
|
+
# then strip tags/entities yourself, or eyeball the rendered HTML — this is
|
|
185
|
+
# an HTML fragment, not JSON, so `jq` doesn't apply.
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Fetching a message body *may* mark it read on some district configs
|
|
189
|
+
(unconfirmed either way).
|
|
190
|
+
|
|
191
|
+
## Fees (two endpoints in parallel; either can 404 independently)
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
ic_get "/campus/api/portal/fees/feeAssignments?personID=$PID" | jq .
|
|
195
|
+
ic_get "/campus/api/portal/fees/feeTransactionDetail/totalSurplus/-1?personID=$PID" | jq .
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Both 404 → module is off for this district. One 404 → report the other
|
|
199
|
+
side plus a note that the failing endpoint may be disabled.
|
|
200
|
+
|
|
201
|
+
## Feature flags (displayOptions — check before assuming a 404 is a bug)
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
ic_get "/campus/api/portal/displayOptions/$STRUCTURE_ID?personID=$PID" \
|
|
205
|
+
| jq '{attendance, behavior, assessment, documents, foodService, grades, schedule}'
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`false` = district has that module turned off for this enrollment; `true`
|
|
209
|
+
or absent = available. ~90 flags total; the MCP caches this per
|
|
210
|
+
`(district, structureID)` for the session lifetime since it rarely changes
|
|
211
|
+
mid-session — worth doing the same if you're calling it more than once.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Not covered here (out of scope for this skill)
|
|
216
|
+
|
|
217
|
+
- **CUPS linked-district discovery/switching** — a parent with kids at 2+
|
|
218
|
+
IC-hosted districts under shared SSO. Multi-step: fetch
|
|
219
|
+
`cups/linkedAccounts`, `userAccountSwitch/originalDistrict`,
|
|
220
|
+
`districts/current`, then per linked account a `cups/loginToken` POST
|
|
221
|
+
followed by a second `verify.jsp`-style POST at the *linked* district's
|
|
222
|
+
own host with the CUPS token. Fully discoverable in `src/client.ts`
|
|
223
|
+
(`discoverLinkedDistricts`) but a lot of shell for a one-shot skill —
|
|
224
|
+
just re-run the Login block against the linked district's own
|
|
225
|
+
`IC_BASE_URL`/`IC_DISTRICT` instead.
|