infinitecampus-mcp 2.3.5 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/dist/bundle.js +487 -151
- package/dist/client.js +161 -114
- package/dist/index.js +1 -1
- package/dist/tools/_shared.js +2 -11
- package/dist/tools/assessments.js +3 -2
- package/dist/tools/assignments.js +2 -2
- package/dist/tools/attendance.js +3 -2
- package/dist/tools/attendance_events.js +3 -2
- package/dist/tools/behavior.js +3 -2
- package/dist/tools/calendar.js +3 -2
- package/dist/tools/districts.js +2 -2
- package/dist/tools/documents.js +5 -4
- package/dist/tools/features.js +3 -2
- package/dist/tools/fees.js +3 -2
- package/dist/tools/foodservice.js +3 -2
- package/dist/tools/grades.js +2 -2
- package/dist/tools/messages.js +4 -3
- package/dist/tools/recent_grades.js +2 -2
- package/dist/tools/schedule.js +2 -2
- package/dist/tools/students.js +2 -2
- package/dist/tools/teachers.js +3 -2
- package/package.json +4 -4
- package/server.json +2 -2
- package/skills/infinitecampus-api/SKILL.md +142 -0
- package/skills/infinitecampus-api/references/endpoints.md +225 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { textResult } from '@chrischall/mcp-utils';
|
|
1
2
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
3
|
+
import { is404, featureDisabled, findStudent, studentNotFound, checkFeatureDisabled } from './_shared.js';
|
|
3
4
|
const argsSchema = z.object({
|
|
4
5
|
district: z.string(),
|
|
5
6
|
studentId: z.string(),
|
|
@@ -27,7 +28,7 @@ export function registerFoodServiceTools(server, client) {
|
|
|
27
28
|
params.set('endDate', args.until);
|
|
28
29
|
try {
|
|
29
30
|
const data = await client.request(args.district, `/campus/resources/portal/foodService?${params}`);
|
|
30
|
-
return
|
|
31
|
+
return textResult(data);
|
|
31
32
|
}
|
|
32
33
|
catch (e) {
|
|
33
34
|
if (is404(e))
|
package/dist/tools/grades.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { textResult } from '@chrischall/mcp-utils';
|
|
1
2
|
import { z } from 'zod';
|
|
2
|
-
import { textContent } from './_shared.js';
|
|
3
3
|
const argsSchema = z.object({
|
|
4
4
|
district: z.string(),
|
|
5
5
|
studentId: z.string(),
|
|
@@ -16,6 +16,6 @@ export function registerGradeTools(server, client) {
|
|
|
16
16
|
if (args.termId)
|
|
17
17
|
params.set('termID', args.termId);
|
|
18
18
|
const data = await client.request(args.district, `/campus/resources/portal/grades?${params}`);
|
|
19
|
-
return
|
|
19
|
+
return textResult(data);
|
|
20
20
|
});
|
|
21
21
|
}
|
package/dist/tools/messages.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { textResult } from '@chrischall/mcp-utils';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
import { extractPlainTextFromHtml } from '@chrischall/mcp-utils/html';
|
|
3
|
-
import {
|
|
4
|
+
import { toArray } from './_shared.js';
|
|
4
5
|
const listArgs = z.object({
|
|
5
6
|
district: z.string(),
|
|
6
7
|
limit: z.number().int().positive().describe('Number of prism notifications to retrieve (default 20). Does not affect inbox or announcements.').optional(),
|
|
@@ -95,7 +96,7 @@ export function registerMessageTools(server, client) {
|
|
|
95
96
|
return { count: 0, items: [], error: e instanceof Error ? e.message : String(e) };
|
|
96
97
|
});
|
|
97
98
|
const [notifications, inbox, announcements] = await Promise.all([prismPromise, inboxPromise, noticePromise]);
|
|
98
|
-
return
|
|
99
|
+
return textResult({ notifications, inbox, announcements });
|
|
99
100
|
});
|
|
100
101
|
server.registerTool('ic_get_message', {
|
|
101
102
|
description: "Fetch the HTML body of an inbox message and return it parsed into { subject, date, body, url }. Takes a `messageUrl` which is the `url` field from an item returned by ic_list_messages' inbox section (e.g. 'portal/messageView.xsl?x=messenger.MessengerEngine-getMessageRecipientView&messageID=...'). Relative and /campus/-prefixed URLs are both accepted. Note: fetching the HTML body may mark the message as read on some district configurations; probe against an empty inbox could not confirm the side effect.",
|
|
@@ -106,6 +107,6 @@ export function registerMessageTools(server, client) {
|
|
|
106
107
|
const path = normalizeMessageUrl(args.messageUrl);
|
|
107
108
|
const html = await client.request(args.district, path, { responseType: 'text' });
|
|
108
109
|
const parsed = parseMessageHtml(html ?? '', path);
|
|
109
|
-
return
|
|
110
|
+
return textResult(parsed);
|
|
110
111
|
});
|
|
111
112
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { textResult } from '@chrischall/mcp-utils';
|
|
1
2
|
import { z } from 'zod';
|
|
2
|
-
import { textContent } from './_shared.js';
|
|
3
3
|
const argsSchema = z.object({
|
|
4
4
|
district: z.string(),
|
|
5
5
|
studentId: z.string(),
|
|
@@ -34,6 +34,6 @@ export function registerRecentGradesTools(server, client) {
|
|
|
34
34
|
}
|
|
35
35
|
return out;
|
|
36
36
|
});
|
|
37
|
-
return
|
|
37
|
+
return textResult(trimmed);
|
|
38
38
|
});
|
|
39
39
|
}
|
package/dist/tools/schedule.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { textResult } from '@chrischall/mcp-utils';
|
|
1
2
|
import { z } from 'zod';
|
|
2
|
-
import { textContent } from './_shared.js';
|
|
3
3
|
const argsSchema = z.object({
|
|
4
4
|
district: z.string().describe('District name from ic_list_districts'),
|
|
5
5
|
studentId: z.string().describe('Student personID from ic_list_students'),
|
|
@@ -15,6 +15,6 @@ export function registerScheduleTools(server, client) {
|
|
|
15
15
|
const args = argsSchema.parse(rawArgs);
|
|
16
16
|
const params = new URLSearchParams({ personID: args.studentId });
|
|
17
17
|
const data = await client.request(args.district, `/campus/resources/portal/roster?${params}`);
|
|
18
|
-
return
|
|
18
|
+
return textResult(data);
|
|
19
19
|
});
|
|
20
20
|
}
|
package/dist/tools/students.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { textResult } from '@chrischall/mcp-utils';
|
|
1
2
|
import { z } from 'zod';
|
|
2
|
-
import { textContent } from './_shared.js';
|
|
3
3
|
const argsSchema = z.object({
|
|
4
4
|
district: z.string().describe('District name from ic_list_districts'),
|
|
5
5
|
});
|
|
@@ -11,6 +11,6 @@ export function registerStudentTools(server, client) {
|
|
|
11
11
|
}, async (rawArgs) => {
|
|
12
12
|
const args = argsSchema.parse(rawArgs);
|
|
13
13
|
const data = await client.request(args.district, '/campus/api/portal/students');
|
|
14
|
-
return
|
|
14
|
+
return textResult(data);
|
|
15
15
|
});
|
|
16
16
|
}
|
package/dist/tools/teachers.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { textResult } from '@chrischall/mcp-utils';
|
|
1
2
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
3
|
+
import { is404, toArray } from './_shared.js';
|
|
3
4
|
const argsSchema = z.object({
|
|
4
5
|
district: z.string(),
|
|
5
6
|
studentId: z.string(),
|
|
@@ -41,6 +42,6 @@ export function registerTeacherTools(server, client) {
|
|
|
41
42
|
const [teachersRaw, counselorsRaw] = await Promise.all([teachersPromise, counselorsPromise]);
|
|
42
43
|
const teachers = toArray(teachersRaw).map((t) => trimRecord(t));
|
|
43
44
|
const counselors = toArray(counselorsRaw).map((c) => trimRecord(c));
|
|
44
|
-
return
|
|
45
|
+
return textResult({ counselors, teachers });
|
|
45
46
|
});
|
|
46
47
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinitecampus-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/infinitecampus-mcp",
|
|
5
5
|
"description": "Infinite Campus (Campus Parent) MCP server — multi-district read + message/document write",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -42,19 +42,19 @@
|
|
|
42
42
|
"test:watch": "vitest"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@chrischall/mcp-utils": "^0.
|
|
45
|
+
"@chrischall/mcp-utils": "^0.13.0",
|
|
46
46
|
"@fetchproxy/bootstrap": "^1.0.0",
|
|
47
47
|
"@fetchproxy/server": "^1.0.0",
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
49
|
"dotenv": "^17.4.2",
|
|
50
|
-
"node-html-parser": "^
|
|
50
|
+
"node-html-parser": "^9.0.0",
|
|
51
51
|
"zod": "^4.4.3"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "^26.0.0",
|
|
55
55
|
"@vitest/coverage-v8": "^4.1.7",
|
|
56
56
|
"esbuild": "^0.28.0",
|
|
57
|
-
"typescript": "^
|
|
57
|
+
"typescript": "^7.0.2",
|
|
58
58
|
"vitest": "^4.1.7"
|
|
59
59
|
}
|
|
60
60
|
}
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/infinitecampus-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.4.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "infinitecampus-mcp",
|
|
14
|
-
"version": "2.
|
|
14
|
+
"version": "2.4.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: infinitecampus-api
|
|
3
|
+
description: >-
|
|
4
|
+
Query an Infinite Campus (Campus Parent portal) district directly with curl
|
|
5
|
+
instead of running the infinitecampus-mcp server — log in with a real
|
|
6
|
+
username/password, capture the session cookie + XSRF token, and curl grades,
|
|
7
|
+
attendance, assignments, schedule, messages, documents, and fees. Use when
|
|
8
|
+
you want IC data without the MCP, in a script, or on a machine where the MCP
|
|
9
|
+
isn't installed. Infinite Campus is per-district: the base URL and district
|
|
10
|
+
app name are configurable, not hardcoded.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Infinite Campus via curl (no MCP)
|
|
14
|
+
|
|
15
|
+
Infinite Campus's Campus Parent portal is a classic server-rendered site
|
|
16
|
+
(no public API, no bot wall). A plain form POST to `verify.jsp` logs a
|
|
17
|
+
parent in and hands back a session cookie — no browser bridge needed. This
|
|
18
|
+
is the same login `infinitecampus-mcp`'s `src/client.ts`/`src/auth.ts` do;
|
|
19
|
+
this skill reproduces it with `curl` + a cookie jar.
|
|
20
|
+
|
|
21
|
+
**Infinite Campus is per-district.** Every parent's portal lives at their
|
|
22
|
+
own district's host (`https://campus.<district>.k12.example.us` or similar)
|
|
23
|
+
under an `appName` path segment. There is no shared IC endpoint — carry the
|
|
24
|
+
base URL and district from the same config the MCP uses:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
: "${IC_BASE_URL:?set to your district's portal URL, e.g. https://campus.yourdistrict.org}"
|
|
28
|
+
: "${IC_DISTRICT:?set to the appName path segment, e.g. yourdistrict}"
|
|
29
|
+
: "${IC_USERNAME:?parent portal username}"
|
|
30
|
+
: "${IC_PASSWORD:?parent portal password}"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
(Same 4 env vars the MCP reads — reuse the repo's `.env` or the host's
|
|
34
|
+
secret store; don't hardcode a district.)
|
|
35
|
+
|
|
36
|
+
## One-time setup
|
|
37
|
+
|
|
38
|
+
None — `curl` and `jq` are the only tools needed (`brew install jq` if
|
|
39
|
+
missing). No profile/pairing step, no extension.
|
|
40
|
+
|
|
41
|
+
## Login (capture cookies + XSRF token)
|
|
42
|
+
|
|
43
|
+
`verify.jsp` accepts a urlencoded form POST and returns 200 with an
|
|
44
|
+
`<AUTHENTICATION>state</AUTHENTICATION>` marker in the body — **not**
|
|
45
|
+
via HTTP status — plus ~20 `Set-Cookie` headers. Use a cookie jar so curl
|
|
46
|
+
does the Set-Cookie parsing/deduping for you:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
JAR=$(mktemp)
|
|
50
|
+
BODY=$(curl -sS -c "$JAR" \
|
|
51
|
+
"$IC_BASE_URL/campus/verify.jsp?nonBrowser=true" \
|
|
52
|
+
--data-urlencode "username=$IC_USERNAME" \
|
|
53
|
+
--data-urlencode "password=$IC_PASSWORD" \
|
|
54
|
+
--data-urlencode "appName=$IC_DISTRICT" \
|
|
55
|
+
--data-urlencode "portalLoginPage=parents")
|
|
56
|
+
|
|
57
|
+
echo "$BODY" | grep -o '<AUTHENTICATION>[^<]*' # AUTHENTICATIONsuccess = good
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`password-error` / `account-locked` / any non-`success` state means the
|
|
61
|
+
login failed — check that string before doing anything else. Credentials
|
|
62
|
+
go in the form body (never the query string — it lands in access logs).
|
|
63
|
+
|
|
64
|
+
Pull the XSRF token out of the jar (IC's JS reads it back and echoes it on
|
|
65
|
+
every request as `X-XSRF-TOKEN`; `JSESSIONID` alone is not enough):
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
XSRF=$(awk -F'\t' '$6=="XSRF-TOKEN"{print $7}' "$JAR")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Core call pattern
|
|
72
|
+
|
|
73
|
+
Every subsequent request rides the jar + the XSRF header:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
ic_get() { # ic_get <path-with-query>
|
|
77
|
+
curl -sS -b "$JAR" -H "X-XSRF-TOKEN: $XSRF" -H 'Accept: application/json' \
|
|
78
|
+
"$IC_BASE_URL$1"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
ic_get '/campus/api/portal/students' | jq .
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A **401** means the session expired (IC sessions last ~5-6h) — re-run the
|
|
85
|
+
Login step to get a fresh `$JAR`/`$XSRF` and retry once. There is no retry
|
|
86
|
+
budget beyond that in this shell version (the MCP's `CookieSessionManager`
|
|
87
|
+
does this automatically; here just redo the login block).
|
|
88
|
+
|
|
89
|
+
## The one rule: resolve student + enrollment first
|
|
90
|
+
|
|
91
|
+
Almost every read is scoped to a `personID` (student), and several need the
|
|
92
|
+
student's **enrollment** (`enrollmentID` / `calendarID` / `structureID`).
|
|
93
|
+
Always start with:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
ic_get '/campus/api/portal/students' | jq '.[] | {personID, firstName, lastName, enrollments}'
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Take `personID` for `studentId`/`personID` params, and the first (or
|
|
100
|
+
relevant) `enrollments[]` entry's `enrollmentID`/`calendarID`/`structureID`
|
|
101
|
+
for the endpoints that need them (attendance, school days, assessments,
|
|
102
|
+
features). There is no name→id search — `ic_list_students` (i.e. this call)
|
|
103
|
+
*is* the resolve step.
|
|
104
|
+
|
|
105
|
+
## Endpoints
|
|
106
|
+
|
|
107
|
+
All 17 read endpoints (grades, attendance, assignments, schedule, messages,
|
|
108
|
+
documents, fees, teachers, assessments, feature flags) with ready-to-run
|
|
109
|
+
`curl`+`jq` recipes are in `references/endpoints.md`. Two (`behavior`,
|
|
110
|
+
`food_service`) have real paths that were never confirmed live against a
|
|
111
|
+
district with that module enabled — try them, expect a 404 if the module
|
|
112
|
+
is off or the district's shape differs.
|
|
113
|
+
|
|
114
|
+
## Downloads
|
|
115
|
+
|
|
116
|
+
`ic_download_document`'s `url` field (from the documents list) may be a
|
|
117
|
+
relative `/campus/...` path or an absolute URL. Same jar + header pattern,
|
|
118
|
+
just write to a file instead of piping to `jq`:
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
curl -sS -b "$JAR" -H "X-XSRF-TOKEN: $XSRF" -o report-card.pdf "$IC_BASE_URL$DOC_URL"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## FeatureDisabled districts
|
|
125
|
+
|
|
126
|
+
Some districts turn whole modules off (behavior, food service, assessments,
|
|
127
|
+
documents, attendance). The MCP checks `displayOptions` first and falls
|
|
128
|
+
back to treating a 404 as "disabled." Doing the same by hand: fetch
|
|
129
|
+
`/campus/api/portal/displayOptions/{structureID}?personID=X` (see
|
|
130
|
+
references) and check the flag before assuming a 404 is a bug.
|
|
131
|
+
|
|
132
|
+
## Notes
|
|
133
|
+
|
|
134
|
+
- Read-only in practice: every recipe here is a GET. `ic_get_message`
|
|
135
|
+
fetching a message body *may* mark it read on some district configs — the
|
|
136
|
+
MCP's own probe couldn't confirm the side effect either way.
|
|
137
|
+
- No CUPS linked-district switching in this skill (a parent with kids in
|
|
138
|
+
2+ IC instances under shared SSO) — that's a multi-step token-exchange
|
|
139
|
+
flow (`src/client.ts`'s `discoverLinkedDistricts`); out of scope here.
|
|
140
|
+
Run the login flow again against the linked district's own
|
|
141
|
+
`IC_BASE_URL`/`IC_DISTRICT` if you need it.
|
|
142
|
+
- This project is developed and maintained by AI (Claude).
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Infinite Campus endpoints for curl
|
|
2
|
+
|
|
3
|
+
Assumes the Login block from `../SKILL.md` has already run (`$JAR`, `$XSRF`,
|
|
4
|
+
`$IC_BASE_URL` set) and the `ic_get` helper is defined:
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
ic_get() { curl -sS -b "$JAR" -H "X-XSRF-TOKEN: $XSRF" -H 'Accept: application/json' "$IC_BASE_URL$1"; }
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
All paths are transcribed from `infinitecampus-mcp`'s `src/tools/*.ts` and
|
|
11
|
+
`docs/endpoints.md` — the same requests the `ic_*` MCP tools make. `$PID` =
|
|
12
|
+
a student's `personID` from the resolve-first step.
|
|
13
|
+
|
|
14
|
+
Some IC/prism responses serialize a 1-item collection as a bare object
|
|
15
|
+
instead of a 1-element array — pipe through `jq 'if type=="array" then . else [.] end'`
|
|
16
|
+
if a recipe below assumes an array and you get an object back.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Students (resolve-first — always start here)
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
ic_get '/campus/api/portal/students' \
|
|
24
|
+
| jq '.[] | {personID, firstName, lastName, enrollments}'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Each `enrollments[]` entry carries `enrollmentID`, `calendarID`,
|
|
28
|
+
`structureID`, `schoolName`, `endDate` — needed by several endpoints below.
|
|
29
|
+
|
|
30
|
+
## Schedule
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
ic_get "/campus/resources/portal/roster?personID=$PID" | jq .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
(Only `personID` is honored server-side — no working date/term filter, even
|
|
37
|
+
though the MCP tool's schema accepts `date`/`termFilter` args.)
|
|
38
|
+
|
|
39
|
+
## Assignments
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
# all assignments (full term history, ~hundreds of rows)
|
|
43
|
+
ic_get "/campus/api/portal/assignment/listView?personID=$PID" | jq .
|
|
44
|
+
|
|
45
|
+
# scoped to one course (sectionID from the schedule call above)
|
|
46
|
+
ic_get "/campus/api/portal/assignment/listView?personID=$PID§ionID=$SECTION_ID" | jq .
|
|
47
|
+
|
|
48
|
+
# client-side filters (server ignores date/missing params entirely):
|
|
49
|
+
ic_get "/campus/api/portal/assignment/listView?personID=$PID" \
|
|
50
|
+
| jq --arg since 2026-01-01 '[.[] | select(.dueDate >= $since)]'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Recent (recently-graded) assignments
|
|
54
|
+
|
|
55
|
+
Server-side filtered by `modifiedDate` (defaults to 14 days ago in the MCP;
|
|
56
|
+
reproduce with `date -u -v-14d '+%Y-%m-%dT00:00:00'` on macOS):
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
SINCE=$(date -u -v-14d '+%Y-%m-%dT00:00:00')
|
|
60
|
+
ic_get "/campus/api/portal/assignment/recentlyScored?modifiedDate=$SINCE&personID=$PID" | jq .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Grades
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
ic_get "/campus/resources/portal/grades?personID=$PID" | jq .
|
|
67
|
+
# scoped to one term:
|
|
68
|
+
ic_get "/campus/resources/portal/grades?personID=$PID&termID=$TERM_ID" | jq .
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Attendance (per-course summary, loop over each enrollment)
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
ic_get "/campus/resources/portal/attendance/$ENROLLMENT_ID?courseSummary=true&personID=$PID" \
|
|
75
|
+
| jq '.terms[]?.courses[]? | {absentList, tardyList, presentList, earlyReleaseList}'
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Feature-gated: check `displayOptions.attendance` first (see Feature flags,
|
|
79
|
+
below) — a 404 here on a district with attendance disabled is expected, not
|
|
80
|
+
a bug.
|
|
81
|
+
|
|
82
|
+
## Attendance events (individual absences/tardies, loop over each enrollment)
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
ic_get "/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID" \
|
|
86
|
+
| jq '.events[]? | {date: .localDate, code, description, excuse, comments}'
|
|
87
|
+
|
|
88
|
+
# excused-only, since a date (client-side filter):
|
|
89
|
+
ic_get "/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID" \
|
|
90
|
+
| jq --arg since 2026-01-01 '[.events[]? | select((.localDate[0:10] >= $since) and (.excuse == "E"))]'
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## School days / calendar (loop over each enrollment)
|
|
94
|
+
|
|
95
|
+
Two calls per enrollment, then join term windows to days client-side:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
ic_get "/campus/resources/term?structureID=$STRUCTURE_ID" | jq . # term boundaries
|
|
99
|
+
ic_get "/campus/resources/calendar/instructionalDay?calendarID=$CALENDAR_ID" | jq . # day list
|
|
100
|
+
|
|
101
|
+
# days within term N's [startDate,endDate]:
|
|
102
|
+
jq --slurpfile terms terms.json --arg t "$TERM_ID" \
|
|
103
|
+
'[.[] | select(.date >= $terms[0][0].startDate and .date <= $terms[0][0].endDate)]' days.json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Assessments (standardized tests, loop over each enrollment)
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
ic_get "/campus/resources/prism/portal/assessments?personID=$PID&calendarID=$CALENDAR_ID" \
|
|
110
|
+
| jq '{stateTests, nationalTests, districtTests}'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Feature-gated on `displayOptions.assessment`; test-item shape varies by
|
|
114
|
+
district/test type — pass through unchanged, don't assume field names
|
|
115
|
+
beyond `stateTests`/`nationalTests`/`districtTests`.
|
|
116
|
+
|
|
117
|
+
## Behavior *(path unconfirmed — never verified live against a district with the module on)*
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
ic_get "/campus/resources/portal/behavior?personID=$PID" | jq .
|
|
121
|
+
# with a date range:
|
|
122
|
+
ic_get "/campus/resources/portal/behavior?personID=$PID&startDate=2026-01-01&endDate=2026-06-01" | jq .
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Feature-gated on `displayOptions.behavior` — a 404 is the expected result
|
|
126
|
+
on most districts (few enable this module for parent portal).
|
|
127
|
+
|
|
128
|
+
## Food service *(path unconfirmed — same caveat as behavior)*
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
ic_get "/campus/resources/portal/foodService?personID=$PID" | jq .
|
|
132
|
+
ic_get "/campus/resources/portal/foodService?personID=$PID&startDate=2026-01-01&endDate=2026-06-01" | jq .
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Feature-gated on `displayOptions.foodService`.
|
|
136
|
+
|
|
137
|
+
## Documents + download
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
ic_get "/campus/resources/portal/report/all?personID=$PID" \
|
|
141
|
+
| jq '.[] | {name, type, url, moduleLabel, endYear}'
|
|
142
|
+
|
|
143
|
+
# download (url may be relative /campus/... or an absolute URL — handle both):
|
|
144
|
+
DOC_URL='/campus/resources/portal/report/...' # from the .url field above
|
|
145
|
+
curl -sS -b "$JAR" -H "X-XSRF-TOKEN: $XSRF" -o report-card.pdf \
|
|
146
|
+
"$( [[ "$DOC_URL" == http* ]] && echo "$DOC_URL" || echo "$IC_BASE_URL$DOC_URL" )"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Feature-gated on `displayOptions.documents`.
|
|
150
|
+
|
|
151
|
+
## Teachers + counselors
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
ic_get "/campus/resources/portal/section/contacts?personID=$PID" | jq .
|
|
155
|
+
ic_get "/campus/resources/portal/studentCounselor/byUser?personID=$PID" | jq .
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Either endpoint 404ing is treated as "no results" (not FeatureDisabled) by
|
|
159
|
+
the MCP — do the same.
|
|
160
|
+
|
|
161
|
+
## Messages (three independent sources — combine client-side)
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
# 1. prism notifications (assignment/grade/attendance alerts)
|
|
165
|
+
ic_get '/campus/prism?x=notifications.Notification-retrieve&limitCount=20' \
|
|
166
|
+
| jq '.data.NotificationList.Notification'
|
|
167
|
+
|
|
168
|
+
# 2. Messenger 2.0 inbox (teacher messages, district announcements)
|
|
169
|
+
ic_get '/campus/api/portal/process-message' | jq .
|
|
170
|
+
|
|
171
|
+
# 3. portal userNotice announcements
|
|
172
|
+
ic_get '/campus/resources/portal/userNotice' | jq .
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Each inbox item's `url` field feeds `ic_get_message` below (normalize a bare
|
|
176
|
+
or `portal/...`-relative URL to `/campus/portal/...` first).
|
|
177
|
+
|
|
178
|
+
## Get one message body (HTML → text)
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
# normalize: bare "portal/..." → "/campus/portal/..."; already-absolute /campus/... stays as-is
|
|
182
|
+
MSG_PATH='/campus/portal/messageView.xsl?x=messenger.MessengerEngine-getMessageRecipientView&messageID=...&messageRecipientID=...'
|
|
183
|
+
curl -sS -b "$JAR" -H "X-XSRF-TOKEN: $XSRF" -H 'Accept: text/html' "$IC_BASE_URL$MSG_PATH"
|
|
184
|
+
# then strip tags/entities yourself, or eyeball the rendered HTML — this is
|
|
185
|
+
# an HTML fragment, not JSON, so `jq` doesn't apply.
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Fetching a message body *may* mark it read on some district configs
|
|
189
|
+
(unconfirmed either way).
|
|
190
|
+
|
|
191
|
+
## Fees (two endpoints in parallel; either can 404 independently)
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
ic_get "/campus/api/portal/fees/feeAssignments?personID=$PID" | jq .
|
|
195
|
+
ic_get "/campus/api/portal/fees/feeTransactionDetail/totalSurplus/-1?personID=$PID" | jq .
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Both 404 → module is off for this district. One 404 → report the other
|
|
199
|
+
side plus a note that the failing endpoint may be disabled.
|
|
200
|
+
|
|
201
|
+
## Feature flags (displayOptions — check before assuming a 404 is a bug)
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
ic_get "/campus/api/portal/displayOptions/$STRUCTURE_ID?personID=$PID" \
|
|
205
|
+
| jq '{attendance, behavior, assessment, documents, foodService, grades, schedule}'
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`false` = district has that module turned off for this enrollment; `true`
|
|
209
|
+
or absent = available. ~90 flags total; the MCP caches this per
|
|
210
|
+
`(district, structureID)` for the session lifetime since it rarely changes
|
|
211
|
+
mid-session — worth doing the same if you're calling it more than once.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Not covered here (out of scope for this skill)
|
|
216
|
+
|
|
217
|
+
- **CUPS linked-district discovery/switching** — a parent with kids at 2+
|
|
218
|
+
IC-hosted districts under shared SSO. Multi-step: fetch
|
|
219
|
+
`cups/linkedAccounts`, `userAccountSwitch/originalDistrict`,
|
|
220
|
+
`districts/current`, then per linked account a `cups/loginToken` POST
|
|
221
|
+
followed by a second `verify.jsp`-style POST at the *linked* district's
|
|
222
|
+
own host with the CUPS token. Fully discoverable in `src/client.ts`
|
|
223
|
+
(`discoverLinkedDistricts`) but a lot of shell for a one-shot skill —
|
|
224
|
+
just re-run the Login block against the linked district's own
|
|
225
|
+
`IC_BASE_URL`/`IC_DISTRICT` instead.
|