infinitecampus-mcp 0.1.1 → 0.1.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 CHANGED
@@ -15,9 +15,9 @@ MCP server for Infinite Campus (Campus Parent portal). Single-account config —
15
15
  | Behavior | `ic_list_behavior` |
16
16
  | Food service | `ic_list_food_service` |
17
17
  | Documents | `ic_list_documents`, `ic_download_document` |
18
- | Messages | `ic_list_messages`, `ic_get_message`, `ic_list_message_recipients`, `ic_send_message` |
18
+ | Notifications | `ic_list_messages` (prism notifications), `ic_get_message` (unread count) |
19
19
 
20
- Tools that the harness will gate as write/IO operations: `ic_send_message`, `ic_download_document`.
20
+ Tools that the harness will gate as write/IO operations: `ic_download_document`.
21
21
 
22
22
  ## Configuration
23
23
 
package/dist/bundle.js CHANGED
@@ -30155,8 +30155,13 @@ var ICClient = class {
30155
30155
  sessions = /* @__PURE__ */ new Map();
30156
30156
  linkedTo = /* @__PURE__ */ new Map();
30157
30157
  // linkedDistrictName → primaryDistrictName
30158
+ primaryName;
30158
30159
  constructor(account2) {
30159
30160
  this.accounts.set(account2.name, account2);
30161
+ this.primaryName = account2.name;
30162
+ }
30163
+ async ensureDiscovery() {
30164
+ await this.ensureSession(this.accounts.get(this.primaryName));
30160
30165
  }
30161
30166
  listDistricts() {
30162
30167
  return [...this.accounts.values()].map((a) => ({
@@ -30454,6 +30459,7 @@ function registerDistrictTools(server2, client2) {
30454
30459
  description: "List Infinite Campus districts configured for this MCP server. Returns names + base URLs (no credentials).",
30455
30460
  annotations: { readOnlyHint: true }
30456
30461
  }, async () => {
30462
+ await client2.ensureDiscovery();
30457
30463
  const data = client2.listDistricts();
30458
30464
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30459
30465
  });
@@ -30687,24 +30693,40 @@ var downloadArgs = external_exports3.object({
30687
30693
  });
30688
30694
  function registerDocumentTools(server2, client2) {
30689
30695
  server2.registerTool("ic_list_documents", {
30690
- description: "List a student's available documents (report cards, transcripts, etc.). Returns metadata only \u2014 use ic_download_document to fetch the file.",
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.",
30691
30697
  annotations: { readOnlyHint: true },
30692
30698
  inputSchema: listArgs2.shape
30693
30699
  }, async (rawArgs) => {
30694
30700
  const args = listArgs2.parse(rawArgs);
30695
- const data = await client2.request(args.district, `/campus/resources/portal/documents?personID=${encodeURIComponent(args.studentId)}`);
30696
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30701
+ 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) }] };
30704
+ } 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
+ }
30709
+ throw e;
30710
+ }
30697
30711
  });
30698
30712
  server2.registerTool("ic_download_document", {
30699
- description: "Download a student's document (PDF) to disk. documentId is the downloadUrl returned by ic_list_documents.",
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.",
30700
30714
  annotations: { destructiveHint: true },
30701
30715
  inputSchema: downloadArgs.shape
30702
30716
  }, async (rawArgs) => {
30703
30717
  const args = downloadArgs.parse(rawArgs);
30704
- const meta3 = await client2.download(args.district, args.documentId, args.destinationPath, {
30705
- overwrite: args.overwrite ?? false
30706
- });
30707
- return { content: [{ type: "text", text: JSON.stringify(meta3, null, 2) }] };
30718
+ try {
30719
+ const meta3 = await client2.download(args.district, args.documentId, args.destinationPath, {
30720
+ overwrite: args.overwrite ?? false
30721
+ });
30722
+ return { content: [{ type: "text", text: JSON.stringify(meta3, null, 2) }] };
30723
+ } catch (e) {
30724
+ 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) }] };
30727
+ }
30728
+ throw e;
30729
+ }
30708
30730
  });
30709
30731
  }
30710
30732
 
