infinitecampus-mcp 2.0.2 → 2.1.1
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 +36 -0
- package/.claude-plugin/plugin.json +20 -8
- package/.mcp.json +15 -0
- package/README.md +3 -3
- package/dist/bundle.js +27 -8
- package/dist/client.js +37 -12
- package/dist/index.js +1 -1
- package/package.json +4 -2
- package/server.json +53 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
3
|
+
"name": "infinitecampus-mcp",
|
|
4
|
+
"owner": {
|
|
5
|
+
"name": "Chris Hall",
|
|
6
|
+
"email": "chris.c.hall@gmail.com"
|
|
7
|
+
},
|
|
8
|
+
"metadata": {
|
|
9
|
+
"description": "MCP server for Infinite Campus (Campus Parent) — grades, attendance, assignments, messages, documents",
|
|
10
|
+
"version": "2.1.1"
|
|
11
|
+
},
|
|
12
|
+
"plugins": [
|
|
13
|
+
{
|
|
14
|
+
"name": "infinitecampus-mcp",
|
|
15
|
+
"displayName": "Infinite Campus",
|
|
16
|
+
"source": "./",
|
|
17
|
+
"description": "Infinite Campus (Campus Parent) MCP server for Claude — grades, attendance, assignments, messages, and documents via natural language",
|
|
18
|
+
"version": "2.1.1",
|
|
19
|
+
"author": {
|
|
20
|
+
"name": "Chris Hall"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/chrischall/infinitecampus-mcp",
|
|
23
|
+
"repository": "https://github.com/chrischall/infinitecampus-mcp",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"keywords": [
|
|
26
|
+
"infinite-campus",
|
|
27
|
+
"campus-parent",
|
|
28
|
+
"school",
|
|
29
|
+
"education",
|
|
30
|
+
"k12",
|
|
31
|
+
"mcp"
|
|
32
|
+
],
|
|
33
|
+
"category": "productivity"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinitecampus-mcp",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
"displayName": "Infinite Campus",
|
|
4
|
+
"version": "2.1.1",
|
|
5
|
+
"description": "Infinite Campus (Campus Parent) MCP server for Claude — grades, attendance, assignments, messages, and documents",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Chris Hall",
|
|
8
|
+
"email": "chris.c.hall@gmail.com"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/chrischall/infinitecampus-mcp",
|
|
11
|
+
"repository": "https://github.com/chrischall/infinitecampus-mcp",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"infinite-campus",
|
|
15
|
+
"campus-parent",
|
|
16
|
+
"school",
|
|
17
|
+
"education",
|
|
18
|
+
"k12",
|
|
19
|
+
"mcp"
|
|
20
|
+
],
|
|
21
|
+
"skills": "./SKILL.md",
|
|
22
|
+
"mcp": "./.mcp.json"
|
|
11
23
|
}
|
package/.mcp.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"infinitecampus": {
|
|
4
|
+
"command": "node",
|
|
5
|
+
"args": ["${CLAUDE_PLUGIN_ROOT}/dist/bundle.js"],
|
|
6
|
+
"env": {
|
|
7
|
+
"IC_BASE_URL": "${IC_BASE_URL}",
|
|
8
|
+
"IC_DISTRICT": "${IC_DISTRICT}",
|
|
9
|
+
"IC_USERNAME": "${IC_USERNAME}",
|
|
10
|
+
"IC_PASSWORD": "${IC_PASSWORD}",
|
|
11
|
+
"IC_NAME": "${IC_NAME}"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
package/README.md
CHANGED
|
@@ -31,11 +31,11 @@ Tools that the harness will gate as write/IO operations: `ic_download_document`.
|
|
|
31
31
|
Set a single set of env vars for your primary Infinite Campus account:
|
|
32
32
|
|
|
33
33
|
```
|
|
34
|
-
IC_BASE_URL=https://
|
|
35
|
-
IC_DISTRICT=
|
|
34
|
+
IC_BASE_URL=https://campus.springfield.k12.example.us
|
|
35
|
+
IC_DISTRICT=springfield
|
|
36
36
|
IC_USERNAME=parent@example.com
|
|
37
37
|
IC_PASSWORD=...
|
|
38
|
-
IC_NAME=
|
|
38
|
+
IC_NAME=Springfield # optional, defaults to IC_DISTRICT
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Linked districts (via CUPS SSO) are auto-discovered after primary login — a parent with kids in two districts only configures the primary. No extra config needed. If you have truly separate IC instances with different credentials, run two MCP instances.
|
package/dist/bundle.js
CHANGED
|
@@ -30188,8 +30188,12 @@ var ICClient = class {
|
|
|
30188
30188
|
return data;
|
|
30189
30189
|
}
|
|
30190
30190
|
async request(district, path, opts = {}) {
|
|
30191
|
-
|
|
30192
|
-
if (!account2)
|
|
30191
|
+
let account2 = this.accounts.get(district);
|
|
30192
|
+
if (!account2) {
|
|
30193
|
+
await this.ensureDiscovery();
|
|
30194
|
+
account2 = this.accounts.get(district);
|
|
30195
|
+
if (!account2) throw new UnknownDistrictError(district, [...this.accounts.keys()]);
|
|
30196
|
+
}
|
|
30193
30197
|
await this.ensureSession(account2);
|
|
30194
30198
|
return this.doRequest(account2, path, opts, false);
|
|
30195
30199
|
}
|
|
@@ -30218,11 +30222,21 @@ var ICClient = class {
|
|
|
30218
30222
|
);
|
|
30219
30223
|
if (postRes.status >= 500) throw new PortalUnreachableError(account2.name, postRes.status);
|
|
30220
30224
|
const body = await postRes.text();
|
|
30221
|
-
if (postRes.status >= 400
|
|
30222
|
-
throw new AuthFailedError(account2.name);
|
|
30225
|
+
if (postRes.status >= 400) {
|
|
30226
|
+
throw new AuthFailedError(account2.name, `HTTP ${postRes.status} from verify.jsp`);
|
|
30227
|
+
}
|
|
30228
|
+
const authMatch = body.match(/<AUTHENTICATION>([^<]+)<\/AUTHENTICATION>/i);
|
|
30229
|
+
const authState = authMatch?.[1]?.trim().toLowerCase() ?? "";
|
|
30230
|
+
if (authState === "password-error") {
|
|
30231
|
+
throw new AuthFailedError(account2.name, "IC returned password-error \u2014 wrong username or password");
|
|
30232
|
+
}
|
|
30233
|
+
if (authState && authState !== "success") {
|
|
30234
|
+
throw new AuthFailedError(account2.name, `IC returned authentication state '${authState}'`);
|
|
30223
30235
|
}
|
|
30224
30236
|
const cookies = parseSetCookies(postRes.headers);
|
|
30225
|
-
if (!cookies.cookieHeader)
|
|
30237
|
+
if (!cookies.cookieHeader) {
|
|
30238
|
+
throw new AuthFailedError(account2.name, "login response missing session cookies");
|
|
30239
|
+
}
|
|
30226
30240
|
const session = this.sessions.get(account2.name);
|
|
30227
30241
|
session.cookie = cookies.cookieHeader;
|
|
30228
30242
|
session.xsrfToken = cookies.xsrfToken;
|
|
@@ -30425,12 +30439,17 @@ var UnknownDistrictError = class extends Error {
|
|
|
30425
30439
|
available;
|
|
30426
30440
|
};
|
|
30427
30441
|
var AuthFailedError = class extends Error {
|
|
30428
|
-
constructor(district) {
|
|
30429
|
-
|
|
30442
|
+
constructor(district, reason) {
|
|
30443
|
+
const detail = reason ? ` (${reason})` : "";
|
|
30444
|
+
super(
|
|
30445
|
+
`Login failed for district '${district}'${detail}. Check IC_USERNAME and IC_PASSWORD; if those are correct, the account may be locked or the portal may be down.`
|
|
30446
|
+
);
|
|
30430
30447
|
this.district = district;
|
|
30448
|
+
this.reason = reason;
|
|
30431
30449
|
this.name = "AuthFailedError";
|
|
30432
30450
|
}
|
|
30433
30451
|
district;
|
|
30452
|
+
reason;
|
|
30434
30453
|
};
|
|
30435
30454
|
var PortalUnreachableError = class extends Error {
|
|
30436
30455
|
constructor(district, status) {
|
|
@@ -31318,7 +31337,7 @@ try {
|
|
|
31318
31337
|
}
|
|
31319
31338
|
var account = loadAccount();
|
|
31320
31339
|
var client = new ICClient(account);
|
|
31321
|
-
var server = new McpServer({ name: "infinitecampus", version: "2.
|
|
31340
|
+
var server = new McpServer({ name: "infinitecampus", version: "2.1.1" });
|
|
31322
31341
|
registerDistrictTools(server, client);
|
|
31323
31342
|
registerStudentTools(server, client);
|
|
31324
31343
|
registerScheduleTools(server, client);
|
package/dist/client.js
CHANGED
|
@@ -37,9 +37,18 @@ export class ICClient {
|
|
|
37
37
|
return data;
|
|
38
38
|
}
|
|
39
39
|
async request(district, path, opts = {}) {
|
|
40
|
-
|
|
41
|
-
if (!account)
|
|
42
|
-
|
|
40
|
+
let account = this.accounts.get(district);
|
|
41
|
+
if (!account) {
|
|
42
|
+
// Cold-start: linked districts are only added to the accounts map after
|
|
43
|
+
// primary login + CUPS discovery. If the caller asks for a linked
|
|
44
|
+
// district before any other request triggered login, we'd otherwise
|
|
45
|
+
// throw UnknownDistrictError despite the district being valid. Run
|
|
46
|
+
// discovery once before giving up.
|
|
47
|
+
await this.ensureDiscovery();
|
|
48
|
+
account = this.accounts.get(district);
|
|
49
|
+
if (!account)
|
|
50
|
+
throw new UnknownDistrictError(district, [...this.accounts.keys()]);
|
|
51
|
+
}
|
|
43
52
|
await this.ensureSession(account);
|
|
44
53
|
return this.doRequest(account, path, opts, false);
|
|
45
54
|
}
|
|
@@ -69,16 +78,27 @@ export class ICClient {
|
|
|
69
78
|
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' });
|
|
70
79
|
if (postRes.status >= 500)
|
|
71
80
|
throw new PortalUnreachableError(account.name, postRes.status);
|
|
72
|
-
//
|
|
73
|
-
// body
|
|
81
|
+
// IC's verify.jsp returns 200 with an <AUTHENTICATION>X</AUTHENTICATION>
|
|
82
|
+
// body where X is the auth state (success / password-error / account-locked
|
|
83
|
+
// / etc.). 4xx is also possible on misconfigured endpoints. Surface the
|
|
84
|
+
// actual reason so the LLM can give the user something useful.
|
|
74
85
|
const body = await postRes.text();
|
|
75
|
-
if (postRes.status >= 400
|
|
76
|
-
throw new AuthFailedError(account.name);
|
|
86
|
+
if (postRes.status >= 400) {
|
|
87
|
+
throw new AuthFailedError(account.name, `HTTP ${postRes.status} from verify.jsp`);
|
|
88
|
+
}
|
|
89
|
+
const authMatch = body.match(/<AUTHENTICATION>([^<]+)<\/AUTHENTICATION>/i);
|
|
90
|
+
const authState = authMatch?.[1]?.trim().toLowerCase() ?? '';
|
|
91
|
+
if (authState === 'password-error') {
|
|
92
|
+
throw new AuthFailedError(account.name, 'IC returned password-error — wrong username or password');
|
|
93
|
+
}
|
|
94
|
+
if (authState && authState !== 'success') {
|
|
95
|
+
throw new AuthFailedError(account.name, `IC returned authentication state '${authState}'`);
|
|
77
96
|
}
|
|
78
97
|
// Capture cookies, deduplicating and filtering out deletions (Max-Age=0).
|
|
79
98
|
const cookies = parseSetCookies(postRes.headers);
|
|
80
|
-
if (!cookies.cookieHeader)
|
|
81
|
-
throw new AuthFailedError(account.name);
|
|
99
|
+
if (!cookies.cookieHeader) {
|
|
100
|
+
throw new AuthFailedError(account.name, 'login response missing session cookies');
|
|
101
|
+
}
|
|
82
102
|
// Mutate the in-map session in place so concurrent callers'
|
|
83
103
|
// references stay live (see ensureSession).
|
|
84
104
|
const session = this.sessions.get(account.name);
|
|
@@ -271,7 +291,7 @@ export class ICClient {
|
|
|
271
291
|
* Parse Set-Cookie headers into a deduplicated cookie string + XSRF token.
|
|
272
292
|
*
|
|
273
293
|
* IC's login response sets ~20 cookies including deletion markers (Max-Age=0).
|
|
274
|
-
* Sending both `appName=` (delete) and `appName=
|
|
294
|
+
* Sending both `appName=` (delete) and `appName=springfield` (set) causes IC to
|
|
275
295
|
* reject requests with "conflicting app name values". This parser:
|
|
276
296
|
* - Filters out cookies with Max-Age=0 (deletion markers)
|
|
277
297
|
* - Deduplicates by name (last value wins)
|
|
@@ -319,9 +339,14 @@ export class UnknownDistrictError extends Error {
|
|
|
319
339
|
}
|
|
320
340
|
export class AuthFailedError extends Error {
|
|
321
341
|
district;
|
|
322
|
-
|
|
323
|
-
|
|
342
|
+
reason;
|
|
343
|
+
constructor(district, reason) {
|
|
344
|
+
const detail = reason ? ` (${reason})` : '';
|
|
345
|
+
super(`Login failed for district '${district}'${detail}. ` +
|
|
346
|
+
`Check IC_USERNAME and IC_PASSWORD; ` +
|
|
347
|
+
`if those are correct, the account may be locked or the portal may be down.`);
|
|
324
348
|
this.district = district;
|
|
349
|
+
this.reason = reason;
|
|
325
350
|
this.name = 'AuthFailedError';
|
|
326
351
|
}
|
|
327
352
|
}
|
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ import { registerFeeTools } from './tools/fees.js';
|
|
|
34
34
|
import { registerFeaturesTools } from './tools/features.js';
|
|
35
35
|
const account = loadAccount();
|
|
36
36
|
const client = new ICClient(account);
|
|
37
|
-
const server = new McpServer({ name: 'infinitecampus', version: '2.
|
|
37
|
+
const server = new McpServer({ name: 'infinitecampus', version: '2.1.1' });
|
|
38
38
|
registerDistrictTools(server, client);
|
|
39
39
|
registerStudentTools(server, client);
|
|
40
40
|
registerScheduleTools(server, client);
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinitecampus-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
|
+
"mcpName": "io.github.chrischall/infinitecampus-mcp",
|
|
4
5
|
"description": "Infinite Campus (Campus Parent) MCP server — multi-district read + message/document write",
|
|
5
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
6
7
|
"repository": {
|
|
@@ -30,7 +31,8 @@
|
|
|
30
31
|
"dist",
|
|
31
32
|
".claude-plugin",
|
|
32
33
|
"skills",
|
|
33
|
-
".mcp.json"
|
|
34
|
+
".mcp.json",
|
|
35
|
+
"server.json"
|
|
34
36
|
],
|
|
35
37
|
"scripts": {
|
|
36
38
|
"build": "tsc && npm run bundle",
|
package/server.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.chrischall/infinitecampus-mcp",
|
|
4
|
+
"description": "Infinite Campus (Campus Parent) for Claude — grades, attendance, assignments, messages",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/chrischall/infinitecampus-mcp",
|
|
7
|
+
"source": "github"
|
|
8
|
+
},
|
|
9
|
+
"version": "2.1.1",
|
|
10
|
+
"packages": [
|
|
11
|
+
{
|
|
12
|
+
"registryType": "npm",
|
|
13
|
+
"identifier": "infinitecampus-mcp",
|
|
14
|
+
"version": "2.1.1",
|
|
15
|
+
"transport": {
|
|
16
|
+
"type": "stdio"
|
|
17
|
+
},
|
|
18
|
+
"environmentVariables": [
|
|
19
|
+
{
|
|
20
|
+
"name": "IC_BASE_URL",
|
|
21
|
+
"description": "Campus Parent portal base URL, e.g. https://campus.district.k12.example.us",
|
|
22
|
+
"isRequired": true,
|
|
23
|
+
"format": "string"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "IC_DISTRICT",
|
|
27
|
+
"description": "Infinite Campus district app name (appName path segment from the portal URL)",
|
|
28
|
+
"isRequired": true,
|
|
29
|
+
"format": "string"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "IC_USERNAME",
|
|
33
|
+
"description": "Campus Parent portal username",
|
|
34
|
+
"isRequired": true,
|
|
35
|
+
"format": "string"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "IC_PASSWORD",
|
|
39
|
+
"description": "Campus Parent portal password",
|
|
40
|
+
"isRequired": true,
|
|
41
|
+
"format": "string",
|
|
42
|
+
"isSecret": true
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "IC_NAME",
|
|
46
|
+
"description": "Friendly name for the district (defaults to district appname)",
|
|
47
|
+
"isRequired": false,
|
|
48
|
+
"format": "string"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|