{"id":"4f6cfe4a-468c-482b-9e25-b9d3070281bb","entityType":"agent","slug":"clawhub-chrischall-infinitecampus-mcp","name":"Tmp.ChIoCYDxiX","canonicalUrl":"https://www.xpersona.co/agent/clawhub-chrischall-infinitecampus-mcp","canonicalPath":"/agent/clawhub-chrischall-infinitecampus-mcp","generatedAt":"2026-10-10T14:49:27.332Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-10T11:55:54.457Z","emptyReason":null},"description":"This skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s). Triggers on phrases like \"check grades\", \"what'... Skill: Tmp.ChIoCYDxiX Owner: chrischall Summary: This skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s). Triggers on phrases like \"check grades\", \"what'... Tags: latest:2.3.6 Version history: v2.3.6 | 2026-07-07T23:57:08.914Z | auto Version 2.3.6 of infinitecampus-mcp - No file changes detected in this release. - No new features, fixes, or documentation updates. v","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.4K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:infinitecampus-mcp","sourceUrl":"https://clawhub.ai/chrischall/infinitecampus-mcp","homepage":"https://clawhub.ai/chrischall/skills/infinitecampus-mcp","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/chrischall/infinitecampus-mcp","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/chrischall/skills/infinitecampus-mcp","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":63,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"This skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s). Triggers on phrases like \"check grades\", \"what'..."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T11:55:54.457Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T11:55:54.457Z","emptyReason":null},"stars":null,"forks":null,"downloads":1449,"packageName":null,"latestVersion":"2.3.6","tractionLabel":"1.4K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T11:55:54.443Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T11:55:54.457Z","lastCrawledAt":"2026-10-10T11:55:54.443Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T11:55:54.443Z","lastVerifiedAt":null,"highlights":[{"version":"2.3.6","createdAt":"2026-07-07T23:57:08.914Z","changelog":"Version 2.3.6 of infinitecampus-mcp - No file changes detected in this release. - No new features, fixes, or documentation updates.","fileCount":3,"zipByteSize":3060},{"version":"2.3.5","createdAt":"2026-07-05T22:38:16.143Z","changelog":"- Removed the file skill-card.md. - No other changes to features or functionality.","fileCount":3,"zipByteSize":3131},{"version":"2.3.4","createdAt":"2026-06-13T00:54:43.637Z","changelog":"- Removed the file skill-card.md. - No functional changes to the skill itself.","fileCount":3,"zipByteSize":3074},{"version":"2.3.3","createdAt":"2026-06-10T02:10:13.517Z","changelog":"- Removed the skill-card.md documentation file. - No functional or user-facing changes; only internal documentation cleanup.","fileCount":3,"zipByteSize":3040},{"version":"2.3.2","createdAt":"2026-06-04T04:29:37.863Z","changelog":"- Removed the file skill-card.md. - No user-facing changes to features or functionality.","fileCount":3,"zipByteSize":3004},{"version":"2.3.1","createdAt":"2026-05-29T21:35:59.423Z","changelog":"- Removed the file skill-card.md. - No other changes to documentation or functionality.","fileCount":3,"zipByteSize":3047},{"version":"2.3.0","createdAt":"2026-05-28T01:30:18.899Z","changelog":"- Removed the file skill-card.md. - No changes to features or functionality.","fileCount":3,"zipByteSize":3084},{"version":"2.2.3","createdAt":"2026-05-26T17:09:49.884Z","changelog":"Version 2.2.3 of infinitecampus-mcp - No file changes detected; this is a metadata or documentation-only release. - No impact to functionality or features.","fileCount":3,"zipByteSize":2989}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:infinitecampus-mcp","setupComplexity":"low","setupSteps":["Setup complexity is classified as HIGH. You must provision dedicated cloud infrastructure or an isolated VM. Do not run this directly on your local workstation.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-infinitecampus-mcp/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-infinitecampus-mcp/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-infinitecampus-mcp/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-infinitecampus-mcp/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-infinitecampus-mcp/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-infinitecampus-mcp/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T14:49:27.330Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-infinitecampus-mcp/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-infinitecampus-mcp/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-infinitecampus-mcp/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-infinitecampus-mcp/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-10-10T11:55:54.457Z","emptyReason":null},"readme":"Skill: Tmp.ChIoCYDxiX\n\nOwner: chrischall\n\nSummary: This skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s). Triggers on phrases like \"check grades\", \"what'...\n\nTags: latest:2.3.6\n\nVersion history:\n\nv2.3.6 | 2026-07-07T23:57:08.914Z | auto\n\nVersion 2.3.6 of infinitecampus-mcp\n\n- No file changes detected in this release.\n- No new features, fixes, or documentation updates.\n\nv2.3.5 | 2026-07-05T22:38:16.143Z | auto\n\n- Removed the file skill-card.md.  \n- No other changes to features or functionality.\n\nv2.3.4 | 2026-06-13T00:54:43.637Z | auto\n\n- Removed the file skill-card.md.\n- No functional changes to the skill itself.\n\nv2.3.3 | 2026-06-10T02:10:13.517Z | auto\n\n- Removed the skill-card.md documentation file.\n- No functional or user-facing changes; only internal documentation cleanup.\n\nv2.3.2 | 2026-06-04T04:29:37.863Z | auto\n\n- Removed the file skill-card.md.\n- No user-facing changes to features or functionality.\n\nv2.3.1 | 2026-05-29T21:35:59.423Z | auto\n\n- Removed the file skill-card.md.  \n- No other changes to documentation or functionality.\n\nv2.3.0 | 2026-05-28T01:30:18.899Z | auto\n\n- Removed the file skill-card.md.\n- No changes to features or functionality.\n\nv2.2.3 | 2026-05-26T17:09:49.884Z | auto\n\nVersion 2.2.3 of infinitecampus-mcp\n\n- No file changes detected; this is a metadata or documentation-only release.\n- No impact to functionality or features.\n\nv2.1.3 | 2026-05-21T20:11:57.376Z | auto\n\n- Major cleanup: all code, test, manifest, and documentation files removed; only SKILL.md remains.\n- SKILL.md updated with expanded authentication instructions, including fetchproxy fallback option and related env var details.\n- All implementation details, tools list, and usage notes now consolidated into SKILL.md.\n\nv2.1.1 | 2026-05-01T20:40:09.629Z | auto\n\ninfinitecampus-mcp 2.1.1\n\n- Expanded trigger descriptions to clarify when the skill should be used.\n- Updated documentation with more concise setup, usage instructions, and tool descriptions.\n- Added detailed instructions for both npx and source-based installation.\n- Enhanced usage guidance for authentication and tool functionality.\n\nArchive index:\n\nArchive v2.3.6: 3 files, 3060 bytes\n\nFiles: skill-card.md (2578b), SKILL.md (3264b), _meta.json (137b)\n\nFile v2.3.6:SKILL.md\n\n---\nname: infinitecampus-mcp\ndescription: This skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s). Triggers on phrases like \"check grades\", \"what's my kid's GPA\", \"any new assignments\", \"attendance record\", \"message the teacher\", \"Campus Parent inbox\", \"infinite campus\", or any request involving school grades, assignments, attendance, messages, or documents in Infinite Campus.\n---\n\n# infinitecampus-mcp\n\nMCP server for Infinite Campus Campus Parent — read student grades, attendance, assignments, and messages; download documents; and send messages.\n\n- **npm:** [npmjs.com/package/infinitecampus-mcp](https://www.npmjs.com/package/infinitecampus-mcp)\n- **Source:** [github.com/chrischall/infinitecampus-mcp](https://github.com/chrischall/infinitecampus-mcp)\n\n## Setup\n\n### Option A — npx (recommended)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"infinitecampus\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"infinitecampus-mcp\"],\n      \"env\": {\n        \"IC_BASE_URL\": \"https://campus.yourdistrict.k12.example.us\",\n        \"IC_DISTRICT\": \"your-district-appname\",\n        \"IC_USERNAME\": \"parent@example.com\",\n        \"IC_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — from source\n\n```bash\ngit clone https://github.com/chrischall/infinitecampus-mcp\ncd infinitecampus-mcp\nnpm install && npm run build\n```\n\n## Authentication\n\nTwo paths — the MCP tries them in priority order:\n\n1. **Username/password.** Set `IC_USERNAME` + `IC_PASSWORD` along with `IC_BASE_URL` + `IC_DISTRICT`.\n2. **fetchproxy fallback.** Install the [fetchproxy 0.3.0 extension](https://github.com/chrischall/fetchproxy), sign into your IC portal in the browser, and leave `IC_USERNAME` / `IC_PASSWORD` unset. `IC_BASE_URL` + `IC_DISTRICT` are still required so the MCP knows which host to read cookies from.\n\n`IC_BASE_URL` is your district's portal URL; `IC_DISTRICT` is the app-name path segment from that URL. Set `IC_DISABLE_FETCHPROXY=1` to opt out of the fallback (headless / CI).\n\n## Tools (prefix `ic_`)\n\n### Students & teachers\n- `ic_list_students` — list students linked to your parent account\n- `ic_list_teachers` — list teachers for a student\n\n### Academics\n- `ic_list_grades(studentId)` — class grades\n- `ic_list_recent_grades(studentId)` — recently graded items\n- `ic_list_assignments(studentId)` — current assignments\n- `ic_list_assessments(studentId)` — test scores\n\n### Attendance\n- `ic_list_attendance(studentId)` — attendance summary\n- `ic_list_attendance_events(studentId)` — individual absence/tardy events\n- `ic_list_school_days(studentId)` — calendar days\n\n### Behavior & fees\n- `ic_list_behavior(studentId)` — behavior incidents\n- `ic_list_fees(studentId)` — outstanding fees\n- `ic_list_food_service(studentId)` — cafeteria balance\n\n### Messaging\n- `ic_list_messages` — inbox\n- `ic_get_message(id)` — read a message\n- `ic_list_documents(studentId)` / `ic_download_document(id)` — documents\n\n## Notes\n\n- Set `IC_NAME` if you want a friendly name other than the district appname.\n- Auto-discovers the CUPS (Campus Unified Portal Services) layout from the base URL — no extra config needed for most districts.\n\nFile v2.3.6:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-mcp\",\n  \"version\": \"2.3.6\",\n  \"publishedAt\": 1783468628914\n}\n\nFile v2.3.6:skill-card.md\n\n## Description:\n\nProvides an MCP server for Infinite Campus Campus Parent to read student grades, attendance, assignments, messages, documents, and related account data, and to send messages.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nParents or guardians and their authorized agents use this skill to connect an MCP client to Infinite Campus Campus Parent for retrieving student academic, attendance, fee, food-service, document, and message information, and for sending messages through the portal.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill handles sensitive student records, messages, and documents.\n\nMitigation: Use it only with authorized Campus Parent accounts, confirm before reading messages or downloading documents, and avoid exposing retrieved records beyond the intended user.\n\nRisk: The setup recommends persistent school login credentials and unpinned npm execution.\n\nMitigation: Prefer a secret manager or runtime environment variables over project-level credential files, and pin reviewed package versions or commits before installation.\n\nRisk: Security evidence marks the release suspicious because it combines sensitive education data access with credential handling.\n\nMitigation: Review the npm package and source repository before installing, and use the skill only if the publisher and package are trusted.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/infinitecampus-mcp)\n- [npm package: infinitecampus-mcp](https://www.npmjs.com/package/infinitecampus-mcp)\n- [Source repository: infinitecampus-mcp](https://github.com/chrischall/infinitecampus-mcp)\n- [fetchproxy fallback extension](https://github.com/chrischall/fetchproxy)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, code, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown guidance with JSON configuration and shell command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May propose MCP configuration and commands; installed tools can access sensitive student records, messages, and documents.]\n\n## Skill Version(s):\n\n2.3.6 (source: server release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v2.3.5: 3 files, 3131 bytes\n\nFiles: skill-card.md (2770b), SKILL.md (3264b), _meta.json (137b)\n\nFile v2.3.5:SKILL.md\n\n---\nname: infinitecampus-mcp\ndescription: This skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s). Triggers on phrases like \"check grades\", \"what's my kid's GPA\", \"any new assignments\", \"attendance record\", \"message the teacher\", \"Campus Parent inbox\", \"infinite campus\", or any request involving school grades, assignments, attendance, messages, or documents in Infinite Campus.\n---\n\n# infinitecampus-mcp\n\nMCP server for Infinite Campus Campus Parent — read student grades, attendance, assignments, and messages; download documents; and send messages.\n\n- **npm:** [npmjs.com/package/infinitecampus-mcp](https://www.npmjs.com/package/infinitecampus-mcp)\n- **Source:** [github.com/chrischall/infinitecampus-mcp](https://github.com/chrischall/infinitecampus-mcp)\n\n## Setup\n\n### Option A — npx (recommended)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"infinitecampus\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"infinitecampus-mcp\"],\n      \"env\": {\n        \"IC_BASE_URL\": \"https://campus.yourdistrict.k12.example.us\",\n        \"IC_DISTRICT\": \"your-district-appname\",\n        \"IC_USERNAME\": \"parent@example.com\",\n        \"IC_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — from source\n\n```bash\ngit clone https://github.com/chrischall/infinitecampus-mcp\ncd infinitecampus-mcp\nnpm install && npm run build\n```\n\n## Authentication\n\nTwo paths — the MCP tries them in priority order:\n\n1. **Username/password.** Set `IC_USERNAME` + `IC_PASSWORD` along with `IC_BASE_URL` + `IC_DISTRICT`.\n2. **fetchproxy fallback.** Install the [fetchproxy 0.3.0 extension](https://github.com/chrischall/fetchproxy), sign into your IC portal in the browser, and leave `IC_USERNAME` / `IC_PASSWORD` unset. `IC_BASE_URL` + `IC_DISTRICT` are still required so the MCP knows which host to read cookies from.\n\n`IC_BASE_URL` is your district's portal URL; `IC_DISTRICT` is the app-name path segment from that URL. Set `IC_DISABLE_FETCHPROXY=1` to opt out of the fallback (headless / CI).\n\n## Tools (prefix `ic_`)\n\n### Students & teachers\n- `ic_list_students` — list students linked to your parent account\n- `ic_list_teachers` — list teachers for a student\n\n### Academics\n- `ic_list_grades(studentId)` — class grades\n- `ic_list_recent_grades(studentId)` — recently graded items\n- `ic_list_assignments(studentId)` — current assignments\n- `ic_list_assessments(studentId)` — test scores\n\n### Attendance\n- `ic_list_attendance(studentId)` — attendance summary\n- `ic_list_attendance_events(studentId)` — individual absence/tardy events\n- `ic_list_school_days(studentId)` — calendar days\n\n### Behavior & fees\n- `ic_list_behavior(studentId)` — behavior incidents\n- `ic_list_fees(studentId)` — outstanding fees\n- `ic_list_food_service(studentId)` — cafeteria balance\n\n### Messaging\n- `ic_list_messages` — inbox\n- `ic_get_message(id)` — read a message\n- `ic_list_documents(studentId)` / `ic_download_document(id)` — documents\n\n## Notes\n\n- Set `IC_NAME` if you want a friendly name other than the district appname.\n- Auto-discovers the CUPS (Campus Unified Portal Services) layout from the base URL — no extra config needed for most districts.\n\nFile v2.3.5:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-mcp\",\n  \"version\": \"2.3.5\",\n  \"publishedAt\": 1783291096143\n}\n\nFile v2.3.5:skill-card.md\n\n## Description: <br>\nThis skill connects an agent to Infinite Campus Campus Parent data so authorized parents can read student grades, attendance, assignments, messages, documents, fees, and related school records. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[chrischall](https://clawhub.ai/user/chrischall) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nExternal users with authorized Campus Parent access use this skill to let an agent retrieve and summarize student school information from Infinite Campus. It supports parent-facing workflows such as checking grades, attendance, assignments, school messages, behavior records, fees, cafeteria balances, and downloadable documents. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill can expose sensitive student records, including grades, attendance, behavior, fees, food service balances, documents, and messages. <br>\nMitigation: Use it only with an authorized Campus Parent account and avoid sending student records into shared LLM contexts unless there is a clear privacy basis. <br>\nRisk: The setup can rely on stored credentials or browser session cookies. <br>\nMitigation: Avoid storing passwords in shared or plaintext configuration where possible, and disable the fetchproxy cookie fallback when it is not needed. <br>\nRisk: The security verdict is suspicious because the skill handles sensitive student data with limited scoping and warning in the skill text. <br>\nMitigation: Review the skill carefully before installing and restrict use to the intended authorized account and district portal. <br>\n\n\n## Reference(s): <br>\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/infinitecampus-mcp) <br>\n- [infinitecampus-mcp npm package](https://www.npmjs.com/package/infinitecampus-mcp) <br>\n- [fetchproxy fallback extension](https://github.com/chrischall/fetchproxy) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [text, configuration, shell commands, API calls, guidance] <br>\n**Output Format:** [Markdown with JSON configuration examples, shell commands, and MCP tool responses] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Outputs may include sensitive student, school, message, document, fee, and authentication-related information from the user's authorized Campus Parent account.] <br>\n\n## Skill Version(s): <br>\n2.3.5 (source: server release metadata) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v2.3.4: 3 files, 3074 bytes\n\nFiles: skill-card.md (2704b), SKILL.md (3264b), _meta.json (137b)\n\nFile v2.3.4:SKILL.md\n\n---\nname: infinitecampus-mcp\ndescription: This skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s). Triggers on phrases like \"check grades\", \"what's my kid's GPA\", \"any new assignments\", \"attendance record\", \"message the teacher\", \"Campus Parent inbox\", \"infinite campus\", or any request involving school grades, assignments, attendance, messages, or documents in Infinite Campus.\n---\n\n# infinitecampus-mcp\n\nMCP server for Infinite Campus Campus Parent — read student grades, attendance, assignments, and messages; download documents; and send messages.\n\n- **npm:** [npmjs.com/package/infinitecampus-mcp](https://www.npmjs.com/package/infinitecampus-mcp)\n- **Source:** [github.com/chrischall/infinitecampus-mcp](https://github.com/chrischall/infinitecampus-mcp)\n\n## Setup\n\n### Option A — npx (recommended)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"infinitecampus\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"infinitecampus-mcp\"],\n      \"env\": {\n        \"IC_BASE_URL\": \"https://campus.yourdistrict.k12.example.us\",\n        \"IC_DISTRICT\": \"your-district-appname\",\n        \"IC_USERNAME\": \"parent@example.com\",\n        \"IC_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — from source\n\n```bash\ngit clone https://github.com/chrischall/infinitecampus-mcp\ncd infinitecampus-mcp\nnpm install && npm run build\n```\n\n## Authentication\n\nTwo paths — the MCP tries them in priority order:\n\n1. **Username/password.** Set `IC_USERNAME` + `IC_PASSWORD` along with `IC_BASE_URL` + `IC_DISTRICT`.\n2. **fetchproxy fallback.** Install the [fetchproxy 0.3.0 extension](https://github.com/chrischall/fetchproxy), sign into your IC portal in the browser, and leave `IC_USERNAME` / `IC_PASSWORD` unset. `IC_BASE_URL` + `IC_DISTRICT` are still required so the MCP knows which host to read cookies from.\n\n`IC_BASE_URL` is your district's portal URL; `IC_DISTRICT` is the app-name path segment from that URL. Set `IC_DISABLE_FETCHPROXY=1` to opt out of the fallback (headless / CI).\n\n## Tools (prefix `ic_`)\n\n### Students & teachers\n- `ic_list_students` — list students linked to your parent account\n- `ic_list_teachers` — list teachers for a student\n\n### Academics\n- `ic_list_grades(studentId)` — class grades\n- `ic_list_recent_grades(studentId)` — recently graded items\n- `ic_list_assignments(studentId)` — current assignments\n- `ic_list_assessments(studentId)` — test scores\n\n### Attendance\n- `ic_list_attendance(studentId)` — attendance summary\n- `ic_list_attendance_events(studentId)` — individual absence/tardy events\n- `ic_list_school_days(studentId)` — calendar days\n\n### Behavior & fees\n- `ic_list_behavior(studentId)` — behavior incidents\n- `ic_list_fees(studentId)` — outstanding fees\n- `ic_list_food_service(studentId)` — cafeteria balance\n\n### Messaging\n- `ic_list_messages` — inbox\n- `ic_get_message(id)` — read a message\n- `ic_list_documents(studentId)` / `ic_download_document(id)` — documents\n\n## Notes\n\n- Set `IC_NAME` if you want a friendly name other than the district appname.\n- Auto-discovers the CUPS (Campus Unified Portal Services) layout from the base URL — no extra config needed for most districts.\n\nFile v2.3.4:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-mcp\",\n  \"version\": \"2.3.4\",\n  \"publishedAt\": 1781312083637\n}\n\nFile v2.3.4:skill-card.md\n\n## Description: <br>\nThis skill helps agents use an Infinite Campus Campus Parent MCP server to access student grades, attendance, assignments, messages, documents, behavior, fees, and food service information. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[chrischall](https://clawhub.ai/user/chrischall) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nExternal users and developers use this skill to configure an MCP server that lets an agent answer questions about a user's own Infinite Campus Campus Parent records, including grades, assignments, attendance, messages, and documents. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill can expose sensitive student records from Infinite Campus through an MCP server. <br>\nMitigation: Install only for accounts and environments intended to access those records, treat outputs as confidential student information, and avoid shared LLM contexts. <br>\nRisk: The skill requires credentials or browser-cookie based access to a Campus Parent portal. <br>\nMitigation: Store credentials carefully, restrict access to the MCP configuration, and disable the fetchproxy fallback in headless or CI environments when browser cookie access is not intended. <br>\nRisk: Documentation mentions sending messages, while the security guidance says users should clarify whether the package actually supports message sending before account actions. <br>\nMitigation: Confirm supported account actions before enabling any write-like behavior beyond reading records and downloading documents. <br>\n\n\n## Reference(s): <br>\n- [ClawHub skill page](https://clawhub.ai/chrischall/infinitecampus-mcp) <br>\n- [npm package: infinitecampus-mcp](https://www.npmjs.com/package/infinitecampus-mcp) <br>\n- [Source repository: infinitecampus-mcp](https://github.com/chrischall/infinitecampus-mcp) <br>\n- [fetchproxy fallback extension](https://github.com/chrischall/fetchproxy) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [Guidance, Configuration, Shell commands] <br>\n**Output Format:** [Markdown with JSON and shell command examples] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Outputs may include sensitive student-record content returned through the configured MCP server.] <br>\n\n## Skill Version(s): <br>\n2.3.4 (source: server release metadata) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v2.3.3: 3 files, 3040 bytes\n\nFiles: skill-card.md (2581b), SKILL.md (3264b), _meta.json (137b)\n\nFile v2.3.3:SKILL.md\n\n---\nname: infinitecampus-mcp\ndescription: This skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s). Triggers on phrases like \"check grades\", \"what's my kid's GPA\", \"any new assignments\", \"attendance record\", \"message the teacher\", \"Campus Parent inbox\", \"infinite campus\", or any request involving school grades, assignments, attendance, messages, or documents in Infinite Campus.\n---\n\n# infinitecampus-mcp\n\nMCP server for Infinite Campus Campus Parent — read student grades, attendance, assignments, and messages; download documents; and send messages.\n\n- **npm:** [npmjs.com/package/infinitecampus-mcp](https://www.npmjs.com/package/infinitecampus-mcp)\n- **Source:** [github.com/chrischall/infinitecampus-mcp](https://github.com/chrischall/infinitecampus-mcp)\n\n## Setup\n\n### Option A — npx (recommended)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"infinitecampus\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"infinitecampus-mcp\"],\n      \"env\": {\n        \"IC_BASE_URL\": \"https://campus.yourdistrict.k12.example.us\",\n        \"IC_DISTRICT\": \"your-district-appname\",\n        \"IC_USERNAME\": \"parent@example.com\",\n        \"IC_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — from source\n\n```bash\ngit clone https://github.com/chrischall/infinitecampus-mcp\ncd infinitecampus-mcp\nnpm install && npm run build\n```\n\n## Authentication\n\nTwo paths — the MCP tries them in priority order:\n\n1. **Username/password.** Set `IC_USERNAME` + `IC_PASSWORD` along with `IC_BASE_URL` + `IC_DISTRICT`.\n2. **fetchproxy fallback.** Install the [fetchproxy 0.3.0 extension](https://github.com/chrischall/fetchproxy), sign into your IC portal in the browser, and leave `IC_USERNAME` / `IC_PASSWORD` unset. `IC_BASE_URL` + `IC_DISTRICT` are still required so the MCP knows which host to read cookies from.\n\n`IC_BASE_URL` is your district's portal URL; `IC_DISTRICT` is the app-name path segment from that URL. Set `IC_DISABLE_FETCHPROXY=1` to opt out of the fallback (headless / CI).\n\n## Tools (prefix `ic_`)\n\n### Students & teachers\n- `ic_list_students` — list students linked to your parent account\n- `ic_list_teachers` — list teachers for a student\n\n### Academics\n- `ic_list_grades(studentId)` — class grades\n- `ic_list_recent_grades(studentId)` — recently graded items\n- `ic_list_assignments(studentId)` — current assignments\n- `ic_list_assessments(studentId)` — test scores\n\n### Attendance\n- `ic_list_attendance(studentId)` — attendance summary\n- `ic_list_attendance_events(studentId)` — individual absence/tardy events\n- `ic_list_school_days(studentId)` — calendar days\n\n### Behavior & fees\n- `ic_list_behavior(studentId)` — behavior incidents\n- `ic_list_fees(studentId)` — outstanding fees\n- `ic_list_food_service(studentId)` — cafeteria balance\n\n### Messaging\n- `ic_list_messages` — inbox\n- `ic_get_message(id)` — read a message\n- `ic_list_documents(studentId)` / `ic_download_document(id)` — documents\n\n## Notes\n\n- Set `IC_NAME` if you want a friendly name other than the district appname.\n- Auto-discovers the CUPS (Campus Unified Portal Services) layout from the base URL — no extra config needed for most districts.\n\nFile v2.3.3:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-mcp\",\n  \"version\": \"2.3.3\",\n  \"publishedAt\": 1781057413517\n}\n\nFile v2.3.3:skill-card.md\n\n## Description: <br>\nMCP server for Infinite Campus Campus Parent that helps agents read student grades, attendance, assignments, messages, and documents, and send messages. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[chrischall](https://clawhub.ai/user/chrischall) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nExternal users and developers use this skill to connect an agent to an Infinite Campus Campus Parent account for student records, school messages, documents, and related account actions. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill can access sensitive Campus Parent student records, including grades, attendance, assignments, messages, fees, cafeteria balances, and documents. <br>\nMitigation: Install only when this access is expected, keep prompts scoped to explicit school-record tasks, and avoid sharing outputs in public or shared AI contexts. <br>\nRisk: The MCP setup may use stored Campus Parent credentials or an authenticated browser session through fetchproxy. <br>\nMitigation: Protect credentials and browser sessions, prefer the least persistent authentication path that fits the environment, and disable fetchproxy in headless or CI contexts when it is not intended. <br>\nRisk: District policy or Infinite Campus terms may limit this kind of automation. <br>\nMitigation: Confirm that account automation is allowed by the user's district and Infinite Campus terms before deployment. <br>\n\n\n## Reference(s): <br>\n- [ClawHub skill page](https://clawhub.ai/chrischall/infinitecampus-mcp) <br>\n- [npm package](https://www.npmjs.com/package/infinitecampus-mcp) <br>\n- [Project repository](https://github.com/chrischall/infinitecampus-mcp) <br>\n- [fetchproxy extension](https://github.com/chrischall/fetchproxy) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [Text, Markdown, Shell commands, Configuration, Guidance] <br>\n**Output Format:** [Markdown with JSON configuration and shell command examples] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [May configure MCP tools that access Campus Parent records and downloaded documents.] <br>\n\n## Skill Version(s): <br>\n2.3.3 (source: server-resolved release evidence) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v2.3.2: 3 files, 3004 bytes\n\nFiles: skill-card.md (2514b), SKILL.md (3264b), _meta.json (137b)\n\nFile v2.3.2:SKILL.md\n\n---\nname: infinitecampus-mcp\ndescription: This skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s). Triggers on phrases like \"check grades\", \"what's my kid's GPA\", \"any new assignments\", \"attendance record\", \"message the teacher\", \"Campus Parent inbox\", \"infinite campus\", or any request involving school grades, assignments, attendance, messages, or documents in Infinite Campus.\n---\n\n# infinitecampus-mcp\n\nMCP server for Infinite Campus Campus Parent — read student grades, attendance, assignments, and messages; download documents; and send messages.\n\n- **npm:** [npmjs.com/package/infinitecampus-mcp](https://www.npmjs.com/package/infinitecampus-mcp)\n- **Source:** [github.com/chrischall/infinitecampus-mcp](https://github.com/chrischall/infinitecampus-mcp)\n\n## Setup\n\n### Option A — npx (recommended)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"infinitecampus\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"infinitecampus-mcp\"],\n      \"env\": {\n        \"IC_BASE_URL\": \"https://campus.yourdistrict.k12.example.us\",\n        \"IC_DISTRICT\": \"your-district-appname\",\n        \"IC_USERNAME\": \"parent@example.com\",\n        \"IC_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — from source\n\n```bash\ngit clone https://github.com/chrischall/infinitecampus-mcp\ncd infinitecampus-mcp\nnpm install && npm run build\n```\n\n## Authentication\n\nTwo paths — the MCP tries them in priority order:\n\n1. **Username/password.** Set `IC_USERNAME` + `IC_PASSWORD` along with `IC_BASE_URL` + `IC_DISTRICT`.\n2. **fetchproxy fallback.** Install the [fetchproxy 0.3.0 extension](https://github.com/chrischall/fetchproxy), sign into your IC portal in the browser, and leave `IC_USERNAME` / `IC_PASSWORD` unset. `IC_BASE_URL` + `IC_DISTRICT` are still required so the MCP knows which host to read cookies from.\n\n`IC_BASE_URL` is your district's portal URL; `IC_DISTRICT` is the app-name path segment from that URL. Set `IC_DISABLE_FETCHPROXY=1` to opt out of the fallback (headless / CI).\n\n## Tools (prefix `ic_`)\n\n### Students & teachers\n- `ic_list_students` — list students linked to your parent account\n- `ic_list_teachers` — list teachers for a student\n\n### Academics\n- `ic_list_grades(studentId)` — class grades\n- `ic_list_recent_grades(studentId)` — recently graded items\n- `ic_list_assignments(studentId)` — current assignments\n- `ic_list_assessments(studentId)` — test scores\n\n### Attendance\n- `ic_list_attendance(studentId)` — attendance summary\n- `ic_list_attendance_events(studentId)` — individual absence/tardy events\n- `ic_list_school_days(studentId)` — calendar days\n\n### Behavior & fees\n- `ic_list_behavior(studentId)` — behavior incidents\n- `ic_list_fees(studentId)` — outstanding fees\n- `ic_list_food_service(studentId)` — cafeteria balance\n\n### Messaging\n- `ic_list_messages` — inbox\n- `ic_get_message(id)` — read a message\n- `ic_list_documents(studentId)` / `ic_download_document(id)` — documents\n\n## Notes\n\n- Set `IC_NAME` if you want a friendly name other than the district appname.\n- Auto-discovers the CUPS (Campus Unified Portal Services) layout from the base URL — no extra config needed for most districts.\n\nFile v2.3.2:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-mcp\",\n  \"version\": \"2.3.2\",\n  \"publishedAt\": 1780547377863\n}\n\nFile v2.3.2:skill-card.md\n\n## Description: <br>\nThis skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s), including grades, assignments, attendance, messages, documents, and related school information. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[chrischall](https://clawhub.ai/user/chrischall) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nExternal users and their agents use this skill to configure and operate an Infinite Campus Campus Parent MCP server for authorized access to student grades, attendance, assignments, messages, documents, fees, and related school records. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill can access sensitive school records and account data. <br>\nMitigation: Install and use it only for an authorized student or parent account, and apply least-privilege configuration. <br>\nRisk: Credentials and browser-session access can expose an Infinite Campus account. <br>\nMitigation: Treat IC_USERNAME, IC_PASSWORD, and browser-session cookies as sensitive, and disable fetchproxy when browser cookie use is not desired. <br>\nRisk: Downloaded documents or outbound messages may contain sensitive or unintended content. <br>\nMitigation: Review downloaded documents and any outbound messages before allowing the agent to act on or send them. <br>\n\n\n## Reference(s): <br>\n- [ClawHub skill page](https://clawhub.ai/chrischall/infinitecampus-mcp) <br>\n- [npm package: infinitecampus-mcp](https://www.npmjs.com/package/infinitecampus-mcp) <br>\n- [Source repository: infinitecampus-mcp](https://github.com/chrischall/infinitecampus-mcp) <br>\n- [fetchproxy fallback extension](https://github.com/chrischall/fetchproxy) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [Text, Markdown, Code, Shell commands, Configuration, Guidance] <br>\n**Output Format:** [Markdown with JSON and bash code blocks] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [May include MCP setup configuration and guidance for accessing authorized Infinite Campus Campus Parent data.] <br>\n\n## Skill Version(s): <br>\n2.3.2 (source: server release evidence) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v2.3.1: 3 files, 3047 bytes\n\nFiles: skill-card.md (2431b), SKILL.md (3264b), _meta.json (137b)\n\nFile v2.3.1:SKILL.md\n\n---\nname: infinitecampus-mcp\ndescription: This skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s). Triggers on phrases like \"check grades\", \"what's my kid's GPA\", \"any new assignments\", \"attendance record\", \"message the teacher\", \"Campus Parent inbox\", \"infinite campus\", or any request involving school grades, assignments, attendance, messages, or documents in Infinite Campus.\n---\n\n# infinitecampus-mcp\n\nMCP server for Infinite Campus Campus Parent — read student grades, attendance, assignments, and messages; download documents; and send messages.\n\n- **npm:** [npmjs.com/package/infinitecampus-mcp](https://www.npmjs.com/package/infinitecampus-mcp)\n- **Source:** [github.com/chrischall/infinitecampus-mcp](https://github.com/chrischall/infinitecampus-mcp)\n\n## Setup\n\n### Option A — npx (recommended)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"infinitecampus\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"infinitecampus-mcp\"],\n      \"env\": {\n        \"IC_BASE_URL\": \"https://campus.yourdistrict.k12.example.us\",\n        \"IC_DISTRICT\": \"your-district-appname\",\n        \"IC_USERNAME\": \"parent@example.com\",\n        \"IC_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — from source\n\n```bash\ngit clone https://github.com/chrischall/infinitecampus-mcp\ncd infinitecampus-mcp\nnpm install && npm run build\n```\n\n## Authentication\n\nTwo paths — the MCP tries them in priority order:\n\n1. **Username/password.** Set `IC_USERNAME` + `IC_PASSWORD` along with `IC_BASE_URL` + `IC_DISTRICT`.\n2. **fetchproxy fallback.** Install the [fetchproxy 0.3.0 extension](https://github.com/chrischall/fetchproxy), sign into your IC portal in the browser, and leave `IC_USERNAME` / `IC_PASSWORD` unset. `IC_BASE_URL` + `IC_DISTRICT` are still required so the MCP knows which host to read cookies from.\n\n`IC_BASE_URL` is your district's portal URL; `IC_DISTRICT` is the app-name path segment from that URL. Set `IC_DISABLE_FETCHPROXY=1` to opt out of the fallback (headless / CI).\n\n## Tools (prefix `ic_`)\n\n### Students & teachers\n- `ic_list_students` — list students linked to your parent account\n- `ic_list_teachers` — list teachers for a student\n\n### Academics\n- `ic_list_grades(studentId)` — class grades\n- `ic_list_recent_grades(studentId)` — recently graded items\n- `ic_list_assignments(studentId)` — current assignments\n- `ic_list_assessments(studentId)` — test scores\n\n### Attendance\n- `ic_list_attendance(studentId)` — attendance summary\n- `ic_list_attendance_events(studentId)` — individual absence/tardy events\n- `ic_list_school_days(studentId)` — calendar days\n\n### Behavior & fees\n- `ic_list_behavior(studentId)` — behavior incidents\n- `ic_list_fees(studentId)` — outstanding fees\n- `ic_list_food_service(studentId)` — cafeteria balance\n\n### Messaging\n- `ic_list_messages` — inbox\n- `ic_get_message(id)` — read a message\n- `ic_list_documents(studentId)` / `ic_download_document(id)` — documents\n\n## Notes\n\n- Set `IC_NAME` if you want a friendly name other than the district appname.\n- Auto-discovers the CUPS (Campus Unified Portal Services) layout from the base URL — no extra config needed for most districts.\n\nFile v2.3.1:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-mcp\",\n  \"version\": \"2.3.1\",\n  \"publishedAt\": 1780090559423\n}\n\nFile v2.3.1:skill-card.md\n\n## Description: <br>\nThis skill helps an agent use an Infinite Campus Campus Parent MCP server to read student grades, attendance, assignments, messages, behavior, fees, food service balances, and documents, and to send messages. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[chrischall](https://clawhub.ai/user/chrischall) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nExternal users with authorized Campus Parent access use this skill to configure and operate an MCP server for checking student records, school communications, documents, fees, and related Infinite Campus information. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill can access sensitive student records and school-portal credentials. <br>\nMitigation: Use it only with accounts the user is authorized to access, prefer scoped credentials where available, and avoid storing passwords in shared project files. <br>\nRisk: The browser-session fallback can rely on an existing signed-in portal session. <br>\nMitigation: Set IC_DISABLE_FETCHPROXY=1 when cookie-based access is not desired, especially in shared, headless, or CI environments. <br>\nRisk: Messaging and document download actions may affect school communications or expose private documents. <br>\nMitigation: Require explicit confirmation before sending messages or downloading documents. <br>\n\n\n## Reference(s): <br>\n- [ClawHub skill page](https://clawhub.ai/chrischall/infinitecampus-mcp) <br>\n- [npm package](https://www.npmjs.com/package/infinitecampus-mcp) <br>\n- [fetchproxy fallback extension](https://github.com/chrischall/fetchproxy) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance] <br>\n**Output Format:** [Markdown with JSON and shell command examples] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [May guide an agent to configure MCP environment variables and use Infinite Campus tools for authorized account data.] <br>\n\n## Skill Version(s): <br>\n2.3.1 (source: server evidence release.version) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v2.3.0: 3 files, 3084 bytes\n\nFiles: skill-card.md (2658b), SKILL.md (3264b), _meta.json (137b)\n\nFile v2.3.0:SKILL.md\n\n---\nname: infinitecampus-mcp\ndescription: This skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s). Triggers on phrases like \"check grades\", \"what's my kid's GPA\", \"any new assignments\", \"attendance record\", \"message the teacher\", \"Campus Parent inbox\", \"infinite campus\", or any request involving school grades, assignments, attendance, messages, or documents in Infinite Campus.\n---\n\n# infinitecampus-mcp\n\nMCP server for Infinite Campus Campus Parent — read student grades, attendance, assignments, and messages; download documents; and send messages.\n\n- **npm:** [npmjs.com/package/infinitecampus-mcp](https://www.npmjs.com/package/infinitecampus-mcp)\n- **Source:** [github.com/chrischall/infinitecampus-mcp](https://github.com/chrischall/infinitecampus-mcp)\n\n## Setup\n\n### Option A — npx (recommended)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"infinitecampus\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"infinitecampus-mcp\"],\n      \"env\": {\n        \"IC_BASE_URL\": \"https://campus.yourdistrict.k12.example.us\",\n        \"IC_DISTRICT\": \"your-district-appname\",\n        \"IC_USERNAME\": \"parent@example.com\",\n        \"IC_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — from source\n\n```bash\ngit clone https://github.com/chrischall/infinitecampus-mcp\ncd infinitecampus-mcp\nnpm install && npm run build\n```\n\n## Authentication\n\nTwo paths — the MCP tries them in priority order:\n\n1. **Username/password.** Set `IC_USERNAME` + `IC_PASSWORD` along with `IC_BASE_URL` + `IC_DISTRICT`.\n2. **fetchproxy fallback.** Install the [fetchproxy 0.3.0 extension](https://github.com/chrischall/fetchproxy), sign into your IC portal in the browser, and leave `IC_USERNAME` / `IC_PASSWORD` unset. `IC_BASE_URL` + `IC_DISTRICT` are still required so the MCP knows which host to read cookies from.\n\n`IC_BASE_URL` is your district's portal URL; `IC_DISTRICT` is the app-name path segment from that URL. Set `IC_DISABLE_FETCHPROXY=1` to opt out of the fallback (headless / CI).\n\n## Tools (prefix `ic_`)\n\n### Students & teachers\n- `ic_list_students` — list students linked to your parent account\n- `ic_list_teachers` — list teachers for a student\n\n### Academics\n- `ic_list_grades(studentId)` — class grades\n- `ic_list_recent_grades(studentId)` — recently graded items\n- `ic_list_assignments(studentId)` — current assignments\n- `ic_list_assessments(studentId)` — test scores\n\n### Attendance\n- `ic_list_attendance(studentId)` — attendance summary\n- `ic_list_attendance_events(studentId)` — individual absence/tardy events\n- `ic_list_school_days(studentId)` — calendar days\n\n### Behavior & fees\n- `ic_list_behavior(studentId)` — behavior incidents\n- `ic_list_fees(studentId)` — outstanding fees\n- `ic_list_food_service(studentId)` — cafeteria balance\n\n### Messaging\n- `ic_list_messages` — inbox\n- `ic_get_message(id)` — read a message\n- `ic_list_documents(studentId)` / `ic_download_document(id)` — documents\n\n## Notes\n\n- Set `IC_NAME` if you want a friendly name other than the district appname.\n- Auto-discovers the CUPS (Campus Unified Portal Services) layout from the base URL — no extra config needed for most districts.\n\nFile v2.3.0:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-mcp\",\n  \"version\": \"2.3.0\",\n  \"publishedAt\": 1779931818899\n}\n\nFile v2.3.0:skill-card.md\n\n## Description: <br>\nMCP server for Infinite Campus Campus Parent that lets agents read student grades, attendance, assignments, messages, documents, behavior, fees, and food-service balances, and send messages. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[chrischall](https://clawhub.ai/user/chrischall) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nParents, guardians, and their assistants use this skill to configure and operate an Infinite Campus Campus Parent MCP server for checking student academics, attendance, school messages, documents, and related account data. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill gives an assistant broad access to sensitive student records and login or session credentials. <br>\nMitigation: Install only when the assistant is intended to access the student's Infinite Campus account; keep credentials in a private user-level MCP configuration and avoid shared project configurations. <br>\nRisk: The fetchproxy fallback can use a signed-in browser session when username and password environment variables are unset. <br>\nMitigation: Disable the browser-session fallback with IC_DISABLE_FETCHPROXY=1 when it is not required, especially in headless or CI environments. <br>\nRisk: The available tools can download documents or send messages through the school account. <br>\nMitigation: Require explicit user confirmation before downloading documents or sending messages. <br>\n\n\n## Reference(s): <br>\n- [ClawHub skill page](https://clawhub.ai/chrischall/infinitecampus-mcp) <br>\n- [npm package](https://www.npmjs.com/package/infinitecampus-mcp) <br>\n- [Source repository listed by artifact](https://github.com/chrischall/infinitecampus-mcp) <br>\n- [fetchproxy fallback referenced by artifact](https://github.com/chrischall/fetchproxy) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [Text, Markdown, Shell commands, Configuration, Guidance] <br>\n**Output Format:** [Markdown with JSON configuration and shell command snippets] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [May involve MCP tool calls that read Infinite Campus records, download documents, or send messages when the configured account permits it.] <br>\n\n## Skill Version(s): <br>\n2.3.0 (source: release evidence) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v2.2.3: 3 files, 2989 bytes\n\nFiles: skill-card.md (2460b), SKILL.md (3264b), _meta.json (137b)\n\nFile v2.2.3:SKILL.md\n\n---\nname: infinitecampus-mcp\ndescription: This skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s). Triggers on phrases like \"check grades\", \"what's my kid's GPA\", \"any new assignments\", \"attendance record\", \"message the teacher\", \"Campus Parent inbox\", \"infinite campus\", or any request involving school grades, assignments, attendance, messages, or documents in Infinite Campus.\n---\n\n# infinitecampus-mcp\n\nMCP server for Infinite Campus Campus Parent — read student grades, attendance, assignments, and messages; download documents; and send messages.\n\n- **npm:** [npmjs.com/package/infinitecampus-mcp](https://www.npmjs.com/package/infinitecampus-mcp)\n- **Source:** [github.com/chrischall/infinitecampus-mcp](https://github.com/chrischall/infinitecampus-mcp)\n\n## Setup\n\n### Option A — npx (recommended)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"infinitecampus\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"infinitecampus-mcp\"],\n      \"env\": {\n        \"IC_BASE_URL\": \"https://campus.yourdistrict.k12.example.us\",\n        \"IC_DISTRICT\": \"your-district-appname\",\n        \"IC_USERNAME\": \"parent@example.com\",\n        \"IC_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — from source\n\n```bash\ngit clone https://github.com/chrischall/infinitecampus-mcp\ncd infinitecampus-mcp\nnpm install && npm run build\n```\n\n## Authentication\n\nTwo paths — the MCP tries them in priority order:\n\n1. **Username/password.** Set `IC_USERNAME` + `IC_PASSWORD` along with `IC_BASE_URL` + `IC_DISTRICT`.\n2. **fetchproxy fallback.** Install the [fetchproxy 0.3.0 extension](https://github.com/chrischall/fetchproxy), sign into your IC portal in the browser, and leave `IC_USERNAME` / `IC_PASSWORD` unset. `IC_BASE_URL` + `IC_DISTRICT` are still required so the MCP knows which host to read cookies from.\n\n`IC_BASE_URL` is your district's portal URL; `IC_DISTRICT` is the app-name path segment from that URL. Set `IC_DISABLE_FETCHPROXY=1` to opt out of the fallback (headless / CI).\n\n## Tools (prefix `ic_`)\n\n### Students & teachers\n- `ic_list_students` — list students linked to your parent account\n- `ic_list_teachers` — list teachers for a student\n\n### Academics\n- `ic_list_grades(studentId)` — class grades\n- `ic_list_recent_grades(studentId)` — recently graded items\n- `ic_list_assignments(studentId)` — current assignments\n- `ic_list_assessments(studentId)` — test scores\n\n### Attendance\n- `ic_list_attendance(studentId)` — attendance summary\n- `ic_list_attendance_events(studentId)` — individual absence/tardy events\n- `ic_list_school_days(studentId)` — calendar days\n\n### Behavior & fees\n- `ic_list_behavior(studentId)` — behavior incidents\n- `ic_list_fees(studentId)` — outstanding fees\n- `ic_list_food_service(studentId)` — cafeteria balance\n\n### Messaging\n- `ic_list_messages` — inbox\n- `ic_get_message(id)` — read a message\n- `ic_list_documents(studentId)` / `ic_download_document(id)` — documents\n\n## Notes\n\n- Set `IC_NAME` if you want a friendly name other than the district appname.\n- Auto-discovers the CUPS (Campus Unified Portal Services) layout from the base URL — no extra config needed for most districts.\n\nFile v2.2.3:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-mcp\",\n  \"version\": \"2.2.3\",\n  \"publishedAt\": 1779815389884\n}\n\nFile v2.2.3:skill-card.md\n\n## Description: <br>\nConnects an agent to an Infinite Campus Campus Parent account to read student grades, attendance, assignments, messages, documents, fees, and related school data, and to send messages. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[chrischall](https://clawhub.ai/user/chrischall) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nParents, guardians, or authorized assistants use this skill to access Infinite Campus Campus Parent records for their students, including grades, assignments, attendance, behavior, fees, food service, messages, and documents. The skill can also help send messages through the connected Campus Parent account. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill can access sensitive Campus Parent credentials and student records. <br>\nMitigation: Use only authorized accounts, prefer the least-privilege account available, and keep credentials out of shared project configuration. <br>\nRisk: Browser-session fallback can expose an active school-portal session through fetchproxy. <br>\nMitigation: Disable fetchproxy unless browser-session login is required and review the active portal session before use. <br>\nRisk: Messaging and document download actions may affect private school communications or records. <br>\nMitigation: Require explicit user approval before sending messages or downloading documents. <br>\n\n\n## Reference(s): <br>\n- [ClawHub release page](https://clawhub.ai/chrischall/infinitecampus-mcp) <br>\n- [npm package](https://www.npmjs.com/package/infinitecampus-mcp) <br>\n- [fetchproxy fallback extension](https://github.com/chrischall/fetchproxy) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance] <br>\n**Output Format:** [Markdown guidance with JSON configuration and shell command snippets] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [May guide MCP setup and propose tool calls for school records, messages, and document access.] <br>\n\n## Skill Version(s): <br>\n2.2.3 (source: server release metadata) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v2.1.3: 2 files, 1773 bytes\n\nFiles: SKILL.md (3264b), _meta.json (137b)\n\nFile v2.1.3:SKILL.md\n\n---\nname: infinitecampus-mcp\ndescription: This skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s). Triggers on phrases like \"check grades\", \"what's my kid's GPA\", \"any new assignments\", \"attendance record\", \"message the teacher\", \"Campus Parent inbox\", \"infinite campus\", or any request involving school grades, assignments, attendance, messages, or documents in Infinite Campus.\n---\n\n# infinitecampus-mcp\n\nMCP server for Infinite Campus Campus Parent — read student grades, attendance, assignments, and messages; download documents; and send messages.\n\n- **npm:** [npmjs.com/package/infinitecampus-mcp](https://www.npmjs.com/package/infinitecampus-mcp)\n- **Source:** [github.com/chrischall/infinitecampus-mcp](https://github.com/chrischall/infinitecampus-mcp)\n\n## Setup\n\n### Option A — npx (recommended)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"infinitecampus\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"infinitecampus-mcp\"],\n      \"env\": {\n        \"IC_BASE_URL\": \"https://campus.yourdistrict.k12.example.us\",\n        \"IC_DISTRICT\": \"your-district-appname\",\n        \"IC_USERNAME\": \"parent@example.com\",\n        \"IC_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — from source\n\n```bash\ngit clone https://github.com/chrischall/infinitecampus-mcp\ncd infinitecampus-mcp\nnpm install && npm run build\n```\n\n## Authentication\n\nTwo paths — the MCP tries them in priority order:\n\n1. **Username/password.** Set `IC_USERNAME` + `IC_PASSWORD` along with `IC_BASE_URL` + `IC_DISTRICT`.\n2. **fetchproxy fallback.** Install the [fetchproxy 0.3.0 extension](https://github.com/chrischall/fetchproxy), sign into your IC portal in the browser, and leave `IC_USERNAME` / `IC_PASSWORD` unset. `IC_BASE_URL` + `IC_DISTRICT` are still required so the MCP knows which host to read cookies from.\n\n`IC_BASE_URL` is your district's portal URL; `IC_DISTRICT` is the app-name path segment from that URL. Set `IC_DISABLE_FETCHPROXY=1` to opt out of the fallback (headless / CI).\n\n## Tools (prefix `ic_`)\n\n### Students & teachers\n- `ic_list_students` — list students linked to your parent account\n- `ic_list_teachers` — list teachers for a student\n\n### Academics\n- `ic_list_grades(studentId)` — class grades\n- `ic_list_recent_grades(studentId)` — recently graded items\n- `ic_list_assignments(studentId)` — current assignments\n- `ic_list_assessments(studentId)` — test scores\n\n### Attendance\n- `ic_list_attendance(studentId)` — attendance summary\n- `ic_list_attendance_events(studentId)` — individual absence/tardy events\n- `ic_list_school_days(studentId)` — calendar days\n\n### Behavior & fees\n- `ic_list_behavior(studentId)` — behavior incidents\n- `ic_list_fees(studentId)` — outstanding fees\n- `ic_list_food_service(studentId)` — cafeteria balance\n\n### Messaging\n- `ic_list_messages` — inbox\n- `ic_get_message(id)` — read a message\n- `ic_list_documents(studentId)` / `ic_download_document(id)` — documents\n\n## Notes\n\n- Set `IC_NAME` if you want a friendly name other than the district appname.\n- Auto-discovers the CUPS (Campus Unified Portal Services) layout from the base URL — no extra config needed for most districts.\n\nFile v2.1.3:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-mcp\",\n  \"version\": \"2.1.3\",\n  \"publishedAt\": 1779394317376\n}\n\nArchive v2.1.1: 56 files, 138147 bytes\n\nFiles: CLAUDE.md (2933b), docs/endpoints.md (9613b), docs/submissions/README.md (1927b), docs/superpowers/plans/2026-04-15-infinitecampus-mcp.md (86624b), docs/superpowers/specs/2026-04-15-infinitecampus-mcp-design.md (12776b), manifest.json (1741b), package-lock.json (106950b), package.json (1390b), README.md (2114b), server.json (1616b), SKILL.md (2849b), skills/ic/SKILL.md (8783b), src/client.ts (17034b), src/config.ts (1084b), src/index.ts (2897b), src/tools/_shared.ts (3319b), src/tools/assessments.ts (3237b), src/tools/assignments.ts (2614b), src/tools/attendance_events.ts (5604b), src/tools/attendance.ts (4458b), src/tools/behavior.ts (1612b), src/tools/calendar.ts (3980b), src/tools/districts.ts (590b), src/tools/documents.ts (3311b), src/tools/features.ts (1833b), src/tools/fees.ts (2711b), src/tools/foodservice.ts (1739b), src/tools/grades.ts (913b), src/tools/messages.ts (7548b), src/tools/recent_grades.ts (2741b), src/tools/schedule.ts (1082b), src/tools/students.ts (778b), src/tools/teachers.ts (2734b), tests/client.test.ts (52759b), tests/config.test.ts (1871b), tests/tools/_shared.test.ts (8442b), tests/tools/assessments.test.ts (11298b), tests/tools/assignments.test.ts (5009b), tests/tools/attendance_events.test.ts (17601b), tests/tools/attendance.test.ts (16791b), tests/tools/behavior.test.ts (4626b), tests/tools/calendar.test.ts (8430b), tests/tools/districts.test.ts (1503b), tests/tools/documents.test.ts (7957b), tests/tools/features.test.ts (3474b), tests/tools/fees.test.ts (6713b), tests/tools/foodservice.test.ts (4655b), tests/tools/grades.test.ts (1867b), tests/tools/messages.test.ts (21949b), tests/tools/recent_grades.test.ts (4296b), tests/tools/schedule.test.ts (2323b), tests/tools/students.test.ts (1689b), tests/tools/teachers.test.ts (7918b), tsconfig.json (328b), vitest.config.ts (288b), _meta.json (137b)\n\nFile v2.1.1:SKILL.md\n\n---\nname: infinitecampus-mcp\ndescription: This skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s). Triggers on phrases like \"check grades\", \"what's my kid's GPA\", \"any new assignments\", \"attendance record\", \"message the teacher\", \"Campus Parent inbox\", \"infinite campus\", or any request involving school grades, assignments, attendance, messages, or documents in Infinite Campus.\n---\n\n# infinitecampus-mcp\n\nMCP server for Infinite Campus Campus Parent — read student grades, attendance, assignments, and messages; download documents; and send messages.\n\n- **npm:** [npmjs.com/package/infinitecampus-mcp](https://www.npmjs.com/package/infinitecampus-mcp)\n- **Source:** [github.com/chrischall/infinitecampus-mcp](https://github.com/chrischall/infinitecampus-mcp)\n\n## Setup\n\n### Option A — npx (recommended)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"infinitecampus\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"infinitecampus-mcp\"],\n      \"env\": {\n        \"IC_BASE_URL\": \"https://campus.yourdistrict.k12.example.us\",\n        \"IC_DISTRICT\": \"your-district-appname\",\n        \"IC_USERNAME\": \"parent@example.com\",\n        \"IC_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — from source\n\n```bash\ngit clone https://github.com/chrischall/infinitecampus-mcp\ncd infinitecampus-mcp\nnpm install && npm run build\n```\n\n## Authentication\n\nInfinite Campus uses username/password — log in with the same credentials you use for the Campus Parent portal. `IC_BASE_URL` is your district's portal URL; `IC_DISTRICT` is the app-name path segment from that URL.\n\n## Tools (prefix `ic_`)\n\n### Students & teachers\n- `ic_list_students` — list students linked to your parent account\n- `ic_list_teachers` — list teachers for a student\n\n### Academics\n- `ic_list_grades(studentId)` — class grades\n- `ic_list_recent_grades(studentId)` — recently graded items\n- `ic_list_assignments(studentId)` — current assignments\n- `ic_list_assessments(studentId)` — test scores\n\n### Attendance\n- `ic_list_attendance(studentId)` — attendance summary\n- `ic_list_attendance_events(studentId)` — individual absence/tardy events\n- `ic_list_school_days(studentId)` — calendar days\n\n### Behavior & fees\n- `ic_list_behavior(studentId)` — behavior incidents\n- `ic_list_fees(studentId)` — outstanding fees\n- `ic_list_food_service(studentId)` — cafeteria balance\n\n### Messaging\n- `ic_list_messages` — inbox\n- `ic_get_message(id)` — read a message\n- `ic_list_documents(studentId)` / `ic_download_document(id)` — documents\n\n## Notes\n\n- Set `IC_NAME` if you want a friendly name other than the district appname.\n- Auto-discovers the CUPS (Campus Unified Portal Services) layout from the base URL — no extra config needed for most districts.\n\nFile v2.1.1:skills/ic/SKILL.md\n\n---\nname: ic\ndescription: 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.\n---\n\n# infinitecampus-mcp\n\nMCP 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.\n\n- **Source:** [github.com/chrischall/infinitecampus-mcp](https://github.com/chrischall/infinitecampus-mcp)\n- **npm:** [npmjs.com/package/infinitecampus-mcp](https://www.npmjs.com/package/infinitecampus-mcp)\n\n## Setup\n\nSingle-account config. Set env vars for your primary IC account; linked districts come from CUPS discovery.\n\n### Option A — Claude Code (direct MCP, no mcporter)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"ic\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"infinitecampus-mcp\"],\n      \"env\": {\n        \"IC_BASE_URL\": \"https://campus.springfield.k12.example.us\",\n        \"IC_DISTRICT\": \"springfield\",\n        \"IC_USERNAME\": \"you@example.com\",\n        \"IC_PASSWORD\": \"yourpassword\",\n        \"IC_NAME\": \"Springfield\"\n      }\n    }\n  }\n}\n```\n\nOnly `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.\n\n### Option B — mcporter\n\n#### 1. Install\n\n```bash\nnpm install -g infinitecampus-mcp\n```\n\nOr from source:\n```bash\ngit clone https://github.com/<owner>/infinitecampus-mcp\ncd infinitecampus-mcp\nnpm install && npm run build\n```\n\n#### 2. Configure credentials\n\n```bash\ncp .env.example .env\n# Edit .env: set IC_BASE_URL, IC_DISTRICT, IC_USERNAME, IC_PASSWORD\n# IC_NAME is optional (defaults to IC_DISTRICT)\n```\n\n#### 3. Register with mcporter\n\n```bash\nmcporter config add ic \\\n  --command \"infinitecampus-mcp\" \\\n  --env \"IC_BASE_URL=https://campus.springfield.k12.example.us\" \\\n  --env \"IC_DISTRICT=springfield\" \\\n  --env \"IC_USERNAME=you@example.com\" \\\n  --env \"IC_PASSWORD=yourpassword\" \\\n  --env \"IC_NAME=Springfield\" \\\n  --config ~/.mcporter/mcporter.json\n```\n\n#### 4. Verify\n\n```bash\nmcporter list --config ~/.mcporter/mcporter.json\nmcporter call ic.ic_list_districts --config ~/.mcporter/mcporter.json\n```\n\n## Calling tools (mcporter)\n\n```bash\nmcporter call ic.<tool_name> [key=value ...] --config ~/.mcporter/mcporter.json\n```\n\nAlways pass `--config ~/.mcporter/mcporter.json` unless a local `config/mcporter.json` exists.\n\nEvery tool except `ic_list_districts` takes `district` as its first arg (the district name from `ic_list_districts`). Most student-scoped tools also take `studentId` (the personID from `ic_list_students`).\n\n## Tools\n\n### Districts & Students\n| Tool | Notes |\n|------|-------|\n| `ic_list_districts` | Lists configured districts (primary + any CUPS-linked). Call this first — other tools need the `district` name. |\n| `ic_list_students(district)` | Lists students (kids) attached to the portal account for a district. Use the `personID` for `studentId` on other tools. |\n\n### Academics\n| Tool | Notes |\n|------|-------|\n| `ic_get_schedule(district, studentId)` | Today's class schedule by default, with section placements. |\n| `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. |\n| `ic_list_grades(district, studentId, termId?)` | Term + in-progress grade summary. Omit `termId` for all terms. |\n| `ic_list_recent_grades(district, studentId, since?)` | Recently-scored assignments. Defaults to a 14-day window; pass `since` (YYYY-MM-DD) to widen. |\n| `ic_list_assessments(district, studentId)` | Standardized test scores (state/national/district tests). FeatureDisabled-aware. |\n| `ic_list_teachers(district, studentId)` | Teachers per enrolled section + assigned counselor(s). |\n\n### Daily life\n| Tool | Notes |\n|------|-------|\n| `ic_list_school_days(district, studentId)` | Instructional calendar with term boundaries. |\n| `ic_list_attendance(district, studentId, since?, until?)` | Per-course attendance summary grouped by term. |\n| `ic_list_attendance_events(district, studentId, since?, until?, excusedOnly?)` | Individual absence/tardy events with codes and human comments. |\n| `ic_list_behavior(district, studentId, since?, until?)` | Returns a `FeatureDisabled` warning if the district has no behavior module enabled — this is not an error. |\n| `ic_list_food_service(district, studentId, since?, until?)` | Lunch balance and transactions. Same `FeatureDisabled` fallback as behavior. |\n| `ic_list_fees(district, studentId)` | Fee assignments + surplus/balance. Returns `PartialSuccess` if only one endpoint works, `FeatureDisabled` if both 404. |\n\n### Documents\n| Tool | Notes |\n|------|-------|\n| `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. |\n| `ic_download_document(district, url, destinationPath)` | Writes the document to `destinationPath` on disk. **`destinationPath` is required** — confirm the path with the user before calling. |\n\n### Messaging\n| Tool | Notes |\n|------|-------|\n| `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. |\n| `ic_get_message(district, url)` | Fetches and parses the HTML body of an inbox message. Returns `{ subject, date, body, url }`. |\n\n### Features\n| Tool | Notes |\n|------|-------|\n| `ic_get_features(district, studentId)` | Raw per-enrollment `displayOptions` flags (attendance/behavior/assessment/documents/…). Useful for diagnosing why another tool returned `FeatureDisabled`. |\n\n## Workflows\n\n**Discovery (first time):**\n1. `ic_list_districts` → see configured + linked districts\n2. For each district: `ic_list_students(district)` → collect kids and personIDs\n\n**Is everything OK at school?**\n1. `ic_list_districts`\n2. For each student:\n   - `ic_list_recent_grades(district, studentId)` — last 14 days of scored work\n   - `ic_list_attendance_events(district, studentId, excusedOnly=false)` — recent absences/tardies\n   - `ic_list_messages(district)` — any alerts from teachers or the district\n\n**What got graded this week?**\n- `ic_list_recent_grades(district, studentId, since=\"YYYY-MM-DD\")`\n\n**Check upcoming or missing assignments:**\n- `ic_list_assignments(district, studentId, missingOnly=true)` — only the late/missing set\n- Or `ic_list_assignments(district, studentId, since=..., until=...)` for a date window\n\n**Today's schedule:**\n- `ic_get_schedule(district, studentId)` — returns today's classes by default\n\n**Weather closure or priority announcement?**\n- `ic_list_messages(district)` — scan the Messenger 2.0 inbox entries for priority subjects\n- `ic_get_message(district, url)` to read a specific one\n\n**Download a report card:**\n1. `ic_list_documents(district, studentId)` → find the report card's `url`\n2. Confirm destination path with the user\n3. `ic_download_document(district, url, destinationPath=\"/Users/.../report-card.pdf\")`\n\n## Caution\n\n- `ic_download_document` writes to disk at `destinationPath` — confirm the path with the user; overwrites silently.\n- `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.\n- 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.\n\nFile v2.1.1:docs/submissions/README.md\n\n# Registry submissions — infinitecampus-mcp\n\nReady-to-paste copy for registries that need a manual browser-form submission. Automated pipelines fire on every `v*` tag via `.github/workflows/release.yml`.\n\n## Coverage matrix\n\n| Registry                          | Automated?                               | Where |\n| --- | --- | --- |\n| npm                               | ✅ `release.yml`                          | `npm publish --provenance` |\n| GitHub Releases                   | ✅ `release.yml`                          | `.skill` + `.mcpb` attached |\n| modelcontextprotocol/registry     | ✅ `release.yml` (OIDC)                   | `mcp-publisher publish` using `server.json` |\n| PulseMCP                          | ✅ transitive (auto-ingests weekly)       | — |\n| ClawHub (OpenClaw)                | ✅ conditional on `CLAWHUB_TOKEN`         | `clawhub skill publish` |\n| mcpservers.org                    | ❌ manual — [mcpservers.org/submit](https://mcpservers.org/submit) | |\n| Anthropic community plugins       | ❌ manual — [clau.de/plugin-directory-submission](https://clau.de/plugin-directory-submission) | |\n\n## mcpservers.org\n\n- **Server Name:** `infinitecampus-mcp`\n- **Short Description:** `Infinite Campus (Campus Parent) MCP server for Claude — grades, attendance, assignments, messages, and documents via natural language`\n- **Link:** `https://github.com/chrischall/infinitecampus-mcp`\n- **Category:** `Productivity`\n- **Contact Email:** `chris.c.hall@gmail.com`\n\n## Anthropic community plugins\n\n- **Repo URL:** `https://github.com/chrischall/infinitecampus-mcp`\n- **Plugin name:** `infinitecampus-mcp`\n- **Short description:** `Infinite Campus (Campus Parent) MCP server for Claude — grades, attendance, assignments, messages, and documents via natural language`\n- **Category:** Productivity\n- **Tags:** infinite-campus, campus-parent, school, education, k12, grades, attendance, mcp\n\nFile v2.1.1:README.md\n\n# infinitecampus-mcp\n\nMCP server for Infinite Campus (Campus Parent portal). Single-account config — linked districts are auto-discovered via CUPS SSO after login.\n\n## Tools\n\n19 tools across academics, daily life, documents, messaging, and feature discovery.\n\n| Domain | Tools |\n|---|---|\n| Districts | `ic_list_districts` |\n| Students | `ic_list_students` |\n| Schedule | `ic_get_schedule` |\n| Assignments | `ic_list_assignments` (sectionID server-side; `missingOnly` / date filters client-side) |\n| Grades | `ic_list_grades`, `ic_list_recent_grades` (default 14d window) |\n| School calendar | `ic_list_school_days` |\n| Attendance | `ic_list_attendance` (per-course summary), `ic_list_attendance_events` (individual events with codes + comments) |\n| Behavior | `ic_list_behavior` (FeatureDisabled-aware) |\n| Food service | `ic_list_food_service` (FeatureDisabled-aware) |\n| Documents | `ic_list_documents`, `ic_download_document` |\n| 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) |\n| Teachers | `ic_list_teachers` (teachers per section + assigned counselors) |\n| Assessments | `ic_list_assessments` (standardized test scores) |\n| Fees | `ic_list_fees` (assignments + surplus balance) |\n| Features | `ic_get_features` (per-enrollment displayOptions flags) |\n\nTools that the harness will gate as write/IO operations: `ic_download_document`.\n\n## Configuration\n\nSet a single set of env vars for your primary Infinite Campus account:\n\n```\nIC_BASE_URL=https://campus.springfield.k12.example.us\nIC_DISTRICT=springfield\nIC_USERNAME=parent@example.com\nIC_PASSWORD=...\nIC_NAME=Springfield           # optional, defaults to IC_DISTRICT\n```\n\nLinked 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.\n\nSee `.env.example`.\n\n## Status\n\nUnofficial — not affiliated with Infinite Campus. AI-maintained.\n\nFile v2.1.1:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-mcp\",\n  \"version\": \"2.1.1\",\n  \"publishedAt\": 1777668009629\n}\n\nFile v2.1.1:CLAUDE.md\n\n# infinitecampus-mcp\n\nMCP server for Infinite Campus Campus Parent portal — single-account config with CUPS auto-discovery of linked districts.\n\n## Build & Test\n\n```bash\nnpm run build        # tsc + esbuild bundle\nnpm test             # vitest run (all tests)\nnpm run test:watch   # vitest in watch mode\n```\n\n`dist/` is gitignored; the bundle is built fresh by CI and ships via npm (per the `files` array in `package.json`). Rebuild locally with `npm run build` before publishing or when verifying a change end-to-end.\n\n## Versioning\n\nVersion appears in three places — all must match:\n\n1. `package.json` → `\"version\"`\n2. `package-lock.json` → run `npm install --package-lock-only` after changing\n3. `src/index.ts` → `McpServer` constructor `version` field\n\n### Important\n\nDo NOT manually bump versions or create tags unless the user explicitly asks. Version bumps happen in their own commits at the end of a release cycle (current: v2.0.x).\n\n## Architecture\n\n- `src/index.ts` — MCP server setup, tool routing\n- `src/config.ts` — single-account env loader (`IC_BASE_URL`, `IC_DISTRICT`, `IC_USERNAME`, `IC_PASSWORD`, optional `IC_NAME`)\n- `src/client.ts` — `ICClient` with per-district session pool, lazy login, 401 retry, download method. Constructor takes a single `Account`; linked districts are added dynamically via CUPS SSO discovery.\n- `src/tools/` — one file per domain. Each exports `register<Domain>Tools(server, client)`. Tool schemas use the `argsSchema = z.object({...})` const pattern: SDK gets `argsSchema.shape`, handler does `args = argsSchema.parse(rawArgs)`. This gives us a single source of truth for the schema and runtime safety in case the handler is invoked outside of the MCP request path (e.g., direct unit-test call).\n- `tests/tools/` — mirrors `src/tools/`, mocks `ICClient.request` (or `ICClient.download` for documents) via `vi.spyOn`\n\n## Coverage\n\n`vitest.config.ts` enforces 100% lines/functions/branches/statements across `src/` (excluding `src/index.ts`, the stdio entry point). Adding a new tool or branch requires a test to keep CI green.\n\n## IC Notes\n\n- Parent portal uses Spring Security session cookies (`JSESSIONID`); login flow is `POST /campus/verify.jsp?nonBrowser=true&...`\n- Sessions expire ~6h; client uses 5h TTL with 401-retry as a backup\n- Many districts disable optional modules (behavior, food service); tools for those return `{warning: 'FeatureDisabled', ...}` on 404 instead of throwing\n- Endpoint paths in this repo are derived from `schwartzpub/ic_parent_api` (Python). When IC ships portal updates, that repo is the first place to check for new patterns\n- Single-account config: `loadAccount()` reads flat `IC_*` env vars. CUPS SSO auto-discovers linked districts after primary login, adding them dynamically to the client's account/session maps. Every tool takes `district` as its first arg; `ic_list_districts` returns valid names (primary + any linked).\n\nFile v2.1.1:docs/endpoints.md\n\n# IC Endpoint Coverage\n\nInventory of every Infinite Campus endpoint we've discovered, whether it's wired into an MCP tool, and why/why not. Updated whenever a new endpoint is probed or a tool is added/removed.\n\nSources for discovery:\n- Live browser sniffing on Springfield (Springfield HS) and Westside Academy\n- `schwartzpub/ic_parent_api` (Python reference library)\n- `tonyzimbinski/infinite-campus` (Node reference library)\n- `gilesgc/Infinite-Campus-API` (Python reference library)\n\nStatus legend: ✅ shipped · 🚧 planned · ⏸️ deferred · 🚫 skipped · ❓ unknown\n\n---\n\n## Shipped tools (current MCP surface)\n\n| Tool | Endpoint | Notes |\n|---|---|---|\n| `ic_list_districts` | (internal — config + CUPS discovery) | Includes linked districts auto-discovered via CUPS |\n| `ic_list_students` | `GET /campus/api/portal/students` | ✅ Confirmed working both districts |\n| `ic_get_schedule` | `GET /campus/resources/portal/roster?personID=X` | ✅ Returns courses with section placements |\n| `ic_list_assignments` | `GET /campus/api/portal/assignment/listView?personID=X[&sectionID=Y]` | ✅ `sectionID` is the only server-side filter; date/state filters are ignored by the server so we apply client-side |\n| `ic_list_grades` | `GET /campus/resources/portal/grades?personID=X[&termID=Y]` | ✅ |\n| `ic_list_school_days` | `GET /campus/api/portal/students` + `/campus/resources/term?structureID=X` + `/campus/resources/calendar/instructionalDay?calendarID=Y` | ✅ Grades-shaped: enrollment → terms → days |\n| `ic_list_attendance` | `GET /campus/resources/portal/attendance/{enrollmentID}?courseSummary=true&personID=X` | ✅ Auto-resolves enrollmentID from `ic_list_students`. Returns per-course/term summary with absent/tardy/present/earlyRelease lists. Trims lists by since/until client-side. Each list entry's `sectionPlacements` is trimmed to `{ periodName, startTime, endTime }` (the parent course already carries the rest). Short-circuits via `displayOptions.attendance` when disabled. |\n| `ic_list_attendance_events` | `GET /campus/resources/portal/attendance/events?enrollmentID=X&personID=X` | ✅ Individual absence/tardy events with code, description, excuse, human comments. Auto-resolves enrollmentID; supports since/until/excusedOnly filters. Each event's `sectionPlacements` is trimmed to `{ periodName, startTime, endTime }`. Short-circuits via `displayOptions.attendance` when disabled. |\n| `ic_list_behavior` | `GET /campus/resources/portal/behavior?personID=X` | ⚠️ Unknown real path; short-circuits via `displayOptions.behavior` when disabled, 404 backstop otherwise |\n| `ic_list_food_service` | `GET /campus/resources/portal/foodService?personID=X` | ⚠️ Unknown real path; short-circuits via `displayOptions.foodService` when disabled, 404 backstop otherwise |\n| `ic_list_documents` | `GET /campus/resources/portal/report/all?personID=X` | ✅ Returns document metadata (name, type, url, moduleLabel, endYear). Use `url` field with `ic_download_document`. Short-circuits via `displayOptions.documents` when disabled. |\n| `ic_download_document` | (client download to disk using `url` from `ic_list_documents`) | ✅ |\n| `ic_list_recent_grades` | `GET /campus/api/portal/assignment/recentlyScored?modifiedDate=ISO&personID=X` | ✅ Recently-scored assignments. Takes `since` (YYYY-MM-DD), defaults to 14 days ago. |\n| `ic_list_teachers` | `GET /campus/resources/portal/section/contacts?personID=X` + `GET /campus/resources/portal/studentCounselor/byUser?personID=X` | ✅ Combined teachers (per enrolled section) + counselors. 404 on either sub-endpoint is treated as empty list. |\n| `ic_list_messages` | `GET /campus/prism?x=notifications.Notification-retrieve&limitCount=N` + `GET /campus/api/portal/process-message` + `GET /campus/resources/portal/userNotice` | ✅ Combines three sources: prism notifications (assignment/grade/attendance alerts), Messenger 2.0 inbox (teacher messages, district announcements), and portal userNotice announcements. `limit` caps prism only (high-volume source). Per-section `error` field if any source fails. Listing is treated as read-only; live probe against an empty inbox could not confirm a read-tracking side effect and the count endpoint the earlier tool used was removed as redundant with the `newMessage` flag already returned here. |\n| `ic_get_message` | `GET <messageUrl>` (relative `portal/messageView.xsl?x=messenger.MessengerEngine-getMessageRecipientView&messageID=...&messageRecipientID=...&processMessageID=...` from inbox items, normalized to absolute `/campus/...`) | ✅ Fetches the HTML body of an inbox message and parses into `{ subject, date, body, url }`. Strips `<script>`/`<style>` blocks, strips tags, decodes common HTML entities, collapses whitespace, splits on the \"Date: MM/DD/YYYY\" line. Treated as `readOnlyHint: true`; probe against an empty inbox could not confirm whether fetching the body marks the message as read — some district configurations may do so. |\n| `ic_list_assessments` | `GET /campus/resources/prism/portal/assessments?personID=X&calendarID=Y` | ✅ Standardized test scores (stateTests, nationalTests, districtTests). Auto-resolves calendarID per enrollment; loops through all enrollments. Drops `assessmentHTML`. Short-circuits via `displayOptions.assessment` when disabled; 404 on every enrollment → FeatureDisabled backstop. |\n| `ic_get_features` | `GET /campus/api/portal/displayOptions/{structureID}?personID=X` | ✅ Returns the district's ~90-flag allow-list per enrollment (attendance, behavior, assessment, documents, grades, schedule, academicPlanner, etc.). Results cached per (district, structureID) for the session TTL. Used internally by `ic_list_behavior`, `ic_list_food_service`, `ic_list_assessments`, `ic_list_attendance`, `ic_list_attendance_events`, and `ic_list_documents` to short-circuit disabled features before hitting the real endpoint — cleaner than 404-catch alone, though 404 remains as a backstop. |\n| `ic_list_fees` | `GET /campus/api/portal/fees/feeAssignments?personID=X` + `GET /campus/api/portal/fees/feeTransactionDetail/totalSurplus/-1?personID=X` | ✅ Fee assignments (charges owed) + surplus/balance (raw number). Both endpoints run in parallel. FeatureDisabled only if both 404; partial success returns the working side with top-level `warning: 'PartialSuccess'` and an `issues: string[]` array (parallel to the `warning: 'FeatureDisabled'` shape). |\n\n---\n\n## Planned — new tools\n\n_(none currently)_\n\n---\n\n## Deferred — low priority (situational)\n\nUseful only if a specific kid uses the feature. All would auto-detect via `displayOptions` flags.\n\n| Tool | Endpoint | displayOption flag | Reason deferred |\n|---|---|---|---|\n| `ic_list_plans` | `GET /campus/api/portal/plan?personID=X&portalHomeOnly=false` | `plan`, `documentsIEP` | IEP/504 plans — only for students in special ed |\n| `ic_list_progress_reports` | `GET /campus/api/portal/progressReport?personID=X` | `progressReport` | Teacher commentary reports — varies district-to-district |\n| `ic_list_evaluations` | `GET /campus/api/portal/evaluation?personID=X` | `documentsIEP` | Special ed evaluations |\n| `ic_list_prior_written_notices` | `GET /campus/api/portal/priorWrittenNotice?personID=X` | `documentsIEP` | Special ed notices |\n| `ic_list_forms` | `GET /campus/api/portal/custom-forms/forms?personID=X` | `customForms` | Back-to-school forms, permission slips — mostly one-time |\n\n---\n\n## Skipped — won't implement\n\n| Endpoint | Why skipped |\n|---|---|\n| `GET /campus/api/portal/hand-raise/list?personID=X` | Campus virtual hand-raise feature. Returned `[]` on both districts — near-zero signal. |\n| `GET /campus/api/portal/studentParticipation/attendanceInfo?date=X&personID=X` | Virtual learning check-ins. Data is thin (just `hasCheckIns`, `isVirtual`) and covered by attendance events. |\n| `GET /campus/api/portal/requiredActions/portalLogin` | Returns a single boolean `olrRequired` (Online Registration pending). Too narrow for a tool. |\n| `GET /campus/api/portal/crossSite/instructionalDay?structureID=X&personID=X` | Cross-site enrollments only (rare). Regular instructionalDay endpoint covers the common case. |\n| `GET /campus/resources/portal/dayEvent/byEnrollment?personID=X` | Overlaps heavily with `instructionalDay` (comments already include event labels). |\n| Academic Planner (graduation progress) | Uses the legacy prism API style (`portal.PortalAcademicPlanner-*`), Springfield only. Grades tool already shows progress toward term GPAs. |\n| `GET /campus/resources/portal/attendance/events` (Messenger 2.0 replies) | No response/reply endpoint discovered that works for parents. Districts route responses through other systems (email, phone, ParentSquare). |\n\n---\n\n## Not yet probed\n\nCandidates to investigate later:\n- Transportation / bus routing\n- Activities / athletics participation\n- Health (immunizations, meds)\n- Lockers (we see a `lockers` field in `generalInfo` but haven't pulled the detail)\n- Household members / contact preferences\n\n---\n\n## Conventions\n\n- Every tool takes `district: string` as its first arg (even single-district configs use it to scope across CUPS-linked districts).\n- Student-scoped tools take `studentId: string` (the `personID` from `ic_list_students`).\n- Enrollment-scoped data is resolved inside the tool from `studentId` → student's primary enrollment → `enrollmentID` / `calendarID` / `structureID`.\n- When an endpoint is known to be disabled per district, we prefer `displayOptions`-based early return over 404-catch-and-fallback. The 404 fallback remains as a backstop for undetectable cases.\n\nFile v2.1.1:docs/superpowers/plans/2026-04-15-infinitecampus-mcp.md\n\n# Infinite Campus MCP Implementation Plan\n\n> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.\n\n**Goal:** Build a Node.js MCP server exposing the Campus Parent portal of Infinite Campus (multi-district) with 13 tools across 10 domains.\n\n**Architecture:** TypeScript over `@modelcontextprotocol/sdk` (stdio), mirrors the [ofw-mcp](https://github.com/chrischall/ofw-mcp) repo. Single `ICClient` owns a `Map<district, Session>` for per-district cookie jars; lazy login on first request, re-login once on 401. Each tool module exports `register<Domain>Tools(server, client)`. Vitest with `vi.spyOn(client, 'request')` for tool tests.\n\n**Tech Stack:** Node 22, TypeScript (ESM, NodeNext), `@modelcontextprotocol/sdk`, `zod`, `dotenv`, `vitest`, `esbuild`.\n\n**Reference repos** (for endpoint paths and response shapes — implementer reads these per-tool):\n- Primary: https://github.com/schwartzpub/ic_parent_api (Python, parent portal)\n- Secondary: https://github.com/tonyzimbinski/infinite-campus (Node, request patterns)\n- Tertiary: https://github.com/gilesgc/Infinite-Campus-API (Python, cross-check)\n\n**Spec:** [`docs/superpowers/specs/2026-04-15-infinitecampus-mcp-design.md`](../specs/2026-04-15-infinitecampus-mcp-design.md)\n\n---\n\n## File Structure\n\n```\ninfinitecampus-mcp/\n├── .gitignore\n├── .env.example\n├── package.json\n├── tsconfig.json\n├── vitest.config.ts\n├── README.md\n├── CLAUDE.md\n├── manifest.json\n├── .claude-plugin/plugin.json\n├── src/\n│   ├── index.ts              # MCP server bootstrap, tool registration\n│   ├── config.ts             # loadAccounts(env) → Account[]\n│   ├── client.ts             # ICClient: per-district sessions, request, download\n│   └── tools/\n│       ├── districts.ts      # ic_list_districts\n│       ├── students.ts       # ic_list_students\n│       ├── schedule.ts       # ic_get_schedule\n│       ├── assignments.ts    # ic_list_assignments\n│       ├── grades.ts         # ic_list_grades\n│       ├── attendance.ts     # ic_list_attendance\n│       ├── messages.ts       # ic_list_messages, ic_get_message,\n│       │                     # ic_send_message, ic_list_message_recipients\n│       ├── behavior.ts       # ic_list_behavior\n│       ├── foodservice.ts    # ic_list_food_service\n│       └── documents.ts      # ic_list_documents, ic_download_document\n└── tests/\n    ├── config.test.ts\n    ├── client.test.ts\n    ├── fixtures/             # sanitized JSON captures (created per-tool when implementing)\n    └── tools/                # one .test.ts per src/tools/ file\n```\n\n---\n\n## Phase 1 — Project scaffolding\n\n### Task 1: Create package.json, tsconfig, vitest config, .gitignore, .env.example\n\n**Files:**\n- Create: `package.json`\n- Create: `tsconfig.json`\n- Create: `vitest.config.ts`\n- Create: `.gitignore`\n- Create: `.env.example`\n\n- [ ] **Step 1: Write `package.json`**\n\n```json\n{\n  \"name\": \"infinitecampus-mcp\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Infinite Campus (Campus Parent) MCP server — multi-district read + message/document write\",\n  \"author\": \"Claude Code (AI) <https://www.anthropic.com/claude>\",\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"bin\": {\n    \"infinitecampus-mcp\": \"dist/index.js\"\n  },\n  \"files\": [\"dist\", \".claude-plugin\", \"skills\", \".mcp.json\"],\n  \"scripts\": {\n    \"build\": \"tsc && npm run bundle\",\n    \"bundle\": \"esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --outfile=dist/bundle.js\",\n    \"dev\": \"node --env-file=.env dist/index.js\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\"\n  },\n  \"dependencies\": {\n    \"@modelcontextprotocol/sdk\": \"^1.29.0\",\n    \"dotenv\": \"^17.4.0\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^25.5.2\",\n    \"@vitest/coverage-v8\": \"^4.1.2\",\n    \"esbuild\": \"^0.28.0\",\n    \"typescript\": \"^6.0.2\",\n    \"vitest\": \"^4.1.2\"\n  }\n}\n```\n\n- [ ] **Step 2: Write `tsconfig.json`**\n\n```json\n{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n```\n\n- [ ] **Step 3: Write `vitest.config.ts`**\n\n```ts\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  test: {\n    coverage: {\n      provider: 'v8',\n      include: ['src/**/*.ts'],\n      exclude: ['src/index.ts'],\n      thresholds: { lines: 100, functions: 100, branches: 100, statements: 100 },\n    },\n  },\n});\n```\n\n- [ ] **Step 4: Write `.gitignore`**\n\n```\nnode_modules/\ndist/\ncoverage/\n.env\n*.log\n.DS_Store\n```\n\n- [ ] **Step 5: Write `.env.example`**\n\n```\n# Each Infinite Campus parent account is a numbered IC_N_* group.\n# Loader scans IC_1_*, IC_2_*, ... until it hits a gap.\n\nIC_1_NAME=anoka\nIC_1_BASE_URL=https://anoka.infinitecampus.org\nIC_1_DISTRICT=anoka\nIC_1_USERNAME=parent@example.com\nIC_1_PASSWORD=\n\nIC_2_NAME=\nIC_2_BASE_URL=\nIC_2_DISTRICT=\nIC_2_USERNAME=\nIC_2_PASSWORD=\n\nIC_3_NAME=\nIC_3_BASE_URL=\nIC_3_DISTRICT=\nIC_3_USERNAME=\nIC_3_PASSWORD=\n```\n\n- [ ] **Step 6: Install deps**\n\nRun: `npm install`\nExpected: `node_modules/` populated, `package-lock.json` created.\n\n- [ ] **Step 7: Commit**\n\n```bash\ngit add package.json package-lock.json tsconfig.json vitest.config.ts .gitignore .env.example\ngit commit -m \"chore: project scaffold (package.json, tsconfig, vitest, env example)\"\n```\n\n---\n\n### Task 2: Stub `src/index.ts` so the project builds\n\n**Files:**\n- Create: `src/index.ts`\n\n- [ ] **Step 1: Write minimal entry**\n\n```ts\n#!/usr/bin/env node\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\n\nconst server = new McpServer({ name: 'infinitecampus', version: '0.1.0' });\n\nconsole.error('[infinitecampus-mcp] This project was developed and is maintained by AI (Claude). Use at your own discretion.');\n\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n```\n\n- [ ] **Step 2: Verify build**\n\nRun: `npm run build`\nExpected: `dist/index.js` and `dist/bundle.js` produced, no errors.\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add src/index.ts\ngit commit -m \"chore: stub MCP server entry\"\n```\n\n---\n\n## Phase 2 — Config\n\n### Task 3: TDD `loadAccounts()` — happy path + gap-stops-scan\n\n**Files:**\n- Create: `src/config.ts`\n- Create: `tests/config.test.ts`\n\n- [ ] **Step 1: Write the failing test**\n\n```ts\n// tests/config.test.ts\nimport { describe, it, expect } from 'vitest';\nimport { loadAccounts } from '../src/config.js';\n\nconst baseEnv = {\n  IC_1_NAME: 'anoka',\n  IC_1_BASE_URL: 'https://anoka.infinitecampus.org',\n  IC_1_DISTRICT: 'anoka',\n  IC_1_USERNAME: 'parent@example.com',\n  IC_1_PASSWORD: 'secret',\n};\n\ndescribe('loadAccounts', () => {\n  it('parses a single account', () => {\n    const accounts = loadAccounts(baseEnv);\n    expect(accounts).toEqual([{\n      name: 'anoka',\n      baseUrl: 'https://anoka.infinitecampus.org',\n      district: 'anoka',\n      username: 'parent@example.com',\n      password: 'secret',\n    }]);\n  });\n\n  it('parses three sequential accounts', () => {\n    const env = {\n      ...baseEnv,\n      IC_2_NAME: 'mpls', IC_2_BASE_URL: 'https://mpls.infinitecampus.org',\n      IC_2_DISTRICT: 'mpls', IC_2_USERNAME: 'u2', IC_2_PASSWORD: 'p2',\n      IC_3_NAME: 'stp', IC_3_BASE_URL: 'https://stp.infinitecampus.org',\n      IC_3_DISTRICT: 'stp', IC_3_USERNAME: 'u3', IC_3_PASSWORD: 'p3',\n    };\n    expect(loadAccounts(env)).toHaveLength(3);\n  });\n\n  it('stops scanning at the first gap', () => {\n    const env = {\n      ...baseEnv,\n      // IC_2_* deliberately missing\n      IC_3_NAME: 'stp', IC_3_BASE_URL: 'https://stp.infinitecampus.org',\n      IC_3_DISTRICT: 'stp', IC_3_USERNAME: 'u3', IC_3_PASSWORD: 'p3',\n    };\n    expect(loadAccounts(env)).toHaveLength(1);\n  });\n});\n```\n\n- [ ] **Step 2: Run test to verify it fails**\n\nRun: `npm test`\nExpected: FAIL — module `src/config.js` not found.\n\n- [ ] **Step 3: Write `src/config.ts`**\n\n```ts\nexport interface Account {\n  name: string;\n  baseUrl: string;\n  district: string;\n  username: string;\n  password: string;\n}\n\nconst FIELDS = ['NAME', 'BASE_URL', 'DISTRICT', 'USERNAME', 'PASSWORD'] as const;\n\nfunction readSlot(env: NodeJS.ProcessEnv | Record<string, string | undefined>, n: number) {\n  const out: Partial<Record<typeof FIELDS[number], string>> = {};\n  let any = false;\n  for (const f of FIELDS) {\n    const v = env[`IC_${n}_${f}`];\n    if (v && v.length > 0) {\n      out[f] = v;\n      any = true;\n    }\n  }\n  return { any, fields: out };\n}\n\nexport function loadAccounts(env = process.env): Account[] {\n  const accounts: Account[] = [];\n  const seenNames = new Set<string>();\n\n  for (let n = 1; n < 1000; n++) {\n    const { any, fields } = readSlot(env, n);\n    if (!any) break;\n\n    const missing = FIELDS.filter((f) => !fields[f]);\n    if (missing.length > 0) {\n      throw new Error(\n        `Account IC_${n} is incomplete: missing ${missing.join(', ')}. ` +\n        `Required vars: ${FIELDS.join(', ')}.`,\n      );\n    }\n\n    if (seenNames.has(fields.NAME!)) {\n      throw new Error(\n        `Duplicate district name '${fields.NAME}' in IC_${n}. Names must be unique.`,\n      );\n    }\n    seenNames.add(fields.NAME!);\n\n    if (!/^https:\\/\\//.test(fields.BASE_URL!)) {\n      throw new Error(`IC_${n}_BASE_URL is not a valid https URL: '${fields.BASE_URL}'`);\n    }\n\n    accounts.push({\n      name: fields.NAME!,\n      baseUrl: fields.BASE_URL!.replace(/\\/$/, ''),\n      district: fields.DISTRICT!,\n      username: fields.USERNAME!,\n      password: fields.PASSWORD!,\n    });\n  }\n\n  if (accounts.length === 0) {\n    throw new Error(\n      'No Infinite Campus accounts configured. Set IC_1_NAME, IC_1_BASE_URL, ' +\n      'IC_1_DISTRICT, IC_1_USERNAME, IC_1_PASSWORD (and IC_2_*, IC_3_* for more).',\n    );\n  }\n\n  return accounts;\n}\n```\n\n- [ ] **Step 4: Run test to verify it passes**\n\nRun: `npm test`\nExpected: PASS for all 3 happy-path tests.\n\n- [ ] **Step 5: Commit**\n\n```bash\ngit add src/config.ts tests/config.test.ts\ngit commit -m \"feat(config): loadAccounts parses IC_N_* env vars with gap-stop scan\"\n```\n\n---\n\n### Task 4: TDD `loadAccounts()` — error paths\n\n**Files:**\n- Modify: `tests/config.test.ts`\n\n- [ ] **Step 1: Add failing tests for each error path**\n\n```ts\ndescribe('loadAccounts errors', () => {\n  it('throws when no accounts configured', () => {\n    expect(() => loadAccounts({})).toThrow(/No Infinite Campus accounts configured/);\n  });\n\n  it('throws on partial account', () => {\n    const env = { ...baseEnv, IC_1_PASSWORD: undefined as unknown as string };\n    expect(() => loadAccounts(env)).toThrow(/Account IC_1 is incomplete: missing PASSWORD/);\n  });\n\n  it('throws on duplicate district name', () => {\n    const env = {\n      ...baseEnv,\n      IC_2_NAME: 'anoka',\n      IC_2_BASE_URL: 'https://anoka2.infinitecampus.org',\n      IC_2_DISTRICT: 'anoka', IC_2_USERNAME: 'u2', IC_2_PASSWORD: 'p2',\n    };\n    expect(() => loadAccounts(env)).toThrow(/Duplicate district name 'anoka' in IC_2/);\n  });\n\n  it('throws on non-https BASE_URL', () => {\n    const env = { ...baseEnv, IC_1_BASE_URL: 'http://anoka.infinitecampus.org' };\n    expect(() => loadAccounts(env)).toThrow(/IC_1_BASE_URL is not a valid https URL/);\n  });\n\n  it('strips trailing slash from BASE_URL', () => {\n    const env = { ...baseEnv, IC_1_BASE_URL: 'https://anoka.infinitecampus.org/' };\n    expect(loadAccounts(env)[0].baseUrl).toBe('https://anoka.infinitecampus.org');\n  });\n});\n```\n\n- [ ] **Step 2: Run tests**\n\nRun: `npm test`\nExpected: All PASS — error paths are already implemented in Task 3.\n\n- [ ] **Step 3: Verify coverage**\n\nRun: `npx vitest run --coverage`\nExpected: `src/config.ts` at 100% lines/branches/functions.\n\n- [ ] **Step 4: Commit**\n\n```bash\ngit add tests/config.test.ts\ngit commit -m \"test(config): cover all error paths\"\n```\n\n---\n\n## Phase 3 — Client\n\n### Task 5: `ICClient` skeleton + `listDistricts`\n\n**Files:**\n- Create: `src/client.ts`\n- Create: `tests/client.test.ts`\n\n- [ ] **Step 1: Write the failing test**\n\n```ts\n// tests/client.test.ts\nimport { describe, it, expect } from 'vitest';\nimport { ICClient } from '../src/client.js';\nimport type { Account } from '../src/config.js';\n\nconst accounts: Account[] = [\n  { name: 'anoka', baseUrl: 'https://anoka.infinitecampus.org', district: 'anoka',\n    username: 'u', password: 'p' },\n  { name: 'mpls', baseUrl: 'https://mpls.infinitecampus.org', district: 'mpls',\n    username: 'u', password: 'p' },\n];\n\ndescribe('ICClient.listDistricts', () => {\n  it('returns name + baseUrl for each configured account, no creds', () => {\n    const client = new ICClient(accounts);\n    expect(client.listDistricts()).toEqual([\n      { name: 'anoka', baseUrl: 'https://anoka.infinitecampus.org' },\n      { name: 'mpls', baseUrl: 'https://mpls.infinitecampus.org' },\n    ]);\n  });\n});\n```\n\n- [ ] **Step 2: Run test to verify it fails**\n\nRun: `npm test`\nExpected: FAIL — `src/client.js` not found.\n\n- [ ] **Step 3: Write `src/client.ts` skeleton**\n\n```ts\nimport { writeFile } from 'fs/promises';\nimport { dirname } from 'path';\nimport { mkdir, stat } from 'fs/promises';\nimport type { Account } from './config.js';\n\ninterface Session {\n  cookie: string;          // serialized \"name=value; name2=value2\" header\n  loggedInAt: number;\n  loginInFlight: Promise<void> | null;\n}\n\nconst SESSION_TTL_MS = 5 * 60 * 60 * 1000; // 5h, slightly under IC's typical 6h\n\nexport interface RequestOpts {\n  method?: 'GET' | 'POST';\n  body?: BodyInit;\n  headers?: Record<string, string>;\n}\n\nexport class ICClient {\n  private accounts = new Map<string, Account>();\n  private sessions = new Map<string, Session>();\n\n  constructor(accounts: Account[]) {\n    for (const a of accounts) this.accounts.set(a.name, a);\n  }\n\n  listDistricts(): { name: string; baseUrl: string }[] {\n    return [...this.accounts.values()].map((a) => ({ name: a.name, baseUrl: a.baseUrl }));\n  }\n}\n\nexport function makeUnknownDistrictError(district: string, available: string[]) {\n  return new UnknownDistrictError(district, available);\n}\n\nexport class UnknownDistrictError extends Error {\n  constructor(public district: string, public available: string[]) {\n    super(`Unknown district '${district}'. Configured: [${available.join(', ')}]`);\n    this.name = 'UnknownDistrictError';\n  }\n}\n\nexport class AuthFailedError extends Error {\n  constructor(public district: string) {\n    super(`Login failed for district '${district}'. Check IC_N_USERNAME and IC_N_PASSWORD.`);\n    this.name = 'AuthFailedError';\n  }\n}\n\nexport class PortalUnreachableError extends Error {\n  constructor(public district: string, public status: number) {\n    super(`Portal unreachable for district '${district}' (status ${status})`);\n    this.name = 'PortalUnreachableError';\n  }\n}\n\nexport class SessionExpiredError extends Error {\n  constructor(public district: string) {\n    super(`Session expired for district '${district}' after re-login retry`);\n    this.name = 'SessionExpiredError';\n  }\n}\n```\n\n- [ ] **Step 4: Run test to verify it passes**\n\nRun: `npm test`\nExpected: PASS for `listDistricts` test.\n\n- [ ] **Step 5: Commit**\n\n```bash\ngit add src/client.ts tests/client.test.ts\ngit commit -m \"feat(client): ICClient skeleton with listDistricts and error classes\"\n```\n\n---\n\n### Task 6: `ICClient.request` — login + cookie capture + happy GET\n\n**Note for implementer:** Before writing the login implementation, read [`schwartzpub/ic_parent_api/__init__.py`](https://github.com/schwartzpub/ic_parent_api) and locate the `_login` method. Capture the exact GET/POST sequence and form fields it uses. The skeleton below uses a generic shape; replace with the verified one.\n\n**Files:**\n- Modify: `src/client.ts`\n- Modify: `tests/client.test.ts`\n\n- [ ] **Step 1: Write failing test**\n\n```ts\n// add to tests/client.test.ts\nimport { vi, beforeEach, afterEach } from 'vitest';\n\ndescribe('ICClient.request — login + GET', () => {\n  let fetchSpy: ReturnType<typeof vi.spyOn>;\n\n  beforeEach(() => {\n    fetchSpy = vi.spyOn(globalThis, 'fetch');\n  });\n  afterEach(() => vi.restoreAllMocks());\n\n  function mockLoginThenGet(jsonData: unknown) {\n    // 1st call: GET login form → 200 with Set-Cookie: JSESSIONID=...\n    // 2nd call: POST login → 302 redirect (followed automatically), success\n    // 3rd call: GET data → 200 JSON\n    fetchSpy\n      .mockResolvedValueOnce(new Response('', {\n        status: 200,\n        headers: { 'set-cookie': 'JSESSIONID=abc123; Path=/; HttpOnly' },\n      }))\n      .mockResolvedValueOnce(new Response('', {\n        status: 200,\n        headers: { 'set-cookie': 'JSESSIONID=session-after-login; Path=/' },\n      }))\n      .mockResolvedValueOnce(new Response(JSON.stringify(jsonData), {\n        status: 200,\n        headers: { 'content-type': 'application/json' },\n      }));\n  }\n\n  it('logs in lazily on first request, reuses cookie on second', async () => {\n    const client = new ICClient(accounts);\n    mockLoginThenGet({ ok: true });\n\n    const result = await client.request<{ ok: boolean }>('anoka', '/campus/api/portal/parents/students');\n\n    expect(result).toEqual({ ok: true });\n    expect(fetchSpy).toHaveBeenCalledTimes(3); // GET login, POST login, GET data\n\n    // 2nd request: only one new fetch (data only, login reused)\n    fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ ok: 2 }), {\n      status: 200,\n      headers: { 'content-type': 'application/json' },\n    }));\n    await client.request('anoka', '/campus/api/portal/parents/students');\n    expect(fetchSpy).toHaveBeenCalledTimes(4);\n  });\n\n  it('throws UnknownDistrictError when district not configured', async () => {\n    const client = new ICClient(accounts);\n    await expect(client.request('nope', '/x')).rejects.toThrow(/Unknown district 'nope'/);\n  });\n});\n```\n\n- [ ] **Step 2: Run test to verify it fails**\n\nRun: `npm test`\nExpected: FAIL — `request` not implemented.\n\n- [ ] **Step 3: Implement `request` and `login` on `ICClient`**\n\n```ts\n// add to src/client.ts inside ICClient class\n\nasync request<T>(district: string, path: string, opts: RequestOpts = {}): Promise<T> {\n  const account = this.accounts.get(district);\n  if (!account) throw new UnknownDistrictError(district, [...this.accounts.keys()]);\n  await this.ensureSession(account);\n  return this.doRequest<T>(account, path, opts, false);\n}\n\nprivate async ensureSession(account: Account): Promise<void> {\n  let s = this.sessions.get(account.name);\n  if (s && Date.now() - s.loggedInAt < SESSION_TTL_MS) return;\n  if (s?.loginInFlight) { await s.loginInFlight; return; }\n\n  const flight = this.login(account);\n  if (!s) {\n    s = { cookie: '', loggedInAt: 0, loginInFlight: flight };\n    this.sessions.set(account.name, s);\n  } else {\n    s.loginInFlight = flight;\n  }\n  try { await flight; } finally { s.loginInFlight = null; }\n}\n\nprivate async login(account: Account): Promise<void> {\n  // Step A: GET login form to capture initial JSESSIONID\n  const initRes = await fetch(\n    `${account.baseUrl}/campus/portal/parents/${account.district}.jsp`,\n    { redirect: 'manual' },\n  );\n  const initCookie = parseSetCookie(initRes.headers.get('set-cookie'));\n\n  // Step B: POST credentials to verify endpoint\n  const postRes = await fetch(\n    `${account.baseUrl}/campus/verify.jsp?nonBrowser=true&username=${encodeURIComponent(account.username)}&password=${encodeURIComponent(account.password)}&appName=${encodeURIComponent(account.district)}`,\n    {\n      method: 'POST',\n      headers: initCookie ? { Cookie: initCookie } : {},\n      redirect: 'manual',\n    },\n  );\n\n  if (postRes.status >= 500) throw new PortalUnreachableError(account.name, postRes.status);\n  const postCookie = parseSetCookie(postRes.headers.get('set-cookie')) || initCookie;\n  if (!postCookie || postRes.status >= 400) throw new AuthFailedError(account.name);\n\n  this.sessions.set(account.name, {\n    cookie: postCookie,\n    loggedInAt: Date.now(),\n    loginInFlight: null,\n  });\n}\n\nprivate async doRequest<T>(\n  account: Account, path: string, opts: RequestOpts, isRetry: boolean,\n): Promise<T> {\n  const session = this.sessions.get(account.name)!;\n  const res = await fetch(`${account.baseUrl}${path}`, {\n    method: opts.method ?? 'GET',\n    headers: { Cookie: session.cookie, Accept: 'application/json', ...(opts.headers ?? {}) },\n    body: opts.body,\n  });\n\n  if (res.status === 401) {\n    if (isRetry) throw new SessionExpiredError(account.name);\n    this.sessions.delete(account.name);\n    await this.ensureSession(account);\n    return this.doRequest<T>(account, path, opts, true);\n  }\n  if (res.status >= 500) throw new PortalUnreachableError(account.name, res.status);\n  if (!res.ok) throw new Error(`IC ${res.status} ${res.statusText} for ${path}`);\n\n  const text = await res.text();\n  return (text ? JSON.parse(text) : null) as T;\n}\n\n// add at module level\nfunction parseSetCookie(header: string | null): string {\n  if (!header) return '';\n  // Take first cookie's name=value, drop attributes\n  return header.split(',').map((c) => c.split(';')[0].trim()).join('; ');\n}\n```\n\n- [ ] **Step 4: Run test to verify it passes**\n\nRun: `npm test`\nExpected: PASS for both new tests.\n\n- [ ] **Step 5: Commit**\n\n```bash\ngit add src/client.ts tests/client.test.ts\ngit commit -m \"feat(client): lazy per-district login, cookie reuse, request method\"\n```\n\n---\n\n### Task 7: `ICClient.request` — 401 retry + concurrent login dedup\n\n**Files:**\n- Modify: `tests/client.test.ts`\n\n- [ ] **Step 1: Write failing tests**\n\n```ts\ndescribe('ICClient.request — retry + concurrency', () => {\n  let fetchSpy: ReturnType<typeof vi.spyOn>;\n  beforeEach(() => { fetchSpy = vi.spyOn(globalThis, 'fetch'); });\n  afterEach(() => vi.restoreAllMocks());\n\n  it('re-logs in once on 401 and retries', async () => {\n    fetchSpy\n      // First login\n      .mockResolvedValueOnce(new Response('', { status: 200,\n        headers: { 'set-cookie': 'JSESSIONID=a; Path=/' } }))\n      .mockResolvedValueOnce(new Response('', { status: 200,\n        headers: { 'set-cookie': 'JSESSIONID=b; Path=/' } }))\n      // GET returns 401\n      .mockResolvedValueOnce(new Response('', { status: 401 }))\n      // Re-login\n      .mockResolvedValueOnce(new Response('', { status: 200,\n        headers: { 'set-cookie': 'JSESSIONID=c; Path=/' } }))\n      .mockResolvedValueOnce(new Response('', { status: 200,\n        headers: { 'set-cookie': 'JSESSIONID=d; Path=/' } }))\n      // Retry succeeds\n      .mockResolvedValueOnce(new Response(JSON.stringify({ ok: true }), {\n        status: 200, headers: { 'content-type': 'application/json' },\n      }));\n\n    const client = new ICClient(accounts);\n    const result = await client.request('anoka', '/x');\n    expect(result).toEqual({ ok: true });\n    expect(fetchSpy).toHaveBeenCalledTimes(6);\n  });\n\n  it('throws SessionExpiredError on second 401', async () => {\n    fetchSpy\n      .mockResolvedValueOnce(new Response('', { status: 200, headers: { 'set-cookie': 'JSESSIONID=a' } }))\n      .mockResolvedValueOnce(new Response('', { status: 200, headers: { 'set-cookie': 'JSESSIONID=b' } }))\n      .mockResolvedValueOnce(new Response('', { status: 401 }))\n      .mockResolvedValueOnce(new Response('', { status: 200, headers: { 'set-cookie': 'JSESSIONID=c' } }))\n      .mockResolvedValueOnce(new Response('', { status: 200, headers: { 'set-cookie': 'JSESSIONID=d' } }))\n      .mockResolvedValueOnce(new Response('', { status: 401 }));\n\n    const client = new ICClient(accounts);\n    await expect(client.request('anoka', '/x')).rejects.toThrow(/Session expired/);\n  });\n\n  it('shares a single in-flight login across concurrent requests to same district', async () => {\n    let loginCount = 0;\n    fetchSpy.mockImplementation(async (url) => {\n      const u = String(url);\n      if (u.includes('/campus/portal/parents/')) {\n        return new Response('', { status: 200, headers: { 'set-cookie': 'JSESSIONID=a' } });\n      }\n      if (u.includes('/campus/verify.jsp')) {\n        loginCount++;\n        return new Response('', { status: 200, headers: { 'set-cookie': 'JSESSIONID=b' } });\n      }\n      return new Response(JSON.stringify({ ok: 1 }), {\n        status: 200, headers: { 'content-type': 'application/json' },\n      });\n    });\n\n    const client = new ICClient(accounts);\n    await Promise.all([\n      client.request('anoka', '/x'),\n      client.request('anoka', '/y'),\n      client.request('anoka', '/z'),\n    ]);\n    expect(loginCount).toBe(1);\n  });\n\n  it('logs in independently for each district', async () => {\n    let loginCount = 0;\n    fetchSpy.mockImplementation(async (url) => {\n      const u = String(url);\n      if (u.includes('/verify.jsp')) loginCount++;\n      if (u.endsWith('.jsp') || u.includes('/verify.jsp')) {\n        return new Response('', { status: 200, headers: { 'set-cookie': 'JSESSIONID=x' } });\n      }\n      return new Response(JSON.stringify({ ok: 1 }), {\n        status: 200, headers: { 'content-type': 'application/json' },\n      });\n    });\n\n    const client = new ICClient(accounts);\n    await Promise.all([\n      client.request('anoka', '/x'),\n      client.request('mpls', '/y'),\n    ]);\n    expect(loginCount).toBe(2);\n  });\n});\n```\n\n- [ ] **Step 2: Run tests**\n\nRun: `npm test`\nExpected: PASS for retry tests; the concurrent-dedup test may FAIL on first run if the implementation kicks off two logins. Inspect and adjust `ensureSession` so the in-flight promise is set BEFORE the first await point.\n\n- [ ] **Step 3: Fix any concurrency issue if needed**\n\nIf concurrent-dedup test fails, ensure `ensureSession` reads the existing session entry, sets `loginInFlight` synchronously, then awaits — without yielding the event loop in between.\n\n- [ ] **Step 4: Commit**\n\n```bash\ngit add tests/client.test.ts src/client.ts\ngit commit -m \"test(client): cover 401 retry, session expiry, concurrent login dedup\"\n```\n\n---\n\n### Task 8: `ICClient.download` — stream PDF to disk\n\n**Files:**\n- Modify: `src/client.ts`\n- Modify: `tests/client.test.ts`\n\n- [ ] **Step 1: Write failing test**\n\n```ts\nimport { mkdtemp, readFile, rm, writeFile as fsWriteFile } from 'fs/promises';\nimport { tmpdir } from 'os';\nimport { join } from 'path';\n\ndescribe('ICClient.download', () => {\n  let dir: string;\n  beforeEach(async () => { dir = await mkdtemp(join(tmpdir(), 'ic-test-')); });\n  afterEach(async () => { await rm(dir, { recursive: true, force: true }); vi.restoreAllMocks(); });\n\n  it('writes response body to destinationPath and returns metadata', async () => {\n    const fetchSpy = vi.spyOn(globalThis, 'fetch');\n    fetchSpy\n      .mockResolvedValueOnce(new Response('', { status: 200, headers: { 'set-cookie': 'JSESSIONID=a' } }))\n      .mockResolvedValueOnce(new Response('', { status: 200, headers: { 'set-cookie': 'JSESSIONID=b' } }))\n      .mockResolvedValueOnce(new Response(new Uint8Array([1,2,3,4,5]), {\n        status: 200,\n        headers: { 'content-type': 'application/pdf' },\n      }));\n\n    const client = new ICClient(accounts);\n    const dest = join(dir, 'report.pdf');\n    const meta = await client.download('anoka', '/campus/path/to/doc', dest);\n\n    expect(meta).toEqual({ path: dest, bytes: 5, contentType: 'application/pdf' });\n    expect((await readFile(dest)).length).toBe(5);\n  });\n\n  it('throws InvalidPath when destination is a directory', async () => {\n    const client = new ICClient(accounts);\n    await expect(client.download('anoka', '/x', dir)).rejects.toThrow(/InvalidPath|destinationPath/);\n  });\n\n  it('throws ParentDirectoryMissing when parent dir does not exist', async () => {\n    const client = new ICClient(accounts);\n    await expect(client.download('anoka', '/x', join(dir, 'nope', 'x.pdf'))).rejects.toThrow(/ParentDirectoryMissing/);\n  });\n\n  it('throws FileExists when file is present and overwrite not set', async () => {\n    const dest = join(dir, 'r.pdf');\n    await fsWriteFile(dest, 'hi');\n    const client = new ICClient(accounts);\n    await expect(client.download('anoka', '/x', dest)).rejects.toThrow(/FileExists/);\n  });\n\n  it('overwrites when overwrite:true', async () => {\n    const fetchSpy = vi.spyOn(globalThis, 'fetch');\n    fetchSpy\n      .mockResolvedValueOnce(new Response('', { status: 200, headers: { 'set-cookie': 'JSESSIONID=a' } }))\n      .mockResolvedValueOnce(new Response('', { status: 200, headers: { 'set-cookie': 'JSESSIONID=b' } }))\n      .mockResolvedValueOnce(new Response(new Uint8Array([9,9,9]), {\n        status: 200, headers: { 'content-type': 'application/pdf' },\n      }));\n    const dest = join(dir, 'r.pdf');\n    await fsWriteFile(dest, 'old');\n    const client = new ICClient(accounts);\n    const meta = await client.download('anoka', '/x', dest, { overwrite: true });\n    expect(meta.bytes).toBe(3);\n  });\n});\n```\n\n- [ ] **Step 2: Run test to verify it fails**\n\nRun: `npm test`\nExpected: FAIL — `download` not implemented.\n\n- [ ] **Step 3: Add `download` and error classes**\n\n```ts\n// add to src/client.ts\n\nexport class InvalidPathError extends Error {\n  constructor(public path: string) {\n    super(`InvalidPath: destinationPath must be a filename, not a directory: ${path}`);\n    this.name = 'InvalidPathError';\n  }\n}\nexport class ParentDirectoryMissingError extends Error {\n  constructor(public path: string) {\n    super(`ParentDirectoryMissing: ${path}`);\n    this.name = 'ParentDirectoryMissingError';\n  }\n}\nexport class FileExistsError extends Error {\n  constructor(public path: string) {\n    super(`FileExists at ${path}. Pass overwrite:true to replace.`);\n    this.name = 'FileExistsError';\n  }\n}\n\n// inside ICClient class\nasync download(\n  district: string, path: string, destinationPath: string,\n  opts: { overwrite?: boolean } = {},\n): Promise<{ path: string; bytes: number; contentType: string }> {\n  // Pre-flight checks before authenticating, so we fail fast on bad paths\n  let destStat: Awaited<ReturnType<typeof stat>> | null = null;\n  try { destStat = await stat(destinationPath); } catch { /* not present, ok */ }\n  if (destStat?.isDirectory()) throw new InvalidPathError(destinationPath);\n  if (destStat && !opts.overwrite) throw new FileExistsError(destinationPath);\n\n  const parent = dirname(destinationPath);\n  try { await stat(parent); } catch { throw new ParentDirectoryMissingError(parent); }\n\n  const account = this.accounts.get(district);\n  if (!account) throw new UnknownDistrictError(district, [...this.accounts.keys()]);\n  await this.ensureSession(account);\n  const session = this.sessions.get(account.name)!;\n\n  const res = await fetch(`${account.baseUrl}${path}`, { headers: { Cookie: session.cookie } });\n  if (!res.ok) throw new Error(`IC download ${res.status} for ${path}`);\n\n  const buf = new Uint8Array(await res.arrayBuffer());\n  await writeFile(destinationPath, buf);\n  return {\n    path: destinationPath,\n    bytes: buf.byteLength,\n    contentType: res.headers.get('content-type') ?? 'application/octet-stream',\n  };\n}\n```\n\n- [ ] **Step 4: Run tests**\n\nRun: `npm test`\nExpected: PASS for all download tests.\n\n- [ ] **Step 5: Commit**\n\n```bash\ngit add src/client.ts tests/client.test.ts\ngit commit -m \"feat(client): download() streams to disk with path safety checks\"\n```\n\n---\n\n## Phase 4 — First two tools end-to-end\n\n### Task 9: `ic_list_districts`\n\n**Files:**\n- Create: `src/tools/districts.ts`\n- Create: `tests/tools/districts.test.ts`\n\n- [ ] **Step 1: Write failing test**\n\n```ts\n// tests/tools/districts.test.ts\nimport { describe, it, expect, vi, afterEach } from 'vitest';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { ICClient } from '../../src/client.js';\nimport { registerDistrictTools } from '../../src/tools/districts.js';\n\ntype ToolHandler = (args: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text: string }> }>;\n\nconst accounts = [\n  { name: 'anoka', baseUrl: 'https://anoka.infinitecampus.org', district: 'anoka', username: 'u', password: 'p' },\n  { name: 'mpls', baseUrl: 'https://mpls.infinitecampus.org', district: 'mpls', username: 'u', password: 'p' },\n];\n\nlet handlers: Map<string, ToolHandler>;\n\nfunction setup() {\n  const client = new ICClient(accounts);\n  const server = new McpServer({ name: 'test', version: '0.0.0' });\n  handlers = new Map();\n  vi.spyOn(server, 'registerTool').mockImplementation((name: string, _config: unknown, cb: unknown) => {\n    handlers.set(name, cb as ToolHandler);\n    return undefined as never;\n  });\n  registerDistrictTools(server, client);\n  return client;\n}\n\nafterEach(() => vi.restoreAllMocks());\n\ndescribe('ic_list_districts', () => {\n  it('returns configured districts (no creds)', async () => {\n    setup();\n    const result = await handlers.get('ic_list_districts')!({});\n    const data = JSON.parse(result.content[0].text);\n    expect(data).toEqual([\n      { name: 'anoka', baseUrl: 'https://anoka.infinitecampus.org' },\n      { name: 'mpls', baseUrl: 'https://mpls.infinitecampus.org' },\n    ]);\n  });\n});\n```\n\n- [ ] **Step 2: Run test to verify it fails**\n\nRun: `npm test`\nExpected: FAIL — module `src/tools/districts.js` not found.\n\n- [ ] **Step 3: Implement `src/tools/districts.ts`**\n\n```ts\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { ICClient } from '../client.js';\n\nexport function registerDistrictTools(server: McpServer, client: ICClient): void {\n  server.registerTool('ic_list_districts', {\n    description: 'List Infinite Campus districts configured for this MCP server. Returns names + base URLs (no credentials).',\n    annotations: { readOnlyHint: true },\n  }, async () => {\n    const data = client.listDistricts();\n    return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] };\n  });\n}\n```\n\n- [ ] **Step 4: Run test**\n\nRun: `npm test`\nExpected: PASS.\n\n- [ ] **Step 5: Commit**\n\n```bash\ngit add src/tools/districts.ts tests/tools/districts.test.ts\ngit commit -m \"feat(tools): ic_list_districts\"\n```\n\n---\n\n### Task 10: `ic_list_students` (first network tool)\n\n**Note for implementer:** Read [`ic_parent_api`'s student listing endpoint](https://github.com/schwartzpub/ic_parent_api) (look for `students` method) to confirm the exact path. The skeleton uses `/campus/api/portal/parents/students`; verify and adjust.\n\n**Files:**\n- Create: `src/tools/students.ts`\n- Create: `tests/tools/students.test.ts`\n\n- [ ] **Step 1: Write failing test**\n\n```ts\n// tests/tools/students.test.ts\nimport { describe, it, expect, vi, afterEach } from 'vitest';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { ICClient } from '../../src/client.js';\nimport { registerStudentTools } from '../../src/tools/students.js';\n\ntype ToolHandler = (args: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text: string }> }>;\n\nconst accounts = [{ name: 'anoka', baseUrl: 'https://anoka.infinitecampus.org', district: 'anoka', username: 'u', password: 'p' }];\n\nlet handlers: Map<string, ToolHandler>;\n\nfunction setup(returnValue: unknown) {\n  const client = new ICClient(accounts);\n  vi.spyOn(client, 'request').mockResolvedValue(returnValue);\n  const server = new McpServer({ name: 'test', version: '0.0.0' });\n  handlers = new Map();\n  vi.spyOn(server, 'registerTool').mockImplementation((name: string, _c: unknown, cb: unknown) => {\n    handlers.set(name, cb as ToolHandler);\n    return undefined as never;\n  });\n  registerStudentTools(server, client);\n  return client;\n}\n\nafterEach(() => vi.restoreAllMocks());\n\ndescribe('ic_list_students', () => {\n  it('calls the parent students endpoint for the given district', async () => {\n    const raw = [{ personID: 12345, firstName: 'Alex', lastName: 'Doe', grade: '07' }];\n    const client = setup(raw);\n    const result = await handlers.get('ic_list_students')!({ district: 'anoka' });\n    expect(client.request).toHaveBeenCalledWith('anoka', expect.stringContaining('/students'));\n    expect(JSON.parse(result.content[0].text)).toEqual(raw);\n  });\n\n  it('rejects when district arg is missing', async () => {\n    setup([]);\n    await expect(handlers.get('ic_list_students')!({})).rejects.toThrow();\n  });\n});\n```\n\n- [ ] **Step 2: Run test to verify it fails**\n\nRun: `npm test`\nExpected: FAIL — module `src/tools/students.js` not found.\n\n- [ ] **Step 3: Implement `src/tools/students.ts`**\n\n```ts\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport type { ICClient } from '../client.js';\n\nexport function registerStudentTools(server: McpServer, client: ICClient): void {\n  server.registerTool('ic_list_students', {\n    description: 'List students enrolled under the parent account for a given district.',\n    annotations: { readOnlyHint: true },\n    inputSchema: {\n      district: z.string().describe('District name from ic_list_districts'),\n    },\n  }, async (args) => {\n    const data = await client.request(args.district, '/campus/api/portal/parents/students');\n    return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] };\n  });\n}\n```\n\n- [ ] **Step 4: Run test**\n\nRun: `npm test`\nExpected: PASS for both tests.\n\n- [ ] **Step 5: Commit**\n\n```bash\ngit add src/tools/students.ts tests/tools/students.test.ts\ngit commit -m \"feat(tools): ic_list_students\"\n```\n\n---\n\n### Task 11: Wire `index.ts` to load config + register all tools so far\n\n**Files:**\n- Modify: `src/index.ts`\n\n- [ ] **Step 1: Update `src/index.ts`**\n\n```ts\n#!/usr/bin/env node\nimport { dirname, join } from 'path';\nimport { fileURLToPath } from 'url';\n\ntry {\n  const { config } = await import('dotenv');\n  const __dirname = dirname(fileURLToPath(import.meta.url));\n  config({ path: join(__dirname, '..', '.env'), override: false });\n} catch {\n  // dotenv not available — rely on process.env\n}\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { loadAccounts } from './config.js';\nimport { ICClient } from './client.js';\nimport { registerDistrictTools } from './tools/districts.js';\nimport { registerStudentTools } from './tools/students.js';\n\nconst accounts = loadAccounts();\nconst client = new ICClient(accounts);\nconst server = new McpServer({ name: 'infinitecampus', version: '0.1.0' });\n\nregisterDistrictTools(server, client);\nregisterStudentTools(server, client);\n\nconsole.error(`[infinitecampus-mcp] Loaded ${accounts.length} district(s): ${accounts.map((a) => a.name).join(', ')}`);\nconsole.error('[infinitecampus-mcp] Developed and maintained by AI (Claude). Use at your own discretion.');\n\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n```\n\n- [ ] **Step 2: Verify build**\n\nRun: `npm run build`\nExpected: builds without errors.\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add src/index.ts\ngit commit -m \"feat: wire config + ICClient + districts/students tools\"\n```\n\n---\n\n## Phase 5 — Read-only domain tools\n\nFor Tasks 12–17 below, follow this same template per tool. Each task is its own commit. The implementer **must** read the relevant section of `ic_parent_api` before each tool to confirm the endpoint path and response shape — the path strings shown are placeholders derived from the spec, not verified.\n\n### Task 12: `ic_get_schedule`\n\n**Files:**\n- Create: `src/tools/schedule.ts`\n- Create: `tests/tools/schedule.test.ts`\n- Modify: `src/index.ts`\n\n**Reference:** `ic_parent_api` — schedule/roster method.\n\n- [ ] **Step 1: Write the test**\n\n```ts\n// tests/tools/schedule.test.ts\nimport { describe, it, expect, vi, afterEach } from 'vitest';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { ICClient } from '../../src/client.js';\nimport { registerScheduleTools } from '../../src/tools/schedule.js';\n\ntype ToolHandler = (args: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text: string }> }>;\nconst accounts = [{ name: 'anoka', baseUrl: 'https://anoka.infinitecampus.org', district: 'anoka', username: 'u', password: 'p' }];\nlet handlers: Map<string, ToolHandler>;\n\nfunction setup(returnValue: unknown) {\n  const client = new ICClient(accounts);\n  vi.spyOn(client, 'request').mockResolvedValue(returnValue);\n  const server = new McpServer({ name: 'test', version: '0.0.0' });\n  handlers = new Map();\n  vi.spyOn(server, 'registerTool').mockImplementation((name: string, _c: unknown, cb: unknown) => {\n    handlers.set(name, cb as ToolHandler); return undefined as never;\n  });\n  registerScheduleTools(server, client);\n  return client;\n}\nafterEach(() => vi.restoreAllMocks());\n\ndescribe('ic_get_schedule', () => {\n  it('calls schedule endpoint with studentId', async () => {\n    const raw = [{ period: 1, course: 'Math', room: '203' }];\n    const client = setup(raw);\n    const result = await handlers.get('ic_get_schedule')!({ district: 'anoka', studentId: '12345' });\n    expect(client.request).toHaveBeenCalledWith('anoka', expect.stringContaining('12345'));\n    expect(JSON.parse(result.content[0].text)).toEqual(raw);\n  });\n\n  it('passes date arg through when provided', async () => {\n    const client = setup([]);\n    await handlers.get('ic_get_schedule')!({ district: 'anoka', studentId: '12345', date: '2026-04-15' });\n    expect(client.request).toHaveBeenCalledWith('anoka', expect.stringContaining('2026-04-15'));\n  });\n\n  it('rejects when studentId is missing', async () => {\n    setup([]);\n    await expect(handlers.get('ic_get_schedule')!({ district: 'anoka' })).rejects.toThrow();\n  });\n});\n```\n\n- [ ] **Step 2: Run test to verify it fails**\n\nRun: `npm test` — Expected: FAIL.\n\n- [ ] **Step 3: Implement `src/tools/schedule.ts`**\n\n```ts\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport type { ICClient } from '../client.js';\n\nexport function registerScheduleTools(server: McpServer, client: ICClient): void {\n  server.registerTool('ic_get_schedule', {\n    description: \"Get a student's class schedule for a given date (default: today).\",\n    annotations: { readOnlyHint: true },\n    inputSchema: {\n      district: z.string().describe('District name from ic_list_districts'),\n      studentId: z.string().describe('Student personID from ic_list_students'),\n      date: z.string().describe('YYYY-MM-DD; defaults to today').optional(),\n      termFilter: z.string().describe('Term name or ID; optional').optional(),\n    },\n  }, async (args) => {\n    const date = args.date ?? new Date().toISOString().slice(0, 10);\n    const params = new URLSearchParams({ personID: args.studentId, date });\n    if (args.termFilter) params.set('term', args.termFilter);\n    const data = await client.request(args.district, `/campus/api/portal/parents/schedule?${params}`);\n    return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] };\n  });\n}\n```\n\n- [ ] **Step 4: Register in `src/index.ts`**\n\nAdd import and `registerScheduleTools(server, client);` call alongside the others.\n\n- [ ] **Step 5: Run tests + build**\n\nRun: `npm test && npm run build`\nExpected: PASS, build succeeds.\n\n- [ ] **Step 6: Commit**\n\n```bash\ngit add src/tools/schedule.ts tests/tools/schedule.test.ts src/index.ts\ngit commit -m \"feat(tools): ic_get_schedule\"\n```\n\n---\n\n### Task 13: `ic_list_assignments`\n\n**Files:**\n- Create: `src/tools/assignments.ts`\n- Create: `tests/tools/assignments.test.ts`\n- Modify: `src/index.ts`\n\n**Reference:** `ic_parent_api` — assignments method.\n\n- [ ] **Step 1: Write the test**\n\n```ts\n// tests/tools/assignments.test.ts\nimport { describe, it, expect, vi, afterEach } from 'vitest';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { ICClient } from '../../src/client.js';\nimport { registerAssignmentTools } from '../../src/tools/assignments.js';\n\ntype ToolHandler = (args: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text: string }> }>;\nconst accounts = [{ name: 'anoka', baseUrl: 'https://anoka.infinitecampus.org', district: 'anoka', username: 'u', password: 'p' }];\nlet handlers: Map<string, ToolHandler>;\n\nfunction setup(returnValue: unknown) {\n  const client = new ICClient(accounts);\n  vi.spyOn(client, 'request').mockResolvedValue(returnValue);\n  const server = new McpServer({ name: 'test', version: '0.0.0' });\n  handlers = new Map();\n  vi.spyOn(server, 'registerTool').mockImplementation((name: string, _c: unknown, cb: unknown) => {\n    handlers.set(name, cb as ToolHandler); return undefined as never;\n  });\n  registerAssignmentTools(server, client);\n  return client;\n}\nafterEach(() => vi.restoreAllMocks());\n\ndescribe('ic_list_assignments', () => {\n  const raw = [\n    { id: 1, courseName: 'Math', title: 'HW1', missing: false, scored: true, points: 10 },\n    { id: 2, courseName: 'Sci', title: 'Lab', missing: true, scored: false, points: null },\n  ];\n\n  it('returns all assignments by default', async () => {\n    setup(raw);\n    const result = await handlers.get('ic_list_assignments')!({ district: 'anoka', studentId: '12345' });\n    expect(JSON.parse(result.content[0].text)).toEqual(raw);\n  });\n\n  it('filters to missingOnly when requested', async () => {\n    setup(raw);\n    const result = await handlers.get('ic_list_assignments')!({\n      district: 'anoka', studentId: '12345', missingOnly: true,\n    });\n    const data = JSON.parse(result.content[0].text);\n    expect(data).toHaveLength(1);\n    expect(data[0].id).toBe(2);\n  });\n\n  it('passes courseId, since, until through to the request URL', async () => {\n    const client = setup([]);\n    await handlers.get('ic_list_assignments')!({\n      district: 'anoka', studentId: '12345', courseId: 'C1', since: '2026-03-01', until: '2026-04-15',\n    });\n    const url = (client.request as ReturnType<typeof vi.fn>).mock.calls[0][1] as string;\n    expect(url).toContain('C1');\n    expect(url).toContain('2026-03-01');\n    expect(url).toContain('2026-04-15');\n  });\n});\n```\n\n- [ ] **Step 2: Run test to verify it fails**\n\nRun: `npm test` — Expected: FAIL.\n\n- [ ] **Step 3: Implement `src/tools/assignments.ts`**\n\n```ts\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport type { ICClient } from '../client.js';\n\ninterface RawAssignment {\n  id: number;\n  courseName: string;\n  title: string;\n  missing: boolean;\n  scored: boolean;\n  points: number | null;\n  due?: string;\n}\n\nexport function registerAssignmentTools(server: McpServer, client: ICClient): void {\n  server.registerTool('ic_list_assignments', {\n    description: \"List a student's assignments. Filterable by course and date range; missingOnly returns only un-submitted past-due work.\",\n    annotations: { readOnlyHint: true },\n    inputSchema: {\n      district: z.string(),\n      studentId: z.string(),\n      courseId: z.string().optional(),\n      since: z.string().describe('YYYY-MM-DD').optional(),\n      until: z.string().describe('YYYY-MM-DD').optional(),\n      missingOnly: z.boolean().optional(),\n    },\n  }, async (args) => {\n    const params = new URLSearchParams({ personID: args.studentId });\n    if (args.courseId) params.set('sectionID', args.courseId);\n    if (args.since) params.set('startDate', args.since);\n    if (args.until) params.set('endDate', args.until);\n    const raw = await client.request<RawAssignment[]>(\n      args.district, `/campus/api/portal/parents/assignments?${params}`,\n    );\n    const data = args.missingOnly ? raw.filter((a) => a.missing) : raw;\n    return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] };\n  });\n}\n```\n\n- [ ] **Step 4: Register in `src/index.ts`** and run `npm test && npm run build`.\n\n- [ ] **Step 5: Commit**\n\n```bash\ngit add src/tools/assignments.ts tests/tools/assignments.test.ts src/index.ts\ngit commit -m \"feat(tools): ic_list_assignments with missingOnly filter\"\n```\n\n---\n\n### Task 14: `ic_list_grades`\n\n**Files:**\n- Create: `src/tools/grades.ts`\n- Create: `tests/tools/grades.test.ts`\n- Modify: `src/index.ts`\n\n**Reference:** `ic_parent_api` — grades method.\n\n- [ ] **Step 1: Write the test**\n\n```ts\n// tests/tools/grades.test.ts\nimport { describe, it, expect, vi, afterEach } from 'vitest';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { ICClient } from '../../src/client.js';\nimport { registerGradeTools } from '../../src/tools/grades.js';\n\ntype ToolHandler = (args: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text: string }> }>;\nconst accounts = [{ name: 'anoka', baseUrl: 'https://anoka.infinitecampus.org', district: 'anoka', username: 'u', password: 'p' }];\nlet handlers: Map<string, ToolHandler>;\n\nfunction setup(returnValue: unknown) {\n  const client = new ICClient(accounts);\n  vi.spyOn(client, 'request').mockResolvedValue(returnValue);\n  const server = new McpServer({ name: 'test', version: '0.0.0' });\n  handlers = new Map();\n  vi.spyOn(server, 'registerTool').mockImplementation((name: string, _c: unknown, cb: unknown) => {\n    handlers.set(name, cb as ToolHandler); return undefined as never;\n  });\n  registerGradeTools(server, client);\n  return client;\n}\nafterEach(() => vi.restoreAllMocks());\n\ndescribe('ic_list_grades', () => {\n  it('calls grades endpoint with studentId', async () => {\n    const raw = [{ courseName: 'Math', grade: 'A-', percent: 91 }];\n    const client = setup(raw);\n    const result = await handlers.get('ic_list_grades')!({ district: 'anoka', studentId: '12345' });\n    expect(client.request).toHaveBeenCalledWith('anoka', expect.stringContaining('12345'));\n    expect(JSON.parse(result.content[0].text)).toEqual(raw);\n  });\n\n  it('passes termId when provided', async () => {\n    const client = setup([]);\n    await handlers.get('ic_list_grades')!({ district: 'anoka', studentId: '12345', termId: 'T3' });\n    const url = (client.request as ReturnType<typeof vi.fn>).mock.calls[0][1] as string;\n    expect(url).toContain('T3');\n  });\n});\n```\n\n- [ ] **Step 2: Run test to verify it fails**\n\nRun: `npm test` — Expected: FAIL.\n\n- [ ] **Step 3: Implement `src/tools/grades.ts`**\n\n```ts\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport\n\nFile v2.1.1:docs/superpowers/specs/2026-04-15-infinitecampus-mcp-design.md\n\n# Infinite Campus MCP — Design\n\n**Date:** 2026-04-15\n**Status:** Approved (brainstorming complete, ready for implementation plan)\n\n## Goal\n\nA Node.js MCP server that exposes the Campus Parent portal of Infinite Campus to\nClaude. Read access to the full parent-facing surface (students, schedules,\ngrades, attendance, assignments, behavior, food service, documents, messages),\nplus message composition and document download. Multi-district from day one — a\nparent with kids in two or three different districts can query and act across\nall of them from one MCP instance.\n\n## Non-goals\n\n- Student-portal authentication (architecture leaves the door open; v1 ships\n  parent only).\n- Food-service payments, account creation, or any flow that touches money.\n- Live integration tests in CI (no shared test districts/credentials exist).\n- Cross-district fan-out tools (the LLM calls per-district tools N times; the\n  MCP doesn't try to merge results internally).\n- PDF rendering or in-context document content (downloads write to disk; LLM\n  receives metadata only).\n\n## References\n\nNo Infinite Campus MCP exists. Three unofficial libraries serve as protocol\nreferences:\n\n- **Primary:** [schwartzpub/ic_parent_api](https://github.com/schwartzpub/ic_parent_api)\n  (Python) — cleanest map of parent-portal endpoints, auth sequence, response\n  shapes. Backs a Home Assistant integration.\n- **Secondary:** [tonyzimbinski/infinite-campus](https://github.com/tonyzimbinski/infinite-campus)\n  (Node) — useful for request-level patterns (cookie jar, login form). Targets\n  student portal, so endpoint paths differ.\n- **Tertiary:** [gilesgc/Infinite-Campus-API](https://github.com/gilesgc/Infinite-Campus-API)\n  (Python) — cross-check for ambiguous endpoints.\n\n## Architecture\n\nMirrors the [ofw-mcp](https://github.com/chrischall/ofw-mcp) repo layout\nexactly, with one new module (`config.ts`) for multi-account support.\n\n```\ninfinitecampus-mcp/\n├── src/\n│   ├── index.ts              # MCP server setup, registers all tool modules\n│   ├── config.ts             # loads IC_N_* env vars → Account[]\n│   ├── client.ts             # ICClient: per-district session pool, request/retry, auth\n│   └── tools/\n│       ├── districts.ts      # ic_list_districts\n│       ├── students.ts       # ic_list_students\n│       ├── schedule.ts       # ic_get_schedule\n│       ├── assignments.ts    # ic_list_assignments\n│       ├── grades.ts         # ic_list_grades\n│       ├── attendance.ts     # ic_list_attendance\n│       ├── messages.ts       # ic_list_messages, ic_get_message,\n│       │                     # ic_send_message, ic_list_message_recipients\n│       ├── behavior.ts       # ic_list_behavior\n│       ├── foodservice.ts    # ic_list_food_service\n│       └── documents.ts      # ic_list_documents, ic_download_document\n├── tests/\n│   ├── config.test.ts\n│   ├── client.test.ts\n│   ├── fixtures/             # sanitized JSON captures from real portals\n│   └── tools/                # one file per tool module\n├── package.json\n├── tsconfig.json\n├── vitest.config.ts\n├── manifest.json\n├── .claude-plugin/\n├── skills/ic/\n└── CLAUDE.md\n```\n\n### Stack\n\nSame as ofw-mcp:\n\n- TypeScript (ESM), bundled by esbuild\n- `@modelcontextprotocol/sdk` for the MCP server + stdio transport\n- `zod` for tool input schemas\n- `dotenv` for local dev\n- `vitest` for tests\n- Cut & Bump GitHub Action for releases (matches ofw-mcp's release flow)\n\n### Key architectural decisions\n\n1. **`config.ts` is new vs ofw-mcp.** Loops `IC_1_*…IC_N_*` (scanning until a\n   gap is hit) and produces a typed `Account[]`. Fails fast at process start\n   for any malformed account.\n2. **`ICClient` owns a `Map<districtName, Session>`.** One cookie jar per\n   district. Lazy login on first request. Re-login once on 401.\n3. **Student-auth door left open.** `ICClient` accepts a `role: 'parent' |\n   'student'` internally; v1 hardcodes `'parent'`. The two roles use different\n   path prefixes (`/parents/` vs `/students/`); a small adapter in the client\n   switches them. Tool signatures stay unchanged when student support is added.\n4. **Tool files follow ofw-mcp shape exactly.** Each exports\n   `register<Domain>Tools(server, client)`, which calls `server.tool(...)` with\n   zod schemas.\n\n## Components\n\n### `config.ts`\n\n```ts\nexport interface Account {\n  name: string;          // IC_N_NAME — used as the `district` arg in tools (must be unique)\n  baseUrl: string;       // IC_N_BASE_URL — e.g. https://anoka.infinitecampus.org\n  district: string;      // IC_N_DISTRICT — district app-name in URL paths\n  username: string;      // IC_N_USERNAME\n  password: string;      // IC_N_PASSWORD\n}\n\nexport function loadAccounts(env = process.env): Account[];\n```\n\nBehavior:\n- Scans `IC_1_*`, `IC_2_*`… until it finds a gap.\n- Initial release supports up to 3 accounts naturally; the loader scales to N.\n- Throws on partial accounts, duplicate names, invalid `https://` URLs, or\n  zero accounts configured.\n\n### `client.ts`\n\n```ts\ninterface Session {\n  cookieJar: string;     // serialized cookie header\n  loggedInAt: number;    // epoch ms; force re-login after ~6h\n}\n\nexport class ICClient {\n  constructor(accounts: Account[]);\n  request<T>(district: string, path: string, opts?: RequestOpts): Promise<T>;\n  download(district: string, path: string, destinationPath: string,\n           opts?: { overwrite?: boolean }): Promise<{ path: string; bytes: number; contentType: string }>;\n  listDistricts(): { name: string; baseUrl: string }[];\n}\n```\n\nBehavior:\n- Cookie jar is a simple `Map<cookieName, value>` serialized to a header\n  (matches ofw-mcp; no external jar lib).\n- Login: GET login page → POST credentials → capture `JSESSIONID`. Sequence\n  derived from `ic_parent_api`.\n- 401 on data request → re-login once, retry the original request once.\n  Second 401 surfaces as `SessionExpired`.\n- Concurrent requests to the same district share a single in-flight login\n  promise (no double-login race).\n- Unknown district arg throws a structured error listing configured names.\n\n### Tool surface\n\nEvery tool takes `district: string` as its first arg.\n\n| Tool | Additional args |\n|---|---|\n| `ic_list_districts` | — |\n| `ic_list_students` | — |\n| `ic_get_schedule` | `studentId`, `date?` (default today), `termFilter?` |\n| `ic_list_assignments` | `studentId`, `courseId?`, `since?`, `until?`, `missingOnly?` |\n| `ic_list_grades` | `studentId`, `termId?` |\n| `ic_list_attendance` | `studentId`, `since?`, `until?` |\n| `ic_list_messages` | `folder?` (inbox/sent), `page?`, `size?` |\n| `ic_get_message` | `messageId` |\n| `ic_list_message_recipients` | `studentId` — teachers + counselors messageable for that student |\n| `ic_send_message` | `recipientIds[]`, `subject`, `body`, `studentId?` |\n| `ic_list_behavior` | `studentId`, `since?`, `until?` |\n| `ic_list_food_service` | `studentId`, `since?`, `until?` |\n| `ic_list_documents` | `studentId` — metadata only: `{id, type, date, downloadUrl}[]` |\n| `ic_download_document` | `documentId`, `destinationPath`, `overwrite?` — writes PDF to disk |\n\nNote: `ic_send_message` and `ic_download_document` are write/IO tools. They\nwill be gated by the harness's standard explicit-permission flow (\"sending\nmessages on behalf of the user\", \"downloading any file\"). No special MCP-side\ncode is required for that — exposing the tool is sufficient.\n\n## Data flow\n\nTypical read tool call:\n\n```\nLLM invokes ic_list_assignments(district=\"anoka\", studentId=\"12345\", missingOnly=true)\n      │\n      ▼\ntool handler in src/tools/assignments.ts\n      │ zod validates args\n      ▼\nclient.request(\"anoka\", \"/campus/api/portal/parents/assignments?studentId=12345\")\n      │\n      ├─ lookup Account for \"anoka\"\n      │     └─ if unknown → throw \"Unknown district 'anoka'. Configured: [...]\"\n      │\n      ├─ lookup Session for \"anoka\"\n      │     └─ if none or stale → login flow, capture JSESSIONID\n      │\n      ├─ send GET with cookie\n      │     └─ if 401 → re-login once, retry\n      │\n      ▼\nraw JSON typed as unknown\n      │ handler trims to documented shape, applies missingOnly filter\n      ▼\nMCP content block: { content: [{ type: \"text\", text: JSON.stringify(trimmed) }] }\n```\n\nDecisions:\n- **Sessions are lazy + per-district.** Querying only Anoka never touches\n  Minneapolis. Process restart resets all sessions.\n- **Response trimming lives in tool handlers, not the client.** Client returns\n  raw `unknown`; each tool owns its output shape. Mirrors ofw-mcp.\n- **No cross-district fan-out.** LLM calls tools N times when it needs N\n  districts. Keeps semantics simple and debuggable.\n- **`ic_download_document` bypasses JSON trimming.** `client.download()`\n  streams the response body to `destinationPath`, returns\n  `{path, bytes, contentType}`.\n\n## Error handling\n\nThree tiers with distinct shapes.\n\n### Tier 1 — Startup (fail fast, exit non-zero)\n\n`config.ts` throws at process start for:\n\n| Cause | Message shape |\n|---|---|\n| No `IC_1_*` vars set | \"No Infinite Campus accounts configured. Set IC_1_NAME, IC_1_BASE_URL, IC_1_DISTRICT, IC_1_USERNAME, IC_1_PASSWORD (and IC_2_*, IC_3_* for more).\" |\n| Partial account | \"Account IC_2 is incomplete: missing PASSWORD. Required vars: NAME, BASE_URL, DISTRICT, USERNAME, PASSWORD.\" |\n| Duplicate `IC_N_NAME` | \"Duplicate district name 'anoka' in IC_1 and IC_3. Names must be unique.\" |\n| Invalid `BASE_URL` | \"IC_2_BASE_URL is not a valid https URL: '...'\" |\n\n### Tier 2 — Per-request (returned as MCP tool errors, recoverable)\n\n| Cause | Result shape |\n|---|---|\n| Unknown district arg | `{ error: \"UnknownDistrict\", message, availableDistricts }` |\n| Login failed (creds) | `{ error: \"AuthFailed\", district, hint }` |\n| Portal unreachable | `{ error: \"PortalUnreachable\", district, status, body? }` |\n| 401 after re-login retry | `{ error: \"SessionExpired\", district }` |\n| 404 | `{ error: \"NotFound\", resource, id }` |\n| District feature disabled | `{ warning: \"FeatureDisabled\", feature, district, data: [] }` (success result, empty data) |\n| Unexpected response shape | `{ error: \"UnexpectedResponse\", district, path, zodIssues }` |\n\n### Tier 3 — Write tools\n\n| Cause | Behavior |\n|---|---|\n| `destinationPath` is a directory | `{ error: \"InvalidPath\", message }` |\n| Parent directory missing | `{ error: \"ParentDirectoryMissing\", path }` (no implicit mkdir) |\n| File exists | `{ error: \"FileExists\", path, hint }` (override with `overwrite: true`) |\n| `ic_send_message` with invalid recipient IDs | `{ error: \"InvalidRecipient\", invalidIds, validIds }` |\n\nCross-cutting rule: errors always include `district` when district-scoped.\n\n## Testing\n\nThree layers, vitest, mirrors ofw-mcp.\n\n### `tests/config.test.ts`\nPure tests of `loadAccounts(env)`. Covers happy path, gap-stops-scan, every\nTier-1 error, secrets-not-logged.\n\n### `tests/client.test.ts`\n`vi.spyOn(globalThis, 'fetch')` returns canned responses. Covers:\n- First request triggers login; second reuses cookie\n- 401 triggers re-login + retry; second 401 surfaces","readmeExcerpt":"Skill: Tmp.ChIoCYDxiX Owner: chrischall Summary: This skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s). Triggers on phrases like \"check grades\", \"what'... Tags: latest:2.3.6 Version history: v2.3.6 | 2026-07-07T23:57:08.914Z | auto Version 2.3.6 of infinitecampus-mcp - No file changes detected in this release. - No new features, fixes, or documentation updates. v","codeSnippets":[],"executableExamples":[{"language":"json","snippet":"{\n  \"mcpServers\": {\n    \"infinitecampus\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"infinitecampus-mcp\"],\n      \"env\": {\n        \"IC_BASE_URL\": \"https://campus.yourdistrict.k12.example.us\",\n        \"IC_DISTRICT\": \"your-district-appname\",\n        \"IC_USERNAME\": \"parent@example.com\",\n        \"IC_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}"},{"language":"bash","snippet":"git clone https://github.com/chrischall/infinitecampus-mcp\ncd infinitecampus-mcp\nnpm install && npm run build"},{"language":"json","snippet":"{\n  \"mcpServers\": {\n    \"infinitecampus\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"infinitecampus-mcp\"],\n      \"env\": {\n        \"IC_BASE_URL\": \"https://campus.yourdistrict.k12.example.us\",\n        \"IC_DISTRICT\": \"your-district-appname\",\n        \"IC_USERNAME\": \"parent@example.com\",\n        \"IC_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}"},{"language":"bash","snippet":"git clone https://github.com/chrischall/infinitecampus-mcp\ncd infinitecampus-mcp\nnpm install && npm run build"},{"language":"json","snippet":"{\n  \"mcpServers\": {\n    \"infinitecampus\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"infinitecampus-mcp\"],\n      \"env\": {\n        \"IC_BASE_URL\": \"https://campus.yourdistrict.k12.example.us\",\n        \"IC_DISTRICT\": \"your-district-appname\",\n        \"IC_USERNAME\": \"parent@example.com\",\n        \"IC_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}"},{"language":"bash","snippet":"git clone https://github.com/chrischall/infinitecampus-mcp\ncd infinitecampus-mcp\nnpm install && npm run build"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: infinitecampus-mcp\ndescription: This skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s). Triggers on phrases like \"check grades\", \"what's my kid's GPA\", \"any new assignments\", \"attendance record\", \"message the teacher\", \"Campus Parent inbox\", \"infinite campus\", or any request involving school grades, assignments, attendance, messages, or documents in Infinite Campus.\n---\n\n# infinitecampus-mcp\n\nMCP server for Infinite Campus Campus Parent — read student grades, attendance, assignments, and messages; download documents; and send messages.\n\n- **npm:** [npmjs.com/package/infinitecampus-mcp](https://www.npmjs.com/package/infinitecampus-mcp)\n- **Source:** [github.com/chrischall/infinitecampus-mcp](https://github.com/chrischall/infinitecampus-mcp)\n\n## Setup\n\n### Option A — npx (recommended)\n\nAdd to `.mcp.json` in your project or `~/.claude/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"infinitecampus\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"infinitecampus-mcp\"],\n      \"env\": {\n        \"IC_BASE_URL\": \"https://campus.yourdistrict.k12.example.us\",\n        \"IC_DISTRICT\": \"your-district-appname\",\n        \"IC_USERNAME\": \"parent@example.com\",\n        \"IC_PASSWORD\": \"yourpassword\"\n      }\n    }\n  }\n}\n```\n\n### Option B — from source\n\n```bash\ngit clone https://github.com/chrischall/infinitecampus-mcp\ncd infinitecampus-mcp\nnpm install && npm run build\n```\n\n## Authentication\n\nTwo paths — the MCP tries them in priority order:\n\n1. **Username/password.** Set `IC_USERNAME` + `IC_PASSWORD` along with `IC_BASE_URL` + `IC_DISTRICT`.\n2. **fetchproxy fallback.** Install the [fetchproxy 0.3.0 extension](https://github.com/chrischall/fetchproxy), sign into your IC portal in the browser, and leave `IC_USERNAME` / `IC_PASSWORD` unset. `IC_BASE_URL` + `IC_DISTRICT` are still required so the MCP knows which host to read cookies from.\n\n`IC_BASE_URL` is your district's portal URL; `IC_DISTRICT` is the app-name path segment from that URL. Set `IC_DISABLE_FETCHPROXY=1` to opt out of the fallback (headless / CI).\n\n## Tools (prefix `ic_`)\n\n### Students & teachers\n- `ic_list_students` — list students linked to your parent account\n- `ic_list_teachers` — list teachers for a student\n\n### Academics\n- `ic_list_grades(studentId)` — class grades\n- `ic_list_recent_grades(studentId)` — recently graded items\n- `ic_list_assignments(studentId)` — current assignments\n- `ic_list_assessments(studentId)` — test scores\n\n### Attendance\n- `ic_list_attendance(studentId)` — attendance summary\n- `ic_list_attendance_events(studentId)` — individual absence/tardy events\n- `ic_list_school_days(studentId)` — calendar days\n\n### Behavior & fees\n- `ic_list_behavior(studentId)` — behavior incidents\n- `ic_list_fees(studentId)` — outstanding fees\n- `ic_list_food_service(studentId)` — cafeteria balance\n\n### Messaging\n- `ic_list_messages` — inbox\n- `ic_get_message(id)` — read a message\n- `ic_list_documents(studentId)` / `ic_download_document(id)` — doc"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-mcp\",\n  \"version\": \"2.3.6\",\n  \"publishedAt\": 1783468628914\n}"},{"path":"skill-card.md","content":"## Description:\n\nProvides an MCP server for Infinite Campus Campus Parent to read student grades, attendance, assignments, messages, documents, and related account data, and to send messages.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nParents or guardians and their authorized agents use this skill to connect an MCP client to Infinite Campus Campus Parent for retrieving student academic, attendance, fee, food-service, document, and message information, and for sending messages through the portal.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill handles sensitive student records, messages, and documents.\n\nMitigation: Use it only with authorized Campus Parent accounts, confirm before reading messages or downloading documents, and avoid exposing retrieved records beyond the intended user.\n\nRisk: The setup recommends persistent school login credentials and unpinned npm execution.\n\nMitigation: Prefer a secret manager or runtime environment variables over project-level credential files, and pin reviewed package versions or commits before installation.\n\nRisk: Security evidence marks the release suspicious because it combines sensitive education data access with credential handling.\n\nMitigation: Review the npm package and source repository before installing, and use the skill only if the publisher and package are trusted.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/infinitecampus-mcp)\n- [npm package: infinitecampus-mcp](https://www.npmjs.com/package/infinitecampus-mcp)\n- [Source repository: infinitecampus-mcp](https://github.com/chrischall/infinitecampus-mcp)\n- [fetchproxy fallback extension](https://github.com/chrischall/fetchproxy)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, code, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown guidance with JSON configuration and shell command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May propose MCP configuration and commands; installed tools can access sensitive student records, messages, and documents.]\n\n## Skill Version(s):\n\n2.3.6 (source: server release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"This skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s). Triggers on phrases like \"check grades\", \"what'... Skill: Tmp.ChIoCYDxiX Owner: chrischall Summary: This skill should be used when the user asks about Infinite Campus (Campus Parent) data for their student(s). Triggers on phrases like \"check grades\", \"what'... Tags: latest:2.3.6 Version history: v2.3.6 | 2026-07-07T23:57:08.914Z | auto Version 2.3.6 of infinitecampus-mcp - No file changes detected in this release. - No new features, fixes, or documentation updates. v","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1145,"uniquenessScore":46,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T11:55:54.457Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-10T11:55:54.457Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T14:49:27.332Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}