infinitecampus-mcp 0.1.2 → 2.0.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/dist/bundle.js CHANGED
@@ -29746,11 +29746,11 @@ var McpServer = class {
29746
29746
  }
29747
29747
  return registeredResourceTemplate;
29748
29748
  }
29749
- _createRegisteredPrompt(name, title, description, argsSchema8, callback) {
29749
+ _createRegisteredPrompt(name, title, description, argsSchema14, callback) {
29750
29750
  const registeredPrompt = {
29751
29751
  title,
29752
29752
  description,
29753
- argsSchema: argsSchema8 === void 0 ? void 0 : objectFromShape(argsSchema8),
29753
+ argsSchema: argsSchema14 === void 0 ? void 0 : objectFromShape(argsSchema14),
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 (argsSchema8) {
29780
- const hasCompletable = Object.values(argsSchema8).some((field) => {
29779
+ if (argsSchema14) {
29780
+ const hasCompletable = Object.values(argsSchema14).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 argsSchema8;
29887
+ let argsSchema14;
29888
29888
  if (rest.length > 1) {
29889
- argsSchema8 = rest.shift();
29889
+ argsSchema14 = rest.shift();
29890
29890
  }
29891
29891
  const cb = rest[0];
29892
- const registeredPrompt = this._createRegisteredPrompt(name, void 0, description, argsSchema8, cb);
29892
+ const registeredPrompt = this._createRegisteredPrompt(name, void 0, description, argsSchema14, 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: argsSchema8 } = config2;
29905
- const registeredPrompt = this._createRegisteredPrompt(name, title, description, argsSchema8, cb);
29904
+ const { title, description, argsSchema: argsSchema14 } = config2;
29905
+ const registeredPrompt = this._createRegisteredPrompt(name, title, description, argsSchema14, cb);
29906
29906
  this.setPromptRequestHandlers();
29907
29907
  this.sendPromptListChanged();
29908
29908
  return registeredPrompt;
@@ -30320,7 +30320,8 @@ var ICClient = class {
30320
30320
  if (!account2) throw new UnknownDistrictError(district, [...this.accounts.keys()]);
30321
30321
  await this.ensureSession(account2);
30322
30322
  const session = this.sessions.get(account2.name);
30323
- const res = await fetch(`${account2.baseUrl}${path}`, {
30323
+ const url2 = /^https?:\/\//i.test(path) ? path : `${account2.baseUrl}${path}`;
30324
+ const res = await fetch(url2, {
30324
30325
  headers: {
30325
30326
  Cookie: session.cookie,
30326
30327
  ...session.xsrfToken ? { "X-XSRF-TOKEN": session.xsrfToken } : {}
@@ -30337,11 +30338,12 @@ var ICClient = class {
30337
30338
  }
30338
30339
  async doRequest(account2, path, opts, isRetry) {
30339
30340
  const session = this.sessions.get(account2.name);
30341
+ const accept = opts.responseType === "text" ? "text/html, text/plain, */*" : "application/json";
30340
30342
  const res = await fetch(`${account2.baseUrl}${path}`, {
30341
30343
  method: opts.method ?? "GET",
30342
30344
  headers: {
30343
30345
  Cookie: session.cookie,
30344
- Accept: "application/json",
30346
+ Accept: accept,
30345
30347
  ...session.xsrfToken ? { "X-XSRF-TOKEN": session.xsrfToken } : {},
30346
30348
  ...opts.headers ?? {}
30347
30349
  },
@@ -30366,6 +30368,9 @@ var ICClient = class {
30366
30368
  if (res.status >= 500) throw new PortalUnreachableError(account2.name, res.status);
30367
30369
  if (!res.ok) throw new Error(`IC ${res.status} ${res.statusText} for ${path}`);
30368
30370
  const text = await res.text();
30371
+ if (opts.responseType === "text") {
30372
+ return text;
30373
+ }
30369
30374
  return text ? JSON.parse(text) : null;
30370
30375
  }
30371
30376
  };
@@ -30453,6 +30458,28 @@ var FileExistsError = class extends Error {
30453
30458
  path;
30454
30459
  };
30455
30460
 
30461
+ // src/tools/_shared.ts
30462
+ function textContent(data) {
30463
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30464
+ }
30465
+ function is404(e) {
30466
+ return e instanceof Error && e.message.startsWith("IC 404 ");
30467
+ }
30468
+ function featureDisabled(feature, district, data = []) {
30469
+ return textContent({ warning: "FeatureDisabled", feature, district, data });
30470
+ }
30471
+ async function findStudent(client2, district, studentId) {
30472
+ const students = await client2.request(district, "/campus/api/portal/students");
30473
+ return students.find((s) => String(s.personID) === studentId) ?? null;
30474
+ }
30475
+ function studentNotFound(studentId) {
30476
+ return textContent({ error: "StudentNotFound", studentId });
30477
+ }
30478
+ function toArray(value) {
30479
+ if (value === null || value === void 0) return [];
30480
+ return Array.isArray(value) ? value : [value];
30481
+ }
30482
+
30456
30483
  // src/tools/districts.ts
30457
30484
  function registerDistrictTools(server2, client2) {
30458
30485
  server2.registerTool("ic_list_districts", {
@@ -30461,7 +30488,7 @@ function registerDistrictTools(server2, client2) {
30461
30488
  }, async () => {
30462
30489
  await client2.ensureDiscovery();
30463
30490
  const data = client2.listDistricts();
30464
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30491
+ return textContent(data);
30465
30492
  });
30466
30493
  }
30467
30494
 
@@ -30477,7 +30504,7 @@ function registerStudentTools(server2, client2) {
30477
30504
  }, async (rawArgs) => {
30478
30505
  const args = argsSchema.parse(rawArgs);
30479
30506
  const data = await client2.request(args.district, "/campus/api/portal/students");
30480
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30507
+ return textContent(data);
30481
30508
  });
30482
30509
  }
30483
30510
 
@@ -30497,7 +30524,7 @@ function registerScheduleTools(server2, client2) {
30497
30524
  const args = argsSchema2.parse(rawArgs);
30498
30525
  const params = new URLSearchParams({ personID: args.studentId });
30499
30526
  const data = await client2.request(args.district, `/campus/resources/portal/roster?${params}`);
30500
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30527
+ return textContent(data);
30501
30528
  });
30502
30529
  }
30503
30530
 
@@ -30505,28 +30532,37 @@ function registerScheduleTools(server2, client2) {
30505
30532
  var argsSchema3 = external_exports3.object({
30506
30533
  district: external_exports3.string(),
30507
30534
  studentId: external_exports3.string(),
30508
- courseId: external_exports3.string().optional(),
30509
- since: external_exports3.string().describe("YYYY-MM-DD").optional(),
30510
- until: external_exports3.string().describe("YYYY-MM-DD").optional(),
30511
- missingOnly: external_exports3.boolean().optional()
30535
+ courseId: external_exports3.string().describe("sectionID (optional, from ic_get_schedule). The endpoint supports server-side filtering by sectionID only.").optional(),
30536
+ since: external_exports3.string().describe("YYYY-MM-DD; filters dueDate >= since (client-side)").optional(),
30537
+ until: external_exports3.string().describe("YYYY-MM-DD; filters dueDate <= until (client-side)").optional(),
30538
+ missingOnly: external_exports3.boolean().describe("Only return assignments flagged missing by the teacher").optional()
30512
30539
  });
30513
30540
  function registerAssignmentTools(server2, client2) {
30514
30541
  server2.registerTool("ic_list_assignments", {
30515
- description: "List a student's assignments. Filterable by course and date range; missingOnly returns only un-submitted past-due work.",
30542
+ description: "List a student's assignments. The IC endpoint returns the full term history (~hundreds of items); date and missing filters are applied client-side. For a single course, pass courseId (the sectionID from ic_get_schedule).",
30516
30543
  annotations: { readOnlyHint: true },
30517
30544
  inputSchema: argsSchema3.shape
30518
30545
  }, async (rawArgs) => {
30519
30546
  const args = argsSchema3.parse(rawArgs);
30520
30547
  const params = new URLSearchParams({ personID: args.studentId });
30521
30548
  if (args.courseId) params.set("sectionID", args.courseId);
30522
- if (args.since) params.set("startDate", args.since);
30523
- if (args.until) params.set("endDate", args.until);
30524
30549
  const raw = await client2.request(
30525
30550
  args.district,
30526
30551
  `/campus/api/portal/assignment/listView?${params}`
30527
30552
  );
30528
- const data = args.missingOnly ? raw.filter((a) => a.missing) : raw;
30529
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30553
+ let data = raw;
30554
+ if (args.since) {
30555
+ const since = args.since;
30556
+ data = data.filter((a) => typeof a.dueDate === "string" && a.dueDate >= since);
30557
+ }
30558
+ if (args.until) {
30559
+ const until = args.until;
30560
+ data = data.filter((a) => typeof a.dueDate === "string" && a.dueDate.substring(0, 10) <= until);
30561
+ }
30562
+ if (args.missingOnly) {
30563
+ data = data.filter((a) => a.missing);
30564
+ }
30565
+ return textContent(data);
30530
30566
  });
30531
30567
  }
30532
30568
 
@@ -30546,7 +30582,7 @@ function registerGradeTools(server2, client2) {
30546
30582
  const params = new URLSearchParams({ personID: args.studentId });
30547
30583
  if (args.termId) params.set("termID", args.termId);
30548
30584
  const data = await client2.request(args.district, `/campus/resources/portal/grades?${params}`);
30549
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30585
+ return textContent(data);
30550
30586
  });
30551
30587
  }
30552
30588
 
@@ -30557,24 +30593,64 @@ var argsSchema5 = external_exports3.object({
30557
30593
  since: external_exports3.string().describe("YYYY-MM-DD").optional(),
30558
30594
  until: external_exports3.string().describe("YYYY-MM-DD").optional()
30559
30595
  });
30596
+ function inRange(date5, since, until) {
30597
+ if (typeof date5 !== "string") return true;
30598
+ const d = date5.substring(0, 10);
30599
+ if (since && d < since) return false;
30600
+ if (until && d > until) return false;
30601
+ return true;
30602
+ }
30603
+ function trimSectionPlacement(sp) {
30604
+ const out = {};
30605
+ if (sp.periodName !== void 0) out.periodName = sp.periodName;
30606
+ if (sp.startTime !== void 0) out.startTime = sp.startTime;
30607
+ if (sp.endTime !== void 0) out.endTime = sp.endTime;
30608
+ return out;
30609
+ }
30610
+ function trimEntry(e) {
30611
+ if (e.sectionPlacements === void 0) return e;
30612
+ const sps = toArray(e.sectionPlacements);
30613
+ return { ...e, sectionPlacements: sps.map(trimSectionPlacement) };
30614
+ }
30615
+ function processList(list, since, until) {
30616
+ if (list === void 0) return list;
30617
+ return toArray(list).filter((e) => inRange(e.date, since, until)).map(trimEntry);
30618
+ }
30560
30619
  function registerAttendanceTools(server2, client2) {
30561
30620
  server2.registerTool("ic_list_attendance", {
30562
- description: "List a student's absences and tardies in a date range.",
30621
+ description: "List a student's absences and tardies (per-course summary grouped by term). Auto-resolves enrollmentID from the student record.",
30563
30622
  annotations: { readOnlyHint: true },
30564
30623
  inputSchema: argsSchema5.shape
30565
30624
  }, async (rawArgs) => {
30566
30625
  const args = argsSchema5.parse(rawArgs);
30567
- const params = new URLSearchParams({ personID: args.studentId });
30568
- if (args.since) params.set("startDate", args.since);
30569
- if (args.until) params.set("endDate", args.until);
30626
+ const student = await findStudent(client2, args.district, args.studentId);
30627
+ if (!student) return studentNotFound(args.studentId);
30628
+ const enrollments = student.enrollments ?? [];
30629
+ const results = [];
30570
30630
  try {
30571
- const data = await client2.request(args.district, `/campus/resources/portal/attendance?${params}`);
30572
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30573
- } catch (e) {
30574
- if (e instanceof Error && e.message.startsWith("IC 404 ")) {
30575
- const warn = { warning: "FeatureDisabled", feature: "attendance", district: args.district, data: [] };
30576
- return { content: [{ type: "text", text: JSON.stringify(warn, null, 2) }] };
30631
+ for (const enr of enrollments) {
30632
+ const data = await client2.request(
30633
+ args.district,
30634
+ `/campus/resources/portal/attendance/${enr.enrollmentID}?courseSummary=true&personID=${encodeURIComponent(args.studentId)}`
30635
+ );
30636
+ const entries = toArray(data);
30637
+ for (const entry of entries) {
30638
+ const trimmedTerms = toArray(entry.terms).map((t) => ({
30639
+ ...t,
30640
+ courses: toArray(t.courses).map((c) => ({
30641
+ ...c,
30642
+ absentList: processList(c.absentList, args.since, args.until),
30643
+ tardyList: processList(c.tardyList, args.since, args.until),
30644
+ presentList: processList(c.presentList, args.since, args.until),
30645
+ earlyReleaseList: processList(c.earlyReleaseList, args.since, args.until)
30646
+ }))
30647
+ }));
30648
+ results.push({ ...entry, terms: trimmedTerms });
30649
+ }
30577
30650
  }
30651
+ return textContent(results);
30652
+ } catch (e) {
30653
+ if (is404(e)) return featureDisabled("attendance", args.district);
30578
30654
  throw e;
30579
30655
  }
30580
30656
  });
@@ -30599,12 +30675,9 @@ function registerBehaviorTools(server2, client2) {
30599
30675
  if (args.until) params.set("endDate", args.until);
30600
30676
  try {
30601
30677
  const data = await client2.request(args.district, `/campus/resources/portal/behavior?${params}`);
30602
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30678
+ return textContent(data);
30603
30679
  } catch (e) {
30604
- if (e instanceof Error && e.message.startsWith("IC 404 ")) {
30605
- const warn = { warning: "FeatureDisabled", feature: "behavior", district: args.district, data: [] };
30606
- return { content: [{ type: "text", text: JSON.stringify(warn, null, 2) }] };
30607
- }
30680
+ if (is404(e)) return featureDisabled("behavior", args.district);
30608
30681
  throw e;
30609
30682
  }
30610
30683
  });
@@ -30629,12 +30702,9 @@ function registerFoodServiceTools(server2, client2) {
30629
30702
  if (args.until) params.set("endDate", args.until);
30630
30703
  try {
30631
30704
  const data = await client2.request(args.district, `/campus/resources/portal/foodService?${params}`);
30632
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30705
+ return textContent(data);
30633
30706
  } catch (e) {
30634
- if (e instanceof Error && e.message.startsWith("IC 404 ")) {
30635
- const warn = { warning: "FeatureDisabled", feature: "foodService", district: args.district, data: { balance: null, transactions: [] } };
30636
- return { content: [{ type: "text", text: JSON.stringify(warn, null, 2) }] };
30637
- }
30707
+ if (is404(e)) return featureDisabled("foodService", args.district, { balance: null, transactions: [] });
30638
30708
  throw e;
30639
30709
  }
30640
30710
  });
@@ -30643,40 +30713,111 @@ function registerFoodServiceTools(server2, client2) {
30643
30713
  // src/tools/messages.ts
30644
30714
  var listArgs = external_exports3.object({
30645
30715
  district: external_exports3.string(),
30646
- limit: external_exports3.number().int().positive().describe("Number of notifications to retrieve (default 20)").optional()
30647
- });
30648
- var countArgs = external_exports3.object({
30649
- district: external_exports3.string()
30716
+ limit: external_exports3.number().int().positive().describe("Number of prism notifications to retrieve (default 20). Does not affect inbox or announcements.").optional()
30650
30717
  });
30651
30718
  var getArgs = external_exports3.object({
30652
30719
  district: external_exports3.string(),
30653
- messageId: external_exports3.string()
30654
- });
30720
+ messageUrl: external_exports3.string().describe("The `url` field from an inbox item returned by ic_list_messages (e.g. 'portal/messageView.xsl?x=...&messageID=...'). Accepts relative or /campus/-prefixed paths.")
30721
+ });
30722
+ var NOTIFICATION_KEEP = [
30723
+ "notificationID",
30724
+ "creationTimestamp",
30725
+ "read",
30726
+ "notificationText",
30727
+ "notificationTypeText",
30728
+ "displayedDate"
30729
+ ];
30730
+ var INBOX_KEEP = [
30731
+ "messageID",
30732
+ "date",
30733
+ "name",
30734
+ "sender",
30735
+ "messageType",
30736
+ "courseName",
30737
+ "studentName",
30738
+ "newMessage",
30739
+ "actionRequired",
30740
+ "dueDate",
30741
+ "url"
30742
+ ];
30743
+ function pick2(obj, keys) {
30744
+ const out = {};
30745
+ for (const k of keys) {
30746
+ if (k in obj) out[k] = obj[k];
30747
+ }
30748
+ return out;
30749
+ }
30750
+ function normalizeMessageUrl(input) {
30751
+ if (input.startsWith("/campus/")) return input;
30752
+ if (input.startsWith("/")) return `/campus${input}`;
30753
+ return `/campus/${input}`;
30754
+ }
30755
+ function parseMessageHtml(html, url2) {
30756
+ const titleMatch = html.match(/<title>([^<]*)<\/title>/i);
30757
+ let subject = titleMatch ? titleMatch[1].trim() : "";
30758
+ subject = subject.replace(/^Message\s*--\s*/i, "");
30759
+ let text = html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ");
30760
+ text = text.replace(/<[^>]+>/g, " ");
30761
+ text = text.replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
30762
+ text = text.replace(/\s+/g, " ").trim();
30763
+ const dateMatch = text.match(/Date:\s*(\d{1,2}\/\d{1,2}\/\d{2,4})/);
30764
+ const date5 = dateMatch ? dateMatch[1] : null;
30765
+ let body = text;
30766
+ if (dateMatch) {
30767
+ const idx = text.indexOf(dateMatch[0]);
30768
+ body = text.substring(idx + dateMatch[0].length).trim();
30769
+ }
30770
+ return { subject, date: date5, body, url: url2 };
30771
+ }
30655
30772
  function registerMessageTools(server2, client2) {
30656
30773
  server2.registerTool("ic_list_messages", {
30657
- description: "List portal notifications (district announcements, teacher messages, system alerts). Uses the IC prism notification system.",
30774
+ description: "List all parent-visible messages from three IC sources combined: (1) prism notifications (assignment alerts, grade postings, attendance alerts), (2) Messenger 2.0 inbox (teacher messages, district announcements with newMessage/actionRequired flags), and (3) portal userNotice announcements. Each section has its own count and items; if any source errors, that section contains an error field and the others still return normally. The `limit` arg caps the prism notifications only (the high-volume source). Note: listing inbox messages does not mark them as read in normal portal behavior, but some district configurations may update read-tracking; use ic_get_message for the full HTML body.",
30658
30775
  annotations: { readOnlyHint: true },
30659
30776
  inputSchema: listArgs.shape
30660
30777
  }, async (rawArgs) => {
30661
30778
  const args = listArgs.parse(rawArgs);
30662
30779
  const limit = args.limit ?? 20;
30663
- const data = await client2.request(
30780
+ const prismPromise = client2.request(
30664
30781
  args.district,
30665
30782
  `/campus/prism?x=notifications.Notification-retrieve&limitCount=${limit}`
30666
- );
30667
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30783
+ ).then((raw) => {
30784
+ const items = toArray(raw?.data?.NotificationList?.Notification);
30785
+ const trimmed = items.map((n) => pick2(n, NOTIFICATION_KEEP));
30786
+ return { count: trimmed.length, items: trimmed };
30787
+ }).catch((e) => {
30788
+ return { count: 0, items: [], error: e instanceof Error ? e.message : String(e) };
30789
+ });
30790
+ const inboxPromise = client2.request(
30791
+ args.district,
30792
+ "/campus/api/portal/process-message"
30793
+ ).then((raw) => {
30794
+ const items = toArray(raw).map((m) => pick2(m, INBOX_KEEP));
30795
+ return { count: items.length, items };
30796
+ }).catch((e) => {
30797
+ return { count: 0, items: [], error: e instanceof Error ? e.message : String(e) };
30798
+ });
30799
+ const noticePromise = client2.request(
30800
+ args.district,
30801
+ "/campus/resources/portal/userNotice"
30802
+ ).then((raw) => {
30803
+ const items = toArray(raw);
30804
+ return { count: items.length, items };
30805
+ }).catch((e) => {
30806
+ return { count: 0, items: [], error: e instanceof Error ? e.message : String(e) };
30807
+ });
30808
+ const [notifications, inbox, announcements] = await Promise.all([prismPromise, inboxPromise, noticePromise]);
30809
+ return textContent({ notifications, inbox, announcements });
30668
30810
  });
30669
30811
  server2.registerTool("ic_get_message", {
30670
- description: "Get unread notification/message count.",
30812
+ 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.",
30671
30813
  annotations: { readOnlyHint: true },
30672
- inputSchema: countArgs.shape
30814
+ inputSchema: getArgs.shape
30673
30815
  }, async (rawArgs) => {
30674
- const args = countArgs.parse(rawArgs);
30675
- const data = await client2.request(
30676
- args.district,
30677
- "/campus/prism?x=notifications.NotificationUser-countUnviewed"
30678
- );
30679
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30816
+ const args = getArgs.parse(rawArgs);
30817
+ const path = normalizeMessageUrl(args.messageUrl);
30818
+ const html = await client2.request(args.district, path, { responseType: "text" });
30819
+ const parsed = parseMessageHtml(html ?? "", path);
30820
+ return textContent(parsed);
30680
30821
  });
30681
30822
  }
30682
30823
 
@@ -30687,30 +30828,39 @@ var listArgs2 = external_exports3.object({
30687
30828
  });
30688
30829
  var downloadArgs = external_exports3.object({
30689
30830
  district: external_exports3.string(),
30690
- documentId: external_exports3.string().describe("The downloadUrl from ic_list_documents"),
30831
+ documentId: external_exports3.string().describe("The url field returned by ic_list_documents"),
30691
30832
  destinationPath: external_exports3.string().describe("Absolute path where the PDF should be written"),
30692
30833
  overwrite: external_exports3.boolean().optional()
30693
30834
  });
30694
30835
  function registerDocumentTools(server2, client2) {
30695
30836
  server2.registerTool("ic_list_documents", {
30696
- description: "List a student's available documents (report cards, transcripts, etc.). Returns metadata only \u2014 use ic_download_document to fetch the file. Returns FeatureDisabled if the district has the module turned off.",
30837
+ description: "List a student's available documents (report cards, transcripts, schedules). Returns metadata only \u2014 use ic_download_document to fetch the file. Returns FeatureDisabled if the district has the module turned off.",
30697
30838
  annotations: { readOnlyHint: true },
30698
30839
  inputSchema: listArgs2.shape
30699
30840
  }, async (rawArgs) => {
30700
30841
  const args = listArgs2.parse(rawArgs);
30701
30842
  try {
30702
- const data = await client2.request(args.district, `/campus/resources/portal/documents?personID=${encodeURIComponent(args.studentId)}`);
30703
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30843
+ const raw = await client2.request(
30844
+ args.district,
30845
+ `/campus/resources/portal/report/all?personID=${encodeURIComponent(args.studentId)}`
30846
+ );
30847
+ const trimmed = toArray(raw).map((d) => {
30848
+ const out = {};
30849
+ if (d.name !== void 0) out.name = d.name;
30850
+ if (d.type !== void 0) out.type = d.type;
30851
+ if (d.url !== void 0) out.url = d.url;
30852
+ if (d.moduleLabel !== void 0) out.moduleLabel = d.moduleLabel;
30853
+ if (d.endYear !== void 0) out.endYear = d.endYear;
30854
+ return out;
30855
+ });
30856
+ return textContent(trimmed);
30704
30857
  } catch (e) {
30705
- if (e instanceof Error && e.message.startsWith("IC 404 ")) {
30706
- const warn = { warning: "FeatureDisabled", feature: "documents", district: args.district, data: [] };
30707
- return { content: [{ type: "text", text: JSON.stringify(warn, null, 2) }] };
30708
- }
30858
+ if (is404(e)) return featureDisabled("documents", args.district);
30709
30859
  throw e;
30710
30860
  }
30711
30861
  });
30712
30862
  server2.registerTool("ic_download_document", {
30713
- description: "Download a student's document (PDF) to disk. documentId is the downloadUrl returned by ic_list_documents. Returns FeatureDisabled if the district has the module turned off.",
30863
+ description: "Download a student's document (PDF) to disk. documentId is the url field returned by ic_list_documents. Returns FeatureDisabled if the district has the module turned off.",
30714
30864
  annotations: { destructiveHint: true },
30715
30865
  inputSchema: downloadArgs.shape
30716
30866
  }, async (rawArgs) => {
@@ -30719,17 +30869,360 @@ function registerDocumentTools(server2, client2) {
30719
30869
  const meta3 = await client2.download(args.district, args.documentId, args.destinationPath, {
30720
30870
  overwrite: args.overwrite ?? false
30721
30871
  });
30722
- return { content: [{ type: "text", text: JSON.stringify(meta3, null, 2) }] };
30872
+ return textContent(meta3);
30723
30873
  } catch (e) {
30724
30874
  if (e instanceof Error && e.message.startsWith("IC download 404")) {
30725
- const warn = { warning: "FeatureDisabled", feature: "documents", district: args.district };
30726
- return { content: [{ type: "text", text: JSON.stringify(warn, null, 2) }] };
30875
+ return textContent({ warning: "FeatureDisabled", feature: "documents", district: args.district });
30876
+ }
30877
+ throw e;
30878
+ }
30879
+ });
30880
+ }
30881
+
30882
+ // src/tools/calendar.ts
30883
+ var argsSchema8 = external_exports3.object({
30884
+ district: external_exports3.string(),
30885
+ studentId: external_exports3.string().describe("Student personID from ic_list_students"),
30886
+ since: external_exports3.string().describe("YYYY-MM-DD; include only days on or after this date").optional(),
30887
+ until: external_exports3.string().describe("YYYY-MM-DD; include only days on or before this date").optional()
30888
+ });
30889
+ function registerCalendarTools(server2, client2) {
30890
+ server2.registerTool("ic_list_school_days", {
30891
+ description: "List a student's school days (instructional calendar) grouped by term. Returns one entry per enrollment, with term boundaries (Q1-Q4 start/end dates) and the school days inside each term \u2014 including comments like 'Teacher Workday' or 'Spring Break'. Use since/until to narrow the range.",
30892
+ annotations: { readOnlyHint: true },
30893
+ inputSchema: argsSchema8.shape
30894
+ }, async (rawArgs) => {
30895
+ const args = argsSchema8.parse(rawArgs);
30896
+ const student = await findStudent(client2, args.district, args.studentId);
30897
+ if (!student) return studentNotFound(args.studentId);
30898
+ const result = [];
30899
+ for (const enr of student.enrollments ?? []) {
30900
+ const [termsRaw, daysRaw] = await Promise.all([
30901
+ client2.request(args.district, `/campus/resources/term?structureID=${enr.structureID}`),
30902
+ client2.request(args.district, `/campus/resources/calendar/instructionalDay?calendarID=${enr.calendarID}`)
30903
+ ]);
30904
+ const terms = toArray(termsRaw);
30905
+ const days = toArray(daysRaw);
30906
+ const filteredDays = days.filter((d) => {
30907
+ if (args.since && d.date < args.since) return false;
30908
+ if (args.until && d.date > args.until) return false;
30909
+ return true;
30910
+ });
30911
+ const enrollmentTerms = terms.filter((t) => t.structureID === enr.structureID).sort((a, b) => a.seq - b.seq);
30912
+ const trimmedTerms = enrollmentTerms.map((t) => ({
30913
+ termID: t.termID,
30914
+ termName: t.termName,
30915
+ startDate: t.startDate,
30916
+ endDate: t.endDate,
30917
+ days: filteredDays.filter((d) => d.date >= t.startDate && d.date <= t.endDate).map((d) => {
30918
+ const out = { date: d.date, requiresAttendance: d.requiresAttendance };
30919
+ if (d.comments) out.comments = d.comments;
30920
+ return out;
30921
+ })
30922
+ })).filter((t) => t.days.length > 0 || !args.since && !args.until);
30923
+ result.push({
30924
+ enrollmentID: enr.enrollmentID,
30925
+ calendarID: enr.calendarID,
30926
+ structureID: enr.structureID,
30927
+ calendarName: enr.calendarName,
30928
+ terms: trimmedTerms
30929
+ });
30930
+ }
30931
+ return textContent(result);
30932
+ });
30933
+ }
30934
+
30935
+ // src/tools/attendance_events.ts
30936
+ var argsSchema9 = external_exports3.object({
30937
+ district: external_exports3.string(),
30938
+ studentId: external_exports3.string().describe("Student personID from ic_list_students"),
30939
+ since: external_exports3.string().describe("YYYY-MM-DD; include only events on or after this date").optional(),
30940
+ until: external_exports3.string().describe("YYYY-MM-DD; include only events on or before this date").optional(),
30941
+ excusedOnly: external_exports3.boolean().describe("Only include events with excuse=E").optional()
30942
+ });
30943
+ var EVENT_KEYS = [
30944
+ "attendanceID",
30945
+ "date",
30946
+ "localDate",
30947
+ "code",
30948
+ "description",
30949
+ "excuse",
30950
+ "excuseType",
30951
+ "comments",
30952
+ "termID",
30953
+ "status",
30954
+ "periodID",
30955
+ "modifiedDate",
30956
+ "wholeDayAbsence"
30957
+ ];
30958
+ var ENROLLMENT_KEYS = [
30959
+ "calendarID",
30960
+ "calendarName",
30961
+ "enrollmentID",
30962
+ "structureID",
30963
+ "schoolName",
30964
+ "crossSiteEnrollment",
30965
+ "endDate"
30966
+ ];
30967
+ function trimSectionPlacement2(sp) {
30968
+ const out = {};
30969
+ if (sp.periodName !== void 0) out.periodName = sp.periodName;
30970
+ if (sp.startTime !== void 0) out.startTime = sp.startTime;
30971
+ if (sp.endTime !== void 0) out.endTime = sp.endTime;
30972
+ return out;
30973
+ }
30974
+ function trimEvent(e) {
30975
+ const out = {};
30976
+ for (const key of EVENT_KEYS) {
30977
+ const v = e[key];
30978
+ if (v !== void 0) out[key] = v;
30979
+ }
30980
+ if (e.sectionPlacements !== void 0) {
30981
+ out.sectionPlacements = toArray(e.sectionPlacements).map(trimSectionPlacement2);
30982
+ }
30983
+ return out;
30984
+ }
30985
+ function registerAttendanceEventsTools(server2, client2) {
30986
+ server2.registerTool("ic_list_attendance_events", {
30987
+ description: "List individual attendance events (absences, tardies, early releases) for a student. Each event has a code, description, excuse reason, and optional human-readable comments. Auto-resolves enrollmentID from the student record. Use since/until to filter by date and excusedOnly to show only excused events.",
30988
+ annotations: { readOnlyHint: true },
30989
+ inputSchema: argsSchema9.shape
30990
+ }, async (rawArgs) => {
30991
+ const args = argsSchema9.parse(rawArgs);
30992
+ const student = await findStudent(client2, args.district, args.studentId);
30993
+ if (!student) return studentNotFound(args.studentId);
30994
+ const enrollments = student.enrollments ?? [];
30995
+ const results = [];
30996
+ try {
30997
+ for (const enr of enrollments) {
30998
+ const raw = await client2.request(
30999
+ args.district,
31000
+ `/campus/resources/portal/attendance/events?enrollmentID=${enr.enrollmentID}&personID=${encodeURIComponent(args.studentId)}`
31001
+ );
31002
+ const entries = toArray(raw);
31003
+ for (const entry of entries) {
31004
+ const trimmed = { events: [] };
31005
+ for (const key of ENROLLMENT_KEYS) {
31006
+ const v = entry[key];
31007
+ if (v !== void 0) trimmed[key] = v;
31008
+ }
31009
+ const events = toArray(entry.events).filter((e) => {
31010
+ const d = typeof e.localDate === "string" ? e.localDate.substring(0, 10) : void 0;
31011
+ if (args.since && (d === void 0 || d < args.since)) return false;
31012
+ if (args.until && (d === void 0 || d > args.until)) return false;
31013
+ if (args.excusedOnly && e.excuse !== "E") return false;
31014
+ return true;
31015
+ }).map(trimEvent);
31016
+ trimmed.events = events;
31017
+ results.push(trimmed);
31018
+ }
30727
31019
  }
31020
+ return textContent(results);
31021
+ } catch (e) {
31022
+ if (is404(e)) return featureDisabled("attendance_events", args.district);
30728
31023
  throw e;
30729
31024
  }
30730
31025
  });
30731
31026
  }
30732
31027
 
31028
+ // src/tools/recent_grades.ts
31029
+ var argsSchema10 = external_exports3.object({
31030
+ district: external_exports3.string(),
31031
+ studentId: external_exports3.string(),
31032
+ since: external_exports3.string().describe("YYYY-MM-DD; defaults to 14 days ago. Passed to the endpoint as an ISO timestamp (modifiedDate filter).").optional()
31033
+ });
31034
+ var KEYS = [
31035
+ "assignmentName",
31036
+ "courseName",
31037
+ "sectionID",
31038
+ "dueDate",
31039
+ "scoreModifiedDate",
31040
+ "score",
31041
+ "scorePoints",
31042
+ "scorePercentage",
31043
+ "totalPoints",
31044
+ "missing",
31045
+ "late",
31046
+ "turnedIn",
31047
+ "feedback",
31048
+ "comments"
31049
+ ];
31050
+ function defaultSinceDate(now) {
31051
+ const d = new Date(now);
31052
+ d.setUTCDate(d.getUTCDate() - 14);
31053
+ return d.toISOString().substring(0, 10);
31054
+ }
31055
+ function registerRecentGradesTools(server2, client2) {
31056
+ server2.registerTool("ic_list_recent_grades", {
31057
+ description: "List recently-graded assignments for a student. Server-side filtered by scoreModifiedDate. Pass since=YYYY-MM-DD to set the cutoff; defaults to 14 days ago.",
31058
+ annotations: { readOnlyHint: true },
31059
+ inputSchema: argsSchema10.shape
31060
+ }, async (rawArgs) => {
31061
+ const args = argsSchema10.parse(rawArgs);
31062
+ const sinceDate = args.since ?? defaultSinceDate(/* @__PURE__ */ new Date());
31063
+ const modifiedDate = `${sinceDate}T00:00:00`;
31064
+ const raw = await client2.request(
31065
+ args.district,
31066
+ `/campus/api/portal/assignment/recentlyScored?modifiedDate=${encodeURIComponent(modifiedDate)}&personID=${encodeURIComponent(args.studentId)}`
31067
+ );
31068
+ const trimmed = (raw ?? []).map((g) => {
31069
+ const out = {};
31070
+ for (const key of KEYS) {
31071
+ const v = g[key];
31072
+ if (v !== void 0) out[key] = v;
31073
+ }
31074
+ return out;
31075
+ });
31076
+ return textContent(trimmed);
31077
+ });
31078
+ }
31079
+
31080
+ // src/tools/teachers.ts
31081
+ var argsSchema11 = external_exports3.object({
31082
+ district: external_exports3.string(),
31083
+ studentId: external_exports3.string()
31084
+ });
31085
+ var DROP_KEYS = /* @__PURE__ */ new Set([
31086
+ "_id",
31087
+ "_model",
31088
+ "_hashCode",
31089
+ "mTime",
31090
+ "action",
31091
+ "personID",
31092
+ "isKentucky",
31093
+ "pairID",
31094
+ "pairedEvent"
31095
+ ]);
31096
+ function trimRecord(raw) {
31097
+ const out = {};
31098
+ for (const [k, v] of Object.entries(raw)) {
31099
+ if (DROP_KEYS.has(k)) continue;
31100
+ if (v === void 0) continue;
31101
+ out[k] = v;
31102
+ }
31103
+ return out;
31104
+ }
31105
+ function registerTeacherTools(server2, client2) {
31106
+ server2.registerTool("ic_list_teachers", {
31107
+ description: "List a student's teachers (per enrolled section) and assigned counselor(s). Combines two endpoints (section/contacts and studentCounselor/byUser). Response field shapes may vary slightly by district \u2014 core fields (firstName, lastName, email) are consistent; additional fields are passed through.",
31108
+ annotations: { readOnlyHint: true },
31109
+ inputSchema: argsSchema11.shape
31110
+ }, async (rawArgs) => {
31111
+ const args = argsSchema11.parse(rawArgs);
31112
+ const personID = encodeURIComponent(args.studentId);
31113
+ const teachersPromise = client2.request(
31114
+ args.district,
31115
+ `/campus/resources/portal/section/contacts?personID=${personID}`
31116
+ ).catch((e) => {
31117
+ if (is404(e)) return null;
31118
+ throw e;
31119
+ });
31120
+ const counselorsPromise = client2.request(
31121
+ args.district,
31122
+ `/campus/resources/portal/studentCounselor/byUser?personID=${personID}`
31123
+ ).catch((e) => {
31124
+ if (is404(e)) return null;
31125
+ throw e;
31126
+ });
31127
+ const [teachersRaw, counselorsRaw] = await Promise.all([teachersPromise, counselorsPromise]);
31128
+ const teachers = toArray(teachersRaw).map((t) => trimRecord(t));
31129
+ const counselors = toArray(counselorsRaw).map((c) => trimRecord(c));
31130
+ return textContent({ counselors, teachers });
31131
+ });
31132
+ }
31133
+
31134
+ // src/tools/assessments.ts
31135
+ var argsSchema12 = external_exports3.object({
31136
+ district: external_exports3.string(),
31137
+ studentId: external_exports3.string().describe("Student personID from ic_list_students")
31138
+ });
31139
+ function registerAssessmentTools(server2, client2) {
31140
+ server2.registerTool("ic_list_assessments", {
31141
+ description: "List a student's standardized test scores (state, national, district tests). Auto-resolves calendarID from each of the student's enrollments and returns one entry per enrollment. The shape of individual test records varies by district and test type \u2014 fields are passed through unchanged.",
31142
+ annotations: { readOnlyHint: true },
31143
+ inputSchema: argsSchema12.shape
31144
+ }, async (rawArgs) => {
31145
+ const args = argsSchema12.parse(rawArgs);
31146
+ const student = await findStudent(client2, args.district, args.studentId);
31147
+ if (!student) return studentNotFound(args.studentId);
31148
+ const personIDEnc = encodeURIComponent(args.studentId);
31149
+ const result = [];
31150
+ let feature404 = false;
31151
+ for (const enr of student.enrollments ?? []) {
31152
+ try {
31153
+ const raw = await client2.request(
31154
+ args.district,
31155
+ `/campus/resources/prism/portal/assessments?personID=${personIDEnc}&calendarID=${enr.calendarID}`
31156
+ );
31157
+ result.push({
31158
+ enrollmentID: enr.enrollmentID,
31159
+ calendarID: enr.calendarID,
31160
+ calendarName: enr.calendarName,
31161
+ stateTests: toArray(raw.stateTests),
31162
+ nationalTests: toArray(raw.nationalTests),
31163
+ districtTests: {
31164
+ tests: toArray(raw.districtTests?.tests),
31165
+ typeTests: toArray(raw.districtTests?.typeTests)
31166
+ }
31167
+ });
31168
+ } catch (e) {
31169
+ if (is404(e)) {
31170
+ feature404 = true;
31171
+ continue;
31172
+ }
31173
+ throw e;
31174
+ }
31175
+ }
31176
+ if (feature404 && result.length === 0) {
31177
+ return featureDisabled("assessments", args.district);
31178
+ }
31179
+ return textContent(result);
31180
+ });
31181
+ }
31182
+
31183
+ // src/tools/fees.ts
31184
+ var argsSchema13 = external_exports3.object({
31185
+ district: external_exports3.string(),
31186
+ studentId: external_exports3.string().describe("Student personID from ic_list_students")
31187
+ });
31188
+ function registerFeeTools(server2, client2) {
31189
+ 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 a note.",
31191
+ annotations: { readOnlyHint: true },
31192
+ inputSchema: argsSchema13.shape
31193
+ }, async (rawArgs) => {
31194
+ const args = argsSchema13.parse(rawArgs);
31195
+ const personIDEnc = encodeURIComponent(args.studentId);
31196
+ const assignmentsPromise = client2.request(
31197
+ args.district,
31198
+ `/campus/api/portal/fees/feeAssignments?personID=${personIDEnc}`
31199
+ ).then((v) => ({ ok: true, value: v })).catch((e) => {
31200
+ if (is404(e)) return { ok: false, status: 404 };
31201
+ throw e;
31202
+ });
31203
+ const surplusPromise = client2.request(
31204
+ args.district,
31205
+ `/campus/api/portal/fees/feeTransactionDetail/totalSurplus/-1?personID=${personIDEnc}`
31206
+ ).then((v) => ({ ok: true, value: v })).catch((e) => {
31207
+ if (is404(e)) return { ok: false, status: 404 };
31208
+ throw e;
31209
+ });
31210
+ const [assignments, surplus] = await Promise.all([assignmentsPromise, surplusPromise]);
31211
+ if (!assignments.ok && !surplus.ok) {
31212
+ return featureDisabled("fees", args.district, { totalSurplus: null, feeAssignments: [] });
31213
+ }
31214
+ const response = {
31215
+ totalSurplus: surplus.ok ? surplus.value : null,
31216
+ feeAssignments: assignments.ok ? assignments.value : []
31217
+ };
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
+ return textContent(response);
31223
+ });
31224
+ }
31225
+
30733
31226
  // src/index.ts
30734
31227
  try {
30735
31228
  const { config: config2 } = await import("dotenv");
@@ -30739,7 +31232,7 @@ try {
30739
31232
  }
30740
31233
  var account = loadAccount();
30741
31234
  var client = new ICClient(account);
30742
- var server = new McpServer({ name: "infinitecampus", version: "0.1.2" });
31235
+ var server = new McpServer({ name: "infinitecampus", version: "2.0.0" });
30743
31236
  registerDistrictTools(server, client);
30744
31237
  registerStudentTools(server, client);
30745
31238
  registerScheduleTools(server, client);
@@ -30750,6 +31243,12 @@ registerBehaviorTools(server, client);
30750
31243
  registerFoodServiceTools(server, client);
30751
31244
  registerMessageTools(server, client);
30752
31245
  registerDocumentTools(server, client);
31246
+ registerCalendarTools(server, client);
31247
+ registerAttendanceEventsTools(server, client);
31248
+ registerRecentGradesTools(server, client);
31249
+ registerTeacherTools(server, client);
31250
+ registerAssessmentTools(server, client);
31251
+ registerFeeTools(server, client);
30753
31252
  console.error(`[infinitecampus-mcp] District: ${account.name} (${account.baseUrl})`);
30754
31253
  console.error("[infinitecampus-mcp] Developed and maintained by AI (Claude). Use at your own discretion.");
30755
31254
  var transport = new StdioServerTransport();