infinitecampus-mcp 2.4.0 → 2.4.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/README.md +1 -1
- package/dist/auth.js +94 -56
- package/dist/bundle.js +383 -152
- package/dist/client.js +38 -39
- package/dist/index.js +4 -4
- package/package.json +4 -4
- package/server.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for Infinite Campus (Campus Parent) — grades, attendance, assignments, messages, documents",
|
|
10
|
-
"version": "2.4.
|
|
10
|
+
"version": "2.4.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.4.
|
|
18
|
+
"version": "2.4.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.4.
|
|
4
|
+
"version": "2.4.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/README.md
CHANGED
|
@@ -42,7 +42,7 @@ Tools that the harness will gate as write/IO operations: `ic_download_document`.
|
|
|
42
42
|
IC_PASSWORD=...
|
|
43
43
|
IC_NAME=Springfield # optional, defaults to IC_DISTRICT
|
|
44
44
|
```
|
|
45
|
-
2. **fetchproxy fallback (no password needed).** Set only `IC_BASE_URL` + `IC_DISTRICT` (still required so the MCP knows which host to talk to and which district to dispatch on), then install the [fetchproxy 0.3.0 extension](https://github.com/chrischall/fetchproxy) (Chrome Web Store / Safari `.dmg`) and sign into your IC portal once. The MCP reads the `JSESSIONID` (HttpOnly) + `XSRF-TOKEN` cookies
|
|
45
|
+
2. **fetchproxy fallback (no password needed).** Set only `IC_BASE_URL` + `IC_DISTRICT` (still required so the MCP knows which host to talk to and which district to dispatch on), then install the [fetchproxy 0.3.0 extension](https://github.com/chrischall/fetchproxy) (Chrome Web Store / Safari `.dmg`) and sign into your IC portal once. The MCP reads the `JSESSIONID` (HttpOnly) + `XSRF-TOKEN` cookies from that tab and goes direct-to-API from Node thereafter — the extension is **not** in the request hot path. The read is repeated whenever the session lapses (JSESSIONID is a servlet session with a short idle timeout), so you stay signed in without restarting the MCP.
|
|
46
46
|
|
|
47
47
|
Set `IC_DISABLE_FETCHPROXY=1` to opt out of the fallback (turns missing credentials into a hard error — useful in headless CI).
|
|
48
48
|
|
package/dist/auth.js
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
// - `@fetchproxy/bootstrap` is mocked at the module boundary in tests.
|
|
47
47
|
// - `loadAccount()` (the existing env-var resolver) is reused as-is so the
|
|
48
48
|
// legacy paths keep working unchanged.
|
|
49
|
-
import {
|
|
49
|
+
import { createSessionLifter } from '@fetchproxy/bootstrap';
|
|
50
50
|
import { classifyBridgeError } from '@chrischall/mcp-utils/fetchproxy';
|
|
51
51
|
import { parseBoolEnv } from '@chrischall/mcp-utils';
|
|
52
52
|
import { loadAccount } from './config.js';
|
|
@@ -71,65 +71,103 @@ export async function resolveAuth() {
|
|
|
71
71
|
return { account, source: 'env' };
|
|
72
72
|
}
|
|
73
73
|
// ── Path 2: fetchproxy fallback.
|
|
74
|
+
//
|
|
75
|
+
// NOTE: the browser is NOT read here. `resolveAuth()` runs once at process
|
|
76
|
+
// start; the lift is handed to the client as `refresh` and runs lazily on
|
|
77
|
+
// the first request and again on every expiry. Two bugs this avoids:
|
|
78
|
+
//
|
|
79
|
+
// * The lapsed-session dead end. JSESSIONID is a servlet session with a
|
|
80
|
+
// short idle timeout. A cookie captured at boot left the server
|
|
81
|
+
// unauthenticated the moment it lapsed, with no way back — the account
|
|
82
|
+
// has empty creds, so verify.jsp is not an option.
|
|
83
|
+
// * The sticky startup failure. A lift that failed at boot (user not yet
|
|
84
|
+
// signed in) was cached for the life of the process, so signing in
|
|
85
|
+
// afterwards changed nothing.
|
|
74
86
|
if (!fetchproxyDisabled()) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// IC tenants live on per-district hosts (campus.<district>.org,
|
|
81
|
-
// <district>.infinitecampus.org, etc.). Declare just this host
|
|
82
|
-
// — each district is its own root, no wildcard needed.
|
|
83
|
-
domains: [host],
|
|
84
|
-
declare: {
|
|
85
|
-
// JSESSIONID is HttpOnly (chrome.cookies.get sees it; the
|
|
86
|
-
// security gate is the declared key list). XSRF-TOKEN is
|
|
87
|
-
// non-HttpOnly because the page JS reads it back and echoes
|
|
88
|
-
// it on every state-changing call as the X-XSRF-TOKEN header.
|
|
89
|
-
cookies: ['JSESSIONID', 'XSRF-TOKEN'],
|
|
90
|
-
localStorage: [],
|
|
91
|
-
sessionStorage: [],
|
|
92
|
-
captureHeaders: [],
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
const jsessionid = session.cookies['JSESSIONID'];
|
|
96
|
-
const xsrf = session.cookies['XSRF-TOKEN'];
|
|
97
|
-
if (!jsessionid) {
|
|
98
|
-
throw new Error(`JSESSIONID cookie not found on ${host}. ` +
|
|
99
|
-
'Sign into your IC portal in your browser (with the fetchproxy extension installed) and retry.');
|
|
100
|
-
}
|
|
101
|
-
if (!xsrf) {
|
|
102
|
-
throw new Error(`XSRF-TOKEN cookie not found on ${host}. ` +
|
|
103
|
-
'Sign into your IC portal in your browser (with the fetchproxy extension installed) and retry.');
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
106
|
-
account,
|
|
107
|
-
preloaded: {
|
|
108
|
-
cookieHeader: `JSESSIONID=${jsessionid}; XSRF-TOKEN=${xsrf}`,
|
|
109
|
-
xsrfToken: xsrf,
|
|
110
|
-
},
|
|
111
|
-
source: 'fetchproxy',
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
catch (e) {
|
|
115
|
-
// 0.8.0+ typed-error discrimination. The fetchproxy server already
|
|
116
|
-
// retries once on SW eviction (bridgeReviveDelayMs=2000 default), so
|
|
117
|
-
// a thrown FetchproxyBridgeDownError means the retry also failed —
|
|
118
|
-
// the extension's service worker is genuinely down and the user
|
|
119
|
-
// needs to wake it. The `.hint` is the actionable copy
|
|
120
|
-
// ("click the extension toolbar icon...") that we'd otherwise have
|
|
121
|
-
// to hand-write here. Surface it verbatim so users in path 2 get
|
|
122
|
-
// the same self-service guidance as path 3.
|
|
123
|
-
if (classifyBridgeError(e) === 'bridge_down') {
|
|
124
|
-
const downErr = e;
|
|
125
|
-
throw new Error(`IC auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`);
|
|
126
|
-
}
|
|
127
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
128
|
-
throw new Error('IC auth: no IC_USERNAME/IC_PASSWORD set, and fetchproxy fallback failed: ' + msg);
|
|
129
|
-
}
|
|
87
|
+
return {
|
|
88
|
+
account,
|
|
89
|
+
refresh: () => liftBrowserSession(account),
|
|
90
|
+
source: 'fetchproxy',
|
|
91
|
+
};
|
|
130
92
|
}
|
|
131
93
|
// ── Path 3: nothing configured and fetchproxy explicitly disabled.
|
|
132
94
|
throw new Error('IC auth: set IC_USERNAME + IC_PASSWORD, ' +
|
|
133
95
|
'or install the fetchproxy extension and sign into your IC portal ' +
|
|
134
96
|
'(unset IC_DISABLE_FETCHPROXY if it is set).');
|
|
135
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* One lifter per host, built lazily and reused.
|
|
100
|
+
*
|
|
101
|
+
* Unlike most MCPs the declared scope is not static — IC tenants live on
|
|
102
|
+
* per-district hosts — so the lifter cannot be a module-level constant.
|
|
103
|
+
* Caching by host still matters: `createSessionLifter` single-flights
|
|
104
|
+
* concurrent calls, and that only helps if renewals share ONE lifter instead
|
|
105
|
+
* of constructing a fresh one each time.
|
|
106
|
+
*/
|
|
107
|
+
const liftersByHost = new Map();
|
|
108
|
+
function lifterFor(host) {
|
|
109
|
+
let lift = liftersByHost.get(host);
|
|
110
|
+
if (!lift) {
|
|
111
|
+
lift = createSessionLifter({
|
|
112
|
+
serverName: pkg.name,
|
|
113
|
+
version: pkg.version,
|
|
114
|
+
// IC tenants live on per-district hosts (campus.<district>.org,
|
|
115
|
+
// <district>.infinitecampus.org, etc.). Declare just this host
|
|
116
|
+
// — each district is its own root, no wildcard needed.
|
|
117
|
+
domains: [host],
|
|
118
|
+
declare: {
|
|
119
|
+
// JSESSIONID is HttpOnly (chrome.cookies.get sees it; the
|
|
120
|
+
// security gate is the declared key list). XSRF-TOKEN is
|
|
121
|
+
// non-HttpOnly because the page JS reads it back and echoes
|
|
122
|
+
// it on every state-changing call as the X-XSRF-TOKEN header.
|
|
123
|
+
cookies: ['JSESSIONID', 'XSRF-TOKEN'],
|
|
124
|
+
localStorage: [],
|
|
125
|
+
sessionStorage: [],
|
|
126
|
+
captureHeaders: [],
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
liftersByHost.set(host, lift);
|
|
130
|
+
}
|
|
131
|
+
return lift;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Lift a fresh IC session out of the user's signed-in portal tab.
|
|
135
|
+
*
|
|
136
|
+
* Runs on every login/renewal, not once at startup — fetchproxy is not in the
|
|
137
|
+
* request hot path, only the renewal path.
|
|
138
|
+
*/
|
|
139
|
+
async function liftBrowserSession(account) {
|
|
140
|
+
try {
|
|
141
|
+
const host = new URL(account.baseUrl).hostname;
|
|
142
|
+
const session = await lifterFor(host)();
|
|
143
|
+
const jsessionid = session.cookies['JSESSIONID'];
|
|
144
|
+
const xsrf = session.cookies['XSRF-TOKEN'];
|
|
145
|
+
if (!jsessionid) {
|
|
146
|
+
throw new Error(`JSESSIONID cookie not found on ${host}. ` +
|
|
147
|
+
'Sign into your IC portal in your browser (with the fetchproxy extension installed) and retry.');
|
|
148
|
+
}
|
|
149
|
+
if (!xsrf) {
|
|
150
|
+
throw new Error(`XSRF-TOKEN cookie not found on ${host}. ` +
|
|
151
|
+
'Sign into your IC portal in your browser (with the fetchproxy extension installed) and retry.');
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
cookieHeader: `JSESSIONID=${jsessionid}; XSRF-TOKEN=${xsrf}`,
|
|
155
|
+
xsrfToken: xsrf,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
// 0.8.0+ typed-error discrimination. The fetchproxy server already
|
|
160
|
+
// retries once on SW eviction (bridgeReviveDelayMs=2000 default), so
|
|
161
|
+
// a thrown FetchproxyBridgeDownError means the retry also failed —
|
|
162
|
+
// the extension's service worker is genuinely down and the user
|
|
163
|
+
// needs to wake it. The `.hint` is the actionable copy
|
|
164
|
+
// ("click the extension toolbar icon...") that we'd otherwise have
|
|
165
|
+
// to hand-write here.
|
|
166
|
+
if (classifyBridgeError(e) === 'bridge_down') {
|
|
167
|
+
const downErr = e;
|
|
168
|
+
throw new Error(`IC auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`);
|
|
169
|
+
}
|
|
170
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
171
|
+
throw new Error('IC auth: no IC_USERNAME/IC_PASSWORD set, and fetchproxy lift failed: ' + msg);
|
|
172
|
+
}
|
|
173
|
+
}
|
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,
|
|
@@ -26732,17 +26729,33 @@ function normalizeObjectSchema(schema) {
|
|
|
26732
26729
|
}
|
|
26733
26730
|
return void 0;
|
|
26734
26731
|
}
|
|
26732
|
+
function getDotPath(path) {
|
|
26733
|
+
if (path.length === 0) {
|
|
26734
|
+
return "object root";
|
|
26735
|
+
}
|
|
26736
|
+
return path.reduce((acc, seg, index) => {
|
|
26737
|
+
if (index === 0) {
|
|
26738
|
+
return String(seg);
|
|
26739
|
+
}
|
|
26740
|
+
if (typeof seg === "number") {
|
|
26741
|
+
return `${acc}[${seg}]`;
|
|
26742
|
+
}
|
|
26743
|
+
return `${acc}.${seg}`;
|
|
26744
|
+
}, "");
|
|
26745
|
+
}
|
|
26735
26746
|
function getParseErrorMessage(error51) {
|
|
26736
26747
|
if (error51 && typeof error51 === "object") {
|
|
26748
|
+
if ("issues" in error51 && Array.isArray(error51.issues) && error51.issues.length > 0) {
|
|
26749
|
+
return error51.issues.map((i) => {
|
|
26750
|
+
if (!i.path?.length) {
|
|
26751
|
+
return i.message;
|
|
26752
|
+
}
|
|
26753
|
+
return `${i.message} at ${getDotPath(i.path)}`;
|
|
26754
|
+
}).join("\n");
|
|
26755
|
+
}
|
|
26737
26756
|
if ("message" in error51 && typeof error51.message === "string") {
|
|
26738
26757
|
return error51.message;
|
|
26739
26758
|
}
|
|
26740
|
-
if ("issues" in error51 && Array.isArray(error51.issues) && error51.issues.length > 0) {
|
|
26741
|
-
const firstIssue = error51.issues[0];
|
|
26742
|
-
if (firstIssue && typeof firstIssue === "object" && "message" in firstIssue) {
|
|
26743
|
-
return String(firstIssue.message);
|
|
26744
|
-
}
|
|
26745
|
-
}
|
|
26746
26759
|
try {
|
|
26747
26760
|
return JSON.stringify(error51);
|
|
26748
26761
|
} catch {
|
|
@@ -33357,16 +33370,7 @@ var Server = class extends Protocol {
|
|
|
33357
33370
|
if (!methodSchema) {
|
|
33358
33371
|
throw new Error("Schema is missing a method literal");
|
|
33359
33372
|
}
|
|
33360
|
-
|
|
33361
|
-
if (isZ4Schema(methodSchema)) {
|
|
33362
|
-
const v4Schema = methodSchema;
|
|
33363
|
-
const v4Def = v4Schema._zod?.def;
|
|
33364
|
-
methodValue = v4Def?.value ?? v4Schema.value;
|
|
33365
|
-
} else {
|
|
33366
|
-
const v3Schema = methodSchema;
|
|
33367
|
-
const legacyDef = v3Schema._def;
|
|
33368
|
-
methodValue = legacyDef?.value ?? v3Schema.value;
|
|
33369
|
-
}
|
|
33373
|
+
const methodValue = getLiteralValue(methodSchema);
|
|
33370
33374
|
if (typeof methodValue !== "string") {
|
|
33371
33375
|
throw new Error("Schema method literal must be a string");
|
|
33372
33376
|
}
|
|
@@ -34554,8 +34558,17 @@ var EMPTY_COMPLETION_RESULT = {
|
|
|
34554
34558
|
import process3 from "node:process";
|
|
34555
34559
|
|
|
34556
34560
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
34561
|
+
var STDIO_DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
34557
34562
|
var ReadBuffer = class {
|
|
34563
|
+
constructor(options) {
|
|
34564
|
+
this._maxBufferSize = options?.maxBufferSize ?? STDIO_DEFAULT_MAX_BUFFER_SIZE;
|
|
34565
|
+
}
|
|
34558
34566
|
append(chunk2) {
|
|
34567
|
+
const newSize = (this._buffer?.length ?? 0) + chunk2.length;
|
|
34568
|
+
if (newSize > this._maxBufferSize) {
|
|
34569
|
+
this.clear();
|
|
34570
|
+
throw new Error(`ReadBuffer exceeded maximum size of ${this._maxBufferSize} bytes`);
|
|
34571
|
+
}
|
|
34559
34572
|
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk2]) : chunk2;
|
|
34560
34573
|
}
|
|
34561
34574
|
readMessage() {
|
|
@@ -34583,18 +34596,24 @@ function serializeMessage(message) {
|
|
|
34583
34596
|
|
|
34584
34597
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
34585
34598
|
var StdioServerTransport = class {
|
|
34586
|
-
constructor(_stdin = process3.stdin, _stdout = process3.stdout) {
|
|
34599
|
+
constructor(_stdin = process3.stdin, _stdout = process3.stdout, options) {
|
|
34587
34600
|
this._stdin = _stdin;
|
|
34588
34601
|
this._stdout = _stdout;
|
|
34589
|
-
this._readBuffer = new ReadBuffer();
|
|
34590
34602
|
this._started = false;
|
|
34591
34603
|
this._ondata = (chunk2) => {
|
|
34592
|
-
|
|
34593
|
-
|
|
34604
|
+
try {
|
|
34605
|
+
this._readBuffer.append(chunk2);
|
|
34606
|
+
this.processReadBuffer();
|
|
34607
|
+
} catch (error51) {
|
|
34608
|
+
this.onerror?.(error51);
|
|
34609
|
+
this.close().catch(() => {
|
|
34610
|
+
});
|
|
34611
|
+
}
|
|
34594
34612
|
};
|
|
34595
34613
|
this._onerror = (error51) => {
|
|
34596
34614
|
this.onerror?.(error51);
|
|
34597
34615
|
};
|
|
34616
|
+
this._readBuffer = new ReadBuffer({ maxBufferSize: options?.maxBufferSize });
|
|
34598
34617
|
}
|
|
34599
34618
|
/**
|
|
34600
34619
|
* Starts listening for messages on stdin.
|
|
@@ -34846,7 +34865,8 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
|
34846
34865
|
"capture_redirect",
|
|
34847
34866
|
"read_indexed_db",
|
|
34848
34867
|
"read_dom",
|
|
34849
|
-
"download"
|
|
34868
|
+
"download",
|
|
34869
|
+
"graphql"
|
|
34850
34870
|
]);
|
|
34851
34871
|
|
|
34852
34872
|
// node_modules/@fetchproxy/protocol/dist/mcp-id.js
|
|
@@ -35149,6 +35169,7 @@ function assertIndexedDbScopesArray(value, label) {
|
|
|
35149
35169
|
}
|
|
35150
35170
|
var DOM_SELECTOR_RE = /^[^-]{1,512}$/;
|
|
35151
35171
|
var DOM_ATTRIBUTE_RE = /^[A-Za-z_:][A-Za-z0-9_:.\-]{0,127}$/;
|
|
35172
|
+
var GRAPHQL_OP_NAME_RE = /^[_A-Za-z][_0-9A-Za-z]{0,127}$/;
|
|
35152
35173
|
function assertDomSelectorsArray(value, label) {
|
|
35153
35174
|
if (!Array.isArray(value)) {
|
|
35154
35175
|
throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
|
|
@@ -35185,6 +35206,37 @@ function assertDomSelectorsArray(value, label) {
|
|
|
35185
35206
|
}
|
|
35186
35207
|
}
|
|
35187
35208
|
}
|
|
35209
|
+
function assertGraphqlOpsArray(value, label) {
|
|
35210
|
+
if (!Array.isArray(value)) {
|
|
35211
|
+
throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
|
|
35212
|
+
}
|
|
35213
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35214
|
+
for (let i = 0; i < value.length; i++) {
|
|
35215
|
+
const entry = value[i];
|
|
35216
|
+
assertObject(entry, `${label}[${i}]`);
|
|
35217
|
+
if (entry.name === void 0) {
|
|
35218
|
+
throw new ProtocolError(`${label}[${i}].name: missing`);
|
|
35219
|
+
}
|
|
35220
|
+
if (entry.operationName === void 0) {
|
|
35221
|
+
throw new ProtocolError(`${label}[${i}].operationName: missing`);
|
|
35222
|
+
}
|
|
35223
|
+
if (typeof entry.name !== "string" || !SCOPE_KEY_RE.test(entry.name)) {
|
|
35224
|
+
throw new ProtocolError(`${label}[${i}].name: invalid ${JSON.stringify(entry.name)}`);
|
|
35225
|
+
}
|
|
35226
|
+
if (typeof entry.operationName !== "string" || !GRAPHQL_OP_NAME_RE.test(entry.operationName)) {
|
|
35227
|
+
throw new ProtocolError(`${label}[${i}].operationName: invalid ${JSON.stringify(entry.operationName)}`);
|
|
35228
|
+
}
|
|
35229
|
+
if (seen.has(entry.name)) {
|
|
35230
|
+
throw new ProtocolError(`${label}: duplicate name ${JSON.stringify(entry.name)}`);
|
|
35231
|
+
}
|
|
35232
|
+
seen.add(entry.name);
|
|
35233
|
+
for (const k of Object.keys(entry)) {
|
|
35234
|
+
if (k !== "name" && k !== "operationName") {
|
|
35235
|
+
throw new ProtocolError(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
|
|
35236
|
+
}
|
|
35237
|
+
}
|
|
35238
|
+
}
|
|
35239
|
+
}
|
|
35188
35240
|
function validateFrame(raw) {
|
|
35189
35241
|
assertObject(raw, "frame");
|
|
35190
35242
|
const t = raw.type;
|
|
@@ -35263,6 +35315,9 @@ function validateHello(raw) {
|
|
|
35263
35315
|
if (raw.domSelectors !== void 0) {
|
|
35264
35316
|
assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
|
|
35265
35317
|
}
|
|
35318
|
+
if (raw.graphqlOps !== void 0) {
|
|
35319
|
+
assertGraphqlOpsArray(raw.graphqlOps, "hello.graphqlOps");
|
|
35320
|
+
}
|
|
35266
35321
|
assertBase64(raw.identityX25519Pub, "hello.identityX25519Pub");
|
|
35267
35322
|
assertBase64(raw.identityEd25519Pub, "hello.identityEd25519Pub");
|
|
35268
35323
|
assertBase64(raw.sessionNonce, "hello.sessionNonce");
|
|
@@ -35512,6 +35567,28 @@ function validateInnerRequest(raw) {
|
|
|
35512
35567
|
}
|
|
35513
35568
|
return raw;
|
|
35514
35569
|
}
|
|
35570
|
+
if (raw.op === "graphql_query") {
|
|
35571
|
+
assertObject(raw.init, "inner.init");
|
|
35572
|
+
if (raw.init.name === void 0)
|
|
35573
|
+
throw new ProtocolError("inner.init.name: missing");
|
|
35574
|
+
if (raw.init.variables === void 0) {
|
|
35575
|
+
throw new ProtocolError("inner.init.variables: missing");
|
|
35576
|
+
}
|
|
35577
|
+
assertString(raw.init.name, "inner.init.name");
|
|
35578
|
+
if (raw.init.name.length === 0) {
|
|
35579
|
+
throw new ProtocolError("inner.init.name: must be non-empty");
|
|
35580
|
+
}
|
|
35581
|
+
assertObject(raw.init.variables, "inner.init.variables");
|
|
35582
|
+
if (raw.init.tabUrl !== void 0) {
|
|
35583
|
+
assertString(raw.init.tabUrl, "inner.init.tabUrl");
|
|
35584
|
+
}
|
|
35585
|
+
for (const k of Object.keys(raw.init)) {
|
|
35586
|
+
if (k !== "name" && k !== "variables" && k !== "tabUrl") {
|
|
35587
|
+
throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on graphql_query`);
|
|
35588
|
+
}
|
|
35589
|
+
}
|
|
35590
|
+
return raw;
|
|
35591
|
+
}
|
|
35515
35592
|
if (raw.op === "download") {
|
|
35516
35593
|
assertObject(raw.init, "inner.init");
|
|
35517
35594
|
if (raw.init.url === void 0) {
|
|
@@ -35537,7 +35614,7 @@ function validateInnerRequest(raw) {
|
|
|
35537
35614
|
}
|
|
35538
35615
|
return raw;
|
|
35539
35616
|
}
|
|
35540
|
-
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"; got ${JSON.stringify(raw.op)}`);
|
|
35617
|
+
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)}`);
|
|
35541
35618
|
}
|
|
35542
35619
|
function assertNonEmptyKeyArray(value, label) {
|
|
35543
35620
|
if (!Array.isArray(value)) {
|
|
@@ -35568,6 +35645,10 @@ function assertStringMap(value, label) {
|
|
|
35568
35645
|
}
|
|
35569
35646
|
}
|
|
35570
35647
|
}
|
|
35648
|
+
var KNOWN_RESPONSE_OPS = /* @__PURE__ */ new Set([
|
|
35649
|
+
...KNOWN_CAPABILITIES,
|
|
35650
|
+
"graphql_query"
|
|
35651
|
+
]);
|
|
35571
35652
|
function validateInnerResponse(raw) {
|
|
35572
35653
|
assertPositiveInt(raw.id, "inner.id");
|
|
35573
35654
|
if (raw.ok === true) {
|
|
@@ -35629,6 +35710,13 @@ function validateInnerResponse(raw) {
|
|
|
35629
35710
|
assertStringMap(raw.values, "inner.values");
|
|
35630
35711
|
return raw;
|
|
35631
35712
|
}
|
|
35713
|
+
if (op === "graphql_query") {
|
|
35714
|
+
if (raw.data === void 0) {
|
|
35715
|
+
throw new ProtocolError("inner.data: missing on graphql_query response");
|
|
35716
|
+
}
|
|
35717
|
+
assertObject(raw.data, "inner.data");
|
|
35718
|
+
return raw;
|
|
35719
|
+
}
|
|
35632
35720
|
if (op === "download") {
|
|
35633
35721
|
assertObject(raw.value, "inner.value");
|
|
35634
35722
|
assertString(raw.value.path, "inner.value.path");
|
|
@@ -35653,7 +35741,7 @@ function validateInnerResponse(raw) {
|
|
|
35653
35741
|
if (raw.ok === false) {
|
|
35654
35742
|
assertString(raw.error, "inner.error");
|
|
35655
35743
|
if (raw.op !== void 0) {
|
|
35656
|
-
if (typeof raw.op !== "string" || !
|
|
35744
|
+
if (typeof raw.op !== "string" || !KNOWN_RESPONSE_OPS.has(raw.op)) {
|
|
35657
35745
|
throw new ProtocolError(`inner.op: unknown response op ${JSON.stringify(raw.op)}`);
|
|
35658
35746
|
}
|
|
35659
35747
|
}
|
|
@@ -35845,11 +35933,33 @@ async function sealInnerFrame(sessionKey, mcpId, seq, inner) {
|
|
|
35845
35933
|
};
|
|
35846
35934
|
}
|
|
35847
35935
|
async function openEncryptedFrame(sessionKey, frame) {
|
|
35848
|
-
const
|
|
35849
|
-
|
|
35850
|
-
|
|
35851
|
-
|
|
35852
|
-
|
|
35936
|
+
const result = await openEncryptedFrameDetailed(sessionKey, frame);
|
|
35937
|
+
if (result.stage === "ok")
|
|
35938
|
+
return result.inner;
|
|
35939
|
+
throw result.error instanceof Error ? result.error : new Error(String(result.error));
|
|
35940
|
+
}
|
|
35941
|
+
async function openEncryptedFrameDetailed(sessionKey, frame) {
|
|
35942
|
+
let pt;
|
|
35943
|
+
try {
|
|
35944
|
+
const iv = fromB64(frame.iv);
|
|
35945
|
+
const ct = fromB64(frame.ciphertext);
|
|
35946
|
+
pt = await aesGcmOpen(sessionKey, iv, ct);
|
|
35947
|
+
} catch (error51) {
|
|
35948
|
+
return { stage: "decrypt-failed", error: error51 };
|
|
35949
|
+
}
|
|
35950
|
+
let parsed;
|
|
35951
|
+
try {
|
|
35952
|
+
parsed = JSON.parse(dec.decode(pt));
|
|
35953
|
+
} catch (error51) {
|
|
35954
|
+
return { stage: "validation-failed", error: error51, recoveredId: void 0 };
|
|
35955
|
+
}
|
|
35956
|
+
try {
|
|
35957
|
+
const inner = validateInnerFrame(parsed);
|
|
35958
|
+
return { stage: "ok", inner };
|
|
35959
|
+
} catch (error51) {
|
|
35960
|
+
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;
|
|
35961
|
+
return { stage: "validation-failed", error: error51, recoveredId };
|
|
35962
|
+
}
|
|
35853
35963
|
}
|
|
35854
35964
|
|
|
35855
35965
|
// node_modules/@fetchproxy/server/dist/election.js
|
|
@@ -35975,6 +36085,12 @@ async function buildServerHello(opts) {
|
|
|
35975
36085
|
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
35976
36086
|
}));
|
|
35977
36087
|
}
|
|
36088
|
+
if (opts.graphqlOps && opts.graphqlOps.length > 0) {
|
|
36089
|
+
hello.graphqlOps = opts.graphqlOps.map((d) => ({
|
|
36090
|
+
name: d.name,
|
|
36091
|
+
operationName: d.operationName
|
|
36092
|
+
}));
|
|
36093
|
+
}
|
|
35978
36094
|
return hello;
|
|
35979
36095
|
}
|
|
35980
36096
|
|
|
@@ -36065,7 +36181,8 @@ async function startHost(opts) {
|
|
|
36065
36181
|
indexedDbScopes: opts.ownIndexedDbScopes,
|
|
36066
36182
|
localStoragePointers: opts.ownLocalStoragePointers,
|
|
36067
36183
|
sessionStoragePointers: opts.ownSessionStoragePointers,
|
|
36068
|
-
domSelectors: opts.ownDomSelectors
|
|
36184
|
+
domSelectors: opts.ownDomSelectors,
|
|
36185
|
+
graphqlOps: opts.ownGraphqlOps
|
|
36069
36186
|
});
|
|
36070
36187
|
const ownSessionNonce = fromB64(ownHello.sessionNonce);
|
|
36071
36188
|
let extensionWs = null;
|
|
@@ -36302,7 +36419,8 @@ async function startPeer(opts) {
|
|
|
36302
36419
|
indexedDbScopes: opts.indexedDbScopes,
|
|
36303
36420
|
domSelectors: opts.domSelectors,
|
|
36304
36421
|
localStoragePointers: opts.localStoragePointers,
|
|
36305
|
-
sessionStoragePointers: opts.sessionStoragePointers
|
|
36422
|
+
sessionStoragePointers: opts.sessionStoragePointers,
|
|
36423
|
+
graphqlOps: opts.graphqlOps
|
|
36306
36424
|
});
|
|
36307
36425
|
const sessionNonce = fromB64(hello.sessionNonce);
|
|
36308
36426
|
ws.send(JSON.stringify(hello));
|
|
@@ -36346,10 +36464,20 @@ async function startPeer(opts) {
|
|
|
36346
36464
|
return;
|
|
36347
36465
|
if (!session.acceptInboundSeq(frame.seq))
|
|
36348
36466
|
return;
|
|
36349
|
-
|
|
36350
|
-
|
|
36351
|
-
innerListeners.forEach((cb) => cb(inner));
|
|
36352
|
-
}
|
|
36467
|
+
const result = await openEncryptedFrameDetailed(session.sessionKey, frame);
|
|
36468
|
+
if (result.stage === "ok") {
|
|
36469
|
+
innerListeners.forEach((cb) => cb(result.inner));
|
|
36470
|
+
} else if (result.stage === "decrypt-failed") {
|
|
36471
|
+
} else {
|
|
36472
|
+
console.error("[fetchproxy] peer: received a frame that decrypted OK but failed validation:", result.error);
|
|
36473
|
+
if (result.recoveredId !== void 0) {
|
|
36474
|
+
innerListeners.forEach((cb) => cb({
|
|
36475
|
+
type: "response",
|
|
36476
|
+
id: result.recoveredId,
|
|
36477
|
+
ok: false,
|
|
36478
|
+
error: `malformed response failed protocol validation: ${String(result.error)}`
|
|
36479
|
+
}));
|
|
36480
|
+
}
|
|
36353
36481
|
}
|
|
36354
36482
|
}
|
|
36355
36483
|
} catch (e) {
|
|
@@ -36626,6 +36754,10 @@ var FetchproxyServer = class {
|
|
|
36626
36754
|
pendingIdb = /* @__PURE__ */ new Map();
|
|
36627
36755
|
// download awaiters resolve the saved-file metadata (path + size + mime).
|
|
36628
36756
|
pendingDownload = /* @__PURE__ */ new Map();
|
|
36757
|
+
// 1.x+: graphql_query awaiters resolve the GraphQL `data` object. Its
|
|
36758
|
+
// shape is operation-specific, so the awaiter resolves `unknown` and the
|
|
36759
|
+
// caller narrows.
|
|
36760
|
+
pendingGraphql = /* @__PURE__ */ new Map();
|
|
36629
36761
|
mcpId = null;
|
|
36630
36762
|
identity = null;
|
|
36631
36763
|
// 0.5.3+: in-flight role-election / handle-start promise. Set the
|
|
@@ -36713,6 +36845,10 @@ var FetchproxyServer = class {
|
|
|
36713
36845
|
selector: d.selector,
|
|
36714
36846
|
...d.attribute !== void 0 ? { attribute: d.attribute } : {}
|
|
36715
36847
|
})),
|
|
36848
|
+
graphqlOps: (opts.graphqlOps ?? []).map((d) => ({
|
|
36849
|
+
name: d.name,
|
|
36850
|
+
operationName: d.operationName
|
|
36851
|
+
})),
|
|
36716
36852
|
// 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
|
|
36717
36853
|
// adapter was about to set these to the same numbers anyway; the
|
|
36718
36854
|
// back-door is `0` (explicit opt-out) if a caller genuinely wants
|
|
@@ -36834,6 +36970,7 @@ var FetchproxyServer = class {
|
|
|
36834
36970
|
ownLocalStoragePointers: this.opts.localStoragePointers,
|
|
36835
36971
|
ownSessionStoragePointers: this.opts.sessionStoragePointers,
|
|
36836
36972
|
ownDomSelectors: this.opts.domSelectors,
|
|
36973
|
+
ownGraphqlOps: this.opts.graphqlOps,
|
|
36837
36974
|
onPairCode: this.opts.onPairCode
|
|
36838
36975
|
});
|
|
36839
36976
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
@@ -36862,7 +36999,8 @@ var FetchproxyServer = class {
|
|
|
36862
36999
|
indexedDbScopes: this.opts.indexedDbScopes,
|
|
36863
37000
|
localStoragePointers: this.opts.localStoragePointers,
|
|
36864
37001
|
sessionStoragePointers: this.opts.sessionStoragePointers,
|
|
36865
|
-
domSelectors: this.opts.domSelectors
|
|
37002
|
+
domSelectors: this.opts.domSelectors,
|
|
37003
|
+
graphqlOps: this.opts.graphqlOps
|
|
36866
37004
|
});
|
|
36867
37005
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
36868
37006
|
this.peerHandle.onRenegotiate(() => {
|
|
@@ -37068,6 +37206,7 @@ var FetchproxyServer = class {
|
|
|
37068
37206
|
this.pendingRedirect.delete(id);
|
|
37069
37207
|
this.pendingDownload.delete(id);
|
|
37070
37208
|
this.pendingIdb.delete(id);
|
|
37209
|
+
this.pendingGraphql.delete(id);
|
|
37071
37210
|
}
|
|
37072
37211
|
throw err;
|
|
37073
37212
|
}
|
|
@@ -37877,6 +38016,52 @@ var FetchproxyServer = class {
|
|
|
37877
38016
|
await this.sendInnerFrame(inner);
|
|
37878
38017
|
return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
|
|
37879
38018
|
}
|
|
38019
|
+
/**
|
|
38020
|
+
* 1.x+: run a declared GraphQL operation through the page's own Apollo
|
|
38021
|
+
* client (`window.__APOLLO_CLIENT__`) in the signed-in tab's MAIN world.
|
|
38022
|
+
* Requires `'graphql'` in capabilities AND `name` to match a declared
|
|
38023
|
+
* `graphqlOps` entry. The extension resolves `name` → `operationName` →
|
|
38024
|
+
* the live DocumentNode the page already observed, then invokes
|
|
38025
|
+
* `client.query({ query, variables })` — the site's own request path, so
|
|
38026
|
+
* per-request bot telemetry (Akamai etc.) runs automatically.
|
|
38027
|
+
*
|
|
38028
|
+
* Returns the GraphQL `data` object on success (shape is
|
|
38029
|
+
* operation-specific; the caller narrows). Throws a plain `Error` on
|
|
38030
|
+
* developer mistakes (undeclared capability, undeclared name) and a
|
|
38031
|
+
* descriptive `Error` on the `ok:false` bridge path — which includes the
|
|
38032
|
+
* typed "operation not yet observed on this tab" case (open the site's
|
|
38033
|
+
* page and retry).
|
|
38034
|
+
*/
|
|
38035
|
+
async graphqlQuery(opts) {
|
|
38036
|
+
if (!this.opts.capabilities.includes("graphql")) {
|
|
38037
|
+
throw new Error('FetchproxyServer.graphqlQuery(): MCP did not declare "graphql" in capabilities');
|
|
38038
|
+
}
|
|
38039
|
+
if (typeof opts.name !== "string" || opts.name.length === 0) {
|
|
38040
|
+
throw new Error("FetchproxyServer.graphqlQuery: opts.name must be a non-empty string");
|
|
38041
|
+
}
|
|
38042
|
+
const declaredNames = this.opts.graphqlOps.map((d) => d.name);
|
|
38043
|
+
if (!declaredNames.includes(opts.name)) {
|
|
38044
|
+
throw new Error(`FetchproxyServer.graphqlQuery: operation ${JSON.stringify(opts.name)} not in declared graphqlOps [${declaredNames.map((n) => JSON.stringify(n)).join(", ")}]`);
|
|
38045
|
+
}
|
|
38046
|
+
await this.ensureConnected();
|
|
38047
|
+
this.throwIfPendingPair();
|
|
38048
|
+
const id = this.nextRequestId++;
|
|
38049
|
+
const inner = {
|
|
38050
|
+
type: "request",
|
|
38051
|
+
id,
|
|
38052
|
+
op: "graphql_query",
|
|
38053
|
+
init: {
|
|
38054
|
+
name: opts.name,
|
|
38055
|
+
variables: opts.variables,
|
|
38056
|
+
...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
|
|
38057
|
+
}
|
|
38058
|
+
};
|
|
38059
|
+
const pending = new Promise((resolve, reject) => {
|
|
38060
|
+
this.pendingGraphql.set(id, { resolve, reject });
|
|
38061
|
+
});
|
|
38062
|
+
await this.sendInnerFrame(inner);
|
|
38063
|
+
return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
|
|
38064
|
+
}
|
|
37880
38065
|
assertScopeSubset(requested, declared, label) {
|
|
37881
38066
|
const undeclared = undeclaredKeys(requested, declared);
|
|
37882
38067
|
if (undeclared.length > 0) {
|
|
@@ -38014,6 +38199,20 @@ var FetchproxyServer = class {
|
|
|
38014
38199
|
}
|
|
38015
38200
|
return;
|
|
38016
38201
|
}
|
|
38202
|
+
const graphqlCb = this.pendingGraphql.get(inner.id);
|
|
38203
|
+
if (graphqlCb) {
|
|
38204
|
+
this.pendingGraphql.delete(inner.id);
|
|
38205
|
+
if (inner.ok) {
|
|
38206
|
+
if (inner.op === "graphql_query") {
|
|
38207
|
+
graphqlCb.resolve(inner.data);
|
|
38208
|
+
} else {
|
|
38209
|
+
graphqlCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on graphql_query awaiter`));
|
|
38210
|
+
}
|
|
38211
|
+
} else {
|
|
38212
|
+
graphqlCb.reject(new FetchproxyProtocolError(inner.error));
|
|
38213
|
+
}
|
|
38214
|
+
return;
|
|
38215
|
+
}
|
|
38017
38216
|
const cookiesCb = this.pendingReadCookies.get(inner.id);
|
|
38018
38217
|
if (cookiesCb) {
|
|
38019
38218
|
this.pendingReadCookies.delete(inner.id);
|
|
@@ -38065,6 +38264,9 @@ var FetchproxyServer = class {
|
|
|
38065
38264
|
for (const { reject } of this.pendingDownload.values())
|
|
38066
38265
|
reject(err);
|
|
38067
38266
|
this.pendingDownload.clear();
|
|
38267
|
+
for (const { reject } of this.pendingGraphql.values())
|
|
38268
|
+
reject(err);
|
|
38269
|
+
this.pendingGraphql.clear();
|
|
38068
38270
|
}
|
|
38069
38271
|
/**
|
|
38070
38272
|
* 0.5.2+: read the current pair-pending pair code from whichever handle
|
|
@@ -38118,7 +38320,20 @@ var FetchproxyServer = class {
|
|
|
38118
38320
|
|
|
38119
38321
|
// node_modules/@fetchproxy/bootstrap/dist/index.js
|
|
38120
38322
|
var defaultFactory = (opts) => new FetchproxyServer(opts);
|
|
38121
|
-
|
|
38323
|
+
function createSessionLifter(opts) {
|
|
38324
|
+
let inFlight = null;
|
|
38325
|
+
return () => {
|
|
38326
|
+
if (inFlight)
|
|
38327
|
+
return inFlight;
|
|
38328
|
+
const p = runOneLift(opts).finally(() => {
|
|
38329
|
+
if (inFlight === p)
|
|
38330
|
+
inFlight = null;
|
|
38331
|
+
});
|
|
38332
|
+
inFlight = p;
|
|
38333
|
+
return p;
|
|
38334
|
+
};
|
|
38335
|
+
}
|
|
38336
|
+
async function runOneLift(opts) {
|
|
38122
38337
|
const envVar = opts.serverName.toUpperCase().replace(/[^A-Z0-9]/g, "_").replace(/^_+/, "") + "_DISABLE_FETCHPROXY";
|
|
38123
38338
|
const envVal = process.env[envVar];
|
|
38124
38339
|
if (envVal !== void 0 && envVal !== "" && envVal !== "0" && envVal !== "false") {
|
|
@@ -38245,12 +38460,18 @@ async function bootstrap(opts) {
|
|
|
38245
38460
|
});
|
|
38246
38461
|
indexedDbBucket[`${d.database}/${d.store}`] = values;
|
|
38247
38462
|
}
|
|
38463
|
+
const absent = (declared, got) => declared.filter((k) => !(k in got));
|
|
38248
38464
|
return {
|
|
38249
38465
|
cookies,
|
|
38250
38466
|
localStorage,
|
|
38251
38467
|
sessionStorage,
|
|
38252
38468
|
capturedHeaders,
|
|
38253
|
-
indexedDb: indexedDbBucket
|
|
38469
|
+
indexedDb: indexedDbBucket,
|
|
38470
|
+
missing: {
|
|
38471
|
+
cookies: absent(opts.declare.cookies, cookies),
|
|
38472
|
+
localStorage: absent(opts.declare.localStorage, localStorage),
|
|
38473
|
+
sessionStorage: absent(opts.declare.sessionStorage, sessionStorage)
|
|
38474
|
+
}
|
|
38254
38475
|
};
|
|
38255
38476
|
} finally {
|
|
38256
38477
|
try {
|
|
@@ -38311,7 +38532,7 @@ function loadAccount(env = process.env) {
|
|
|
38311
38532
|
// package.json
|
|
38312
38533
|
var package_default = {
|
|
38313
38534
|
name: "infinitecampus-mcp",
|
|
38314
|
-
version: "2.4.
|
|
38535
|
+
version: "2.4.3",
|
|
38315
38536
|
mcpName: "io.github.chrischall/infinitecampus-mcp",
|
|
38316
38537
|
description: "Infinite Campus (Campus Parent) MCP server \u2014 multi-district read + message/document write",
|
|
38317
38538
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -38353,9 +38574,9 @@ var package_default = {
|
|
|
38353
38574
|
"test:watch": "vitest"
|
|
38354
38575
|
},
|
|
38355
38576
|
dependencies: {
|
|
38356
|
-
"@chrischall/mcp-utils": "^0.
|
|
38357
|
-
"@fetchproxy/bootstrap": "^1.
|
|
38358
|
-
"@fetchproxy/server": "^1.
|
|
38577
|
+
"@chrischall/mcp-utils": "^0.14.0",
|
|
38578
|
+
"@fetchproxy/bootstrap": "^1.9.1",
|
|
38579
|
+
"@fetchproxy/server": "^1.7.0",
|
|
38359
38580
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38360
38581
|
dotenv: "^17.4.2",
|
|
38361
38582
|
"node-html-parser": "^9.0.0",
|
|
@@ -38380,62 +38601,74 @@ async function resolveAuth() {
|
|
|
38380
38601
|
return { account: account2, source: "env" };
|
|
38381
38602
|
}
|
|
38382
38603
|
if (!fetchproxyDisabled()) {
|
|
38383
|
-
|
|
38384
|
-
|
|
38385
|
-
|
|
38386
|
-
|
|
38387
|
-
|
|
38388
|
-
|
|
38389
|
-
|
|
38390
|
-
|
|
38391
|
-
|
|
38392
|
-
|
|
38393
|
-
|
|
38394
|
-
|
|
38395
|
-
|
|
38396
|
-
|
|
38397
|
-
|
|
38398
|
-
|
|
38399
|
-
|
|
38400
|
-
|
|
38401
|
-
|
|
38402
|
-
|
|
38403
|
-
|
|
38404
|
-
|
|
38405
|
-
|
|
38406
|
-
|
|
38407
|
-
|
|
38408
|
-
|
|
38409
|
-
|
|
38410
|
-
|
|
38411
|
-
|
|
38412
|
-
|
|
38413
|
-
);
|
|
38414
|
-
}
|
|
38415
|
-
return {
|
|
38416
|
-
account: account2,
|
|
38417
|
-
preloaded: {
|
|
38418
|
-
cookieHeader: `JSESSIONID=${jsessionid}; XSRF-TOKEN=${xsrf}`,
|
|
38419
|
-
xsrfToken: xsrf
|
|
38420
|
-
},
|
|
38421
|
-
source: "fetchproxy"
|
|
38422
|
-
};
|
|
38423
|
-
} catch (e) {
|
|
38424
|
-
if (classifyBridgeError(e) === "bridge_down") {
|
|
38425
|
-
const downErr = e;
|
|
38426
|
-
throw new Error(
|
|
38427
|
-
`IC auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`
|
|
38428
|
-
);
|
|
38604
|
+
return {
|
|
38605
|
+
account: account2,
|
|
38606
|
+
refresh: () => liftBrowserSession(account2),
|
|
38607
|
+
source: "fetchproxy"
|
|
38608
|
+
};
|
|
38609
|
+
}
|
|
38610
|
+
throw new Error(
|
|
38611
|
+
"IC auth: set IC_USERNAME + IC_PASSWORD, or install the fetchproxy extension and sign into your IC portal (unset IC_DISABLE_FETCHPROXY if it is set)."
|
|
38612
|
+
);
|
|
38613
|
+
}
|
|
38614
|
+
var liftersByHost = /* @__PURE__ */ new Map();
|
|
38615
|
+
function lifterFor(host) {
|
|
38616
|
+
let lift = liftersByHost.get(host);
|
|
38617
|
+
if (!lift) {
|
|
38618
|
+
lift = createSessionLifter({
|
|
38619
|
+
serverName: package_default.name,
|
|
38620
|
+
version: package_default.version,
|
|
38621
|
+
// IC tenants live on per-district hosts (campus.<district>.org,
|
|
38622
|
+
// <district>.infinitecampus.org, etc.). Declare just this host
|
|
38623
|
+
// — each district is its own root, no wildcard needed.
|
|
38624
|
+
domains: [host],
|
|
38625
|
+
declare: {
|
|
38626
|
+
// JSESSIONID is HttpOnly (chrome.cookies.get sees it; the
|
|
38627
|
+
// security gate is the declared key list). XSRF-TOKEN is
|
|
38628
|
+
// non-HttpOnly because the page JS reads it back and echoes
|
|
38629
|
+
// it on every state-changing call as the X-XSRF-TOKEN header.
|
|
38630
|
+
cookies: ["JSESSIONID", "XSRF-TOKEN"],
|
|
38631
|
+
localStorage: [],
|
|
38632
|
+
sessionStorage: [],
|
|
38633
|
+
captureHeaders: []
|
|
38429
38634
|
}
|
|
38430
|
-
|
|
38635
|
+
});
|
|
38636
|
+
liftersByHost.set(host, lift);
|
|
38637
|
+
}
|
|
38638
|
+
return lift;
|
|
38639
|
+
}
|
|
38640
|
+
async function liftBrowserSession(account2) {
|
|
38641
|
+
try {
|
|
38642
|
+
const host = new URL(account2.baseUrl).hostname;
|
|
38643
|
+
const session = await lifterFor(host)();
|
|
38644
|
+
const jsessionid = session.cookies["JSESSIONID"];
|
|
38645
|
+
const xsrf = session.cookies["XSRF-TOKEN"];
|
|
38646
|
+
if (!jsessionid) {
|
|
38647
|
+
throw new Error(
|
|
38648
|
+
`JSESSIONID cookie not found on ${host}. Sign into your IC portal in your browser (with the fetchproxy extension installed) and retry.`
|
|
38649
|
+
);
|
|
38650
|
+
}
|
|
38651
|
+
if (!xsrf) {
|
|
38431
38652
|
throw new Error(
|
|
38432
|
-
|
|
38653
|
+
`XSRF-TOKEN cookie not found on ${host}. Sign into your IC portal in your browser (with the fetchproxy extension installed) and retry.`
|
|
38433
38654
|
);
|
|
38434
38655
|
}
|
|
38656
|
+
return {
|
|
38657
|
+
cookieHeader: `JSESSIONID=${jsessionid}; XSRF-TOKEN=${xsrf}`,
|
|
38658
|
+
xsrfToken: xsrf
|
|
38659
|
+
};
|
|
38660
|
+
} catch (e) {
|
|
38661
|
+
if (classifyBridgeError(e) === "bridge_down") {
|
|
38662
|
+
const downErr = e;
|
|
38663
|
+
throw new Error(
|
|
38664
|
+
`IC auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`
|
|
38665
|
+
);
|
|
38666
|
+
}
|
|
38667
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
38668
|
+
throw new Error(
|
|
38669
|
+
"IC auth: no IC_USERNAME/IC_PASSWORD set, and fetchproxy lift failed: " + msg
|
|
38670
|
+
);
|
|
38435
38671
|
}
|
|
38436
|
-
throw new Error(
|
|
38437
|
-
"IC auth: set IC_USERNAME + IC_PASSWORD, or install the fetchproxy extension and sign into your IC portal (unset IC_DISABLE_FETCHPROXY if it is set)."
|
|
38438
|
-
);
|
|
38439
38672
|
}
|
|
38440
38673
|
|
|
38441
38674
|
// src/client.ts
|
|
@@ -38610,24 +38843,29 @@ var ICClient = class {
|
|
|
38610
38843
|
* first call to `ensureDiscovery()`.
|
|
38611
38844
|
*/
|
|
38612
38845
|
fetchproxyMode = false;
|
|
38613
|
-
|
|
38614
|
-
|
|
38615
|
-
|
|
38616
|
-
|
|
38617
|
-
|
|
38618
|
-
|
|
38619
|
-
|
|
38620
|
-
*
|
|
38621
|
-
*
|
|
38622
|
-
*
|
|
38623
|
-
*
|
|
38846
|
+
/**
|
|
38847
|
+
* fetchproxy lift. Called by the primary manager's login — on the first
|
|
38848
|
+
* request AND on every renewal, which is what keeps the path alive past
|
|
38849
|
+
* JSESSIONID's idle timeout.
|
|
38850
|
+
*/
|
|
38851
|
+
refreshSession = null;
|
|
38852
|
+
/**
|
|
38853
|
+
* `refreshSession` is the fetchproxy escape hatch: when set, it REPLACES
|
|
38854
|
+
* the `verify.jsp` POST as the way this client mints a primary session.
|
|
38855
|
+
* It is called lazily on the first request and again on every expiry, so a
|
|
38856
|
+
* lapsed servlet session recovers by re-reading the browser rather than
|
|
38857
|
+
* dead-ending. CUPS linked-district discovery still runs on first request.
|
|
38858
|
+
*
|
|
38859
|
+
* It must re-read the browser each time rather than return a captured
|
|
38860
|
+
* value: JSESSIONID lapses on a short idle timer and the account has no
|
|
38861
|
+
* credentials to fall back on.
|
|
38624
38862
|
*/
|
|
38625
38863
|
constructor(account2, opts = {}) {
|
|
38626
38864
|
this.accounts.set(account2.name, account2);
|
|
38627
38865
|
this.primaryName = account2.name;
|
|
38628
38866
|
this.managers.set(account2.name, this.makeManager(account2));
|
|
38629
|
-
if (opts.
|
|
38630
|
-
this.
|
|
38867
|
+
if (opts.refreshSession) {
|
|
38868
|
+
this.refreshSession = opts.refreshSession;
|
|
38631
38869
|
this.fetchproxyMode = true;
|
|
38632
38870
|
}
|
|
38633
38871
|
}
|
|
@@ -38682,10 +38920,7 @@ var ICClient = class {
|
|
|
38682
38920
|
}
|
|
38683
38921
|
async ensureDiscovery() {
|
|
38684
38922
|
const session = await this.managers.get(this.primaryName).ensure();
|
|
38685
|
-
|
|
38686
|
-
this.fetchproxyDiscoveryRan = true;
|
|
38687
|
-
await this.discoverLinkedDistricts(this.accounts.get(this.primaryName), session);
|
|
38688
|
-
}
|
|
38923
|
+
void session;
|
|
38689
38924
|
}
|
|
38690
38925
|
listDistricts() {
|
|
38691
38926
|
return [...this.accounts.values()].map((a) => ({
|
|
@@ -38717,11 +38952,7 @@ var ICClient = class {
|
|
|
38717
38952
|
account2 = this.accounts.get(district);
|
|
38718
38953
|
if (!account2) throw new UnknownDistrictError(district, [...this.accounts.keys()]);
|
|
38719
38954
|
}
|
|
38720
|
-
|
|
38721
|
-
if (this.fetchproxyMode && !this.fetchproxyDiscoveryRan && account2.name === this.primaryName) {
|
|
38722
|
-
this.fetchproxyDiscoveryRan = true;
|
|
38723
|
-
await this.discoverLinkedDistricts(account2, session);
|
|
38724
|
-
}
|
|
38955
|
+
await this.managers.get(account2.name).ensure();
|
|
38725
38956
|
return this.doRequest(account2, path, opts);
|
|
38726
38957
|
}
|
|
38727
38958
|
async login(account2) {
|
|
@@ -38738,15 +38969,15 @@ var ICClient = class {
|
|
|
38738
38969
|
}
|
|
38739
38970
|
return restored;
|
|
38740
38971
|
}
|
|
38741
|
-
if (this.
|
|
38742
|
-
const
|
|
38743
|
-
this.
|
|
38744
|
-
return
|
|
38972
|
+
if (this.refreshSession) {
|
|
38973
|
+
const session2 = await this.refreshSession();
|
|
38974
|
+
await this.discoverLinkedDistricts(account2, session2);
|
|
38975
|
+
return session2;
|
|
38745
38976
|
}
|
|
38746
38977
|
if (!account2.username || !account2.password) {
|
|
38747
38978
|
throw new AuthFailedError(
|
|
38748
38979
|
account2.name,
|
|
38749
|
-
"session expired and no IC_USERNAME/IC_PASSWORD set \u2014 sign back into your IC portal in the browser
|
|
38980
|
+
"session expired and no IC_USERNAME/IC_PASSWORD set \u2014 sign back into your IC portal in the browser, or set IC_USERNAME/IC_PASSWORD",
|
|
38750
38981
|
{ permanent: true }
|
|
38751
38982
|
);
|
|
38752
38983
|
}
|
|
@@ -39914,24 +40145,24 @@ await loadDotenvSafely({
|
|
|
39914
40145
|
});
|
|
39915
40146
|
var AI_NOTICE = "[infinitecampus-mcp] Developed and maintained by AI (Claude). Use at your own discretion.";
|
|
39916
40147
|
var account = null;
|
|
39917
|
-
var
|
|
40148
|
+
var refreshSession;
|
|
39918
40149
|
var source;
|
|
39919
40150
|
var configError = null;
|
|
39920
40151
|
try {
|
|
39921
40152
|
const resolved = await resolveAuth();
|
|
39922
40153
|
account = resolved.account;
|
|
39923
|
-
|
|
40154
|
+
refreshSession = resolved.refresh;
|
|
39924
40155
|
source = resolved.source;
|
|
39925
40156
|
} catch (e) {
|
|
39926
40157
|
configError = e;
|
|
39927
40158
|
}
|
|
39928
40159
|
var COMMON = {
|
|
39929
40160
|
name: "infinitecampus",
|
|
39930
|
-
version: "2.4.
|
|
40161
|
+
version: "2.4.3"
|
|
39931
40162
|
// x-release-please-version
|
|
39932
40163
|
};
|
|
39933
40164
|
if (account) {
|
|
39934
|
-
const client = new ICClient(account, {
|
|
40165
|
+
const client = new ICClient(account, { refreshSession });
|
|
39935
40166
|
const tools = [
|
|
39936
40167
|
registerDistrictTools,
|
|
39937
40168
|
registerStudentTools,
|
package/dist/client.js
CHANGED
|
@@ -25,24 +25,29 @@ export class ICClient {
|
|
|
25
25
|
* first call to `ensureDiscovery()`.
|
|
26
26
|
*/
|
|
27
27
|
fetchproxyMode = false;
|
|
28
|
-
fetchproxyDiscoveryRan = false;
|
|
29
|
-
/** fetchproxy cookies, consumed by the primary manager's first login. */
|
|
30
|
-
preloaded = null;
|
|
31
28
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* the
|
|
29
|
+
* fetchproxy lift. Called by the primary manager's login — on the first
|
|
30
|
+
* request AND on every renewal, which is what keeps the path alive past
|
|
31
|
+
* JSESSIONID's idle timeout.
|
|
32
|
+
*/
|
|
33
|
+
refreshSession = null;
|
|
34
|
+
/**
|
|
35
|
+
* `refreshSession` is the fetchproxy escape hatch: when set, it REPLACES
|
|
36
|
+
* the `verify.jsp` POST as the way this client mints a primary session.
|
|
37
|
+
* It is called lazily on the first request and again on every expiry, so a
|
|
38
|
+
* lapsed servlet session recovers by re-reading the browser rather than
|
|
39
|
+
* dead-ending. CUPS linked-district discovery still runs on first request.
|
|
40
|
+
*
|
|
41
|
+
* It must re-read the browser each time rather than return a captured
|
|
42
|
+
* value: JSESSIONID lapses on a short idle timer and the account has no
|
|
43
|
+
* credentials to fall back on.
|
|
39
44
|
*/
|
|
40
45
|
constructor(account, opts = {}) {
|
|
41
46
|
this.accounts.set(account.name, account);
|
|
42
47
|
this.primaryName = account.name;
|
|
43
48
|
this.managers.set(account.name, this.makeManager(account));
|
|
44
|
-
if (opts.
|
|
45
|
-
this.
|
|
49
|
+
if (opts.refreshSession) {
|
|
50
|
+
this.refreshSession = opts.refreshSession;
|
|
46
51
|
this.fetchproxyMode = true;
|
|
47
52
|
}
|
|
48
53
|
}
|
|
@@ -98,16 +103,11 @@ export class ICClient {
|
|
|
98
103
|
async ensureDiscovery() {
|
|
99
104
|
// Ensure primary account is logged in, which triggers CUPS linked-district discovery
|
|
100
105
|
const session = await this.managers.get(this.primaryName).ensure();
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
// itself.
|
|
107
|
-
if (this.fetchproxyMode && !this.fetchproxyDiscoveryRan) {
|
|
108
|
-
this.fetchproxyDiscoveryRan = true;
|
|
109
|
-
await this.discoverLinkedDistricts(this.accounts.get(this.primaryName), session);
|
|
110
|
-
}
|
|
106
|
+
// Nothing further to do: `ensure()` mints the primary session when one is
|
|
107
|
+
// needed, and every primary mint runs discovery — in fetchproxy mode via
|
|
108
|
+
// login()'s lift branch, in env mode via mintSessionCookie(). A cached
|
|
109
|
+
// primary session means discovery already ran for it.
|
|
110
|
+
void session;
|
|
111
111
|
}
|
|
112
112
|
listDistricts() {
|
|
113
113
|
return [...this.accounts.values()].map((a) => ({
|
|
@@ -143,15 +143,7 @@ export class ICClient {
|
|
|
143
143
|
if (!account)
|
|
144
144
|
throw new UnknownDistrictError(district, [...this.accounts.keys()]);
|
|
145
145
|
}
|
|
146
|
-
|
|
147
|
-
// In fetchproxy mode, the preloaded-cookie login never runs verify.jsp
|
|
148
|
-
// and therefore never calls discoverLinkedDistricts. Run discovery once
|
|
149
|
-
// on the first primary call so linked districts are discoverable. (The
|
|
150
|
-
// primary is never in linkedTo by construction.)
|
|
151
|
-
if (this.fetchproxyMode && !this.fetchproxyDiscoveryRan && account.name === this.primaryName) {
|
|
152
|
-
this.fetchproxyDiscoveryRan = true;
|
|
153
|
-
await this.discoverLinkedDistricts(account, session);
|
|
154
|
-
}
|
|
146
|
+
await this.managers.get(account.name).ensure();
|
|
155
147
|
return this.doRequest(account, path, opts);
|
|
156
148
|
}
|
|
157
149
|
async login(account) {
|
|
@@ -179,13 +171,20 @@ export class ICClient {
|
|
|
179
171
|
}
|
|
180
172
|
return restored;
|
|
181
173
|
}
|
|
182
|
-
// fetchproxy mode: the
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
174
|
+
// fetchproxy mode: mint the primary session by re-reading the browser
|
|
175
|
+
// instead of POSTing to verify.jsp. This runs on EVERY login, not just
|
|
176
|
+
// the first — a lapsed JSESSIONID recovers by lifting the live session
|
|
177
|
+
// the browser still holds, which is the whole point.
|
|
178
|
+
if (this.refreshSession) {
|
|
179
|
+
const session = await this.refreshSession();
|
|
180
|
+
// Mirror mintSessionCookie(): a primary re-mint re-runs CUPS discovery
|
|
181
|
+
// so linked districts get re-seeded. Without this a linked district
|
|
182
|
+
// could never recover — its login() defers to the primary and then
|
|
183
|
+
// reads `current`, which would stay unset because the primary's mint
|
|
184
|
+
// short-circuited past discovery. By construction we only reach here
|
|
185
|
+
// for the primary (linked districts return early above).
|
|
186
|
+
await this.discoverLinkedDistricts(account, session);
|
|
187
|
+
return session;
|
|
189
188
|
}
|
|
190
189
|
// fetchproxy mode: empty primary creds, can't post to verify.jsp.
|
|
191
190
|
// The user must re-sign-in in the browser (and the next process start
|
|
@@ -193,7 +192,7 @@ export class ICClient {
|
|
|
193
192
|
// and rethrows this instead of retrying a login that can never succeed.
|
|
194
193
|
if (!account.username || !account.password) {
|
|
195
194
|
throw new AuthFailedError(account.name, 'session expired and no IC_USERNAME/IC_PASSWORD set — ' +
|
|
196
|
-
'sign back into your IC portal in the browser
|
|
195
|
+
'sign back into your IC portal in the browser, or set IC_USERNAME/IC_PASSWORD', { permanent: true });
|
|
197
196
|
}
|
|
198
197
|
// ic_parent_api's pattern: single POST to verify.jsp, let the response
|
|
199
198
|
// set cookies. No pre-login GET needed (unlike OFW's Spring Security).
|
package/dist/index.js
CHANGED
|
@@ -37,13 +37,13 @@ const AI_NOTICE = '[infinitecampus-mcp] Developed and maintained by AI (Claude).
|
|
|
37
37
|
// IC portal in the browser). When not configured we register no tools and
|
|
38
38
|
// log a clear stderr message — far better than the previous crash loop.
|
|
39
39
|
let account = null;
|
|
40
|
-
let
|
|
40
|
+
let refreshSession;
|
|
41
41
|
let source;
|
|
42
42
|
let configError = null;
|
|
43
43
|
try {
|
|
44
44
|
const resolved = await resolveAuth();
|
|
45
45
|
account = resolved.account;
|
|
46
|
-
|
|
46
|
+
refreshSession = resolved.refresh;
|
|
47
47
|
source = resolved.source;
|
|
48
48
|
}
|
|
49
49
|
catch (e) {
|
|
@@ -57,10 +57,10 @@ catch (e) {
|
|
|
57
57
|
// actionable stderr message (banner) instead of a crash loop.
|
|
58
58
|
const COMMON = {
|
|
59
59
|
name: 'infinitecampus',
|
|
60
|
-
version: '2.4.
|
|
60
|
+
version: '2.4.3', // x-release-please-version
|
|
61
61
|
};
|
|
62
62
|
if (account) {
|
|
63
|
-
const client = new ICClient(account, {
|
|
63
|
+
const client = new ICClient(account, { refreshSession });
|
|
64
64
|
const tools = [
|
|
65
65
|
registerDistrictTools,
|
|
66
66
|
registerStudentTools,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinitecampus-mcp",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.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>",
|
|
@@ -42,9 +42,9 @@
|
|
|
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.9.1",
|
|
47
|
+
"@fetchproxy/server": "^1.7.0",
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
49
|
"dotenv": "^17.4.2",
|
|
50
50
|
"node-html-parser": "^9.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.4.
|
|
9
|
+
"version": "2.4.3",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "infinitecampus-mcp",
|
|
14
|
-
"version": "2.4.
|
|
14
|
+
"version": "2.4.3",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|