infinitecampus-mcp 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -8
- package/dist/bundle.js +106 -19
- package/dist/client.js +15 -0
- package/dist/index.js +3 -1
- package/dist/tools/_shared.js +25 -0
- package/dist/tools/assessments.js +4 -1
- package/dist/tools/attendance.js +4 -1
- package/dist/tools/attendance_events.js +4 -1
- package/dist/tools/behavior.js +8 -2
- package/dist/tools/documents.js +7 -1
- package/dist/tools/features.js +29 -0
- package/dist/tools/fees.js +8 -8
- package/dist/tools/foodservice.js +10 -3
- package/dist/tools/teachers.js +1 -1
- package/package.json +1 -1
- package/skills/ic/SKILL.md +48 -21
package/README.md
CHANGED
|
@@ -4,18 +4,25 @@ MCP server for Infinite Campus (Campus Parent portal). Single-account config —
|
|
|
4
4
|
|
|
5
5
|
## Tools
|
|
6
6
|
|
|
7
|
+
19 tools across academics, daily life, documents, messaging, and feature discovery.
|
|
8
|
+
|
|
7
9
|
| Domain | Tools |
|
|
8
10
|
|---|---|
|
|
9
11
|
| Districts | `ic_list_districts` |
|
|
10
12
|
| Students | `ic_list_students` |
|
|
11
13
|
| Schedule | `ic_get_schedule` |
|
|
12
|
-
| Assignments | `ic_list_assignments` (
|
|
13
|
-
| Grades | `ic_list_grades` |
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
14
|
+
| Assignments | `ic_list_assignments` (sectionID server-side; `missingOnly` / date filters client-side) |
|
|
15
|
+
| Grades | `ic_list_grades`, `ic_list_recent_grades` (default 14d window) |
|
|
16
|
+
| School calendar | `ic_list_school_days` |
|
|
17
|
+
| Attendance | `ic_list_attendance` (per-course summary), `ic_list_attendance_events` (individual events with codes + comments) |
|
|
18
|
+
| Behavior | `ic_list_behavior` (FeatureDisabled-aware) |
|
|
19
|
+
| Food service | `ic_list_food_service` (FeatureDisabled-aware) |
|
|
17
20
|
| Documents | `ic_list_documents`, `ic_download_document` |
|
|
18
|
-
|
|
|
21
|
+
| Messaging | `ic_list_messages` (3 sources: prism notifications + Messenger 2.0 inbox + portal announcements), `ic_get_message` (fetch parsed HTML body of an inbox message) |
|
|
22
|
+
| Teachers | `ic_list_teachers` (teachers per section + assigned counselors) |
|
|
23
|
+
| Assessments | `ic_list_assessments` (standardized test scores) |
|
|
24
|
+
| Fees | `ic_list_fees` (assignments + surplus balance) |
|
|
25
|
+
| Features | `ic_get_features` (per-enrollment displayOptions flags) |
|
|
19
26
|
|
|
20
27
|
Tools that the harness will gate as write/IO operations: `ic_download_document`.
|
|
21
28
|
|
|
@@ -31,10 +38,10 @@ IC_PASSWORD=...
|
|
|
31
38
|
IC_NAME=Myers Park # optional, defaults to IC_DISTRICT
|
|
32
39
|
```
|
|
33
40
|
|
|
34
|
-
Linked districts (via CUPS SSO) are auto-discovered after login —
|
|
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.
|
|
35
42
|
|
|
36
43
|
See `.env.example`.
|
|
37
44
|
|
|
38
45
|
## Status
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
Unofficial — not affiliated with Infinite Campus. AI-maintained.
|
package/dist/bundle.js
CHANGED
|
@@ -29746,11 +29746,11 @@ var McpServer = class {
|
|
|
29746
29746
|
}
|
|
29747
29747
|
return registeredResourceTemplate;
|
|
29748
29748
|
}
|
|
29749
|
-
_createRegisteredPrompt(name, title, description,
|
|
29749
|
+
_createRegisteredPrompt(name, title, description, argsSchema15, callback) {
|
|
29750
29750
|
const registeredPrompt = {
|
|
29751
29751
|
title,
|
|
29752
29752
|
description,
|
|
29753
|
-
argsSchema:
|
|
29753
|
+
argsSchema: argsSchema15 === void 0 ? void 0 : objectFromShape(argsSchema15),
|
|
29754
29754
|
callback,
|
|
29755
29755
|
enabled: true,
|
|
29756
29756
|
disable: () => registeredPrompt.update({ enabled: false }),
|
|
@@ -29776,8 +29776,8 @@ var McpServer = class {
|
|
|
29776
29776
|
}
|
|
29777
29777
|
};
|
|
29778
29778
|
this._registeredPrompts[name] = registeredPrompt;
|
|
29779
|
-
if (
|
|
29780
|
-
const hasCompletable = Object.values(
|
|
29779
|
+
if (argsSchema15) {
|
|
29780
|
+
const hasCompletable = Object.values(argsSchema15).some((field) => {
|
|
29781
29781
|
const inner = field instanceof ZodOptional2 ? field._def?.innerType : field;
|
|
29782
29782
|
return isCompletable(inner);
|
|
29783
29783
|
});
|
|
@@ -29884,12 +29884,12 @@ var McpServer = class {
|
|
|
29884
29884
|
if (typeof rest[0] === "string") {
|
|
29885
29885
|
description = rest.shift();
|
|
29886
29886
|
}
|
|
29887
|
-
let
|
|
29887
|
+
let argsSchema15;
|
|
29888
29888
|
if (rest.length > 1) {
|
|
29889
|
-
|
|
29889
|
+
argsSchema15 = rest.shift();
|
|
29890
29890
|
}
|
|
29891
29891
|
const cb = rest[0];
|
|
29892
|
-
const registeredPrompt = this._createRegisteredPrompt(name, void 0, description,
|
|
29892
|
+
const registeredPrompt = this._createRegisteredPrompt(name, void 0, description, argsSchema15, cb);
|
|
29893
29893
|
this.setPromptRequestHandlers();
|
|
29894
29894
|
this.sendPromptListChanged();
|
|
29895
29895
|
return registeredPrompt;
|
|
@@ -29901,8 +29901,8 @@ var McpServer = class {
|
|
|
29901
29901
|
if (this._registeredPrompts[name]) {
|
|
29902
29902
|
throw new Error(`Prompt ${name} is already registered`);
|
|
29903
29903
|
}
|
|
29904
|
-
const { title, description, argsSchema:
|
|
29905
|
-
const registeredPrompt = this._createRegisteredPrompt(name, title, description,
|
|
29904
|
+
const { title, description, argsSchema: argsSchema15 } = config2;
|
|
29905
|
+
const registeredPrompt = this._createRegisteredPrompt(name, title, description, argsSchema15, cb);
|
|
29906
29906
|
this.setPromptRequestHandlers();
|
|
29907
29907
|
this.sendPromptListChanged();
|
|
29908
29908
|
return registeredPrompt;
|
|
@@ -30156,6 +30156,7 @@ var ICClient = class {
|
|
|
30156
30156
|
linkedTo = /* @__PURE__ */ new Map();
|
|
30157
30157
|
// linkedDistrictName → primaryDistrictName
|
|
30158
30158
|
primaryName;
|
|
30159
|
+
featuresCache = /* @__PURE__ */ new Map();
|
|
30159
30160
|
constructor(account2) {
|
|
30160
30161
|
this.accounts.set(account2.name, account2);
|
|
30161
30162
|
this.primaryName = account2.name;
|
|
@@ -30170,6 +30171,22 @@ var ICClient = class {
|
|
|
30170
30171
|
linked: this.linkedTo.has(a.name)
|
|
30171
30172
|
}));
|
|
30172
30173
|
}
|
|
30174
|
+
/**
|
|
30175
|
+
* Fetch the per-structure displayOptions feature-flag allowlist for a student.
|
|
30176
|
+
* Results are cached per (district, structureID) for the duration of the
|
|
30177
|
+
* session TTL — flags rarely change mid-session and the call costs ~1 RT.
|
|
30178
|
+
*/
|
|
30179
|
+
async getFeatures(district, structureID, studentId) {
|
|
30180
|
+
const key = `${district}:${structureID}`;
|
|
30181
|
+
const cached2 = this.featuresCache.get(key);
|
|
30182
|
+
if (cached2 && Date.now() - cached2.fetchedAt < SESSION_TTL_MS) return cached2.data;
|
|
30183
|
+
const data = await this.request(
|
|
30184
|
+
district,
|
|
30185
|
+
`/campus/api/portal/displayOptions/${structureID}?personID=${encodeURIComponent(studentId)}`
|
|
30186
|
+
);
|
|
30187
|
+
this.featuresCache.set(key, { data, fetchedAt: Date.now() });
|
|
30188
|
+
return data;
|
|
30189
|
+
}
|
|
30173
30190
|
async request(district, path, opts = {}) {
|
|
30174
30191
|
const account2 = this.accounts.get(district);
|
|
30175
30192
|
if (!account2) throw new UnknownDistrictError(district, [...this.accounts.keys()]);
|
|
@@ -30475,6 +30492,18 @@ async function findStudent(client2, district, studentId) {
|
|
|
30475
30492
|
function studentNotFound(studentId) {
|
|
30476
30493
|
return textContent({ error: "StudentNotFound", studentId });
|
|
30477
30494
|
}
|
|
30495
|
+
async function checkFeatureDisabled(client2, district, studentId, student, flag, toolName, emptyData = []) {
|
|
30496
|
+
const structureID = student.enrollments?.[0]?.structureID;
|
|
30497
|
+
if (!structureID) return null;
|
|
30498
|
+
try {
|
|
30499
|
+
const features = await client2.getFeatures(district, structureID, studentId);
|
|
30500
|
+
if (features[flag] === false) return featureDisabled(toolName, district, emptyData);
|
|
30501
|
+
return null;
|
|
30502
|
+
} catch (e) {
|
|
30503
|
+
console.error(`[ic] displayOptions check failed for ${district}/${flag}: ${e instanceof Error ? e.message : e}`);
|
|
30504
|
+
return null;
|
|
30505
|
+
}
|
|
30506
|
+
}
|
|
30478
30507
|
function toArray(value) {
|
|
30479
30508
|
if (value === null || value === void 0) return [];
|
|
30480
30509
|
return Array.isArray(value) ? value : [value];
|
|
@@ -30625,6 +30654,8 @@ function registerAttendanceTools(server2, client2) {
|
|
|
30625
30654
|
const args = argsSchema5.parse(rawArgs);
|
|
30626
30655
|
const student = await findStudent(client2, args.district, args.studentId);
|
|
30627
30656
|
if (!student) return studentNotFound(args.studentId);
|
|
30657
|
+
const disabled = await checkFeatureDisabled(client2, args.district, args.studentId, student, "attendance", "attendance");
|
|
30658
|
+
if (disabled) return disabled;
|
|
30628
30659
|
const enrollments = student.enrollments ?? [];
|
|
30629
30660
|
const results = [];
|
|
30630
30661
|
try {
|
|
@@ -30665,11 +30696,15 @@ var argsSchema6 = external_exports3.object({
|
|
|
30665
30696
|
});
|
|
30666
30697
|
function registerBehaviorTools(server2, client2) {
|
|
30667
30698
|
server2.registerTool("ic_list_behavior", {
|
|
30668
|
-
description: "List a student's behavior events / referrals. Returns FeatureDisabled if the district has the behavior module turned off.",
|
|
30699
|
+
description: "List a student's behavior events / referrals. Returns FeatureDisabled if the district has the behavior module turned off (detected via displayOptions or a 404 backstop).",
|
|
30669
30700
|
annotations: { readOnlyHint: true },
|
|
30670
30701
|
inputSchema: argsSchema6.shape
|
|
30671
30702
|
}, async (rawArgs) => {
|
|
30672
30703
|
const args = argsSchema6.parse(rawArgs);
|
|
30704
|
+
const student = await findStudent(client2, args.district, args.studentId);
|
|
30705
|
+
if (!student) return studentNotFound(args.studentId);
|
|
30706
|
+
const disabled = await checkFeatureDisabled(client2, args.district, args.studentId, student, "behavior", "behavior");
|
|
30707
|
+
if (disabled) return disabled;
|
|
30673
30708
|
const params = new URLSearchParams({ personID: args.studentId });
|
|
30674
30709
|
if (args.since) params.set("startDate", args.since);
|
|
30675
30710
|
if (args.until) params.set("endDate", args.until);
|
|
@@ -30690,13 +30725,26 @@ var argsSchema7 = external_exports3.object({
|
|
|
30690
30725
|
since: external_exports3.string().optional(),
|
|
30691
30726
|
until: external_exports3.string().optional()
|
|
30692
30727
|
});
|
|
30728
|
+
var EMPTY_FOOD = { balance: null, transactions: [] };
|
|
30693
30729
|
function registerFoodServiceTools(server2, client2) {
|
|
30694
30730
|
server2.registerTool("ic_list_food_service", {
|
|
30695
|
-
description: "List a student's lunch balance and recent food-service transactions. Returns FeatureDisabled if the district has the module turned off.",
|
|
30731
|
+
description: "List a student's lunch balance and recent food-service transactions. Returns FeatureDisabled if the district has the module turned off (detected via displayOptions or a 404 backstop).",
|
|
30696
30732
|
annotations: { readOnlyHint: true },
|
|
30697
30733
|
inputSchema: argsSchema7.shape
|
|
30698
30734
|
}, async (rawArgs) => {
|
|
30699
30735
|
const args = argsSchema7.parse(rawArgs);
|
|
30736
|
+
const student = await findStudent(client2, args.district, args.studentId);
|
|
30737
|
+
if (!student) return studentNotFound(args.studentId);
|
|
30738
|
+
const disabled = await checkFeatureDisabled(
|
|
30739
|
+
client2,
|
|
30740
|
+
args.district,
|
|
30741
|
+
args.studentId,
|
|
30742
|
+
student,
|
|
30743
|
+
"foodService",
|
|
30744
|
+
"foodService",
|
|
30745
|
+
EMPTY_FOOD
|
|
30746
|
+
);
|
|
30747
|
+
if (disabled) return disabled;
|
|
30700
30748
|
const params = new URLSearchParams({ personID: args.studentId });
|
|
30701
30749
|
if (args.since) params.set("startDate", args.since);
|
|
30702
30750
|
if (args.until) params.set("endDate", args.until);
|
|
@@ -30704,7 +30752,7 @@ function registerFoodServiceTools(server2, client2) {
|
|
|
30704
30752
|
const data = await client2.request(args.district, `/campus/resources/portal/foodService?${params}`);
|
|
30705
30753
|
return textContent(data);
|
|
30706
30754
|
} catch (e) {
|
|
30707
|
-
if (is404(e)) return featureDisabled("foodService", args.district,
|
|
30755
|
+
if (is404(e)) return featureDisabled("foodService", args.district, EMPTY_FOOD);
|
|
30708
30756
|
throw e;
|
|
30709
30757
|
}
|
|
30710
30758
|
});
|
|
@@ -30839,6 +30887,10 @@ function registerDocumentTools(server2, client2) {
|
|
|
30839
30887
|
inputSchema: listArgs2.shape
|
|
30840
30888
|
}, async (rawArgs) => {
|
|
30841
30889
|
const args = listArgs2.parse(rawArgs);
|
|
30890
|
+
const student = await findStudent(client2, args.district, args.studentId);
|
|
30891
|
+
if (!student) return studentNotFound(args.studentId);
|
|
30892
|
+
const disabled = await checkFeatureDisabled(client2, args.district, args.studentId, student, "documents", "documents");
|
|
30893
|
+
if (disabled) return disabled;
|
|
30842
30894
|
try {
|
|
30843
30895
|
const raw = await client2.request(
|
|
30844
30896
|
args.district,
|
|
@@ -30991,6 +31043,8 @@ function registerAttendanceEventsTools(server2, client2) {
|
|
|
30991
31043
|
const args = argsSchema9.parse(rawArgs);
|
|
30992
31044
|
const student = await findStudent(client2, args.district, args.studentId);
|
|
30993
31045
|
if (!student) return studentNotFound(args.studentId);
|
|
31046
|
+
const disabled = await checkFeatureDisabled(client2, args.district, args.studentId, student, "attendance", "attendance_events");
|
|
31047
|
+
if (disabled) return disabled;
|
|
30994
31048
|
const enrollments = student.enrollments ?? [];
|
|
30995
31049
|
const results = [];
|
|
30996
31050
|
try {
|
|
@@ -31089,6 +31143,7 @@ var DROP_KEYS = /* @__PURE__ */ new Set([
|
|
|
31089
31143
|
"mTime",
|
|
31090
31144
|
"action",
|
|
31091
31145
|
"personID",
|
|
31146
|
+
"studentPersonID",
|
|
31092
31147
|
"isKentucky",
|
|
31093
31148
|
"pairID",
|
|
31094
31149
|
"pairedEvent"
|
|
@@ -31145,6 +31200,8 @@ function registerAssessmentTools(server2, client2) {
|
|
|
31145
31200
|
const args = argsSchema12.parse(rawArgs);
|
|
31146
31201
|
const student = await findStudent(client2, args.district, args.studentId);
|
|
31147
31202
|
if (!student) return studentNotFound(args.studentId);
|
|
31203
|
+
const disabled = await checkFeatureDisabled(client2, args.district, args.studentId, student, "assessment", "assessments");
|
|
31204
|
+
if (disabled) return disabled;
|
|
31148
31205
|
const personIDEnc = encodeURIComponent(args.studentId);
|
|
31149
31206
|
const result = [];
|
|
31150
31207
|
let feature404 = false;
|
|
@@ -31187,7 +31244,7 @@ var argsSchema13 = external_exports3.object({
|
|
|
31187
31244
|
});
|
|
31188
31245
|
function registerFeeTools(server2, client2) {
|
|
31189
31246
|
server2.registerTool("ic_list_fees", {
|
|
31190
|
-
description: "List a student's fee assignments (charges owed) and running balance/surplus. Combines two endpoints: fee assignments and totalSurplus. Returns FeatureDisabled only if both endpoints 404; if only one works, returns that with
|
|
31247
|
+
description: "List a student's fee assignments (charges owed) and running balance/surplus. Combines two endpoints: fee assignments and totalSurplus. Returns FeatureDisabled only if both endpoints 404; if only one works, returns that side with warning: 'PartialSuccess' and an issues[] explaining which endpoint failed.",
|
|
31191
31248
|
annotations: { readOnlyHint: true },
|
|
31192
31249
|
inputSchema: argsSchema13.shape
|
|
31193
31250
|
}, async (rawArgs) => {
|
|
@@ -31211,18 +31268,47 @@ function registerFeeTools(server2, client2) {
|
|
|
31211
31268
|
if (!assignments.ok && !surplus.ok) {
|
|
31212
31269
|
return featureDisabled("fees", args.district, { totalSurplus: null, feeAssignments: [] });
|
|
31213
31270
|
}
|
|
31271
|
+
const issues = [];
|
|
31272
|
+
if (!assignments.ok) issues.push("feeAssignments endpoint returned 404 (module may be disabled for this district)");
|
|
31273
|
+
if (!surplus.ok) issues.push("totalSurplus endpoint returned 404 (module may be disabled for this district)");
|
|
31214
31274
|
const response = {
|
|
31275
|
+
...issues.length > 0 ? { warning: "PartialSuccess" } : {},
|
|
31215
31276
|
totalSurplus: surplus.ok ? surplus.value : null,
|
|
31216
|
-
feeAssignments: assignments.ok ? assignments.value : []
|
|
31277
|
+
feeAssignments: assignments.ok ? assignments.value : [],
|
|
31278
|
+
...issues.length > 0 ? { issues } : {}
|
|
31217
31279
|
};
|
|
31218
|
-
const notes = [];
|
|
31219
|
-
if (!assignments.ok) notes.push("feeAssignments endpoint returned 404 (module may be disabled for this district)");
|
|
31220
|
-
if (!surplus.ok) notes.push("totalSurplus endpoint returned 404 (module may be disabled for this district)");
|
|
31221
|
-
if (notes.length > 0) response.notes = notes;
|
|
31222
31280
|
return textContent(response);
|
|
31223
31281
|
});
|
|
31224
31282
|
}
|
|
31225
31283
|
|
|
31284
|
+
// src/tools/features.ts
|
|
31285
|
+
var argsSchema14 = external_exports3.object({
|
|
31286
|
+
district: external_exports3.string(),
|
|
31287
|
+
studentId: external_exports3.string().describe("Student personID from ic_list_students")
|
|
31288
|
+
});
|
|
31289
|
+
function registerFeaturesTools(server2, client2) {
|
|
31290
|
+
server2.registerTool("ic_get_features", {
|
|
31291
|
+
description: "List the district's displayOptions feature-flag allow-list for each of a student's enrollments. Each enrollment's `features` object is a map of ~90 flag names (attendance, behavior, assessment, documents, grades, schedule, etc.) to booleans. A `false` value means the district has that feature disabled for this enrollment; `true` or missing means it's available. Used internally by other tools to short-circuit disabled features, but exposed here so the LLM can answer capability questions directly.",
|
|
31292
|
+
annotations: { readOnlyHint: true },
|
|
31293
|
+
inputSchema: argsSchema14.shape
|
|
31294
|
+
}, async (rawArgs) => {
|
|
31295
|
+
const args = argsSchema14.parse(rawArgs);
|
|
31296
|
+
const student = await findStudent(client2, args.district, args.studentId);
|
|
31297
|
+
if (!student) return studentNotFound(args.studentId);
|
|
31298
|
+
const result = [];
|
|
31299
|
+
for (const enr of student.enrollments ?? []) {
|
|
31300
|
+
const features = await client2.getFeatures(args.district, enr.structureID, args.studentId);
|
|
31301
|
+
result.push({
|
|
31302
|
+
enrollmentID: enr.enrollmentID,
|
|
31303
|
+
structureID: enr.structureID,
|
|
31304
|
+
schoolName: enr.schoolName,
|
|
31305
|
+
features
|
|
31306
|
+
});
|
|
31307
|
+
}
|
|
31308
|
+
return textContent(result);
|
|
31309
|
+
});
|
|
31310
|
+
}
|
|
31311
|
+
|
|
31226
31312
|
// src/index.ts
|
|
31227
31313
|
try {
|
|
31228
31314
|
const { config: config2 } = await import("dotenv");
|
|
@@ -31232,7 +31318,7 @@ try {
|
|
|
31232
31318
|
}
|
|
31233
31319
|
var account = loadAccount();
|
|
31234
31320
|
var client = new ICClient(account);
|
|
31235
|
-
var server = new McpServer({ name: "infinitecampus", version: "2.0.
|
|
31321
|
+
var server = new McpServer({ name: "infinitecampus", version: "2.0.2" });
|
|
31236
31322
|
registerDistrictTools(server, client);
|
|
31237
31323
|
registerStudentTools(server, client);
|
|
31238
31324
|
registerScheduleTools(server, client);
|
|
@@ -31249,6 +31335,7 @@ registerRecentGradesTools(server, client);
|
|
|
31249
31335
|
registerTeacherTools(server, client);
|
|
31250
31336
|
registerAssessmentTools(server, client);
|
|
31251
31337
|
registerFeeTools(server, client);
|
|
31338
|
+
registerFeaturesTools(server, client);
|
|
31252
31339
|
console.error(`[infinitecampus-mcp] District: ${account.name} (${account.baseUrl})`);
|
|
31253
31340
|
console.error("[infinitecampus-mcp] Developed and maintained by AI (Claude). Use at your own discretion.");
|
|
31254
31341
|
var transport = new StdioServerTransport();
|
package/dist/client.js
CHANGED
|
@@ -6,6 +6,7 @@ export class ICClient {
|
|
|
6
6
|
sessions = new Map();
|
|
7
7
|
linkedTo = new Map(); // linkedDistrictName → primaryDistrictName
|
|
8
8
|
primaryName;
|
|
9
|
+
featuresCache = new Map();
|
|
9
10
|
constructor(account) {
|
|
10
11
|
this.accounts.set(account.name, account);
|
|
11
12
|
this.primaryName = account.name;
|
|
@@ -21,6 +22,20 @@ export class ICClient {
|
|
|
21
22
|
linked: this.linkedTo.has(a.name),
|
|
22
23
|
}));
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Fetch the per-structure displayOptions feature-flag allowlist for a student.
|
|
27
|
+
* Results are cached per (district, structureID) for the duration of the
|
|
28
|
+
* session TTL — flags rarely change mid-session and the call costs ~1 RT.
|
|
29
|
+
*/
|
|
30
|
+
async getFeatures(district, structureID, studentId) {
|
|
31
|
+
const key = `${district}:${structureID}`;
|
|
32
|
+
const cached = this.featuresCache.get(key);
|
|
33
|
+
if (cached && Date.now() - cached.fetchedAt < SESSION_TTL_MS)
|
|
34
|
+
return cached.data;
|
|
35
|
+
const data = await this.request(district, `/campus/api/portal/displayOptions/${structureID}?personID=${encodeURIComponent(studentId)}`);
|
|
36
|
+
this.featuresCache.set(key, { data, fetchedAt: Date.now() });
|
|
37
|
+
return data;
|
|
38
|
+
}
|
|
24
39
|
async request(district, path, opts = {}) {
|
|
25
40
|
const account = this.accounts.get(district);
|
|
26
41
|
if (!account)
|
package/dist/index.js
CHANGED
|
@@ -31,9 +31,10 @@ import { registerRecentGradesTools } from './tools/recent_grades.js';
|
|
|
31
31
|
import { registerTeacherTools } from './tools/teachers.js';
|
|
32
32
|
import { registerAssessmentTools } from './tools/assessments.js';
|
|
33
33
|
import { registerFeeTools } from './tools/fees.js';
|
|
34
|
+
import { registerFeaturesTools } from './tools/features.js';
|
|
34
35
|
const account = loadAccount();
|
|
35
36
|
const client = new ICClient(account);
|
|
36
|
-
const server = new McpServer({ name: 'infinitecampus', version: '2.0.
|
|
37
|
+
const server = new McpServer({ name: 'infinitecampus', version: '2.0.2' });
|
|
37
38
|
registerDistrictTools(server, client);
|
|
38
39
|
registerStudentTools(server, client);
|
|
39
40
|
registerScheduleTools(server, client);
|
|
@@ -50,6 +51,7 @@ registerRecentGradesTools(server, client);
|
|
|
50
51
|
registerTeacherTools(server, client);
|
|
51
52
|
registerAssessmentTools(server, client);
|
|
52
53
|
registerFeeTools(server, client);
|
|
54
|
+
registerFeaturesTools(server, client);
|
|
53
55
|
console.error(`[infinitecampus-mcp] District: ${account.name} (${account.baseUrl})`);
|
|
54
56
|
console.error('[infinitecampus-mcp] Developed and maintained by AI (Claude). Use at your own discretion.');
|
|
55
57
|
const transport = new StdioServerTransport();
|
package/dist/tools/_shared.js
CHANGED
|
@@ -19,6 +19,31 @@ export async function findStudent(client, district, studentId) {
|
|
|
19
19
|
export function studentNotFound(studentId) {
|
|
20
20
|
return textContent({ error: 'StudentNotFound', studentId });
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Check a feature flag via the district's displayOptions allow-list for the
|
|
24
|
+
* student's first enrollment's structureID. Returns a FeatureDisabled content
|
|
25
|
+
* block when the flag is explicitly `false`. Returns `null` in every other
|
|
26
|
+
* case (flag is `true`, flag is absent, no enrollments, or the displayOptions
|
|
27
|
+
* call itself fails) — callers then fall through to hit the real endpoint.
|
|
28
|
+
*
|
|
29
|
+
* Non-fatal by design: if the allow-list can't be fetched we don't want to
|
|
30
|
+
* break the tool, since the 404-catch backstop still protects it.
|
|
31
|
+
*/
|
|
32
|
+
export async function checkFeatureDisabled(client, district, studentId, student, flag, toolName, emptyData = []) {
|
|
33
|
+
const structureID = student.enrollments?.[0]?.structureID;
|
|
34
|
+
if (!structureID)
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
const features = await client.getFeatures(district, structureID, studentId);
|
|
38
|
+
if (features[flag] === false)
|
|
39
|
+
return featureDisabled(toolName, district, emptyData);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
console.error(`[ic] displayOptions check failed for ${district}/${flag}: ${e instanceof Error ? e.message : e}`);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
22
47
|
/**
|
|
23
48
|
* Coerce a value to an array. Defensive against IC's prism XML→JSON
|
|
24
49
|
* serializer which returns a bare object (not a 1-element array) for
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { textContent, findStudent, studentNotFound, featureDisabled, is404, toArray } from './_shared.js';
|
|
2
|
+
import { textContent, findStudent, studentNotFound, featureDisabled, is404, toArray, checkFeatureDisabled } from './_shared.js';
|
|
3
3
|
const argsSchema = z.object({
|
|
4
4
|
district: z.string(),
|
|
5
5
|
studentId: z.string().describe('Student personID from ic_list_students'),
|
|
@@ -14,6 +14,9 @@ export function registerAssessmentTools(server, client) {
|
|
|
14
14
|
const student = await findStudent(client, args.district, args.studentId);
|
|
15
15
|
if (!student)
|
|
16
16
|
return studentNotFound(args.studentId);
|
|
17
|
+
const disabled = await checkFeatureDisabled(client, args.district, args.studentId, student, 'assessment', 'assessments');
|
|
18
|
+
if (disabled)
|
|
19
|
+
return disabled;
|
|
17
20
|
const personIDEnc = encodeURIComponent(args.studentId);
|
|
18
21
|
const result = [];
|
|
19
22
|
let feature404 = false;
|
package/dist/tools/attendance.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { textContent, is404, featureDisabled, findStudent, studentNotFound, toArray } from './_shared.js';
|
|
2
|
+
import { textContent, is404, featureDisabled, findStudent, studentNotFound, toArray, checkFeatureDisabled } from './_shared.js';
|
|
3
3
|
const argsSchema = z.object({
|
|
4
4
|
district: z.string(),
|
|
5
5
|
studentId: z.string(),
|
|
@@ -47,6 +47,9 @@ export function registerAttendanceTools(server, client) {
|
|
|
47
47
|
const student = await findStudent(client, args.district, args.studentId);
|
|
48
48
|
if (!student)
|
|
49
49
|
return studentNotFound(args.studentId);
|
|
50
|
+
const disabled = await checkFeatureDisabled(client, args.district, args.studentId, student, 'attendance', 'attendance');
|
|
51
|
+
if (disabled)
|
|
52
|
+
return disabled;
|
|
50
53
|
const enrollments = student.enrollments ?? [];
|
|
51
54
|
const results = [];
|
|
52
55
|
try {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { textContent, is404, featureDisabled, findStudent, studentNotFound, toArray } from './_shared.js';
|
|
2
|
+
import { textContent, is404, featureDisabled, findStudent, studentNotFound, toArray, checkFeatureDisabled } from './_shared.js';
|
|
3
3
|
const argsSchema = z.object({
|
|
4
4
|
district: z.string(),
|
|
5
5
|
studentId: z.string().describe('Student personID from ic_list_students'),
|
|
@@ -48,6 +48,9 @@ export function registerAttendanceEventsTools(server, client) {
|
|
|
48
48
|
const student = await findStudent(client, args.district, args.studentId);
|
|
49
49
|
if (!student)
|
|
50
50
|
return studentNotFound(args.studentId);
|
|
51
|
+
const disabled = await checkFeatureDisabled(client, args.district, args.studentId, student, 'attendance', 'attendance_events');
|
|
52
|
+
if (disabled)
|
|
53
|
+
return disabled;
|
|
51
54
|
const enrollments = student.enrollments ?? [];
|
|
52
55
|
const results = [];
|
|
53
56
|
try {
|
package/dist/tools/behavior.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { textContent, is404, featureDisabled } from './_shared.js';
|
|
2
|
+
import { textContent, is404, featureDisabled, findStudent, studentNotFound, checkFeatureDisabled } from './_shared.js';
|
|
3
3
|
const argsSchema = z.object({
|
|
4
4
|
district: z.string(),
|
|
5
5
|
studentId: z.string(),
|
|
@@ -8,11 +8,17 @@ const argsSchema = z.object({
|
|
|
8
8
|
});
|
|
9
9
|
export function registerBehaviorTools(server, client) {
|
|
10
10
|
server.registerTool('ic_list_behavior', {
|
|
11
|
-
description: "List a student's behavior events / referrals. Returns FeatureDisabled if the district has the behavior module turned off.",
|
|
11
|
+
description: "List a student's behavior events / referrals. Returns FeatureDisabled if the district has the behavior module turned off (detected via displayOptions or a 404 backstop).",
|
|
12
12
|
annotations: { readOnlyHint: true },
|
|
13
13
|
inputSchema: argsSchema.shape,
|
|
14
14
|
}, async (rawArgs) => {
|
|
15
15
|
const args = argsSchema.parse(rawArgs);
|
|
16
|
+
const student = await findStudent(client, args.district, args.studentId);
|
|
17
|
+
if (!student)
|
|
18
|
+
return studentNotFound(args.studentId);
|
|
19
|
+
const disabled = await checkFeatureDisabled(client, args.district, args.studentId, student, 'behavior', 'behavior');
|
|
20
|
+
if (disabled)
|
|
21
|
+
return disabled;
|
|
16
22
|
const params = new URLSearchParams({ personID: args.studentId });
|
|
17
23
|
if (args.since)
|
|
18
24
|
params.set('startDate', args.since);
|
package/dist/tools/documents.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { textContent, is404, featureDisabled, toArray } from './_shared.js';
|
|
2
|
+
import { textContent, is404, featureDisabled, toArray, findStudent, studentNotFound, checkFeatureDisabled } from './_shared.js';
|
|
3
3
|
const listArgs = z.object({
|
|
4
4
|
district: z.string(),
|
|
5
5
|
studentId: z.string(),
|
|
@@ -17,6 +17,12 @@ export function registerDocumentTools(server, client) {
|
|
|
17
17
|
inputSchema: listArgs.shape,
|
|
18
18
|
}, async (rawArgs) => {
|
|
19
19
|
const args = listArgs.parse(rawArgs);
|
|
20
|
+
const student = await findStudent(client, args.district, args.studentId);
|
|
21
|
+
if (!student)
|
|
22
|
+
return studentNotFound(args.studentId);
|
|
23
|
+
const disabled = await checkFeatureDisabled(client, args.district, args.studentId, student, 'documents', 'documents');
|
|
24
|
+
if (disabled)
|
|
25
|
+
return disabled;
|
|
20
26
|
try {
|
|
21
27
|
const raw = await client.request(args.district, `/campus/resources/portal/report/all?personID=${encodeURIComponent(args.studentId)}`);
|
|
22
28
|
const trimmed = toArray(raw).map((d) => {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { textContent, findStudent, studentNotFound } from './_shared.js';
|
|
3
|
+
const argsSchema = z.object({
|
|
4
|
+
district: z.string(),
|
|
5
|
+
studentId: z.string().describe('Student personID from ic_list_students'),
|
|
6
|
+
});
|
|
7
|
+
export function registerFeaturesTools(server, client) {
|
|
8
|
+
server.registerTool('ic_get_features', {
|
|
9
|
+
description: "List the district's displayOptions feature-flag allow-list for each of a student's enrollments. Each enrollment's `features` object is a map of ~90 flag names (attendance, behavior, assessment, documents, grades, schedule, etc.) to booleans. A `false` value means the district has that feature disabled for this enrollment; `true` or missing means it's available. Used internally by other tools to short-circuit disabled features, but exposed here so the LLM can answer capability questions directly.",
|
|
10
|
+
annotations: { readOnlyHint: true },
|
|
11
|
+
inputSchema: argsSchema.shape,
|
|
12
|
+
}, async (rawArgs) => {
|
|
13
|
+
const args = argsSchema.parse(rawArgs);
|
|
14
|
+
const student = await findStudent(client, args.district, args.studentId);
|
|
15
|
+
if (!student)
|
|
16
|
+
return studentNotFound(args.studentId);
|
|
17
|
+
const result = [];
|
|
18
|
+
for (const enr of student.enrollments ?? []) {
|
|
19
|
+
const features = await client.getFeatures(args.district, enr.structureID, args.studentId);
|
|
20
|
+
result.push({
|
|
21
|
+
enrollmentID: enr.enrollmentID,
|
|
22
|
+
structureID: enr.structureID,
|
|
23
|
+
schoolName: enr.schoolName,
|
|
24
|
+
features,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return textContent(result);
|
|
28
|
+
});
|
|
29
|
+
}
|
package/dist/tools/fees.js
CHANGED
|
@@ -6,7 +6,7 @@ const argsSchema = z.object({
|
|
|
6
6
|
});
|
|
7
7
|
export function registerFeeTools(server, client) {
|
|
8
8
|
server.registerTool('ic_list_fees', {
|
|
9
|
-
description: "List a student's fee assignments (charges owed) and running balance/surplus. Combines two endpoints: fee assignments and totalSurplus. Returns FeatureDisabled only if both endpoints 404; if only one works, returns that with
|
|
9
|
+
description: "List a student's fee assignments (charges owed) and running balance/surplus. Combines two endpoints: fee assignments and totalSurplus. Returns FeatureDisabled only if both endpoints 404; if only one works, returns that side with warning: 'PartialSuccess' and an issues[] explaining which endpoint failed.",
|
|
10
10
|
annotations: { readOnlyHint: true },
|
|
11
11
|
inputSchema: argsSchema.shape,
|
|
12
12
|
}, async (rawArgs) => {
|
|
@@ -27,17 +27,17 @@ export function registerFeeTools(server, client) {
|
|
|
27
27
|
if (!assignments.ok && !surplus.ok) {
|
|
28
28
|
return featureDisabled('fees', args.district, { totalSurplus: null, feeAssignments: [] });
|
|
29
29
|
}
|
|
30
|
+
const issues = [];
|
|
31
|
+
if (!assignments.ok)
|
|
32
|
+
issues.push('feeAssignments endpoint returned 404 (module may be disabled for this district)');
|
|
33
|
+
if (!surplus.ok)
|
|
34
|
+
issues.push('totalSurplus endpoint returned 404 (module may be disabled for this district)');
|
|
30
35
|
const response = {
|
|
36
|
+
...(issues.length > 0 ? { warning: 'PartialSuccess' } : {}),
|
|
31
37
|
totalSurplus: surplus.ok ? surplus.value : null,
|
|
32
38
|
feeAssignments: assignments.ok ? assignments.value : [],
|
|
39
|
+
...(issues.length > 0 ? { issues } : {}),
|
|
33
40
|
};
|
|
34
|
-
const notes = [];
|
|
35
|
-
if (!assignments.ok)
|
|
36
|
-
notes.push('feeAssignments endpoint returned 404 (module may be disabled for this district)');
|
|
37
|
-
if (!surplus.ok)
|
|
38
|
-
notes.push('totalSurplus endpoint returned 404 (module may be disabled for this district)');
|
|
39
|
-
if (notes.length > 0)
|
|
40
|
-
response.notes = notes;
|
|
41
41
|
return textContent(response);
|
|
42
42
|
});
|
|
43
43
|
}
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { textContent, is404, featureDisabled } from './_shared.js';
|
|
2
|
+
import { textContent, is404, featureDisabled, findStudent, studentNotFound, checkFeatureDisabled } from './_shared.js';
|
|
3
3
|
const argsSchema = z.object({
|
|
4
4
|
district: z.string(),
|
|
5
5
|
studentId: z.string(),
|
|
6
6
|
since: z.string().optional(),
|
|
7
7
|
until: z.string().optional(),
|
|
8
8
|
});
|
|
9
|
+
const EMPTY_FOOD = { balance: null, transactions: [] };
|
|
9
10
|
export function registerFoodServiceTools(server, client) {
|
|
10
11
|
server.registerTool('ic_list_food_service', {
|
|
11
|
-
description: "List a student's lunch balance and recent food-service transactions. Returns FeatureDisabled if the district has the module turned off.",
|
|
12
|
+
description: "List a student's lunch balance and recent food-service transactions. Returns FeatureDisabled if the district has the module turned off (detected via displayOptions or a 404 backstop).",
|
|
12
13
|
annotations: { readOnlyHint: true },
|
|
13
14
|
inputSchema: argsSchema.shape,
|
|
14
15
|
}, async (rawArgs) => {
|
|
15
16
|
const args = argsSchema.parse(rawArgs);
|
|
17
|
+
const student = await findStudent(client, args.district, args.studentId);
|
|
18
|
+
if (!student)
|
|
19
|
+
return studentNotFound(args.studentId);
|
|
20
|
+
const disabled = await checkFeatureDisabled(client, args.district, args.studentId, student, 'foodService', 'foodService', EMPTY_FOOD);
|
|
21
|
+
if (disabled)
|
|
22
|
+
return disabled;
|
|
16
23
|
const params = new URLSearchParams({ personID: args.studentId });
|
|
17
24
|
if (args.since)
|
|
18
25
|
params.set('startDate', args.since);
|
|
@@ -24,7 +31,7 @@ export function registerFoodServiceTools(server, client) {
|
|
|
24
31
|
}
|
|
25
32
|
catch (e) {
|
|
26
33
|
if (is404(e))
|
|
27
|
-
return featureDisabled('foodService', args.district,
|
|
34
|
+
return featureDisabled('foodService', args.district, EMPTY_FOOD);
|
|
28
35
|
throw e;
|
|
29
36
|
}
|
|
30
37
|
});
|
package/dist/tools/teachers.js
CHANGED
|
@@ -6,7 +6,7 @@ const argsSchema = z.object({
|
|
|
6
6
|
});
|
|
7
7
|
// Fields we drop from teacher/counselor contact records (internal IDs, model markers)
|
|
8
8
|
const DROP_KEYS = new Set([
|
|
9
|
-
'_id', '_model', '_hashCode', 'mTime', 'action', 'personID',
|
|
9
|
+
'_id', '_model', '_hashCode', 'mTime', 'action', 'personID', 'studentPersonID',
|
|
10
10
|
'isKentucky', 'pairID', 'pairedEvent',
|
|
11
11
|
]);
|
|
12
12
|
function trimRecord(raw) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinitecampus-mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "Infinite Campus (Campus Parent) MCP server — multi-district read + message/document write",
|
|
5
5
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
6
6
|
"repository": {
|
package/skills/ic/SKILL.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ic
|
|
3
|
-
description: This skill should be used when the user asks about Infinite Campus (Campus Parent portal) data for their kids. Triggers on phrases like "check IC", "Infinite Campus", "what's my kid's grade", "any missing assignments", "school messages", "report card", "lunch balance", or any request about a student's schedule, grades, attendance, behavior, food service, documents, or
|
|
3
|
+
description: This skill should be used when the user asks about Infinite Campus (Campus Parent portal) data for their kids. Triggers on phrases like "check IC", "Infinite Campus", "what's my kid's grade", "any missing assignments", "school messages", "report card", "lunch balance", "recent grades", "assessments", "fees", or any request about a student's schedule, grades, assignments, attendance, behavior, food service, documents, messages, teachers, assessments, or fees. Linked districts are auto-discovered via CUPS SSO.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# infinitecampus-mcp
|
|
7
7
|
|
|
8
|
-
MCP server for Infinite Campus (Campus Parent portal) —
|
|
8
|
+
MCP server for Infinite Campus (Campus Parent portal) — 19 tools covering schedule, grades (current + recently-scored), assignments, attendance (summary + per-event), behavior, food service, documents, messages (3 sources), teachers, assessments, and fees. Linked districts are auto-discovered via CUPS SSO after primary login.
|
|
9
9
|
|
|
10
10
|
- **Source:** [github.com/chrischall/infinitecampus-mcp](https://github.com/chrischall/infinitecampus-mcp)
|
|
11
11
|
- **npm:** [npmjs.com/package/infinitecampus-mcp](https://www.npmjs.com/package/infinitecampus-mcp)
|
|
12
12
|
|
|
13
13
|
## Setup
|
|
14
14
|
|
|
15
|
+
Single-account config. Set env vars for your primary IC account; linked districts come from CUPS discovery.
|
|
16
|
+
|
|
15
17
|
### Option A — Claude Code (direct MCP, no mcporter)
|
|
16
18
|
|
|
17
19
|
Add to `.mcp.json` in your project or `~/.claude/mcp.json`:
|
|
@@ -34,7 +36,7 @@ Add to `.mcp.json` in your project or `~/.claude/mcp.json`:
|
|
|
34
36
|
}
|
|
35
37
|
```
|
|
36
38
|
|
|
37
|
-
Linked districts (via CUPS SSO) are auto-discovered after login —
|
|
39
|
+
Only `IC_BASE_URL`, `IC_DISTRICT`, `IC_USERNAME`, `IC_PASSWORD` are required. `IC_NAME` is optional (defaults to `IC_DISTRICT`). Linked districts (via CUPS SSO) are auto-discovered after login — a parent with kids in two districts only configures the primary.
|
|
38
40
|
|
|
39
41
|
### Option B — mcporter
|
|
40
42
|
|
|
@@ -100,28 +102,39 @@ Every tool except `ic_list_districts` takes `district` as its first arg (the dis
|
|
|
100
102
|
### Academics
|
|
101
103
|
| Tool | Notes |
|
|
102
104
|
|------|-------|
|
|
103
|
-
| `ic_get_schedule(district, studentId)` | Today's class schedule by default. |
|
|
104
|
-
| `ic_list_assignments(district, studentId, courseId?, since?, until?, missingOnly?)` | Pass `missingOnly=true` to see only missing/late work across all courses. |
|
|
105
|
-
| `ic_list_grades(district, studentId, termId?)` |
|
|
105
|
+
| `ic_get_schedule(district, studentId)` | Today's class schedule by default, with section placements. |
|
|
106
|
+
| `ic_list_assignments(district, studentId, courseId?, since?, until?, missingOnly?)` | `sectionID` is the only server-side filter; `since`/`until`/`missingOnly` are applied client-side. Pass `missingOnly=true` to see only missing/late work across all courses. |
|
|
107
|
+
| `ic_list_grades(district, studentId, termId?)` | Term + in-progress grade summary. Omit `termId` for all terms. |
|
|
108
|
+
| `ic_list_recent_grades(district, studentId, since?)` | Recently-scored assignments. Defaults to a 14-day window; pass `since` (YYYY-MM-DD) to widen. |
|
|
109
|
+
| `ic_list_assessments(district, studentId)` | Standardized test scores (state/national/district tests). FeatureDisabled-aware. |
|
|
110
|
+
| `ic_list_teachers(district, studentId)` | Teachers per enrolled section + assigned counselor(s). |
|
|
106
111
|
|
|
107
112
|
### Daily life
|
|
108
113
|
| Tool | Notes |
|
|
109
114
|
|------|-------|
|
|
110
|
-
| `
|
|
115
|
+
| `ic_list_school_days(district, studentId)` | Instructional calendar with term boundaries. |
|
|
116
|
+
| `ic_list_attendance(district, studentId, since?, until?)` | Per-course attendance summary grouped by term. |
|
|
117
|
+
| `ic_list_attendance_events(district, studentId, since?, until?, excusedOnly?)` | Individual absence/tardy events with codes and human comments. |
|
|
111
118
|
| `ic_list_behavior(district, studentId, since?, until?)` | Returns a `FeatureDisabled` warning if the district has no behavior module enabled — this is not an error. |
|
|
112
119
|
| `ic_list_food_service(district, studentId, since?, until?)` | Lunch balance and transactions. Same `FeatureDisabled` fallback as behavior. |
|
|
120
|
+
| `ic_list_fees(district, studentId)` | Fee assignments + surplus/balance. Returns `PartialSuccess` if only one endpoint works, `FeatureDisabled` if both 404. |
|
|
113
121
|
|
|
114
122
|
### Documents
|
|
115
123
|
| Tool | Notes |
|
|
116
124
|
|------|-------|
|
|
117
|
-
| `ic_list_documents(district, studentId)` | Metadata only (report cards,
|
|
118
|
-
| `ic_download_document(district,
|
|
125
|
+
| `ic_list_documents(district, studentId)` | Metadata only (report cards, schedules, transcripts). Each item has a `url` to pass to `ic_download_document`. Returns `FeatureDisabled` if the documents module is off. |
|
|
126
|
+
| `ic_download_document(district, url, destinationPath)` | Writes the document to `destinationPath` on disk. **`destinationPath` is required** — confirm the path with the user before calling. |
|
|
127
|
+
|
|
128
|
+
### Messaging
|
|
129
|
+
| Tool | Notes |
|
|
130
|
+
|------|-------|
|
|
131
|
+
| `ic_list_messages(district, limit?)` | Combines three sources: **prism notifications** (grade/attendance/assignment alerts), **Messenger 2.0 inbox** (teacher messages, priority announcements like closures), and **portal userNotice** (district banners). `limit` caps prism only. Per-source `error` field if one fails. |
|
|
132
|
+
| `ic_get_message(district, url)` | Fetches and parses the HTML body of an inbox message. Returns `{ subject, date, body, url }`. |
|
|
119
133
|
|
|
120
|
-
###
|
|
134
|
+
### Features
|
|
121
135
|
| Tool | Notes |
|
|
122
136
|
|------|-------|
|
|
123
|
-
| `
|
|
124
|
-
| `ic_get_message(district)` | Unread notification/message count. |
|
|
137
|
+
| `ic_get_features(district, studentId)` | Raw per-enrollment `displayOptions` flags (attendance/behavior/assessment/documents/…). Useful for diagnosing why another tool returned `FeatureDisabled`. |
|
|
125
138
|
|
|
126
139
|
## Workflows
|
|
127
140
|
|
|
@@ -129,20 +142,34 @@ Every tool except `ic_list_districts` takes `district` as its first arg (the dis
|
|
|
129
142
|
1. `ic_list_districts` → see configured + linked districts
|
|
130
143
|
2. For each district: `ic_list_students(district)` → collect kids and personIDs
|
|
131
144
|
|
|
132
|
-
**
|
|
133
|
-
1. `
|
|
134
|
-
2.
|
|
135
|
-
|
|
145
|
+
**Is everything OK at school?**
|
|
146
|
+
1. `ic_list_districts`
|
|
147
|
+
2. For each student:
|
|
148
|
+
- `ic_list_recent_grades(district, studentId)` — last 14 days of scored work
|
|
149
|
+
- `ic_list_attendance_events(district, studentId, excusedOnly=false)` — recent absences/tardies
|
|
150
|
+
- `ic_list_messages(district)` — any alerts from teachers or the district
|
|
151
|
+
|
|
152
|
+
**What got graded this week?**
|
|
153
|
+
- `ic_list_recent_grades(district, studentId, since="YYYY-MM-DD")`
|
|
154
|
+
|
|
155
|
+
**Check upcoming or missing assignments:**
|
|
156
|
+
- `ic_list_assignments(district, studentId, missingOnly=true)` — only the late/missing set
|
|
157
|
+
- Or `ic_list_assignments(district, studentId, since=..., until=...)` for a date window
|
|
136
158
|
|
|
137
159
|
**Today's schedule:**
|
|
138
160
|
- `ic_get_schedule(district, studentId)` — returns today's classes by default
|
|
139
161
|
|
|
140
|
-
**
|
|
141
|
-
|
|
162
|
+
**Weather closure or priority announcement?**
|
|
163
|
+
- `ic_list_messages(district)` — scan the Messenger 2.0 inbox entries for priority subjects
|
|
164
|
+
- `ic_get_message(district, url)` to read a specific one
|
|
165
|
+
|
|
166
|
+
**Download a report card:**
|
|
167
|
+
1. `ic_list_documents(district, studentId)` → find the report card's `url`
|
|
142
168
|
2. Confirm destination path with the user
|
|
143
|
-
3. `ic_download_document(district,
|
|
169
|
+
3. `ic_download_document(district, url, destinationPath="/Users/.../report-card.pdf")`
|
|
144
170
|
|
|
145
171
|
## Caution
|
|
146
172
|
|
|
147
|
-
- `ic_download_document` writes
|
|
148
|
-
-
|
|
173
|
+
- `ic_download_document` writes to disk at `destinationPath` — confirm the path with the user; overwrites silently.
|
|
174
|
+
- `ic_list_messages` / `ic_get_message` are nominally read-only, but on some district configurations fetching an inbox message or enumerating the list may mark entries as read. Behavior was not confirmable against an empty test inbox.
|
|
175
|
+
- Endpoint behavior varies by district. If `ic_list_behavior`, `ic_list_food_service`, `ic_list_documents`, `ic_list_assessments`, or `ic_list_fees` returns a `FeatureDisabled` warning, that module is simply turned off for the district — it's not an error. `ic_list_fees` may also return `PartialSuccess` when only one of its two sub-endpoints works.
|