infinitecampus-mcp 2.1.2 → 2.1.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 +13 -9
- package/dist/auth.js +134 -0
- package/dist/bundle.js +11670 -5024
- package/dist/client.js +55 -1
- package/dist/config.js +27 -7
- package/dist/index.js +46 -22
- package/package.json +4 -3
- package/server.json +12 -6
package/dist/client.js
CHANGED
|
@@ -7,13 +7,50 @@ export class ICClient {
|
|
|
7
7
|
linkedTo = new Map(); // linkedDistrictName → primaryDistrictName
|
|
8
8
|
primaryName;
|
|
9
9
|
featuresCache = new Map();
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* True when the primary account has empty creds (fetchproxy mode). The
|
|
12
|
+
* client uses pre-loaded cookies in place of the `verify.jsp` POST and
|
|
13
|
+
* cannot re-login when those cookies expire — the user must re-sign-in
|
|
14
|
+
* in the browser. CUPS linked-district discovery still runs lazily on
|
|
15
|
+
* first call to `ensureDiscovery()`.
|
|
16
|
+
*/
|
|
17
|
+
fetchproxyMode = false;
|
|
18
|
+
fetchproxyDiscoveryRan = false;
|
|
19
|
+
/**
|
|
20
|
+
* `preloaded` is the fetchproxy escape hatch: when set, the client treats
|
|
21
|
+
* the supplied cookies as a freshly-completed login on the primary account.
|
|
22
|
+
* This skips the `verify.jsp` POST entirely (the account has empty creds
|
|
23
|
+
* in this mode) but otherwise behaves identically — CUPS linked-district
|
|
24
|
+
* discovery still runs on first request, 401 retry still triggers a
|
|
25
|
+
* re-login. On a 401 with empty credentials we can't re-login from Node;
|
|
26
|
+
* the user must re-sign-in in the browser.
|
|
27
|
+
*/
|
|
28
|
+
constructor(account, opts = {}) {
|
|
11
29
|
this.accounts.set(account.name, account);
|
|
12
30
|
this.primaryName = account.name;
|
|
31
|
+
if (opts.preloaded) {
|
|
32
|
+
this.sessions.set(account.name, {
|
|
33
|
+
cookie: opts.preloaded.cookieHeader,
|
|
34
|
+
xsrfToken: opts.preloaded.xsrfToken,
|
|
35
|
+
loggedInAt: Date.now(),
|
|
36
|
+
loginInFlight: null,
|
|
37
|
+
});
|
|
38
|
+
this.fetchproxyMode = true;
|
|
39
|
+
}
|
|
13
40
|
}
|
|
14
41
|
async ensureDiscovery() {
|
|
15
42
|
// Ensure primary account is logged in, which triggers CUPS linked-district discovery
|
|
16
43
|
await this.ensureSession(this.accounts.get(this.primaryName));
|
|
44
|
+
// fetchproxy mode skips login() and therefore the discovery call inside
|
|
45
|
+
// it. Run discovery directly the first time someone asks for it. The
|
|
46
|
+
// primary is never in linkedTo (it's the root of the linkedTo map), so
|
|
47
|
+
// no need to guard on that — the existing guard inside login() is for
|
|
48
|
+
// the inverse case where login() is called for an already-linked
|
|
49
|
+
// district during a TTL refresh.
|
|
50
|
+
if (this.fetchproxyMode && !this.fetchproxyDiscoveryRan) {
|
|
51
|
+
this.fetchproxyDiscoveryRan = true;
|
|
52
|
+
await this.discoverLinkedDistricts(this.accounts.get(this.primaryName));
|
|
53
|
+
}
|
|
17
54
|
}
|
|
18
55
|
listDistricts() {
|
|
19
56
|
return [...this.accounts.values()].map((a) => ({
|
|
@@ -50,6 +87,14 @@ export class ICClient {
|
|
|
50
87
|
throw new UnknownDistrictError(district, [...this.accounts.keys()]);
|
|
51
88
|
}
|
|
52
89
|
await this.ensureSession(account);
|
|
90
|
+
// In fetchproxy mode, login() never runs and therefore never calls
|
|
91
|
+
// discoverLinkedDistricts. Run discovery once on the first primary call
|
|
92
|
+
// so linked districts are discoverable. (The primary is never in
|
|
93
|
+
// linkedTo by construction.)
|
|
94
|
+
if (this.fetchproxyMode && !this.fetchproxyDiscoveryRan && account.name === this.primaryName) {
|
|
95
|
+
this.fetchproxyDiscoveryRan = true;
|
|
96
|
+
await this.discoverLinkedDistricts(account);
|
|
97
|
+
}
|
|
53
98
|
return this.doRequest(account, path, opts, false);
|
|
54
99
|
}
|
|
55
100
|
async ensureSession(account) {
|
|
@@ -73,6 +118,15 @@ export class ICClient {
|
|
|
73
118
|
}
|
|
74
119
|
}
|
|
75
120
|
async login(account) {
|
|
121
|
+
// fetchproxy mode: empty primary creds, can't post to verify.jsp.
|
|
122
|
+
// The user must re-sign-in in the browser (and the next process start
|
|
123
|
+
// will pick up the fresh cookies). Linked accounts have placeholder
|
|
124
|
+
// creds too; their re-auth flows through the primary via CUPS, not
|
|
125
|
+
// verify.jsp directly.
|
|
126
|
+
if (!account.username || !account.password) {
|
|
127
|
+
throw new AuthFailedError(account.name, 'session expired and no IC_USERNAME/IC_PASSWORD set — ' +
|
|
128
|
+
'sign back into your IC portal in the browser and restart the MCP');
|
|
129
|
+
}
|
|
76
130
|
// ic_parent_api's pattern: single POST to verify.jsp, let the response
|
|
77
131
|
// set cookies. No pre-login GET needed (unlike OFW's Spring Security).
|
|
78
132
|
const postRes = await fetch(`${account.baseUrl}/campus/verify.jsp?nonBrowser=true&username=${encodeURIComponent(account.username)}&password=${encodeURIComponent(account.password)}&appName=${encodeURIComponent(account.district)}&portalLoginPage=parents`, { method: 'POST' });
|
package/dist/config.js
CHANGED
|
@@ -16,33 +16,53 @@ function readVar(env, key) {
|
|
|
16
16
|
return undefined;
|
|
17
17
|
return trimmed;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Load the Account from env vars. IC_BASE_URL + IC_DISTRICT are ALWAYS
|
|
21
|
+
* required (the MCP needs to know which host to talk to and which district to
|
|
22
|
+
* dispatch on). IC_USERNAME + IC_PASSWORD must be set together or omitted
|
|
23
|
+
* together — partial creds are treated as a user mistake and throw rather
|
|
24
|
+
* than falling through to fetchproxy (which would mask the typo).
|
|
25
|
+
*
|
|
26
|
+
* When username/password are omitted the resolved Account carries empty
|
|
27
|
+
* strings. The caller (`resolveAuth()`) treats that as "try fetchproxy".
|
|
28
|
+
*/
|
|
19
29
|
export function loadAccount(env = process.env) {
|
|
20
30
|
const baseUrl = readVar(env, 'IC_BASE_URL');
|
|
21
31
|
const district = readVar(env, 'IC_DISTRICT');
|
|
22
32
|
const username = readVar(env, 'IC_USERNAME');
|
|
23
33
|
const password = readVar(env, 'IC_PASSWORD');
|
|
24
34
|
const name = readVar(env, 'IC_NAME') ?? district;
|
|
35
|
+
// IC_BASE_URL + IC_DISTRICT are always required.
|
|
25
36
|
const missing = [];
|
|
26
37
|
if (!baseUrl)
|
|
27
38
|
missing.push('IC_BASE_URL');
|
|
28
39
|
if (!district)
|
|
29
40
|
missing.push('IC_DISTRICT');
|
|
30
|
-
if (!username)
|
|
31
|
-
missing.push('IC_USERNAME');
|
|
32
|
-
if (!password)
|
|
33
|
-
missing.push('IC_PASSWORD');
|
|
34
41
|
if (missing.length > 0) {
|
|
35
42
|
throw new Error(`Missing required env var(s): ${missing.join(', ')}. ` +
|
|
36
|
-
'Set IC_BASE_URL
|
|
43
|
+
'Set IC_BASE_URL (your portal URL) and IC_DISTRICT (the app-name path segment).');
|
|
37
44
|
}
|
|
38
45
|
if (!/^https:\/\//.test(baseUrl)) {
|
|
39
46
|
throw new Error(`IC_BASE_URL must be an https URL, got: '${baseUrl}'`);
|
|
40
47
|
}
|
|
48
|
+
// Username + password must be set together or omitted together. Partial
|
|
49
|
+
// configuration is almost always a typo, so surface it rather than silently
|
|
50
|
+
// falling through to fetchproxy.
|
|
51
|
+
if ((username && !password) || (!username && password)) {
|
|
52
|
+
const partialMissing = [];
|
|
53
|
+
if (!username)
|
|
54
|
+
partialMissing.push('IC_USERNAME');
|
|
55
|
+
if (!password)
|
|
56
|
+
partialMissing.push('IC_PASSWORD');
|
|
57
|
+
throw new Error(`Missing required env var(s) for password auth: ${partialMissing.join(', ')}. ` +
|
|
58
|
+
'Set both IC_USERNAME and IC_PASSWORD, or leave both unset to use the fetchproxy ' +
|
|
59
|
+
'fallback (requires the fetchproxy browser extension and a signed-in IC portal tab).');
|
|
60
|
+
}
|
|
41
61
|
return {
|
|
42
62
|
name: name,
|
|
43
63
|
baseUrl: baseUrl.replace(/\/$/, ''),
|
|
44
64
|
district: district,
|
|
45
|
-
username: username,
|
|
46
|
-
password: password,
|
|
65
|
+
username: username ?? '',
|
|
66
|
+
password: password ?? '',
|
|
47
67
|
};
|
|
48
68
|
}
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ catch {
|
|
|
13
13
|
}
|
|
14
14
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
15
15
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
16
|
-
import {
|
|
16
|
+
import { resolveAuth } from './auth.js';
|
|
17
17
|
import { ICClient } from './client.js';
|
|
18
18
|
import { registerDistrictTools } from './tools/districts.js';
|
|
19
19
|
import { registerStudentTools } from './tools/students.js';
|
|
@@ -32,27 +32,51 @@ import { registerTeacherTools } from './tools/teachers.js';
|
|
|
32
32
|
import { registerAssessmentTools } from './tools/assessments.js';
|
|
33
33
|
import { registerFeeTools } from './tools/fees.js';
|
|
34
34
|
import { registerFeaturesTools } from './tools/features.js';
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
35
|
+
// Defer config errors so the server can still start cleanly when env vars
|
|
36
|
+
// aren't set (e.g. during the host's install-time smoke test, before the
|
|
37
|
+
// user has filled in user_config OR the user hasn't yet signed into their
|
|
38
|
+
// IC portal in the browser). When not configured we register no tools and
|
|
39
|
+
// log a clear stderr message — far better than the previous crash loop.
|
|
40
|
+
let account = null;
|
|
41
|
+
let preloaded;
|
|
42
|
+
let source;
|
|
43
|
+
let configError = null;
|
|
44
|
+
try {
|
|
45
|
+
const resolved = await resolveAuth();
|
|
46
|
+
account = resolved.account;
|
|
47
|
+
preloaded = resolved.preloaded;
|
|
48
|
+
source = resolved.source;
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
configError = e;
|
|
52
|
+
}
|
|
53
|
+
const server = new McpServer({ name: 'infinitecampus', version: '2.1.3' });
|
|
54
|
+
if (account) {
|
|
55
|
+
const client = new ICClient(account, { preloaded });
|
|
56
|
+
registerDistrictTools(server, client);
|
|
57
|
+
registerStudentTools(server, client);
|
|
58
|
+
registerScheduleTools(server, client);
|
|
59
|
+
registerAssignmentTools(server, client);
|
|
60
|
+
registerGradeTools(server, client);
|
|
61
|
+
registerAttendanceTools(server, client);
|
|
62
|
+
registerBehaviorTools(server, client);
|
|
63
|
+
registerFoodServiceTools(server, client);
|
|
64
|
+
registerMessageTools(server, client);
|
|
65
|
+
registerDocumentTools(server, client);
|
|
66
|
+
registerCalendarTools(server, client);
|
|
67
|
+
registerAttendanceEventsTools(server, client);
|
|
68
|
+
registerRecentGradesTools(server, client);
|
|
69
|
+
registerTeacherTools(server, client);
|
|
70
|
+
registerAssessmentTools(server, client);
|
|
71
|
+
registerFeeTools(server, client);
|
|
72
|
+
registerFeaturesTools(server, client);
|
|
73
|
+
const suffix = source === 'fetchproxy' ? ' [via fetchproxy]' : '';
|
|
74
|
+
console.error(`[infinitecampus-mcp] District: ${account.name} (${account.baseUrl})${suffix}`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.error(`[infinitecampus-mcp] Not configured: ${configError?.message ?? 'unknown error'}`);
|
|
78
|
+
console.error('[infinitecampus-mcp] Server is running with no tools registered. Set the required env vars and reinstall.');
|
|
79
|
+
}
|
|
56
80
|
console.error('[infinitecampus-mcp] Developed and maintained by AI (Claude). Use at your own discretion.');
|
|
57
81
|
const transport = new StdioServerTransport();
|
|
58
82
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinitecampus-mcp",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
4
4
|
"mcpName": "io.github.chrischall/infinitecampus-mcp",
|
|
5
|
-
"description": "Infinite Campus (Campus Parent) MCP server
|
|
5
|
+
"description": "Infinite Campus (Campus Parent) MCP server \u2014 multi-district read + message/document write",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -36,12 +36,13 @@
|
|
|
36
36
|
],
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsc && npm run bundle",
|
|
39
|
-
"bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --outfile=dist/bundle.js",
|
|
39
|
+
"bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --banner:js='import { createRequire as __createRequire } from \"module\"; const require = __createRequire(import.meta.url);' --outfile=dist/bundle.js",
|
|
40
40
|
"dev": "node --env-file=.env dist/index.js",
|
|
41
41
|
"test": "vitest run",
|
|
42
42
|
"test:watch": "vitest"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
+
"@fetchproxy/bootstrap": "^0.4.2",
|
|
45
46
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
46
47
|
"dotenv": "^17.4.0",
|
|
47
48
|
"zod": "^4.3.6"
|
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.1.
|
|
9
|
+
"version": "2.1.3",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "infinitecampus-mcp",
|
|
14
|
-
"version": "2.1.
|
|
14
|
+
"version": "2.1.3",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
"name": "IC_USERNAME",
|
|
33
|
-
"description": "Campus Parent portal username",
|
|
34
|
-
"isRequired":
|
|
33
|
+
"description": "Campus Parent portal username. Optional: leave unset to use the fetchproxy browser-extension fallback.",
|
|
34
|
+
"isRequired": false,
|
|
35
35
|
"format": "string"
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
"name": "IC_PASSWORD",
|
|
39
|
-
"description": "Campus Parent portal password",
|
|
40
|
-
"isRequired":
|
|
39
|
+
"description": "Campus Parent portal password. Required only when IC_USERNAME is set.",
|
|
40
|
+
"isRequired": false,
|
|
41
41
|
"format": "string",
|
|
42
42
|
"isSecret": true
|
|
43
43
|
},
|
|
@@ -46,6 +46,12 @@
|
|
|
46
46
|
"description": "Friendly name for the district (defaults to district appname)",
|
|
47
47
|
"isRequired": false,
|
|
48
48
|
"format": "string"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "IC_DISABLE_FETCHPROXY",
|
|
52
|
+
"description": "Set to 1 to skip the fetchproxy browser-extension fallback (missing creds become a hard error — useful in headless CI).",
|
|
53
|
+
"isRequired": false,
|
|
54
|
+
"format": "string"
|
|
49
55
|
}
|
|
50
56
|
]
|
|
51
57
|
}
|