@@ -30717,7 +30739,7 @@ try {
30717
30739
  }
30718
30740
  var account = loadAccount();
30719
30741
  var client = new ICClient(account);
30720
- var server = new McpServer({ name: "infinitecampus", version: "0.1.1" });
30742
+ var server = new McpServer({ name: "infinitecampus", version: "0.1.2" });
30721
30743
  registerDistrictTools(server, client);
30722
30744
  registerStudentTools(server, client);
30723
30745
  registerScheduleTools(server, client);
package/dist/client.js CHANGED
@@ -5,8 +5,14 @@ export class ICClient {
5
5
  accounts = new Map();
6
6
  sessions = new Map();
7
7
  linkedTo = new Map(); // linkedDistrictName → primaryDistrictName
8
+ primaryName;
8
9
  constructor(account) {
9
10
  this.accounts.set(account.name, account);
11
+ this.primaryName = account.name;
12
+ }
13
+ async ensureDiscovery() {
14
+ // Ensure primary account is logged in, which triggers CUPS linked-district discovery
15
+ await this.ensureSession(this.accounts.get(this.primaryName));
10
16
  }
11
17
  listDistricts() {
12
18
  return [...this.accounts.values()].map((a) => ({
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import { registerMessageTools } from './tools/messages.js';
27
27
  import { registerDocumentTools } from './tools/documents.js';
28
28
  const account = loadAccount();
29
29
  const client = new ICClient(account);
30
- const server = new McpServer({ name: 'infinitecampus', version: '0.1.1' });
30
+ const server = new McpServer({ name: 'infinitecampus', version: '0.1.2' });
31
31
  registerDistrictTools(server, client);
32
32
  registerStudentTools(server, client);
33
33
  registerScheduleTools(server, client);
@@ -3,6 +3,7 @@ export function registerDistrictTools(server, client) {
3
3
  description: 'List Infinite Campus districts configured for this MCP server. Returns names + base URLs (no credentials).',
4
4
  annotations: { readOnlyHint: true },
5
5
  }, async () => {
6
+ await client.ensureDiscovery();
6
7
  const data = client.listDistricts();
7
8
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
8
9
  });
@@ -11,23 +11,41 @@ const downloadArgs = z.object({
11
11
  });
12
12
  export function registerDocumentTools(server, client) {
13
13
  server.registerTool('ic_list_documents', {
14
- description: "List a student's available documents (report cards, transcripts, etc.). Returns metadata only — use ic_download_document to fetch the file.",
14
+ description: "List a student's available documents (report cards, transcripts, etc.). Returns metadata only — use ic_download_document to fetch the file. Returns FeatureDisabled if the district has the module turned off.",
15
15
  annotations: { readOnlyHint: true },
16
16
  inputSchema: listArgs.shape,
17
17
  }, async (rawArgs) => {
18
18
  const args = listArgs.parse(rawArgs);
19
- const data = await client.request(args.district, `/campus/resources/portal/documents?personID=${encodeURIComponent(args.studentId)}`);
20
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
19
+ try {
20
+ const data = await client.request(args.district, `/campus/resources/portal/documents?personID=${encodeURIComponent(args.studentId)}`);
21
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
22
+ }
23
+ catch (e) {
24
+ if (e instanceof Error && e.message.startsWith('IC 404 ')) {
25
+ const warn = { warning: 'FeatureDisabled', feature: 'documents', district: args.district, data: [] };
26
+ return { content: [{ type: 'text', text: JSON.stringify(warn, null, 2) }] };
27
+ }
28
+ throw e;
29
+ }
21
30
  });
22
31
  server.registerTool('ic_download_document', {
23
- description: "Download a student's document (PDF) to disk. documentId is the downloadUrl returned by ic_list_documents.",
32
+ 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.",
24
33
  annotations: { destructiveHint: true },
25
34
  inputSchema: downloadArgs.shape,
26
35
  }, async (rawArgs) => {
27
36
  const args = downloadArgs.parse(rawArgs);
28
- const meta = await client.download(args.district, args.documentId, args.destinationPath, {
29
- overwrite: args.overwrite ?? false,
30
- });
31
- return { content: [{ type: 'text', text: JSON.stringify(meta, null, 2) }] };
37
+ try {
38
+ const meta = await client.download(args.district, args.documentId, args.destinationPath, {
39
+ overwrite: args.overwrite ?? false,
40
+ });
41
+ return { content: [{ type: 'text', text: JSON.stringify(meta, null, 2) }] };
42
+ }
43
+ catch (e) {
44
+ if (e instanceof Error && e.message.startsWith('IC download 404')) {
45
+ const warn = { warning: 'FeatureDisabled', feature: 'documents', district: args.district };
46
+ return { content: [{ type: 'text', text: JSON.stringify(warn, null, 2) }] };
47
+ }
48
+ throw e;
49
+ }
32
50
  });
33
51
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinitecampus-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.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": {
@@ -114,16 +114,14 @@ Every tool except `ic_list_districts` takes `district` as its first arg (the dis
114
114
  ### Documents
115
115
  | Tool | Notes |
116
116
  |------|-------|
117
- | `ic_list_documents(district, studentId)` | Metadata only (report cards, transcripts, etc.). |
118
- | `ic_download_document(district, studentId, documentId, destinationPath)` | Writes the PDF to `destinationPath` on disk. **`destinationPath` is required** — confirm the path with the user before calling. |
117
+ | `ic_list_documents(district, studentId)` | Metadata only (report cards, transcripts, etc.). Returns `FeatureDisabled` if the district has the documents module turned off. |
118
+ | `ic_download_document(district, documentId, destinationPath)` | Writes the PDF to `destinationPath` on disk. **`destinationPath` is required** — confirm the path with the user before calling. Returns `FeatureDisabled` if unavailable. |
119
119
 
120
- ### Messages
120
+ ### Notifications
121
121
  | Tool | Notes |
122
122
  |------|-------|
123
- | `ic_list_messages(district, folder?, page?, size?)` | `folder` is `inbox` or `sent`. Paginated. |
124
- | `ic_get_message(district, messageId)` | Full message body. |
125
- | `ic_list_message_recipients(district)` | Valid recipient IDs (teachers, staff) for this district. Call before `ic_send_message` to validate recipients. |
126
- | `ic_send_message(district, subject, body, recipientIds[])` | **Sends a real message through the portal.** Recipients must come from `ic_list_message_recipients` — made-up IDs will fail. |
123
+ | `ic_list_messages(district, limit?)` | Portal notifications (district announcements, teacher messages, system alerts) via prism notification system. |
124
+ | `ic_get_message(district)` | Unread notification/message count. |
127
125
 
128
126
  ## Workflows
129
127
 
@@ -139,11 +137,6 @@ Every tool except `ic_list_districts` takes `district` as its first arg (the dis
139
137
  **Today's schedule:**
140
138
  - `ic_get_schedule(district, studentId)` — returns today's classes by default
141
139
 
142
- **Email a teacher:**
143
- 1. `ic_list_message_recipients(district)` → find the teacher's recipient ID
144
- 2. Draft subject/body with the user and confirm
145
- 3. `ic_send_message(district, subject, body, [teacherRecipientId])`
146
-
147
140
  **Get the report card:**
148
141
  1. `ic_list_documents(district, studentId)` → find the report card's `documentId`
149
142
  2. Confirm destination path with the user
@@ -151,6 +144,5 @@ Every tool except `ic_list_districts` takes `district` as its first arg (the dis
151
144
 
152
145
  ## Caution
153
146
 
154
- - `ic_send_message` actually sends a portal message to teachers/staff — always confirm subject, body, and recipients with the user before calling.
155
147
  - `ic_download_document` writes a PDF to disk at `destinationPath` — confirm the path with the user; overwrites silently.
156
- - Endpoint behavior varies by district. If `ic_list_behavior` or `ic_list_food_service` returns a `FeatureDisabled` warning, that module is simply turned off for the district — it's not an error.
148
+ - Endpoint behavior varies by district. If `ic_list_behavior`, `ic_list_food_service`, `ic_list_documents`, or `ic_download_document` returns a `FeatureDisabled` warning, that module is simply turned off for the district — it's not an error.