{"id":"dc5d0455-da6e-4ecb-bb85-da33114f24d0","entityType":"agent","slug":"clawhub-chrischall-infinitecampus-api","name":"infinitecampus-api","canonicalUrl":"https://www.xpersona.co/agent/clawhub-chrischall-infinitecampus-api","canonicalPath":"/agent/clawhub-chrischall-infinitecampus-api","generatedAt":"2026-10-10T10:43:17.264Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-10T05:17:49.076Z","emptyReason":null},"description":"Query an Infinite Campus (Campus Parent portal) district directly with curl instead of running the infinitecampus-mcp server — log in with a real username/password, capture the session cookie + XSRF token, and curl grades, attendance, assignments, schedule, messages, documents, and fees. Use when you want IC data without the MCP, in a script, or on a machine where the MCP isn't installed. Infinite Campus is per-district: the base URL and district app name are configurable, not hardcoded. Skill: infinitecampus-api Owner: chrischall Summary: Query an Infinite Campus (Campus Parent portal) district directly with curl instead of running the infinitecampus-mcp server — log in with a real username/password, capture the session cookie + XSRF token, and curl grades, attendance, assignments, schedule, messages, documents, and fees. Use when you want IC data without the MCP, in a script, or on a machine where","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.7K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:infinitecampus-api","sourceUrl":"https://clawhub.ai/chrischall/infinitecampus-api","homepage":"https://clawhub.ai/chrischall/skills/infinitecampus-api","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/chrischall/infinitecampus-api","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/chrischall/skills/infinitecampus-api","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":64,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Query an Infinite Campus (Campus Parent portal) district directly with curl instead of running the infinitecampus-mcp server — log in with a real username/passw"},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T05:17:49.076Z","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-10T05:17:49.076Z","emptyReason":null},"stars":null,"forks":null,"downloads":1660,"packageName":null,"latestVersion":"3.1.9","tractionLabel":"1.7K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T05:17:49.075Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T05:17:49.076Z","lastCrawledAt":"2026-10-10T05:17:49.075Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T05:17:49.075Z","lastVerifiedAt":null,"highlights":[{"version":"3.1.9","createdAt":"2026-10-09T23:26:34.808Z","changelog":"- Removed the sample file skill-card.md. - No functional or documentation changes to the skill itself.","fileCount":4,"zipByteSize":7890},{"version":"3.1.8","createdAt":"2026-10-07T13:38:54.059Z","changelog":"- Removed the sample file skill-card.md. - No changes to functionality or usage—documentation and instructions remain the same.","fileCount":4,"zipByteSize":7844},{"version":"3.1.7","createdAt":"2026-10-05T02:49:27.950Z","changelog":"- Removed the file: skill-card.md - No changes to functionality or documentation content.","fileCount":4,"zipByteSize":7833},{"version":"3.1.6","createdAt":"2026-10-03T01:42:10.356Z","changelog":"- Removed the skill-card.md file. - No user-facing changes to functionality or documentation.","fileCount":4,"zipByteSize":7912},{"version":"3.1.5","createdAt":"2026-09-28T13:55:52.577Z","changelog":"- Removed the skill-card.md file; no impact on core functionality. - No changes made to the skill logic or documentation for general users. - Minor cleanup; documentation and features remain unchanged.","fileCount":4,"zipByteSize":7826},{"version":"3.1.4","createdAt":"2026-09-25T15:56:10.895Z","changelog":"- Removed the sample file `skill-card.md`. - No functional or documentation changes were made to the core skill.","fileCount":4,"zipByteSize":7835},{"version":"3.1.3","createdAt":"2026-09-23T21:39:41.974Z","changelog":"- Reference documentation updated: sample endpoint coverage revised in references/endpoints.md. - Obsolete or redundant skill-card.md file removed. - No changes to shell usage or API behavior; core functionality unchanged.","fileCount":4,"zipByteSize":8049},{"version":"3.1.2","createdAt":"2026-09-23T15:45:20.691Z","changelog":"- Removed the sample skill-card.md file. - No changes to skill functionality or documentation, just cleanup of unused files.","fileCount":4,"zipByteSize":8100}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:infinitecampus-api","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-api/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-infinitecampus-api/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-infinitecampus-api/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-infinitecampus-api/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-infinitecampus-api/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-infinitecampus-api/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-10T10:43:17.261Z"}},"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-api/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-infinitecampus-api/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-infinitecampus-api/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-infinitecampus-api/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-10T05:17:49.076Z","emptyReason":null},"readme":"Skill: infinitecampus-api\n\nOwner: chrischall\n\nSummary: Query an Infinite Campus (Campus Parent portal) district directly with curl instead of running the infinitecampus-mcp server — log in with a real username/password, capture the session cookie + XSRF token, and curl grades, attendance, assignments, schedule, messages, documents, and fees. Use when you want IC data without the MCP, in a script, or on a machine where the MCP isn't installed. Infinite Campus is per-district: the base URL and district app name are configurable, not hardcoded.\n\nTags: latest:3.1.9\n\nVersion history:\n\nv3.1.9 | 2026-10-09T23:26:34.808Z | auto\n\n- Removed the sample file skill-card.md.\n- No functional or documentation changes to the skill itself.\n\nv3.1.8 | 2026-10-07T13:38:54.059Z | auto\n\n- Removed the sample file skill-card.md.\n- No changes to functionality or usage—documentation and instructions remain the same.\n\nv3.1.7 | 2026-10-05T02:49:27.950Z | auto\n\n- Removed the file: skill-card.md\n- No changes to functionality or documentation content.\n\nv3.1.6 | 2026-10-03T01:42:10.356Z | auto\n\n- Removed the skill-card.md file.\n- No user-facing changes to functionality or documentation.\n\nv3.1.5 | 2026-09-28T13:55:52.577Z | auto\n\n- Removed the skill-card.md file; no impact on core functionality.\n- No changes made to the skill logic or documentation for general users.\n- Minor cleanup; documentation and features remain unchanged.\n\nv3.1.4 | 2026-09-25T15:56:10.895Z | auto\n\n- Removed the sample file `skill-card.md`.\n- No functional or documentation changes were made to the core skill.\n\nv3.1.3 | 2026-09-23T21:39:41.974Z | auto\n\n- Reference documentation updated: sample endpoint coverage revised in references/endpoints.md.\n- Obsolete or redundant skill-card.md file removed.\n- No changes to shell usage or API behavior; core functionality unchanged.\n\nv3.1.2 | 2026-09-23T15:45:20.691Z | auto\n\n- Removed the sample skill-card.md file.\n- No changes to skill functionality or documentation, just cleanup of unused files.\n\nv3.1.1 | 2026-09-21T04:13:59.230Z | auto\n\n- Removed the file: skill-card.md\n- No changes to functionality or documentation beyond this file removal.\n\nv3.1.0 | 2026-09-20T02:48:56.499Z | auto\n\n- Removed the file: skill-card.md.\n- No user-facing changes to functionality or documentation.\n\nv3.0.0 | 2026-09-19T11:18:56.198Z | auto\n\n- Breaking change: Removed the skill-card.md file from the project.\n- No user-facing or functional changes to the Infinite Campus API usage or documentation.\n- All core usage and instructions remain unchanged.\n\nv2.8.5 | 2026-09-15T19:25:20.405Z | auto\n\n- Removed the file skill-card.md.\n- No changes to core functionality or documentation in this update.\n\nv2.8.4 | 2026-09-14T14:10:03.504Z | auto\n\n- Removed the sample file skill-card.md.\n- No changes to core functionality or documentation.\n\nv2.8.3 | 2026-09-10T17:50:39.780Z | auto\n\n- Removed the skill-card.md file.\n- No changes to functionality or usage; documentation and core instructions remain unchanged.\n\nv2.8.2 | 2026-09-07T22:54:37.640Z | auto\n\n- Removed the file skill-card.md.\n- No changes to core functionality or documentation except file cleanup.\n\nv2.8.1 | 2026-09-05T00:51:39.405Z | auto\n\n- Removed the sample file skill-card.md.\n- No changes to core functionality or documentation in SKILL.md.\n\nv2.8.0 | 2026-09-04T22:32:27.566Z | auto\n\n- Removed the file skill-card.md.\n- No functional or documentation changes to the skill itself.\n- This release is a minor cleanup; no user-facing changes.\n\nv2.7.0 | 2026-08-31T00:22:32.913Z | auto\n\n- Removed the file skill-card.md from the project.\n- No changes to the API or usage.\n- Documentation and functionality remain unchanged.\n\nv2.6.0 | 2026-08-29T13:54:12.484Z | auto\n\n- Removed the file skill-card.md from the project.\n- No functional or user-facing changes; documentation and skill functionality remain unchanged.\n\nv2.5.1 | 2026-08-28T21:07:37.102Z | auto\n\n- Removed the file: skill-card.md.\n- No code changes or functionality updates; documentation only.\n- No impact to usage or features.\n\nv2.5.0 | 2026-08-28T11:35:00.014Z | auto\n\n- Removed the skill-card.md file.\n- No functional or user-facing changes; documentation and usage remain the same.\n\nv2.4.5 | 2026-08-06T00:42:53.945Z | auto\n\n- Removed the sample skill-card.md file.\n- No changes to core functionality or documentation.\n\nv2.4.4 | 2026-08-03T14:43:28.487Z | auto\n\n- Removed the file: skill-card.md\n- No changes to functionality or documentation of the skill itself\n\nv2.4.3 | 2026-08-03T04:33:01.006Z | auto\n\n- Removed the file skill-card.md.\n- No user-facing or functional changes to the Infinite Campus API skill.\n- No changes to usage, endpoints, or documentation for end-users.\n\nv2.4.2 | 2026-07-30T12:54:02.757Z | auto\n\n- Added detailed documentation for querying Infinite Campus Parent Portal directly with curl and jq, bypassing the MCP.\n- Clarified required environment variables for per-district configuration.\n- Provided step-by-step instructions for authentication, session management, and API request patterns.\n- Included guidance for resolving student and enrollment IDs before querying other endpoints.\n- Added endpoint references and download instructions for documents and reports.\n- Documented behavior for feature-disabled districts and module availability.\n\nArchive index:\n\nArchive v3.1.9: 4 files, 7890 bytes\n\nFiles: references/endpoints.md (8406b), skill-card.md (1925b), SKILL.md (5776b), _meta.json (137b)\n\nFile v3.1.9:SKILL.md\n\n---\nname: infinitecampus-api\ndescription: >-\n  Query an Infinite Campus (Campus Parent portal) district directly with curl\n  instead of running the infinitecampus-mcp server — log in with a real\n  username/password, capture the session cookie + XSRF token, and curl grades,\n  attendance, assignments, schedule, messages, documents, and fees. Use when\n  you want IC data without the MCP, in a script, or on a machine where the MCP\n  isn't installed. Infinite Campus is per-district: the base URL and district\n  app name are configurable, not hardcoded.\n---\n\n# Infinite Campus via curl (no MCP)\n\nInfinite Campus's Campus Parent portal is a classic server-rendered site\n(no public API, no bot wall). A plain form POST to `verify.jsp` logs a\nparent in and hands back a session cookie — no browser bridge needed. This\nis the same login `infinitecampus-mcp`'s `src/client.ts`/`src/auth.ts` do;\nthis skill reproduces it with `curl` + a cookie jar.\n\n**Infinite Campus is per-district.** Every parent's portal lives at their\nown district's host (`https://campus.<district>.k12.example.us` or similar)\nunder an `appName` path segment. There is no shared IC endpoint — carry the\nbase URL and district from the same config the MCP uses:\n\n```sh\n: \"${IC_BASE_URL:?set to your district's portal URL, e.g. https://campus.yourdistrict.org}\"\n: \"${IC_DISTRICT:?set to the appName path segment, e.g. yourdistrict}\"\n: \"${IC_USERNAME:?parent portal username}\"\n: \"${IC_PASSWORD:?parent portal password}\"\n```\n\n(Same 4 env vars the MCP reads — reuse the repo's `.env` or the host's\nsecret store; don't hardcode a district.)\n\n## One-time setup\n\nNone — `curl` and `jq` are the only tools needed (`brew install jq` if\nmissing). No profile/pairing step, no extension.\n\n## Login (capture cookies + XSRF token)\n\n`verify.jsp` accepts a urlencoded form POST and returns 200 with an\n`<AUTHENTICATION>state</AUTHENTICATION>` marker in the body — **not**\nvia HTTP status — plus ~20 `Set-Cookie` headers. Use a cookie jar so curl\ndoes the Set-Cookie parsing/deduping for you:\n\n```sh\nJAR=$(mktemp)\nBODY=$(curl -sS -c \"$JAR\" \\\n  \"$IC_BASE_URL/campus/verify.jsp?nonBrowser=true\" \\\n  --data-urlencode \"username=$IC_USERNAME\" \\\n  --data-urlencode \"password=$IC_PASSWORD\" \\\n  --data-urlencode \"appName=$IC_DISTRICT\" \\\n  --data-urlencode \"portalLoginPage=parents\")\n\necho \"$BODY\" | grep -o '<AUTHENTICATION>[^<]*' # AUTHENTICATIONsuccess = good\n```\n\n`password-error` / `account-locked` / any non-`success` state means the\nlogin failed — check that string before doing anything else. Credentials\ngo in the form body (never the query string — it lands in access logs).\n\nPull the XSRF token out of the jar (IC's JS reads it back and echoes it on\nevery request as `X-XSRF-TOKEN`; `JSESSIONID` alone is not enough):\n\n```sh\nXSRF=$(awk -F'\\t' '$6==\"XSRF-TOKEN\"{print $7}' \"$JAR\")\n```\n\n## Core call pattern\n\nEvery subsequent request rides the jar + the XSRF header:\n\n```sh\nic_get() { # ic_get <path-with-query>\n  curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \\\n    \"$IC_BASE_URL$1\"\n}\n\nic_get '/campus/api/portal/students' | jq .\n```\n\nA **401** means the session expired (IC sessions last ~5-6h) — re-run the\nLogin step to get a fresh `$JAR`/`$XSRF` and retry once. There is no retry\nbudget beyond that in this shell version (the MCP's `CookieSessionManager`\ndoes this automatically; here just redo the login block).\n\n## The one rule: resolve student + enrollment first\n\nAlmost every read is scoped to a `personID` (student), and several need the\nstudent's **enrollment** (`enrollmentID` / `calendarID` / `structureID`).\nAlways start with:\n\n```sh\nic_get '/campus/api/portal/students' | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nTake `personID` for `studentId`/`personID` params, and the first (or\nrelevant) `enrollments[]` entry's `enrollmentID`/`calendarID`/`structureID`\nfor the endpoints that need them (attendance, school days, assessments,\nfeatures). There is no name→id search — `ic_list_students` (i.e. this call)\n*is* the resolve step.\n\n## Endpoints\n\nAll 17 read endpoints (grades, attendance, assignments, schedule, messages,\ndocuments, fees, teachers, assessments, feature flags) with ready-to-run\n`curl`+`jq` recipes are in `references/endpoints.md`. Two (`behavior`,\n`food_service`) have real paths that were never confirmed live against a\ndistrict with that module enabled — try them, expect a 404 if the module\nis off or the district's shape differs.\n\n## Downloads\n\n`ic_download_document`'s `url` field (from the documents list) may be a\nrelative `/campus/...` path or an absolute URL. Same jar + header pattern,\njust write to a file instead of piping to `jq`:\n\n```sh\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -o report-card.pdf \"$IC_BASE_URL$DOC_URL\"\n```\n\n## FeatureDisabled districts\n\nSome districts turn whole modules off (behavior, food service, assessments,\ndocuments, attendance). The MCP checks `displayOptions` first and falls\nback to treating a 404 as \"disabled.\" Doing the same by hand: fetch\n`/campus/api/portal/displayOptions/{structureID}?personID=X` (see\nreferences) and check the flag before assuming a 404 is a bug.\n\n## Notes\n\n- Read-only in practice: every recipe here is a GET. `ic_get_message`\n  fetching a message body *may* mark it read on some district configs — the\n  MCP's own probe couldn't confirm the side effect either way.\n- No CUPS linked-district switching in this skill (a parent with kids in\n  2+ IC instances under shared SSO) — that's a multi-step token-exchange\n  flow (`src/client.ts`'s `discoverLinkedDistricts`); out of scope here.\n  Run the login flow again against the linked district's own\n  `IC_BASE_URL`/`IC_DISTRICT` if you need it.\n- This project is developed and maintained by AI (Claude).\n\nFile v3.1.9:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-api\",\n  \"version\": \"3.1.9\",\n  \"publishedAt\": 1791588394808\n}\n\nFile v3.1.9:references/endpoints.md\n\n# Infinite Campus endpoints for curl\n\nAssumes the Login block from `../SKILL.md` has already run (`$JAR`, `$XSRF`,\n`$IC_BASE_URL` set) and the `ic_get` helper is defined:\n\n```sh\nic_get() { curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \"$IC_BASE_URL$1\"; }\n```\n\nAll paths are transcribed from `infinitecampus-mcp`'s `src/tools/*.ts` and\n`docs/endpoints.md` — the same requests the `ic_*` MCP tools make. `$PID` =\na student's `personID` from the resolve-first step.\n\nSome IC/prism responses serialize a 1-item collection as a bare object\ninstead of a 1-element array — pipe through `jq 'if type==\"array\" then . else [.] end'`\nif a recipe below assumes an array and you get an object back.\n\n---\n\n## Students (resolve-first — always start here)\n\n```sh\nic_get '/campus/api/portal/students' \\\n  | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nEach `enrollments[]` entry carries `enrollmentID`, `calendarID`,\n`structureID`, `schoolName`, `endDate` — needed by several endpoints below.\n\n## Schedule\n\n```sh\nic_get \"/campus/resources/portal/roster?personID=$PID\" | jq .\n```\n\n(Only `personID` is honored server-side — there is no date/term filter, so\nthe MCP tool takes none.)\n\n## Assignments\n\n```sh\n# all assignments (full term history, ~hundreds of rows)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" | jq .\n\n# scoped to one course (sectionID from the schedule call above)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID&sectionID=$SECTION_ID\" | jq .\n\n# client-side filters (server ignores date/missing params entirely):\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.[] | select(.dueDate >= $since)]'\n```\n\n## Recent (recently-graded) assignments\n\nServer-side filtered by `modifiedDate` (defaults to 14 days ago in the MCP;\nreproduce with `date -u -v-14d '+%Y-%m-%dT00:00:00'` on macOS):\n\n```sh\nSINCE=$(date -u -v-14d '+%Y-%m-%dT00:00:00')\nic_get \"/campus/api/portal/assignment/recentlyScored?modifiedDate=$SINCE&personID=$PID\" | jq .\n```\n\n## Grades\n\n```sh\nic_get \"/campus/resources/portal/grades?personID=$PID\" | jq .\n# scoped to one term:\nic_get \"/campus/resources/portal/grades?personID=$PID&termID=$TERM_ID\" | jq .\n```\n\n## Attendance (per-course summary, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/$ENROLLMENT_ID?courseSummary=true&personID=$PID\" \\\n  | jq '.terms[]?.courses[]? | {absentList, tardyList, presentList, earlyReleaseList}'\n```\n\nFeature-gated: check `displayOptions.attendance` first (see Feature flags,\nbelow) — a 404 here on a district with attendance disabled is expected, not\na bug.\n\n## Attendance events (individual absences/tardies, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq '.events[]? | {date: .localDate, code, description, excuse, comments}'\n\n# excused-only, since a date (client-side filter):\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.events[]? | select((.localDate[0:10] >= $since) and (.excuse == \"E\"))]'\n```\n\n## School days / calendar (loop over each enrollment)\n\nTwo calls per enrollment, then join term windows to days client-side:\n\n```sh\nic_get \"/campus/resources/term?structureID=$STRUCTURE_ID\" | jq .          # term boundaries\nic_get \"/campus/resources/calendar/instructionalDay?calendarID=$CALENDAR_ID\" | jq .  # day list\n\n# days within term N's [startDate,endDate]:\njq --slurpfile terms terms.json --arg t \"$TERM_ID\" \\\n  '[.[] | select(.date >= $terms[0][0].startDate and .date <= $terms[0][0].endDate)]' days.json\n```\n\n## Assessments (standardized tests, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/prism/portal/assessments?personID=$PID&calendarID=$CALENDAR_ID\" \\\n  | jq '{stateTests, nationalTests, districtTests}'\n```\n\nFeature-gated on `displayOptions.assessment`; test-item shape varies by\ndistrict/test type — pass through unchanged, don't assume field names\nbeyond `stateTests`/`nationalTests`/`districtTests`.\n\n## Behavior *(path unconfirmed — never verified live against a district with the module on)*\n\n```sh\nic_get \"/campus/resources/portal/behavior?personID=$PID\" | jq .\n# with a date range:\nic_get \"/campus/resources/portal/behavior?personID=$PID&startDate=2026-01-01&endDate=2026-06-01\" | jq .\n```\n\nFeature-gated on `displayOptions.behavior` — a 404 is the expected result\non most districts (few enable this module for parent portal).\n\n## Food service *(path unconfirmed — same caveat as behavior)*\n\n```sh\nic_get \"/campus/resources/portal/foodService?personID=$PID\" | jq .\nic_get \"/campus/resources/portal/foodService?personID=$PID&startDate=2026-01-01&endDate=2026-06-01\" | jq .\n```\n\nFeature-gated on `displayOptions.foodService`.\n\n## Documents + download\n\n```sh\nic_get \"/campus/resources/portal/report/all?personID=$PID\" \\\n  | jq '.[] | {name, type, url, moduleLabel, endYear}'\n\n# download (url may be relative /campus/... or an absolute URL — handle both):\nDOC_URL='/campus/resources/portal/report/...'   # from the .url field above\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -o report-card.pdf \\\n  \"$( [[ \"$DOC_URL\" == http* ]] && echo \"$DOC_URL\" || echo \"$IC_BASE_URL$DOC_URL\" )\"\n```\n\nFeature-gated on `displayOptions.documents`.\n\n## Teachers + counselors\n\n```sh\nic_get \"/campus/resources/portal/section/contacts?personID=$PID\" | jq .\nic_get \"/campus/resources/portal/studentCounselor/byUser?personID=$PID\" | jq .\n```\n\nEither endpoint 404ing is treated as \"no results\" (not FeatureDisabled) by\nthe MCP — do the same.\n\n## Messages (three independent sources — combine client-side)\n\n```sh\n# 1. prism notifications (assignment/grade/attendance alerts)\nic_get '/campus/prism?x=notifications.Notification-retrieve&limitCount=20' \\\n  | jq '.data.NotificationList.Notification'\n\n# 2. Messenger 2.0 inbox (teacher messages, district announcements)\nic_get '/campus/api/portal/process-message' | jq .\n\n# 3. portal userNotice announcements\nic_get '/campus/resources/portal/userNotice' | jq .\n```\n\nEach inbox item's `url` field feeds `ic_get_message` below (normalize a bare\nor `portal/...`-relative URL to `/campus/portal/...` first).\n\n## Get one message body (HTML → text)\n\n```sh\n# normalize: bare \"portal/...\" → \"/campus/portal/...\"; already-absolute /campus/... stays as-is\nMSG_PATH='/campus/portal/messageView.xsl?x=messenger.MessengerEngine-getMessageRecipientView&messageID=...&messageRecipientID=...'\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: text/html' \"$IC_BASE_URL$MSG_PATH\"\n# then strip tags/entities yourself, or eyeball the rendered HTML — this is\n# an HTML fragment, not JSON, so `jq` doesn't apply.\n```\n\nFetching a message body *may* mark it read on some district configs\n(unconfirmed either way).\n\n## Fees (two endpoints in parallel; either can 404 independently)\n\n```sh\nic_get \"/campus/api/portal/fees/feeAssignments?personID=$PID\" | jq .\nic_get \"/campus/api/portal/fees/feeTransactionDetail/totalSurplus/-1?personID=$PID\" | jq .\n```\n\nBoth 404 → module is off for this district. One 404 → report the other\nside plus a note that the failing endpoint may be disabled.\n\n## Feature flags (displayOptions — check before assuming a 404 is a bug)\n\n```sh\nic_get \"/campus/api/portal/displayOptions/$STRUCTURE_ID?personID=$PID\" \\\n  | jq '{attendance, behavior, assessment, documents, foodService, grades, schedule}'\n```\n\n`false` = district has that module turned off for this enrollment; `true`\nor absent = available. ~90 flags total; the MCP caches this per\n`(district, structureID)` for the session lifetime since it rarely changes\nmid-session — worth doing the same if you're calling it more than once.\n\n---\n\n## Not covered here (out of scope for this skill)\n\n- **CUPS linked-district discovery/switching** — a parent with kids at 2+\n  IC-hosted districts under shared SSO. Multi-step: fetch\n  `cups/linkedAccounts`, `userAccountSwitch/originalDistrict`,\n  `districts/current`, then per linked account a `cups/loginToken` POST\n  followed by a second `verify.jsp`-style POST at the *linked* district's\n  own host with the CUPS token. Fully discoverable in `src/client.ts`\n  (`discoverLinkedDistricts`) but a lot of shell for a one-shot skill —\n  just re-run the Login block against the linked district's own\n  `IC_BASE_URL`/`IC_DISTRICT` instead.\n\nFile v3.1.9:skill-card.md\n\n## Description:\n\nGuides authorized Campus Parent users through district-specific sign-in and read-only queries for grades, attendance, assignments, schedules, messages, documents, and fees using curl.\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\nAuthorized parents, guardians, and developers use this skill to retrieve student portal information from their own district without installing the MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Portal credentials and session tokens can expose a student's account if disclosed.\n\nMitigation: Use only an account you are authorized to access on a trusted private machine; keep credentials and cookie jars out of logs and shared or synced folders, and delete temporary cookies after use.\n\nRisk: Downloaded documents and query results can contain sensitive student records.\n\nMitigation: Limit access to authorized users, avoid logging record contents, and remove saved documents when finished.\n\n## Reference(s):\n\n- [Infinite Campus endpoint recipes](references/endpoints.md)\n- [ClawHub skill listing](https://clawhub.ai/chrischall/skills/infinitecampus-api)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Configuration guidance, Text]\n\n**Output Format:** [Markdown with curl and jq command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Responses depend on the district's enabled portal modules; some example endpoints are unconfirmed.]\n\n## Skill Version(s):\n\n3.1.9 (source: ClawHub release evidence)\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 v3.1.8: 4 files, 7844 bytes\n\nFiles: references/endpoints.md (8406b), skill-card.md (1886b), SKILL.md (5776b), _meta.json (137b)\n\nFile v3.1.8:SKILL.md\n\n---\nname: infinitecampus-api\ndescription: >-\n  Query an Infinite Campus (Campus Parent portal) district directly with curl\n  instead of running the infinitecampus-mcp server — log in with a real\n  username/password, capture the session cookie + XSRF token, and curl grades,\n  attendance, assignments, schedule, messages, documents, and fees. Use when\n  you want IC data without the MCP, in a script, or on a machine where the MCP\n  isn't installed. Infinite Campus is per-district: the base URL and district\n  app name are configurable, not hardcoded.\n---\n\n# Infinite Campus via curl (no MCP)\n\nInfinite Campus's Campus Parent portal is a classic server-rendered site\n(no public API, no bot wall). A plain form POST to `verify.jsp` logs a\nparent in and hands back a session cookie — no browser bridge needed. This\nis the same login `infinitecampus-mcp`'s `src/client.ts`/`src/auth.ts` do;\nthis skill reproduces it with `curl` + a cookie jar.\n\n**Infinite Campus is per-district.** Every parent's portal lives at their\nown district's host (`https://campus.<district>.k12.example.us` or similar)\nunder an `appName` path segment. There is no shared IC endpoint — carry the\nbase URL and district from the same config the MCP uses:\n\n```sh\n: \"${IC_BASE_URL:?set to your district's portal URL, e.g. https://campus.yourdistrict.org}\"\n: \"${IC_DISTRICT:?set to the appName path segment, e.g. yourdistrict}\"\n: \"${IC_USERNAME:?parent portal username}\"\n: \"${IC_PASSWORD:?parent portal password}\"\n```\n\n(Same 4 env vars the MCP reads — reuse the repo's `.env` or the host's\nsecret store; don't hardcode a district.)\n\n## One-time setup\n\nNone — `curl` and `jq` are the only tools needed (`brew install jq` if\nmissing). No profile/pairing step, no extension.\n\n## Login (capture cookies + XSRF token)\n\n`verify.jsp` accepts a urlencoded form POST and returns 200 with an\n`<AUTHENTICATION>state</AUTHENTICATION>` marker in the body — **not**\nvia HTTP status — plus ~20 `Set-Cookie` headers. Use a cookie jar so curl\ndoes the Set-Cookie parsing/deduping for you:\n\n```sh\nJAR=$(mktemp)\nBODY=$(curl -sS -c \"$JAR\" \\\n  \"$IC_BASE_URL/campus/verify.jsp?nonBrowser=true\" \\\n  --data-urlencode \"username=$IC_USERNAME\" \\\n  --data-urlencode \"password=$IC_PASSWORD\" \\\n  --data-urlencode \"appName=$IC_DISTRICT\" \\\n  --data-urlencode \"portalLoginPage=parents\")\n\necho \"$BODY\" | grep -o '<AUTHENTICATION>[^<]*' # AUTHENTICATIONsuccess = good\n```\n\n`password-error` / `account-locked` / any non-`success` state means the\nlogin failed — check that string before doing anything else. Credentials\ngo in the form body (never the query string — it lands in access logs).\n\nPull the XSRF token out of the jar (IC's JS reads it back and echoes it on\nevery request as `X-XSRF-TOKEN`; `JSESSIONID` alone is not enough):\n\n```sh\nXSRF=$(awk -F'\\t' '$6==\"XSRF-TOKEN\"{print $7}' \"$JAR\")\n```\n\n## Core call pattern\n\nEvery subsequent request rides the jar + the XSRF header:\n\n```sh\nic_get() { # ic_get <path-with-query>\n  curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \\\n    \"$IC_BASE_URL$1\"\n}\n\nic_get '/campus/api/portal/students' | jq .\n```\n\nA **401** means the session expired (IC sessions last ~5-6h) — re-run the\nLogin step to get a fresh `$JAR`/`$XSRF` and retry once. There is no retry\nbudget beyond that in this shell version (the MCP's `CookieSessionManager`\ndoes this automatically; here just redo the login block).\n\n## The one rule: resolve student + enrollment first\n\nAlmost every read is scoped to a `personID` (student), and several need the\nstudent's **enrollment** (`enrollmentID` / `calendarID` / `structureID`).\nAlways start with:\n\n```sh\nic_get '/campus/api/portal/students' | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nTake `personID` for `studentId`/`personID` params, and the first (or\nrelevant) `enrollments[]` entry's `enrollmentID`/`calendarID`/`structureID`\nfor the endpoints that need them (attendance, school days, assessments,\nfeatures). There is no name→id search — `ic_list_students` (i.e. this call)\n*is* the resolve step.\n\n## Endpoints\n\nAll 17 read endpoints (grades, attendance, assignments, schedule, messages,\ndocuments, fees, teachers, assessments, feature flags) with ready-to-run\n`curl`+`jq` recipes are in `references/endpoints.md`. Two (`behavior`,\n`food_service`) have real paths that were never confirmed live against a\ndistrict with that module enabled — try them, expect a 404 if the module\nis off or the district's shape differs.\n\n## Downloads\n\n`ic_download_document`'s `url` field (from the documents list) may be a\nrelative `/campus/...` path or an absolute URL. Same jar + header pattern,\njust write to a file instead of piping to `jq`:\n\n```sh\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -o report-card.pdf \"$IC_BASE_URL$DOC_URL\"\n```\n\n## FeatureDisabled districts\n\nSome districts turn whole modules off (behavior, food service, assessments,\ndocuments, attendance). The MCP checks `displayOptions` first and falls\nback to treating a 404 as \"disabled.\" Doing the same by hand: fetch\n`/campus/api/portal/displayOptions/{structureID}?personID=X` (see\nreferences) and check the flag before assuming a 404 is a bug.\n\n## Notes\n\n- Read-only in practice: every recipe here is a GET. `ic_get_message`\n  fetching a message body *may* mark it read on some district configs — the\n  MCP's own probe couldn't confirm the side effect either way.\n- No CUPS linked-district switching in this skill (a parent with kids in\n  2+ IC instances under shared SSO) — that's a multi-step token-exchange\n  flow (`src/client.ts`'s `discoverLinkedDistricts`); out of scope here.\n  Run the login flow again against the linked district's own\n  `IC_BASE_URL`/`IC_DISTRICT` if you need it.\n- This project is developed and maintained by AI (Claude).\n\nFile v3.1.8:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-api\",\n  \"version\": \"3.1.8\",\n  \"publishedAt\": 1791380334059\n}\n\nFile v3.1.8:references/endpoints.md\n\n# Infinite Campus endpoints for curl\n\nAssumes the Login block from `../SKILL.md` has already run (`$JAR`, `$XSRF`,\n`$IC_BASE_URL` set) and the `ic_get` helper is defined:\n\n```sh\nic_get() { curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \"$IC_BASE_URL$1\"; }\n```\n\nAll paths are transcribed from `infinitecampus-mcp`'s `src/tools/*.ts` and\n`docs/endpoints.md` — the same requests the `ic_*` MCP tools make. `$PID` =\na student's `personID` from the resolve-first step.\n\nSome IC/prism responses serialize a 1-item collection as a bare object\ninstead of a 1-element array — pipe through `jq 'if type==\"array\" then . else [.] end'`\nif a recipe below assumes an array and you get an object back.\n\n---\n\n## Students (resolve-first — always start here)\n\n```sh\nic_get '/campus/api/portal/students' \\\n  | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nEach `enrollments[]` entry carries `enrollmentID`, `calendarID`,\n`structureID`, `schoolName`, `endDate` — needed by several endpoints below.\n\n## Schedule\n\n```sh\nic_get \"/campus/resources/portal/roster?personID=$PID\" | jq .\n```\n\n(Only `personID` is honored server-side — there is no date/term filter, so\nthe MCP tool takes none.)\n\n## Assignments\n\n```sh\n# all assignments (full term history, ~hundreds of rows)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" | jq .\n\n# scoped to one course (sectionID from the schedule call above)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID&sectionID=$SECTION_ID\" | jq .\n\n# client-side filters (server ignores date/missing params entirely):\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.[] | select(.dueDate >= $since)]'\n```\n\n## Recent (recently-graded) assignments\n\nServer-side filtered by `modifiedDate` (defaults to 14 days ago in the MCP;\nreproduce with `date -u -v-14d '+%Y-%m-%dT00:00:00'` on macOS):\n\n```sh\nSINCE=$(date -u -v-14d '+%Y-%m-%dT00:00:00')\nic_get \"/campus/api/portal/assignment/recentlyScored?modifiedDate=$SINCE&personID=$PID\" | jq .\n```\n\n## Grades\n\n```sh\nic_get \"/campus/resources/portal/grades?personID=$PID\" | jq .\n# scoped to one term:\nic_get \"/campus/resources/portal/grades?personID=$PID&termID=$TERM_ID\" | jq .\n```\n\n## Attendance (per-course summary, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/$ENROLLMENT_ID?courseSummary=true&personID=$PID\" \\\n  | jq '.terms[]?.courses[]? | {absentList, tardyList, presentList, earlyReleaseList}'\n```\n\nFeature-gated: check `displayOptions.attendance` first (see Feature flags,\nbelow) — a 404 here on a district with attendance disabled is expected, not\na bug.\n\n## Attendance events (individual absences/tardies, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq '.events[]? | {date: .localDate, code, description, excuse, comments}'\n\n# excused-only, since a date (client-side filter):\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.events[]? | select((.localDate[0:10] >= $since) and (.excuse == \"E\"))]'\n```\n\n## School days / calendar (loop over each enrollment)\n\nTwo calls per enrollment, then join term windows to days client-side:\n\n```sh\nic_get \"/campus/resources/term?structureID=$STRUCTURE_ID\" | jq .          # term boundaries\nic_get \"/campus/resources/calendar/instructionalDay?calendarID=$CALENDAR_ID\" | jq .  # day list\n\n# days within term N's [startDate,endDate]:\njq --slurpfile terms terms.json --arg t \"$TERM_ID\" \\\n  '[.[] | select(.date >= $terms[0][0].startDate and .date <= $terms[0][0].endDate)]' days.json\n```\n\n## Assessments (standardized tests, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/prism/portal/assessments?personID=$PID&calendarID=$CALENDAR_ID\" \\\n  | jq '{stateTests, nationalTests, districtTests}'\n```\n\nFeature-gated on `displayOptions.assessment`; test-item shape varies by\ndistrict/test type — pass through unchanged, don't assume field names\nbeyond `stateTests`/`nationalTests`/`districtTests`.\n\n## Behavior *(path unconfirmed — never verified live against a district with the module on)*\n\n```sh\nic_get \"/campus/resources/portal/behavior?personID=$PID\" | jq .\n# with a date range:\nic_get \"/campus/resources/portal/behavior?personID=$PID&startDate=2026-01-01&endDate=2026-06-01\" | jq .\n```\n\nFeature-gated on `displayOptions.behavior` — a 404 is the expected result\non most districts (few enable this module for parent portal).\n\n## Food service *(path unconfirmed — same caveat as behavior)*\n\n```sh\nic_get \"/campus/resources/portal/foodService?personID=$PID\" | jq .\nic_get \"/campus/resources/portal/foodService?personID=$PID&startDate=2026-01-01&endDate=2026-06-01\" | jq .\n```\n\nFeature-gated on `displayOptions.foodService`.\n\n## Documents + download\n\n```sh\nic_get \"/campus/resources/portal/report/all?personID=$PID\" \\\n  | jq '.[] | {name, type, url, moduleLabel, endYear}'\n\n# download (url may be relative /campus/... or an absolute URL — handle both):\nDOC_URL='/campus/resources/portal/report/...'   # from the .url field above\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -o report-card.pdf \\\n  \"$( [[ \"$DOC_URL\" == http* ]] && echo \"$DOC_URL\" || echo \"$IC_BASE_URL$DOC_URL\" )\"\n```\n\nFeature-gated on `displayOptions.documents`.\n\n## Teachers + counselors\n\n```sh\nic_get \"/campus/resources/portal/section/contacts?personID=$PID\" | jq .\nic_get \"/campus/resources/portal/studentCounselor/byUser?personID=$PID\" | jq .\n```\n\nEither endpoint 404ing is treated as \"no results\" (not FeatureDisabled) by\nthe MCP — do the same.\n\n## Messages (three independent sources — combine client-side)\n\n```sh\n# 1. prism notifications (assignment/grade/attendance alerts)\nic_get '/campus/prism?x=notifications.Notification-retrieve&limitCount=20' \\\n  | jq '.data.NotificationList.Notification'\n\n# 2. Messenger 2.0 inbox (teacher messages, district announcements)\nic_get '/campus/api/portal/process-message' | jq .\n\n# 3. portal userNotice announcements\nic_get '/campus/resources/portal/userNotice' | jq .\n```\n\nEach inbox item's `url` field feeds `ic_get_message` below (normalize a bare\nor `portal/...`-relative URL to `/campus/portal/...` first).\n\n## Get one message body (HTML → text)\n\n```sh\n# normalize: bare \"portal/...\" → \"/campus/portal/...\"; already-absolute /campus/... stays as-is\nMSG_PATH='/campus/portal/messageView.xsl?x=messenger.MessengerEngine-getMessageRecipientView&messageID=...&messageRecipientID=...'\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: text/html' \"$IC_BASE_URL$MSG_PATH\"\n# then strip tags/entities yourself, or eyeball the rendered HTML — this is\n# an HTML fragment, not JSON, so `jq` doesn't apply.\n```\n\nFetching a message body *may* mark it read on some district configs\n(unconfirmed either way).\n\n## Fees (two endpoints in parallel; either can 404 independently)\n\n```sh\nic_get \"/campus/api/portal/fees/feeAssignments?personID=$PID\" | jq .\nic_get \"/campus/api/portal/fees/feeTransactionDetail/totalSurplus/-1?personID=$PID\" | jq .\n```\n\nBoth 404 → module is off for this district. One 404 → report the other\nside plus a note that the failing endpoint may be disabled.\n\n## Feature flags (displayOptions — check before assuming a 404 is a bug)\n\n```sh\nic_get \"/campus/api/portal/displayOptions/$STRUCTURE_ID?personID=$PID\" \\\n  | jq '{attendance, behavior, assessment, documents, foodService, grades, schedule}'\n```\n\n`false` = district has that module turned off for this enrollment; `true`\nor absent = available. ~90 flags total; the MCP caches this per\n`(district, structureID)` for the session lifetime since it rarely changes\nmid-session — worth doing the same if you're calling it more than once.\n\n---\n\n## Not covered here (out of scope for this skill)\n\n- **CUPS linked-district discovery/switching** — a parent with kids at 2+\n  IC-hosted districts under shared SSO. Multi-step: fetch\n  `cups/linkedAccounts`, `userAccountSwitch/originalDistrict`,\n  `districts/current`, then per linked account a `cups/loginToken` POST\n  followed by a second `verify.jsp`-style POST at the *linked* district's\n  own host with the CUPS token. Fully discoverable in `src/client.ts`\n  (`discoverLinkedDistricts`) but a lot of shell for a one-shot skill —\n  just re-run the Login block against the linked district's own\n  `IC_BASE_URL`/`IC_DISTRICT` instead.\n\nFile v3.1.8:skill-card.md\n\n## Description:\n\nGuides parents and their agents through querying their district's Infinite Campus Parent portal with curl for grades, attendance, assignments, schedules, messages, documents, and fees without an MCP server.\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 and developers use this skill to retrieve their own students' portal information from a district-specific Infinite Campus site using curl and jq, without installing the MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Portal credentials and session cookies could expose a parent's account if shared or stored insecurely.\n\nMitigation: Keep credentials, the cookie jar, and the XSRF token in private locations; remove session files after use.\n\nRisk: Downloaded documents and query results may contain sensitive student records.\n\nMitigation: Avoid shared or synced storage for student records and delete downloaded files when finished.\n\n## Reference(s):\n\n- [Infinite Campus curl endpoint recipes](references/endpoints.md)\n- [ClawHub skill listing](https://clawhub.ai/chrischall/skills/infinitecampus-api)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with curl and jq command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Portal responses may include sensitive student records; available endpoints vary by district.]\n\n## Skill Version(s):\n\n3.1.8 (source: server-resolved 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 v3.1.7: 4 files, 7833 bytes\n\nFiles: references/endpoints.md (8406b), skill-card.md (1833b), SKILL.md (5776b), _meta.json (137b)\n\nFile v3.1.7:SKILL.md\n\n---\nname: infinitecampus-api\ndescription: >-\n  Query an Infinite Campus (Campus Parent portal) district directly with curl\n  instead of running the infinitecampus-mcp server — log in with a real\n  username/password, capture the session cookie + XSRF token, and curl grades,\n  attendance, assignments, schedule, messages, documents, and fees. Use when\n  you want IC data without the MCP, in a script, or on a machine where the MCP\n  isn't installed. Infinite Campus is per-district: the base URL and district\n  app name are configurable, not hardcoded.\n---\n\n# Infinite Campus via curl (no MCP)\n\nInfinite Campus's Campus Parent portal is a classic server-rendered site\n(no public API, no bot wall). A plain form POST to `verify.jsp` logs a\nparent in and hands back a session cookie — no browser bridge needed. This\nis the same login `infinitecampus-mcp`'s `src/client.ts`/`src/auth.ts` do;\nthis skill reproduces it with `curl` + a cookie jar.\n\n**Infinite Campus is per-district.** Every parent's portal lives at their\nown district's host (`https://campus.<district>.k12.example.us` or similar)\nunder an `appName` path segment. There is no shared IC endpoint — carry the\nbase URL and district from the same config the MCP uses:\n\n```sh\n: \"${IC_BASE_URL:?set to your district's portal URL, e.g. https://campus.yourdistrict.org}\"\n: \"${IC_DISTRICT:?set to the appName path segment, e.g. yourdistrict}\"\n: \"${IC_USERNAME:?parent portal username}\"\n: \"${IC_PASSWORD:?parent portal password}\"\n```\n\n(Same 4 env vars the MCP reads — reuse the repo's `.env` or the host's\nsecret store; don't hardcode a district.)\n\n## One-time setup\n\nNone — `curl` and `jq` are the only tools needed (`brew install jq` if\nmissing). No profile/pairing step, no extension.\n\n## Login (capture cookies + XSRF token)\n\n`verify.jsp` accepts a urlencoded form POST and returns 200 with an\n`<AUTHENTICATION>state</AUTHENTICATION>` marker in the body — **not**\nvia HTTP status — plus ~20 `Set-Cookie` headers. Use a cookie jar so curl\ndoes the Set-Cookie parsing/deduping for you:\n\n```sh\nJAR=$(mktemp)\nBODY=$(curl -sS -c \"$JAR\" \\\n  \"$IC_BASE_URL/campus/verify.jsp?nonBrowser=true\" \\\n  --data-urlencode \"username=$IC_USERNAME\" \\\n  --data-urlencode \"password=$IC_PASSWORD\" \\\n  --data-urlencode \"appName=$IC_DISTRICT\" \\\n  --data-urlencode \"portalLoginPage=parents\")\n\necho \"$BODY\" | grep -o '<AUTHENTICATION>[^<]*' # AUTHENTICATIONsuccess = good\n```\n\n`password-error` / `account-locked` / any non-`success` state means the\nlogin failed — check that string before doing anything else. Credentials\ngo in the form body (never the query string — it lands in access logs).\n\nPull the XSRF token out of the jar (IC's JS reads it back and echoes it on\nevery request as `X-XSRF-TOKEN`; `JSESSIONID` alone is not enough):\n\n```sh\nXSRF=$(awk -F'\\t' '$6==\"XSRF-TOKEN\"{print $7}' \"$JAR\")\n```\n\n## Core call pattern\n\nEvery subsequent request rides the jar + the XSRF header:\n\n```sh\nic_get() { # ic_get <path-with-query>\n  curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \\\n    \"$IC_BASE_URL$1\"\n}\n\nic_get '/campus/api/portal/students' | jq .\n```\n\nA **401** means the session expired (IC sessions last ~5-6h) — re-run the\nLogin step to get a fresh `$JAR`/`$XSRF` and retry once. There is no retry\nbudget beyond that in this shell version (the MCP's `CookieSessionManager`\ndoes this automatically; here just redo the login block).\n\n## The one rule: resolve student + enrollment first\n\nAlmost every read is scoped to a `personID` (student), and several need the\nstudent's **enrollment** (`enrollmentID` / `calendarID` / `structureID`).\nAlways start with:\n\n```sh\nic_get '/campus/api/portal/students' | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nTake `personID` for `studentId`/`personID` params, and the first (or\nrelevant) `enrollments[]` entry's `enrollmentID`/`calendarID`/`structureID`\nfor the endpoints that need them (attendance, school days, assessments,\nfeatures). There is no name→id search — `ic_list_students` (i.e. this call)\n*is* the resolve step.\n\n## Endpoints\n\nAll 17 read endpoints (grades, attendance, assignments, schedule, messages,\ndocuments, fees, teachers, assessments, feature flags) with ready-to-run\n`curl`+`jq` recipes are in `references/endpoints.md`. Two (`behavior`,\n`food_service`) have real paths that were never confirmed live against a\ndistrict with that module enabled — try them, expect a 404 if the module\nis off or the district's shape differs.\n\n## Downloads\n\n`ic_download_document`'s `url` field (from the documents list) may be a\nrelative `/campus/...` path or an absolute URL. Same jar + header pattern,\njust write to a file instead of piping to `jq`:\n\n```sh\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -o report-card.pdf \"$IC_BASE_URL$DOC_URL\"\n```\n\n## FeatureDisabled districts\n\nSome districts turn whole modules off (behavior, food service, assessments,\ndocuments, attendance). The MCP checks `displayOptions` first and falls\nback to treating a 404 as \"disabled.\" Doing the same by hand: fetch\n`/campus/api/portal/displayOptions/{structureID}?personID=X` (see\nreferences) and check the flag before assuming a 404 is a bug.\n\n## Notes\n\n- Read-only in practice: every recipe here is a GET. `ic_get_message`\n  fetching a message body *may* mark it read on some district configs — the\n  MCP's own probe couldn't confirm the side effect either way.\n- No CUPS linked-district switching in this skill (a parent with kids in\n  2+ IC instances under shared SSO) — that's a multi-step token-exchange\n  flow (`src/client.ts`'s `discoverLinkedDistricts`); out of scope here.\n  Run the login flow again against the linked district's own\n  `IC_BASE_URL`/`IC_DISTRICT` if you need it.\n- This project is developed and maintained by AI (Claude).\n\nFile v3.1.7:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-api\",\n  \"version\": \"3.1.7\",\n  \"publishedAt\": 1791168567950\n}\n\nFile v3.1.7:references/endpoints.md\n\n# Infinite Campus endpoints for curl\n\nAssumes the Login block from `../SKILL.md` has already run (`$JAR`, `$XSRF`,\n`$IC_BASE_URL` set) and the `ic_get` helper is defined:\n\n```sh\nic_get() { curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \"$IC_BASE_URL$1\"; }\n```\n\nAll paths are transcribed from `infinitecampus-mcp`'s `src/tools/*.ts` and\n`docs/endpoints.md` — the same requests the `ic_*` MCP tools make. `$PID` =\na student's `personID` from the resolve-first step.\n\nSome IC/prism responses serialize a 1-item collection as a bare object\ninstead of a 1-element array — pipe through `jq 'if type==\"array\" then . else [.] end'`\nif a recipe below assumes an array and you get an object back.\n\n---\n\n## Students (resolve-first — always start here)\n\n```sh\nic_get '/campus/api/portal/students' \\\n  | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nEach `enrollments[]` entry carries `enrollmentID`, `calendarID`,\n`structureID`, `schoolName`, `endDate` — needed by several endpoints below.\n\n## Schedule\n\n```sh\nic_get \"/campus/resources/portal/roster?personID=$PID\" | jq .\n```\n\n(Only `personID` is honored server-side — there is no date/term filter, so\nthe MCP tool takes none.)\n\n## Assignments\n\n```sh\n# all assignments (full term history, ~hundreds of rows)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" | jq .\n\n# scoped to one course (sectionID from the schedule call above)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID&sectionID=$SECTION_ID\" | jq .\n\n# client-side filters (server ignores date/missing params entirely):\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.[] | select(.dueDate >= $since)]'\n```\n\n## Recent (recently-graded) assignments\n\nServer-side filtered by `modifiedDate` (defaults to 14 days ago in the MCP;\nreproduce with `date -u -v-14d '+%Y-%m-%dT00:00:00'` on macOS):\n\n```sh\nSINCE=$(date -u -v-14d '+%Y-%m-%dT00:00:00')\nic_get \"/campus/api/portal/assignment/recentlyScored?modifiedDate=$SINCE&personID=$PID\" | jq .\n```\n\n## Grades\n\n```sh\nic_get \"/campus/resources/portal/grades?personID=$PID\" | jq .\n# scoped to one term:\nic_get \"/campus/resources/portal/grades?personID=$PID&termID=$TERM_ID\" | jq .\n```\n\n## Attendance (per-course summary, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/$ENROLLMENT_ID?courseSummary=true&personID=$PID\" \\\n  | jq '.terms[]?.courses[]? | {absentList, tardyList, presentList, earlyReleaseList}'\n```\n\nFeature-gated: check `displayOptions.attendance` first (see Feature flags,\nbelow) — a 404 here on a district with attendance disabled is expected, not\na bug.\n\n## Attendance events (individual absences/tardies, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq '.events[]? | {date: .localDate, code, description, excuse, comments}'\n\n# excused-only, since a date (client-side filter):\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.events[]? | select((.localDate[0:10] >= $since) and (.excuse == \"E\"))]'\n```\n\n## School days / calendar (loop over each enrollment)\n\nTwo calls per enrollment, then join term windows to days client-side:\n\n```sh\nic_get \"/campus/resources/term?structureID=$STRUCTURE_ID\" | jq .          # term boundaries\nic_get \"/campus/resources/calendar/instructionalDay?calendarID=$CALENDAR_ID\" | jq .  # day list\n\n# days within term N's [startDate,endDate]:\njq --slurpfile terms terms.json --arg t \"$TERM_ID\" \\\n  '[.[] | select(.date >= $terms[0][0].startDate and .date <= $terms[0][0].endDate)]' days.json\n```\n\n## Assessments (standardized tests, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/prism/portal/assessments?personID=$PID&calendarID=$CALENDAR_ID\" \\\n  | jq '{stateTests, nationalTests, districtTests}'\n```\n\nFeature-gated on `displayOptions.assessment`; test-item shape varies by\ndistrict/test type — pass through unchanged, don't assume field names\nbeyond `stateTests`/`nationalTests`/`districtTests`.\n\n## Behavior *(path unconfirmed — never verified live against a district with the module on)*\n\n```sh\nic_get \"/campus/resources/portal/behavior?personID=$PID\" | jq .\n# with a date range:\nic_get \"/campus/resources/portal/behavior?personID=$PID&startDate=2026-01-01&endDate=2026-06-01\" | jq .\n```\n\nFeature-gated on `displayOptions.behavior` — a 404 is the expected result\non most districts (few enable this module for parent portal).\n\n## Food service *(path unconfirmed — same caveat as behavior)*\n\n```sh\nic_get \"/campus/resources/portal/foodService?personID=$PID\" | jq .\nic_get \"/campus/resources/portal/foodService?personID=$PID&startDate=2026-01-01&endDate=2026-06-01\" | jq .\n```\n\nFeature-gated on `displayOptions.foodService`.\n\n## Documents + download\n\n```sh\nic_get \"/campus/resources/portal/report/all?personID=$PID\" \\\n  | jq '.[] | {name, type, url, moduleLabel, endYear}'\n\n# download (url may be relative /campus/... or an absolute URL — handle both):\nDOC_URL='/campus/resources/portal/report/...'   # from the .url field above\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -o report-card.pdf \\\n  \"$( [[ \"$DOC_URL\" == http* ]] && echo \"$DOC_URL\" || echo \"$IC_BASE_URL$DOC_URL\" )\"\n```\n\nFeature-gated on `displayOptions.documents`.\n\n## Teachers + counselors\n\n```sh\nic_get \"/campus/resources/portal/section/contacts?personID=$PID\" | jq .\nic_get \"/campus/resources/portal/studentCounselor/byUser?personID=$PID\" | jq .\n```\n\nEither endpoint 404ing is treated as \"no results\" (not FeatureDisabled) by\nthe MCP — do the same.\n\n## Messages (three independent sources — combine client-side)\n\n```sh\n# 1. prism notifications (assignment/grade/attendance alerts)\nic_get '/campus/prism?x=notifications.Notification-retrieve&limitCount=20' \\\n  | jq '.data.NotificationList.Notification'\n\n# 2. Messenger 2.0 inbox (teacher messages, district announcements)\nic_get '/campus/api/portal/process-message' | jq .\n\n# 3. portal userNotice announcements\nic_get '/campus/resources/portal/userNotice' | jq .\n```\n\nEach inbox item's `url` field feeds `ic_get_message` below (normalize a bare\nor `portal/...`-relative URL to `/campus/portal/...` first).\n\n## Get one message body (HTML → text)\n\n```sh\n# normalize: bare \"portal/...\" → \"/campus/portal/...\"; already-absolute /campus/... stays as-is\nMSG_PATH='/campus/portal/messageView.xsl?x=messenger.MessengerEngine-getMessageRecipientView&messageID=...&messageRecipientID=...'\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: text/html' \"$IC_BASE_URL$MSG_PATH\"\n# then strip tags/entities yourself, or eyeball the rendered HTML — this is\n# an HTML fragment, not JSON, so `jq` doesn't apply.\n```\n\nFetching a message body *may* mark it read on some district configs\n(unconfirmed either way).\n\n## Fees (two endpoints in parallel; either can 404 independently)\n\n```sh\nic_get \"/campus/api/portal/fees/feeAssignments?personID=$PID\" | jq .\nic_get \"/campus/api/portal/fees/feeTransactionDetail/totalSurplus/-1?personID=$PID\" | jq .\n```\n\nBoth 404 → module is off for this district. One 404 → report the other\nside plus a note that the failing endpoint may be disabled.\n\n## Feature flags (displayOptions — check before assuming a 404 is a bug)\n\n```sh\nic_get \"/campus/api/portal/displayOptions/$STRUCTURE_ID?personID=$PID\" \\\n  | jq '{attendance, behavior, assessment, documents, foodService, grades, schedule}'\n```\n\n`false` = district has that module turned off for this enrollment; `true`\nor absent = available. ~90 flags total; the MCP caches this per\n`(district, structureID)` for the session lifetime since it rarely changes\nmid-session — worth doing the same if you're calling it more than once.\n\n---\n\n## Not covered here (out of scope for this skill)\n\n- **CUPS linked-district discovery/switching** — a parent with kids at 2+\n  IC-hosted districts under shared SSO. Multi-step: fetch\n  `cups/linkedAccounts`, `userAccountSwitch/originalDistrict`,\n  `districts/current`, then per linked account a `cups/loginToken` POST\n  followed by a second `verify.jsp`-style POST at the *linked* district's\n  own host with the CUPS token. Fully discoverable in `src/client.ts`\n  (`discoverLinkedDistricts`) but a lot of shell for a one-shot skill —\n  just re-run the Login block against the linked district's own\n  `IC_BASE_URL`/`IC_DISTRICT` instead.\n\nFile v3.1.7:skill-card.md\n\n## Description:\n\nGuides authenticated, district-specific Infinite Campus Parent portal queries using curl and jq without an MCP server.\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 and authorized developers use this skill to retrieve student grades, attendance, assignments, messages, documents, and other school records from their own district portal without running the MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Portal credentials, session cookies, or student records may be exposed through shell history, logs, or retained files.\n\nMitigation: Use only your authorized account on a trusted machine, keep secrets out of history and logs, protect the cookie jar, and delete cookies and downloaded records when finished.\n\nRisk: An absolute document URL may send an authenticated download request to an unexpected host.\n\nMitigation: Verify absolute download URLs belong to the expected district or Infinite Campus host before requesting them.\n\n## Reference(s):\n\n- [Infinite Campus endpoint recipes](artifact/references/endpoints.md)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Configuration instructions, Guidance]\n\n**Output Format:** [Markdown with curl and jq command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [District-specific endpoints and feature availability.]\n\n## Skill Version(s):\n\n3.1.7 (source: ClawHub 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 v3.1.6: 4 files, 7912 bytes\n\nFiles: references/endpoints.md (8406b), skill-card.md (2019b), SKILL.md (5776b), _meta.json (137b)\n\nFile v3.1.6:SKILL.md\n\n---\nname: infinitecampus-api\ndescription: >-\n  Query an Infinite Campus (Campus Parent portal) district directly with curl\n  instead of running the infinitecampus-mcp server — log in with a real\n  username/password, capture the session cookie + XSRF token, and curl grades,\n  attendance, assignments, schedule, messages, documents, and fees. Use when\n  you want IC data without the MCP, in a script, or on a machine where the MCP\n  isn't installed. Infinite Campus is per-district: the base URL and district\n  app name are configurable, not hardcoded.\n---\n\n# Infinite Campus via curl (no MCP)\n\nInfinite Campus's Campus Parent portal is a classic server-rendered site\n(no public API, no bot wall). A plain form POST to `verify.jsp` logs a\nparent in and hands back a session cookie — no browser bridge needed. This\nis the same login `infinitecampus-mcp`'s `src/client.ts`/`src/auth.ts` do;\nthis skill reproduces it with `curl` + a cookie jar.\n\n**Infinite Campus is per-district.** Every parent's portal lives at their\nown district's host (`https://campus.<district>.k12.example.us` or similar)\nunder an `appName` path segment. There is no shared IC endpoint — carry the\nbase URL and district from the same config the MCP uses:\n\n```sh\n: \"${IC_BASE_URL:?set to your district's portal URL, e.g. https://campus.yourdistrict.org}\"\n: \"${IC_DISTRICT:?set to the appName path segment, e.g. yourdistrict}\"\n: \"${IC_USERNAME:?parent portal username}\"\n: \"${IC_PASSWORD:?parent portal password}\"\n```\n\n(Same 4 env vars the MCP reads — reuse the repo's `.env` or the host's\nsecret store; don't hardcode a district.)\n\n## One-time setup\n\nNone — `curl` and `jq` are the only tools needed (`brew install jq` if\nmissing). No profile/pairing step, no extension.\n\n## Login (capture cookies + XSRF token)\n\n`verify.jsp` accepts a urlencoded form POST and returns 200 with an\n`<AUTHENTICATION>state</AUTHENTICATION>` marker in the body — **not**\nvia HTTP status — plus ~20 `Set-Cookie` headers. Use a cookie jar so curl\ndoes the Set-Cookie parsing/deduping for you:\n\n```sh\nJAR=$(mktemp)\nBODY=$(curl -sS -c \"$JAR\" \\\n  \"$IC_BASE_URL/campus/verify.jsp?nonBrowser=true\" \\\n  --data-urlencode \"username=$IC_USERNAME\" \\\n  --data-urlencode \"password=$IC_PASSWORD\" \\\n  --data-urlencode \"appName=$IC_DISTRICT\" \\\n  --data-urlencode \"portalLoginPage=parents\")\n\necho \"$BODY\" | grep -o '<AUTHENTICATION>[^<]*' # AUTHENTICATIONsuccess = good\n```\n\n`password-error` / `account-locked` / any non-`success` state means the\nlogin failed — check that string before doing anything else. Credentials\ngo in the form body (never the query string — it lands in access logs).\n\nPull the XSRF token out of the jar (IC's JS reads it back and echoes it on\nevery request as `X-XSRF-TOKEN`; `JSESSIONID` alone is not enough):\n\n```sh\nXSRF=$(awk -F'\\t' '$6==\"XSRF-TOKEN\"{print $7}' \"$JAR\")\n```\n\n## Core call pattern\n\nEvery subsequent request rides the jar + the XSRF header:\n\n```sh\nic_get() { # ic_get <path-with-query>\n  curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \\\n    \"$IC_BASE_URL$1\"\n}\n\nic_get '/campus/api/portal/students' | jq .\n```\n\nA **401** means the session expired (IC sessions last ~5-6h) — re-run the\nLogin step to get a fresh `$JAR`/`$XSRF` and retry once. There is no retry\nbudget beyond that in this shell version (the MCP's `CookieSessionManager`\ndoes this automatically; here just redo the login block).\n\n## The one rule: resolve student + enrollment first\n\nAlmost every read is scoped to a `personID` (student), and several need the\nstudent's **enrollment** (`enrollmentID` / `calendarID` / `structureID`).\nAlways start with:\n\n```sh\nic_get '/campus/api/portal/students' | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nTake `personID` for `studentId`/`personID` params, and the first (or\nrelevant) `enrollments[]` entry's `enrollmentID`/`calendarID`/`structureID`\nfor the endpoints that need them (attendance, school days, assessments,\nfeatures). There is no name→id search — `ic_list_students` (i.e. this call)\n*is* the resolve step.\n\n## Endpoints\n\nAll 17 read endpoints (grades, attendance, assignments, schedule, messages,\ndocuments, fees, teachers, assessments, feature flags) with ready-to-run\n`curl`+`jq` recipes are in `references/endpoints.md`. Two (`behavior`,\n`food_service`) have real paths that were never confirmed live against a\ndistrict with that module enabled — try them, expect a 404 if the module\nis off or the district's shape differs.\n\n## Downloads\n\n`ic_download_document`'s `url` field (from the documents list) may be a\nrelative `/campus/...` path or an absolute URL. Same jar + header pattern,\njust write to a file instead of piping to `jq`:\n\n```sh\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -o report-card.pdf \"$IC_BASE_URL$DOC_URL\"\n```\n\n## FeatureDisabled districts\n\nSome districts turn whole modules off (behavior, food service, assessments,\ndocuments, attendance). The MCP checks `displayOptions` first and falls\nback to treating a 404 as \"disabled.\" Doing the same by hand: fetch\n`/campus/api/portal/displayOptions/{structureID}?personID=X` (see\nreferences) and check the flag before assuming a 404 is a bug.\n\n## Notes\n\n- Read-only in practice: every recipe here is a GET. `ic_get_message`\n  fetching a message body *may* mark it read on some district configs — the\n  MCP's own probe couldn't confirm the side effect either way.\n- No CUPS linked-district switching in this skill (a parent with kids in\n  2+ IC instances under shared SSO) — that's a multi-step token-exchange\n  flow (`src/client.ts`'s `discoverLinkedDistricts`); out of scope here.\n  Run the login flow again against the linked district's own\n  `IC_BASE_URL`/`IC_DISTRICT` if you need it.\n- This project is developed and maintained by AI (Claude).\n\nFile v3.1.6:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-api\",\n  \"version\": \"3.1.6\",\n  \"publishedAt\": 1790991730356\n}\n\nFile v3.1.6:references/endpoints.md\n\n# Infinite Campus endpoints for curl\n\nAssumes the Login block from `../SKILL.md` has already run (`$JAR`, `$XSRF`,\n`$IC_BASE_URL` set) and the `ic_get` helper is defined:\n\n```sh\nic_get() { curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \"$IC_BASE_URL$1\"; }\n```\n\nAll paths are transcribed from `infinitecampus-mcp`'s `src/tools/*.ts` and\n`docs/endpoints.md` — the same requests the `ic_*` MCP tools make. `$PID` =\na student's `personID` from the resolve-first step.\n\nSome IC/prism responses serialize a 1-item collection as a bare object\ninstead of a 1-element array — pipe through `jq 'if type==\"array\" then . else [.] end'`\nif a recipe below assumes an array and you get an object back.\n\n---\n\n## Students (resolve-first — always start here)\n\n```sh\nic_get '/campus/api/portal/students' \\\n  | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nEach `enrollments[]` entry carries `enrollmentID`, `calendarID`,\n`structureID`, `schoolName`, `endDate` — needed by several endpoints below.\n\n## Schedule\n\n```sh\nic_get \"/campus/resources/portal/roster?personID=$PID\" | jq .\n```\n\n(Only `personID` is honored server-side — there is no date/term filter, so\nthe MCP tool takes none.)\n\n## Assignments\n\n```sh\n# all assignments (full term history, ~hundreds of rows)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" | jq .\n\n# scoped to one course (sectionID from the schedule call above)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID&sectionID=$SECTION_ID\" | jq .\n\n# client-side filters (server ignores date/missing params entirely):\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.[] | select(.dueDate >= $since)]'\n```\n\n## Recent (recently-graded) assignments\n\nServer-side filtered by `modifiedDate` (defaults to 14 days ago in the MCP;\nreproduce with `date -u -v-14d '+%Y-%m-%dT00:00:00'` on macOS):\n\n```sh\nSINCE=$(date -u -v-14d '+%Y-%m-%dT00:00:00')\nic_get \"/campus/api/portal/assignment/recentlyScored?modifiedDate=$SINCE&personID=$PID\" | jq .\n```\n\n## Grades\n\n```sh\nic_get \"/campus/resources/portal/grades?personID=$PID\" | jq .\n# scoped to one term:\nic_get \"/campus/resources/portal/grades?personID=$PID&termID=$TERM_ID\" | jq .\n```\n\n## Attendance (per-course summary, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/$ENROLLMENT_ID?courseSummary=true&personID=$PID\" \\\n  | jq '.terms[]?.courses[]? | {absentList, tardyList, presentList, earlyReleaseList}'\n```\n\nFeature-gated: check `displayOptions.attendance` first (see Feature flags,\nbelow) — a 404 here on a district with attendance disabled is expected, not\na bug.\n\n## Attendance events (individual absences/tardies, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq '.events[]? | {date: .localDate, code, description, excuse, comments}'\n\n# excused-only, since a date (client-side filter):\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.events[]? | select((.localDate[0:10] >= $since) and (.excuse == \"E\"))]'\n```\n\n## School days / calendar (loop over each enrollment)\n\nTwo calls per enrollment, then join term windows to days client-side:\n\n```sh\nic_get \"/campus/resources/term?structureID=$STRUCTURE_ID\" | jq .          # term boundaries\nic_get \"/campus/resources/calendar/instructionalDay?calendarID=$CALENDAR_ID\" | jq .  # day list\n\n# days within term N's [startDate,endDate]:\njq --slurpfile terms terms.json --arg t \"$TERM_ID\" \\\n  '[.[] | select(.date >= $terms[0][0].startDate and .date <= $terms[0][0].endDate)]' days.json\n```\n\n## Assessments (standardized tests, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/prism/portal/assessments?personID=$PID&calendarID=$CALENDAR_ID\" \\\n  | jq '{stateTests, nationalTests, districtTests}'\n```\n\nFeature-gated on `displayOptions.assessment`; test-item shape varies by\ndistrict/test type — pass through unchanged, don't assume field names\nbeyond `stateTests`/`nationalTests`/`districtTests`.\n\n## Behavior *(path unconfirmed — never verified live against a district with the module on)*\n\n```sh\nic_get \"/campus/resources/portal/behavior?personID=$PID\" | jq .\n# with a date range:\nic_get \"/campus/resources/portal/behavior?personID=$PID&startDate=2026-01-01&endDate=2026-06-01\" | jq .\n```\n\nFeature-gated on `displayOptions.behavior` — a 404 is the expected result\non most districts (few enable this module for parent portal).\n\n## Food service *(path unconfirmed — same caveat as behavior)*\n\n```sh\nic_get \"/campus/resources/portal/foodService?personID=$PID\" | jq .\nic_get \"/campus/resources/portal/foodService?personID=$PID&startDate=2026-01-01&endDate=2026-06-01\" | jq .\n```\n\nFeature-gated on `displayOptions.foodService`.\n\n## Documents + download\n\n```sh\nic_get \"/campus/resources/portal/report/all?personID=$PID\" \\\n  | jq '.[] | {name, type, url, moduleLabel, endYear}'\n\n# download (url may be relative /campus/... or an absolute URL — handle both):\nDOC_URL='/campus/resources/portal/report/...'   # from the .url field above\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -o report-card.pdf \\\n  \"$( [[ \"$DOC_URL\" == http* ]] && echo \"$DOC_URL\" || echo \"$IC_BASE_URL$DOC_URL\" )\"\n```\n\nFeature-gated on `displayOptions.documents`.\n\n## Teachers + counselors\n\n```sh\nic_get \"/campus/resources/portal/section/contacts?personID=$PID\" | jq .\nic_get \"/campus/resources/portal/studentCounselor/byUser?personID=$PID\" | jq .\n```\n\nEither endpoint 404ing is treated as \"no results\" (not FeatureDisabled) by\nthe MCP — do the same.\n\n## Messages (three independent sources — combine client-side)\n\n```sh\n# 1. prism notifications (assignment/grade/attendance alerts)\nic_get '/campus/prism?x=notifications.Notification-retrieve&limitCount=20' \\\n  | jq '.data.NotificationList.Notification'\n\n# 2. Messenger 2.0 inbox (teacher messages, district announcements)\nic_get '/campus/api/portal/process-message' | jq .\n\n# 3. portal userNotice announcements\nic_get '/campus/resources/portal/userNotice' | jq .\n```\n\nEach inbox item's `url` field feeds `ic_get_message` below (normalize a bare\nor `portal/...`-relative URL to `/campus/portal/...` first).\n\n## Get one message body (HTML → text)\n\n```sh\n# normalize: bare \"portal/...\" → \"/campus/portal/...\"; already-absolute /campus/... stays as-is\nMSG_PATH='/campus/portal/messageView.xsl?x=messenger.MessengerEngine-getMessageRecipientView&messageID=...&messageRecipientID=...'\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: text/html' \"$IC_BASE_URL$MSG_PATH\"\n# then strip tags/entities yourself, or eyeball the rendered HTML — this is\n# an HTML fragment, not JSON, so `jq` doesn't apply.\n```\n\nFetching a message body *may* mark it read on some district configs\n(unconfirmed either way).\n\n## Fees (two endpoints in parallel; either can 404 independently)\n\n```sh\nic_get \"/campus/api/portal/fees/feeAssignments?personID=$PID\" | jq .\nic_get \"/campus/api/portal/fees/feeTransactionDetail/totalSurplus/-1?personID=$PID\" | jq .\n```\n\nBoth 404 → module is off for this district. One 404 → report the other\nside plus a note that the failing endpoint may be disabled.\n\n## Feature flags (displayOptions — check before assuming a 404 is a bug)\n\n```sh\nic_get \"/campus/api/portal/displayOptions/$STRUCTURE_ID?personID=$PID\" \\\n  | jq '{attendance, behavior, assessment, documents, foodService, grades, schedule}'\n```\n\n`false` = district has that module turned off for this enrollment; `true`\nor absent = available. ~90 flags total; the MCP caches this per\n`(district, structureID)` for the session lifetime since it rarely changes\nmid-session — worth doing the same if you're calling it more than once.\n\n---\n\n## Not covered here (out of scope for this skill)\n\n- **CUPS linked-district discovery/switching** — a parent with kids at 2+\n  IC-hosted districts under shared SSO. Multi-step: fetch\n  `cups/linkedAccounts`, `userAccountSwitch/originalDistrict`,\n  `districts/current`, then per linked account a `cups/loginToken` POST\n  followed by a second `verify.jsp`-style POST at the *linked* district's\n  own host with the CUPS token. Fully discoverable in `src/client.ts`\n  (`discoverLinkedDistricts`) but a lot of shell for a one-shot skill —\n  just re-run the Login block against the linked district's own\n  `IC_BASE_URL`/`IC_DISTRICT` instead.\n\nFile v3.1.6:skill-card.md\n\n## Description:\n\nGuides direct curl access to district Infinite Campus Parent portals for student grades, attendance, assignments, schedules, messages, documents, and fees without an MCP server.\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 and authorized developers use the skill to retrieve student portal information from their district with curl when they do not use the MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Live portal credentials and session tokens may be exposed through shared machines, logs, or shell history.\n\nMitigation: Use a private machine and secret storage; protect temporary cookie files and avoid logging credentials or tokens.\n\nRisk: Downloaded PDFs and JSON responses may expose sensitive student education records.\n\nMitigation: Choose private output paths, secure temporary files, and delete session files and downloaded records when no longer needed.\n\nRisk: Opening a message body may mark it read in some districts.\n\nMitigation: Confirm this possible side effect before retrieving message bodies.\n\n## Reference(s):\n\n- [Infinite Campus endpoint recipes](references/endpoints.md)\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/infinitecampus-api)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Configuration instructions, Guidance]\n\n**Output Format:** [Markdown with curl and jq examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [District-specific portal settings and student or enrollment IDs are required for requests.]\n\n## Skill Version(s):\n\n3.1.6 (source: ClawHub 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 v3.1.5: 4 files, 7826 bytes\n\nFiles: references/endpoints.md (8406b), skill-card.md (1867b), SKILL.md (5776b), _meta.json (137b)\n\nFile v3.1.5:SKILL.md\n\n---\nname: infinitecampus-api\ndescription: >-\n  Query an Infinite Campus (Campus Parent portal) district directly with curl\n  instead of running the infinitecampus-mcp server — log in with a real\n  username/password, capture the session cookie + XSRF token, and curl grades,\n  attendance, assignments, schedule, messages, documents, and fees. Use when\n  you want IC data without the MCP, in a script, or on a machine where the MCP\n  isn't installed. Infinite Campus is per-district: the base URL and district\n  app name are configurable, not hardcoded.\n---\n\n# Infinite Campus via curl (no MCP)\n\nInfinite Campus's Campus Parent portal is a classic server-rendered site\n(no public API, no bot wall). A plain form POST to `verify.jsp` logs a\nparent in and hands back a session cookie — no browser bridge needed. This\nis the same login `infinitecampus-mcp`'s `src/client.ts`/`src/auth.ts` do;\nthis skill reproduces it with `curl` + a cookie jar.\n\n**Infinite Campus is per-district.** Every parent's portal lives at their\nown district's host (`https://campus.<district>.k12.example.us` or similar)\nunder an `appName` path segment. There is no shared IC endpoint — carry the\nbase URL and district from the same config the MCP uses:\n\n```sh\n: \"${IC_BASE_URL:?set to your district's portal URL, e.g. https://campus.yourdistrict.org}\"\n: \"${IC_DISTRICT:?set to the appName path segment, e.g. yourdistrict}\"\n: \"${IC_USERNAME:?parent portal username}\"\n: \"${IC_PASSWORD:?parent portal password}\"\n```\n\n(Same 4 env vars the MCP reads — reuse the repo's `.env` or the host's\nsecret store; don't hardcode a district.)\n\n## One-time setup\n\nNone — `curl` and `jq` are the only tools needed (`brew install jq` if\nmissing). No profile/pairing step, no extension.\n\n## Login (capture cookies + XSRF token)\n\n`verify.jsp` accepts a urlencoded form POST and returns 200 with an\n`<AUTHENTICATION>state</AUTHENTICATION>` marker in the body — **not**\nvia HTTP status — plus ~20 `Set-Cookie` headers. Use a cookie jar so curl\ndoes the Set-Cookie parsing/deduping for you:\n\n```sh\nJAR=$(mktemp)\nBODY=$(curl -sS -c \"$JAR\" \\\n  \"$IC_BASE_URL/campus/verify.jsp?nonBrowser=true\" \\\n  --data-urlencode \"username=$IC_USERNAME\" \\\n  --data-urlencode \"password=$IC_PASSWORD\" \\\n  --data-urlencode \"appName=$IC_DISTRICT\" \\\n  --data-urlencode \"portalLoginPage=parents\")\n\necho \"$BODY\" | grep -o '<AUTHENTICATION>[^<]*' # AUTHENTICATIONsuccess = good\n```\n\n`password-error` / `account-locked` / any non-`success` state means the\nlogin failed — check that string before doing anything else. Credentials\ngo in the form body (never the query string — it lands in access logs).\n\nPull the XSRF token out of the jar (IC's JS reads it back and echoes it on\nevery request as `X-XSRF-TOKEN`; `JSESSIONID` alone is not enough):\n\n```sh\nXSRF=$(awk -F'\\t' '$6==\"XSRF-TOKEN\"{print $7}' \"$JAR\")\n```\n\n## Core call pattern\n\nEvery subsequent request rides the jar + the XSRF header:\n\n```sh\nic_get() { # ic_get <path-with-query>\n  curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \\\n    \"$IC_BASE_URL$1\"\n}\n\nic_get '/campus/api/portal/students' | jq .\n```\n\nA **401** means the session expired (IC sessions last ~5-6h) — re-run the\nLogin step to get a fresh `$JAR`/`$XSRF` and retry once. There is no retry\nbudget beyond that in this shell version (the MCP's `CookieSessionManager`\ndoes this automatically; here just redo the login block).\n\n## The one rule: resolve student + enrollment first\n\nAlmost every read is scoped to a `personID` (student), and several need the\nstudent's **enrollment** (`enrollmentID` / `calendarID` / `structureID`).\nAlways start with:\n\n```sh\nic_get '/campus/api/portal/students' | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nTake `personID` for `studentId`/`personID` params, and the first (or\nrelevant) `enrollments[]` entry's `enrollmentID`/`calendarID`/`structureID`\nfor the endpoints that need them (attendance, school days, assessments,\nfeatures). There is no name→id search — `ic_list_students` (i.e. this call)\n*is* the resolve step.\n\n## Endpoints\n\nAll 17 read endpoints (grades, attendance, assignments, schedule, messages,\ndocuments, fees, teachers, assessments, feature flags) with ready-to-run\n`curl`+`jq` recipes are in `references/endpoints.md`. Two (`behavior`,\n`food_service`) have real paths that were never confirmed live against a\ndistrict with that module enabled — try them, expect a 404 if the module\nis off or the district's shape differs.\n\n## Downloads\n\n`ic_download_document`'s `url` field (from the documents list) may be a\nrelative `/campus/...` path or an absolute URL. Same jar + header pattern,\njust write to a file instead of piping to `jq`:\n\n```sh\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -o report-card.pdf \"$IC_BASE_URL$DOC_URL\"\n```\n\n## FeatureDisabled districts\n\nSome districts turn whole modules off (behavior, food service, assessments,\ndocuments, attendance). The MCP checks `displayOptions` first and falls\nback to treating a 404 as \"disabled.\" Doing the same by hand: fetch\n`/campus/api/portal/displayOptions/{structureID}?personID=X` (see\nreferences) and check the flag before assuming a 404 is a bug.\n\n## Notes\n\n- Read-only in practice: every recipe here is a GET. `ic_get_message`\n  fetching a message body *may* mark it read on some district configs — the\n  MCP's own probe couldn't confirm the side effect either way.\n- No CUPS linked-district switching in this skill (a parent with kids in\n  2+ IC instances under shared SSO) — that's a multi-step token-exchange\n  flow (`src/client.ts`'s `discoverLinkedDistricts`); out of scope here.\n  Run the login flow again against the linked district's own\n  `IC_BASE_URL`/`IC_DISTRICT` if you need it.\n- This project is developed and maintained by AI (Claude).\n\nFile v3.1.5:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-api\",\n  \"version\": \"3.1.5\",\n  \"publishedAt\": 1790603752577\n}\n\nFile v3.1.5:references/endpoints.md\n\n# Infinite Campus endpoints for curl\n\nAssumes the Login block from `../SKILL.md` has already run (`$JAR`, `$XSRF`,\n`$IC_BASE_URL` set) and the `ic_get` helper is defined:\n\n```sh\nic_get() { curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \"$IC_BASE_URL$1\"; }\n```\n\nAll paths are transcribed from `infinitecampus-mcp`'s `src/tools/*.ts` and\n`docs/endpoints.md` — the same requests the `ic_*` MCP tools make. `$PID` =\na student's `personID` from the resolve-first step.\n\nSome IC/prism responses serialize a 1-item collection as a bare object\ninstead of a 1-element array — pipe through `jq 'if type==\"array\" then . else [.] end'`\nif a recipe below assumes an array and you get an object back.\n\n---\n\n## Students (resolve-first — always start here)\n\n```sh\nic_get '/campus/api/portal/students' \\\n  | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nEach `enrollments[]` entry carries `enrollmentID`, `calendarID`,\n`structureID`, `schoolName`, `endDate` — needed by several endpoints below.\n\n## Schedule\n\n```sh\nic_get \"/campus/resources/portal/roster?personID=$PID\" | jq .\n```\n\n(Only `personID` is honored server-side — there is no date/term filter, so\nthe MCP tool takes none.)\n\n## Assignments\n\n```sh\n# all assignments (full term history, ~hundreds of rows)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" | jq .\n\n# scoped to one course (sectionID from the schedule call above)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID&sectionID=$SECTION_ID\" | jq .\n\n# client-side filters (server ignores date/missing params entirely):\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.[] | select(.dueDate >= $since)]'\n```\n\n## Recent (recently-graded) assignments\n\nServer-side filtered by `modifiedDate` (defaults to 14 days ago in the MCP;\nreproduce with `date -u -v-14d '+%Y-%m-%dT00:00:00'` on macOS):\n\n```sh\nSINCE=$(date -u -v-14d '+%Y-%m-%dT00:00:00')\nic_get \"/campus/api/portal/assignment/recentlyScored?modifiedDate=$SINCE&personID=$PID\" | jq .\n```\n\n## Grades\n\n```sh\nic_get \"/campus/resources/portal/grades?personID=$PID\" | jq .\n# scoped to one term:\nic_get \"/campus/resources/portal/grades?personID=$PID&termID=$TERM_ID\" | jq .\n```\n\n## Attendance (per-course summary, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/$ENROLLMENT_ID?courseSummary=true&personID=$PID\" \\\n  | jq '.terms[]?.courses[]? | {absentList, tardyList, presentList, earlyReleaseList}'\n```\n\nFeature-gated: check `displayOptions.attendance` first (see Feature flags,\nbelow) — a 404 here on a district with attendance disabled is expected, not\na bug.\n\n## Attendance events (individual absences/tardies, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq '.events[]? | {date: .localDate, code, description, excuse, comments}'\n\n# excused-only, since a date (client-side filter):\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.events[]? | select((.localDate[0:10] >= $since) and (.excuse == \"E\"))]'\n```\n\n## School days / calendar (loop over each enrollment)\n\nTwo calls per enrollment, then join term windows to days client-side:\n\n```sh\nic_get \"/campus/resources/term?structureID=$STRUCTURE_ID\" | jq .          # term boundaries\nic_get \"/campus/resources/calendar/instructionalDay?calendarID=$CALENDAR_ID\" | jq .  # day list\n\n# days within term N's [startDate,endDate]:\njq --slurpfile terms terms.json --arg t \"$TERM_ID\" \\\n  '[.[] | select(.date >= $terms[0][0].startDate and .date <= $terms[0][0].endDate)]' days.json\n```\n\n## Assessments (standardized tests, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/prism/portal/assessments?personID=$PID&calendarID=$CALENDAR_ID\" \\\n  | jq '{stateTests, nationalTests, districtTests}'\n```\n\nFeature-gated on `displayOptions.assessment`; test-item shape varies by\ndistrict/test type — pass through unchanged, don't assume field names\nbeyond `stateTests`/`nationalTests`/`districtTests`.\n\n## Behavior *(path unconfirmed — never verified live against a district with the module on)*\n\n```sh\nic_get \"/campus/resources/portal/behavior?personID=$PID\" | jq .\n# with a date range:\nic_get \"/campus/resources/portal/behavior?personID=$PID&startDate=2026-01-01&endDate=2026-06-01\" | jq .\n```\n\nFeature-gated on `displayOptions.behavior` — a 404 is the expected result\non most districts (few enable this module for parent portal).\n\n## Food service *(path unconfirmed — same caveat as behavior)*\n\n```sh\nic_get \"/campus/resources/portal/foodService?personID=$PID\" | jq .\nic_get \"/campus/resources/portal/foodService?personID=$PID&startDate=2026-01-01&endDate=2026-06-01\" | jq .\n```\n\nFeature-gated on `displayOptions.foodService`.\n\n## Documents + download\n\n```sh\nic_get \"/campus/resources/portal/report/all?personID=$PID\" \\\n  | jq '.[] | {name, type, url, moduleLabel, endYear}'\n\n# download (url may be relative /campus/... or an absolute URL — handle both):\nDOC_URL='/campus/resources/portal/report/...'   # from the .url field above\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -o report-card.pdf \\\n  \"$( [[ \"$DOC_URL\" == http* ]] && echo \"$DOC_URL\" || echo \"$IC_BASE_URL$DOC_URL\" )\"\n```\n\nFeature-gated on `displayOptions.documents`.\n\n## Teachers + counselors\n\n```sh\nic_get \"/campus/resources/portal/section/contacts?personID=$PID\" | jq .\nic_get \"/campus/resources/portal/studentCounselor/byUser?personID=$PID\" | jq .\n```\n\nEither endpoint 404ing is treated as \"no results\" (not FeatureDisabled) by\nthe MCP — do the same.\n\n## Messages (three independent sources — combine client-side)\n\n```sh\n# 1. prism notifications (assignment/grade/attendance alerts)\nic_get '/campus/prism?x=notifications.Notification-retrieve&limitCount=20' \\\n  | jq '.data.NotificationList.Notification'\n\n# 2. Messenger 2.0 inbox (teacher messages, district announcements)\nic_get '/campus/api/portal/process-message' | jq .\n\n# 3. portal userNotice announcements\nic_get '/campus/resources/portal/userNotice' | jq .\n```\n\nEach inbox item's `url` field feeds `ic_get_message` below (normalize a bare\nor `portal/...`-relative URL to `/campus/portal/...` first).\n\n## Get one message body (HTML → text)\n\n```sh\n# normalize: bare \"portal/...\" → \"/campus/portal/...\"; already-absolute /campus/... stays as-is\nMSG_PATH='/campus/portal/messageView.xsl?x=messenger.MessengerEngine-getMessageRecipientView&messageID=...&messageRecipientID=...'\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: text/html' \"$IC_BASE_URL$MSG_PATH\"\n# then strip tags/entities yourself, or eyeball the rendered HTML — this is\n# an HTML fragment, not JSON, so `jq` doesn't apply.\n```\n\nFetching a message body *may* mark it read on some district configs\n(unconfirmed either way).\n\n## Fees (two endpoints in parallel; either can 404 independently)\n\n```sh\nic_get \"/campus/api/portal/fees/feeAssignments?personID=$PID\" | jq .\nic_get \"/campus/api/portal/fees/feeTransactionDetail/totalSurplus/-1?personID=$PID\" | jq .\n```\n\nBoth 404 → module is off for this district. One 404 → report the other\nside plus a note that the failing endpoint may be disabled.\n\n## Feature flags (displayOptions — check before assuming a 404 is a bug)\n\n```sh\nic_get \"/campus/api/portal/displayOptions/$STRUCTURE_ID?personID=$PID\" \\\n  | jq '{attendance, behavior, assessment, documents, foodService, grades, schedule}'\n```\n\n`false` = district has that module turned off for this enrollment; `true`\nor absent = available. ~90 flags total; the MCP caches this per\n`(district, structureID)` for the session lifetime since it rarely changes\nmid-session — worth doing the same if you're calling it more than once.\n\n---\n\n## Not covered here (out of scope for this skill)\n\n- **CUPS linked-district discovery/switching** — a parent with kids at 2+\n  IC-hosted districts under shared SSO. Multi-step: fetch\n  `cups/linkedAccounts`, `userAccountSwitch/originalDistrict`,\n  `districts/current`, then per linked account a `cups/loginToken` POST\n  followed by a second `verify.jsp`-style POST at the *linked* district's\n  own host with the CUPS token. Fully discoverable in `src/client.ts`\n  (`discoverLinkedDistricts`) but a lot of shell for a one-shot skill —\n  just re-run the Login block against the linked district's own\n  `IC_BASE_URL`/`IC_DISTRICT` instead.\n\nFile v3.1.5:skill-card.md\n\n## Description:\n\nGuides authorized Campus Parent users through querying district-specific Infinite Campus portal data with curl without an MCP server.\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\nAuthorized parents, guardians, and developers use the skill to retrieve grades, attendance, assignments, schedules, messages, documents, and fees from their district's Campus Parent portal without running an MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Account credentials, session cookies, and student records could be exposed if logged, shared, or left on a shared machine.\n\nMitigation: Use only an account you are authorized to access; keep credentials, tokens, reports, and records private, and remove temporary session files and downloads when finished.\n\nRisk: Fetching a message body may mark the message as read on some district portals.\n\nMitigation: Fetch message bodies only when marking the message read would be acceptable.\n\n## Reference(s):\n\n- [Infinite Campus endpoint recipes](references/endpoints.md)\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/infinitecampus-api)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell code blocks]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [District-specific portal URL and authorized account credentials required.]\n\n## Skill Version(s):\n\n3.1.5 (source: ClawHub 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 v3.1.4: 4 files, 7835 bytes\n\nFiles: references/endpoints.md (8406b), skill-card.md (1908b), SKILL.md (5776b), _meta.json (137b)\n\nFile v3.1.4:SKILL.md\n\n---\nname: infinitecampus-api\ndescription: >-\n  Query an Infinite Campus (Campus Parent portal) district directly with curl\n  instead of running the infinitecampus-mcp server — log in with a real\n  username/password, capture the session cookie + XSRF token, and curl grades,\n  attendance, assignments, schedule, messages, documents, and fees. Use when\n  you want IC data without the MCP, in a script, or on a machine where the MCP\n  isn't installed. Infinite Campus is per-district: the base URL and district\n  app name are configurable, not hardcoded.\n---\n\n# Infinite Campus via curl (no MCP)\n\nInfinite Campus's Campus Parent portal is a classic server-rendered site\n(no public API, no bot wall). A plain form POST to `verify.jsp` logs a\nparent in and hands back a session cookie — no browser bridge needed. This\nis the same login `infinitecampus-mcp`'s `src/client.ts`/`src/auth.ts` do;\nthis skill reproduces it with `curl` + a cookie jar.\n\n**Infinite Campus is per-district.** Every parent's portal lives at their\nown district's host (`https://campus.<district>.k12.example.us` or similar)\nunder an `appName` path segment. There is no shared IC endpoint — carry the\nbase URL and district from the same config the MCP uses:\n\n```sh\n: \"${IC_BASE_URL:?set to your district's portal URL, e.g. https://campus.yourdistrict.org}\"\n: \"${IC_DISTRICT:?set to the appName path segment, e.g. yourdistrict}\"\n: \"${IC_USERNAME:?parent portal username}\"\n: \"${IC_PASSWORD:?parent portal password}\"\n```\n\n(Same 4 env vars the MCP reads — reuse the repo's `.env` or the host's\nsecret store; don't hardcode a district.)\n\n## One-time setup\n\nNone — `curl` and `jq` are the only tools needed (`brew install jq` if\nmissing). No profile/pairing step, no extension.\n\n## Login (capture cookies + XSRF token)\n\n`verify.jsp` accepts a urlencoded form POST and returns 200 with an\n`<AUTHENTICATION>state</AUTHENTICATION>` marker in the body — **not**\nvia HTTP status — plus ~20 `Set-Cookie` headers. Use a cookie jar so curl\ndoes the Set-Cookie parsing/deduping for you:\n\n```sh\nJAR=$(mktemp)\nBODY=$(curl -sS -c \"$JAR\" \\\n  \"$IC_BASE_URL/campus/verify.jsp?nonBrowser=true\" \\\n  --data-urlencode \"username=$IC_USERNAME\" \\\n  --data-urlencode \"password=$IC_PASSWORD\" \\\n  --data-urlencode \"appName=$IC_DISTRICT\" \\\n  --data-urlencode \"portalLoginPage=parents\")\n\necho \"$BODY\" | grep -o '<AUTHENTICATION>[^<]*' # AUTHENTICATIONsuccess = good\n```\n\n`password-error` / `account-locked` / any non-`success` state means the\nlogin failed — check that string before doing anything else. Credentials\ngo in the form body (never the query string — it lands in access logs).\n\nPull the XSRF token out of the jar (IC's JS reads it back and echoes it on\nevery request as `X-XSRF-TOKEN`; `JSESSIONID` alone is not enough):\n\n```sh\nXSRF=$(awk -F'\\t' '$6==\"XSRF-TOKEN\"{print $7}' \"$JAR\")\n```\n\n## Core call pattern\n\nEvery subsequent request rides the jar + the XSRF header:\n\n```sh\nic_get() { # ic_get <path-with-query>\n  curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \\\n    \"$IC_BASE_URL$1\"\n}\n\nic_get '/campus/api/portal/students' | jq .\n```\n\nA **401** means the session expired (IC sessions last ~5-6h) — re-run the\nLogin step to get a fresh `$JAR`/`$XSRF` and retry once. There is no retry\nbudget beyond that in this shell version (the MCP's `CookieSessionManager`\ndoes this automatically; here just redo the login block).\n\n## The one rule: resolve student + enrollment first\n\nAlmost every read is scoped to a `personID` (student), and several need the\nstudent's **enrollment** (`enrollmentID` / `calendarID` / `structureID`).\nAlways start with:\n\n```sh\nic_get '/campus/api/portal/students' | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nTake `personID` for `studentId`/`personID` params, and the first (or\nrelevant) `enrollments[]` entry's `enrollmentID`/`calendarID`/`structureID`\nfor the endpoints that need them (attendance, school days, assessments,\nfeatures). There is no name→id search — `ic_list_students` (i.e. this call)\n*is* the resolve step.\n\n## Endpoints\n\nAll 17 read endpoints (grades, attendance, assignments, schedule, messages,\ndocuments, fees, teachers, assessments, feature flags) with ready-to-run\n`curl`+`jq` recipes are in `references/endpoints.md`. Two (`behavior`,\n`food_service`) have real paths that were never confirmed live against a\ndistrict with that module enabled — try them, expect a 404 if the module\nis off or the district's shape differs.\n\n## Downloads\n\n`ic_download_document`'s `url` field (from the documents list) may be a\nrelative `/campus/...` path or an absolute URL. Same jar + header pattern,\njust write to a file instead of piping to `jq`:\n\n```sh\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -o report-card.pdf \"$IC_BASE_URL$DOC_URL\"\n```\n\n## FeatureDisabled districts\n\nSome districts turn whole modules off (behavior, food service, assessments,\ndocuments, attendance). The MCP checks `displayOptions` first and falls\nback to treating a 404 as \"disabled.\" Doing the same by hand: fetch\n`/campus/api/portal/displayOptions/{structureID}?personID=X` (see\nreferences) and check the flag before assuming a 404 is a bug.\n\n## Notes\n\n- Read-only in practice: every recipe here is a GET. `ic_get_message`\n  fetching a message body *may* mark it read on some district configs — the\n  MCP's own probe couldn't confirm the side effect either way.\n- No CUPS linked-district switching in this skill (a parent with kids in\n  2+ IC instances under shared SSO) — that's a multi-step token-exchange\n  flow (`src/client.ts`'s `discoverLinkedDistricts`); out of scope here.\n  Run the login flow again against the linked district's own\n  `IC_BASE_URL`/`IC_DISTRICT` if you need it.\n- This project is developed and maintained by AI (Claude).\n\nFile v3.1.4:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-api\",\n  \"version\": \"3.1.4\",\n  \"publishedAt\": 1790351770895\n}\n\nFile v3.1.4:references/endpoints.md\n\n# Infinite Campus endpoints for curl\n\nAssumes the Login block from `../SKILL.md` has already run (`$JAR`, `$XSRF`,\n`$IC_BASE_URL` set) and the `ic_get` helper is defined:\n\n```sh\nic_get() { curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \"$IC_BASE_URL$1\"; }\n```\n\nAll paths are transcribed from `infinitecampus-mcp`'s `src/tools/*.ts` and\n`docs/endpoints.md` — the same requests the `ic_*` MCP tools make. `$PID` =\na student's `personID` from the resolve-first step.\n\nSome IC/prism responses serialize a 1-item collection as a bare object\ninstead of a 1-element array — pipe through `jq 'if type==\"array\" then . else [.] end'`\nif a recipe below assumes an array and you get an object back.\n\n---\n\n## Students (resolve-first — always start here)\n\n```sh\nic_get '/campus/api/portal/students' \\\n  | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nEach `enrollments[]` entry carries `enrollmentID`, `calendarID`,\n`structureID`, `schoolName`, `endDate` — needed by several endpoints below.\n\n## Schedule\n\n```sh\nic_get \"/campus/resources/portal/roster?personID=$PID\" | jq .\n```\n\n(Only `personID` is honored server-side — there is no date/term filter, so\nthe MCP tool takes none.)\n\n## Assignments\n\n```sh\n# all assignments (full term history, ~hundreds of rows)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" | jq .\n\n# scoped to one course (sectionID from the schedule call above)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID&sectionID=$SECTION_ID\" | jq .\n\n# client-side filters (server ignores date/missing params entirely):\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.[] | select(.dueDate >= $since)]'\n```\n\n## Recent (recently-graded) assignments\n\nServer-side filtered by `modifiedDate` (defaults to 14 days ago in the MCP;\nreproduce with `date -u -v-14d '+%Y-%m-%dT00:00:00'` on macOS):\n\n```sh\nSINCE=$(date -u -v-14d '+%Y-%m-%dT00:00:00')\nic_get \"/campus/api/portal/assignment/recentlyScored?modifiedDate=$SINCE&personID=$PID\" | jq .\n```\n\n## Grades\n\n```sh\nic_get \"/campus/resources/portal/grades?personID=$PID\" | jq .\n# scoped to one term:\nic_get \"/campus/resources/portal/grades?personID=$PID&termID=$TERM_ID\" | jq .\n```\n\n## Attendance (per-course summary, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/$ENROLLMENT_ID?courseSummary=true&personID=$PID\" \\\n  | jq '.terms[]?.courses[]? | {absentList, tardyList, presentList, earlyReleaseList}'\n```\n\nFeature-gated: check `displayOptions.attendance` first (see Feature flags,\nbelow) — a 404 here on a district with attendance disabled is expected, not\na bug.\n\n## Attendance events (individual absences/tardies, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq '.events[]? | {date: .localDate, code, description, excuse, comments}'\n\n# excused-only, since a date (client-side filter):\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.events[]? | select((.localDate[0:10] >= $since) and (.excuse == \"E\"))]'\n```\n\n## School days / calendar (loop over each enrollment)\n\nTwo calls per enrollment, then join term windows to days client-side:\n\n```sh\nic_get \"/campus/resources/term?structureID=$STRUCTURE_ID\" | jq .          # term boundaries\nic_get \"/campus/resources/calendar/instructionalDay?calendarID=$CALENDAR_ID\" | jq .  # day list\n\n# days within term N's [startDate,endDate]:\njq --slurpfile terms terms.json --arg t \"$TERM_ID\" \\\n  '[.[] | select(.date >= $terms[0][0].startDate and .date <= $terms[0][0].endDate)]' days.json\n```\n\n## Assessments (standardized tests, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/prism/portal/assessments?personID=$PID&calendarID=$CALENDAR_ID\" \\\n  | jq '{stateTests, nationalTests, districtTests}'\n```\n\nFeature-gated on `displayOptions.assessment`; test-item shape varies by\ndistrict/test type — pass through unchanged, don't assume field names\nbeyond `stateTests`/`nationalTests`/`districtTests`.\n\n## Behavior *(path unconfirmed — never verified live against a district with the module on)*\n\n```sh\nic_get \"/campus/resources/portal/behavior?personID=$PID\" | jq .\n# with a date range:\nic_get \"/campus/resources/portal/behavior?personID=$PID&startDate=2026-01-01&endDate=2026-06-01\" | jq .\n```\n\nFeature-gated on `displayOptions.behavior` — a 404 is the expected result\non most districts (few enable this module for parent portal).\n\n## Food service *(path unconfirmed — same caveat as behavior)*\n\n```sh\nic_get \"/campus/resources/portal/foodService?personID=$PID\" | jq .\nic_get \"/campus/resources/portal/foodService?personID=$PID&startDate=2026-01-01&endDate=2026-06-01\" | jq .\n```\n\nFeature-gated on `displayOptions.foodService`.\n\n## Documents + download\n\n```sh\nic_get \"/campus/resources/portal/report/all?personID=$PID\" \\\n  | jq '.[] | {name, type, url, moduleLabel, endYear}'\n\n# download (url may be relative /campus/... or an absolute URL — handle both):\nDOC_URL='/campus/resources/portal/report/...'   # from the .url field above\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -o report-card.pdf \\\n  \"$( [[ \"$DOC_URL\" == http* ]] && echo \"$DOC_URL\" || echo \"$IC_BASE_URL$DOC_URL\" )\"\n```\n\nFeature-gated on `displayOptions.documents`.\n\n## Teachers + counselors\n\n```sh\nic_get \"/campus/resources/portal/section/contacts?personID=$PID\" | jq .\nic_get \"/campus/resources/portal/studentCounselor/byUser?personID=$PID\" | jq .\n```\n\nEither endpoint 404ing is treated as \"no results\" (not FeatureDisabled) by\nthe MCP — do the same.\n\n## Messages (three independent sources — combine client-side)\n\n```sh\n# 1. prism notifications (assignment/grade/attendance alerts)\nic_get '/campus/prism?x=notifications.Notification-retrieve&limitCount=20' \\\n  | jq '.data.NotificationList.Notification'\n\n# 2. Messenger 2.0 inbox (teacher messages, district announcements)\nic_get '/campus/api/portal/process-message' | jq .\n\n# 3. portal userNotice announcements\nic_get '/campus/resources/portal/userNotice' | jq .\n```\n\nEach inbox item's `url` field feeds `ic_get_message` below (normalize a bare\nor `portal/...`-relative URL to `/campus/portal/...` first).\n\n## Get one message body (HTML → text)\n\n```sh\n# normalize: bare \"portal/...\" → \"/campus/portal/...\"; already-absolute /campus/... stays as-is\nMSG_PATH='/campus/portal/messageView.xsl?x=messenger.MessengerEngine-getMessageRecipientView&messageID=...&messageRecipientID=...'\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: text/html' \"$IC_BASE_URL$MSG_PATH\"\n# then strip tags/entities yourself, or eyeball the rendered HTML — this is\n# an HTML fragment, not JSON, so `jq` doesn't apply.\n```\n\nFetching a message body *may* mark it read on some district configs\n(unconfirmed either way).\n\n## Fees (two endpoints in parallel; either can 404 independently)\n\n```sh\nic_get \"/campus/api/portal/fees/feeAssignments?personID=$PID\" | jq .\nic_get \"/campus/api/portal/fees/feeTransactionDetail/totalSurplus/-1?personID=$PID\" | jq .\n```\n\nBoth 404 → module is off for this district. One 404 → report the other\nside plus a note that the failing endpoint may be disabled.\n\n## Feature flags (displayOptions — check before assuming a 404 is a bug)\n\n```sh\nic_get \"/campus/api/portal/displayOptions/$STRUCTURE_ID?personID=$PID\" \\\n  | jq '{attendance, behavior, assessment, documents, foodService, grades, schedule}'\n```\n\n`false` = district has that module turned off for this enrollment; `true`\nor absent = available. ~90 flags total; the MCP caches this per\n`(district, structureID)` for the session lifetime since it rarely changes\nmid-session — worth doing the same if you're calling it more than once.\n\n---\n\n## Not covered here (out of scope for this skill)\n\n- **CUPS linked-district discovery/switching** — a parent with kids at 2+\n  IC-hosted districts under shared SSO. Multi-step: fetch\n  `cups/linkedAccounts`, `userAccountSwitch/originalDistrict`,\n  `districts/current`, then per linked account a `cups/loginToken` POST\n  followed by a second `verify.jsp`-style POST at the *linked* district's\n  own host with the CUPS token. Fully discoverable in `src/client.ts`\n  (`discoverLinkedDistricts`) but a lot of shell for a one-shot skill —\n  just re-run the Login block against the linked district's own\n  `IC_BASE_URL`/`IC_DISTRICT` instead.\n\nFile v3.1.4:skill-card.md\n\n## Description:\n\nGuides authorized Campus Parent users in querying their district's Infinite Campus portal with curl for student grades, attendance, assignments, schedules, messages, documents, and fees.\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\nAuthorized parents, guardians, and developers use this skill to retrieve student portal information from their own district without installing the companion MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Credentials, session cookies, and XSRF tokens may be exposed in logs or shared files.\n\nMitigation: Use only an authorized account, avoid logging or sharing secrets and cookie jars, and delete temporary files after use.\n\nRisk: Grades, attendance, messages, documents, and fees may contain sensitive student records.\n\nMitigation: Limit access and sharing to authorized recipients; avoid retaining or logging raw responses and downloaded documents.\n\n## Reference(s):\n\n- [Infinite Campus API skill listing](https://clawhub.ai/chrischall/skills/infinitecampus-api)\n- [Infinite Campus endpoint recipes](references/endpoints.md)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell commands and JSON query examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [District-specific access requires authorized portal credentials; returned student records may be sensitive.]\n\n## Skill Version(s):\n\n3.1.4 (source: server-resolved 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 v3.1.3: 4 files, 8049 bytes\n\nFiles: references/endpoints.md (8406b), skill-card.md (2348b), SKILL.md (5776b), _meta.json (137b)\n\nFile v3.1.3:SKILL.md\n\n---\nname: infinitecampus-api\ndescription: >-\n  Query an Infinite Campus (Campus Parent portal) district directly with curl\n  instead of running the infinitecampus-mcp server — log in with a real\n  username/password, capture the session cookie + XSRF token, and curl grades,\n  attendance, assignments, schedule, messages, documents, and fees. Use when\n  you want IC data without the MCP, in a script, or on a machine where the MCP\n  isn't installed. Infinite Campus is per-district: the base URL and district\n  app name are configurable, not hardcoded.\n---\n\n# Infinite Campus via curl (no MCP)\n\nInfinite Campus's Campus Parent portal is a classic server-rendered site\n(no public API, no bot wall). A plain form POST to `verify.jsp` logs a\nparent in and hands back a session cookie — no browser bridge needed. This\nis the same login `infinitecampus-mcp`'s `src/client.ts`/`src/auth.ts` do;\nthis skill reproduces it with `curl` + a cookie jar.\n\n**Infinite Campus is per-district.** Every parent's portal lives at their\nown district's host (`https://campus.<district>.k12.example.us` or similar)\nunder an `appName` path segment. There is no shared IC endpoint — carry the\nbase URL and district from the same config the MCP uses:\n\n```sh\n: \"${IC_BASE_URL:?set to your district's portal URL, e.g. https://campus.yourdistrict.org}\"\n: \"${IC_DISTRICT:?set to the appName path segment, e.g. yourdistrict}\"\n: \"${IC_USERNAME:?parent portal username}\"\n: \"${IC_PASSWORD:?parent portal password}\"\n```\n\n(Same 4 env vars the MCP reads — reuse the repo's `.env` or the host's\nsecret store; don't hardcode a district.)\n\n## One-time setup\n\nNone — `curl` and `jq` are the only tools needed (`brew install jq` if\nmissing). No profile/pairing step, no extension.\n\n## Login (capture cookies + XSRF token)\n\n`verify.jsp` accepts a urlencoded form POST and returns 200 with an\n`<AUTHENTICATION>state</AUTHENTICATION>` marker in the body — **not**\nvia HTTP status — plus ~20 `Set-Cookie` headers. Use a cookie jar so curl\ndoes the Set-Cookie parsing/deduping for you:\n\n```sh\nJAR=$(mktemp)\nBODY=$(curl -sS -c \"$JAR\" \\\n  \"$IC_BASE_URL/campus/verify.jsp?nonBrowser=true\" \\\n  --data-urlencode \"username=$IC_USERNAME\" \\\n  --data-urlencode \"password=$IC_PASSWORD\" \\\n  --data-urlencode \"appName=$IC_DISTRICT\" \\\n  --data-urlencode \"portalLoginPage=parents\")\n\necho \"$BODY\" | grep -o '<AUTHENTICATION>[^<]*' # AUTHENTICATIONsuccess = good\n```\n\n`password-error` / `account-locked` / any non-`success` state means the\nlogin failed — check that string before doing anything else. Credentials\ngo in the form body (never the query string — it lands in access logs).\n\nPull the XSRF token out of the jar (IC's JS reads it back and echoes it on\nevery request as `X-XSRF-TOKEN`; `JSESSIONID` alone is not enough):\n\n```sh\nXSRF=$(awk -F'\\t' '$6==\"XSRF-TOKEN\"{print $7}' \"$JAR\")\n```\n\n## Core call pattern\n\nEvery subsequent request rides the jar + the XSRF header:\n\n```sh\nic_get() { # ic_get <path-with-query>\n  curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \\\n    \"$IC_BASE_URL$1\"\n}\n\nic_get '/campus/api/portal/students' | jq .\n```\n\nA **401** means the session expired (IC sessions last ~5-6h) — re-run the\nLogin step to get a fresh `$JAR`/`$XSRF` and retry once. There is no retry\nbudget beyond that in this shell version (the MCP's `CookieSessionManager`\ndoes this automatically; here just redo the login block).\n\n## The one rule: resolve student + enrollment first\n\nAlmost every read is scoped to a `personID` (student), and several need the\nstudent's **enrollment** (`enrollmentID` / `calendarID` / `structureID`).\nAlways start with:\n\n```sh\nic_get '/campus/api/portal/students' | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nTake `personID` for `studentId`/`personID` params, and the first (or\nrelevant) `enrollments[]` entry's `enrollmentID`/`calendarID`/`structureID`\nfor the endpoints that need them (attendance, school days, assessments,\nfeatures). There is no name→id search — `ic_list_students` (i.e. this call)\n*is* the resolve step.\n\n## Endpoints\n\nAll 17 read endpoints (grades, attendance, assignments, schedule, messages,\ndocuments, fees, teachers, assessments, feature flags) with ready-to-run\n`curl`+`jq` recipes are in `references/endpoints.md`. Two (`behavior`,\n`food_service`) have real paths that were never confirmed live against a\ndistrict with that module enabled — try them, expect a 404 if the module\nis off or the district's shape differs.\n\n## Downloads\n\n`ic_download_document`'s `url` field (from the documents list) may be a\nrelative `/campus/...` path or an absolute URL. Same jar + header pattern,\njust write to a file instead of piping to `jq`:\n\n```sh\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -o report-card.pdf \"$IC_BASE_URL$DOC_URL\"\n```\n\n## FeatureDisabled districts\n\nSome districts turn whole modules off (behavior, food service, assessments,\ndocuments, attendance). The MCP checks `displayOptions` first and falls\nback to treating a 404 as \"disabled.\" Doing the same by hand: fetch\n`/campus/api/portal/displayOptions/{structureID}?personID=X` (see\nreferences) and check the flag before assuming a 404 is a bug.\n\n## Notes\n\n- Read-only in practice: every recipe here is a GET. `ic_get_message`\n  fetching a message body *may* mark it read on some district configs — the\n  MCP's own probe couldn't confirm the side effect either way.\n- No CUPS linked-district switching in this skill (a parent with kids in\n  2+ IC instances under shared SSO) — that's a multi-step token-exchange\n  flow (`src/client.ts`'s `discoverLinkedDistricts`); out of scope here.\n  Run the login flow again against the linked district's own\n  `IC_BASE_URL`/`IC_DISTRICT` if you need it.\n- This project is developed and maintained by AI (Claude).\n\nFile v3.1.3:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-api\",\n  \"version\": \"3.1.3\",\n  \"publishedAt\": 1790199581974\n}\n\nFile v3.1.3:references/endpoints.md\n\n# Infinite Campus endpoints for curl\n\nAssumes the Login block from `../SKILL.md` has already run (`$JAR`, `$XSRF`,\n`$IC_BASE_URL` set) and the `ic_get` helper is defined:\n\n```sh\nic_get() { curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \"$IC_BASE_URL$1\"; }\n```\n\nAll paths are transcribed from `infinitecampus-mcp`'s `src/tools/*.ts` and\n`docs/endpoints.md` — the same requests the `ic_*` MCP tools make. `$PID` =\na student's `personID` from the resolve-first step.\n\nSome IC/prism responses serialize a 1-item collection as a bare object\ninstead of a 1-element array — pipe through `jq 'if type==\"array\" then . else [.] end'`\nif a recipe below assumes an array and you get an object back.\n\n---\n\n## Students (resolve-first — always start here)\n\n```sh\nic_get '/campus/api/portal/students' \\\n  | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nEach `enrollments[]` entry carries `enrollmentID`, `calendarID`,\n`structureID`, `schoolName`, `endDate` — needed by several endpoints below.\n\n## Schedule\n\n```sh\nic_get \"/campus/resources/portal/roster?personID=$PID\" | jq .\n```\n\n(Only `personID` is honored server-side — there is no date/term filter, so\nthe MCP tool takes none.)\n\n## Assignments\n\n```sh\n# all assignments (full term history, ~hundreds of rows)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" | jq .\n\n# scoped to one course (sectionID from the schedule call above)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID&sectionID=$SECTION_ID\" | jq .\n\n# client-side filters (server ignores date/missing params entirely):\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.[] | select(.dueDate >= $since)]'\n```\n\n## Recent (recently-graded) assignments\n\nServer-side filtered by `modifiedDate` (defaults to 14 days ago in the MCP;\nreproduce with `date -u -v-14d '+%Y-%m-%dT00:00:00'` on macOS):\n\n```sh\nSINCE=$(date -u -v-14d '+%Y-%m-%dT00:00:00')\nic_get \"/campus/api/portal/assignment/recentlyScored?modifiedDate=$SINCE&personID=$PID\" | jq .\n```\n\n## Grades\n\n```sh\nic_get \"/campus/resources/portal/grades?personID=$PID\" | jq .\n# scoped to one term:\nic_get \"/campus/resources/portal/grades?personID=$PID&termID=$TERM_ID\" | jq .\n```\n\n## Attendance (per-course summary, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/$ENROLLMENT_ID?courseSummary=true&personID=$PID\" \\\n  | jq '.terms[]?.courses[]? | {absentList, tardyList, presentList, earlyReleaseList}'\n```\n\nFeature-gated: check `displayOptions.attendance` first (see Feature flags,\nbelow) — a 404 here on a district with attendance disabled is expected, not\na bug.\n\n## Attendance events (individual absences/tardies, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq '.events[]? | {date: .localDate, code, description, excuse, comments}'\n\n# excused-only, since a date (client-side filter):\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.events[]? | select((.localDate[0:10] >= $since) and (.excuse == \"E\"))]'\n```\n\n## School days / calendar (loop over each enrollment)\n\nTwo calls per enrollment, then join term windows to days client-side:\n\n```sh\nic_get \"/campus/resources/term?structureID=$STRUCTURE_ID\" | jq .          # term boundaries\nic_get \"/campus/resources/calendar/instructionalDay?calendarID=$CALENDAR_ID\" | jq .  # day list\n\n# days within term N's [startDate,endDate]:\njq --slurpfile terms terms.json --arg t \"$TERM_ID\" \\\n  '[.[] | select(.date >= $terms[0][0].startDate and .date <= $terms[0][0].endDate)]' days.json\n```\n\n## Assessments (standardized tests, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/prism/portal/assessments?personID=$PID&calendarID=$CALENDAR_ID\" \\\n  | jq '{stateTests, nationalTests, districtTests}'\n```\n\nFeature-gated on `displayOptions.assessment`; test-item shape varies by\ndistrict/test type — pass through unchanged, don't assume field names\nbeyond `stateTests`/`nationalTests`/`districtTests`.\n\n## Behavior *(path unconfirmed — never verified live against a district with the module on)*\n\n```sh\nic_get \"/campus/resources/portal/behavior?personID=$PID\" | jq .\n# with a date range:\nic_get \"/campus/resources/portal/behavior?personID=$PID&startDate=2026-01-01&endDate=2026-06-01\" | jq .\n```\n\nFeature-gated on `displayOptions.behavior` — a 404 is the expected result\non most districts (few enable this module for parent portal).\n\n## Food service *(path unconfirmed — same caveat as behavior)*\n\n```sh\nic_get \"/campus/resources/portal/foodService?personID=$PID\" | jq .\nic_get \"/campus/resources/portal/foodService?personID=$PID&startDate=2026-01-01&endDate=2026-06-01\" | jq .\n```\n\nFeature-gated on `displayOptions.foodService`.\n\n## Documents + download\n\n```sh\nic_get \"/campus/resources/portal/report/all?personID=$PID\" \\\n  | jq '.[] | {name, type, url, moduleLabel, endYear}'\n\n# download (url may be relative /campus/... or an absolute URL — handle both):\nDOC_URL='/campus/resources/portal/report/...'   # from the .url field above\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -o report-card.pdf \\\n  \"$( [[ \"$DOC_URL\" == http* ]] && echo \"$DOC_URL\" || echo \"$IC_BASE_URL$DOC_URL\" )\"\n```\n\nFeature-gated on `displayOptions.documents`.\n\n## Teachers + counselors\n\n```sh\nic_get \"/campus/resources/portal/section/contacts?personID=$PID\" | jq .\nic_get \"/campus/resources/portal/studentCounselor/byUser?personID=$PID\" | jq .\n```\n\nEither endpoint 404ing is treated as \"no results\" (not FeatureDisabled) by\nthe MCP — do the same.\n\n## Messages (three independent sources — combine client-side)\n\n```sh\n# 1. prism notifications (assignment/grade/attendance alerts)\nic_get '/campus/prism?x=notifications.Notification-retrieve&limitCount=20' \\\n  | jq '.data.NotificationList.Notification'\n\n# 2. Messenger 2.0 inbox (teacher messages, district announcements)\nic_get '/campus/api/portal/process-message' | jq .\n\n# 3. portal userNotice announcements\nic_get '/campus/resources/portal/userNotice' | jq .\n```\n\nEach inbox item's `url` field feeds `ic_get_message` below (normalize a bare\nor `portal/...`-relative URL to `/campus/portal/...` first).\n\n## Get one message body (HTML → text)\n\n```sh\n# normalize: bare \"portal/...\" → \"/campus/portal/...\"; already-absolute /campus/... stays as-is\nMSG_PATH='/campus/portal/messageView.xsl?x=messenger.MessengerEngine-getMessageRecipientView&messageID=...&messageRecipientID=...'\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: text/html' \"$IC_BASE_URL$MSG_PATH\"\n# then strip tags/entities yourself, or eyeball the rendered HTML — this is\n# an HTML fragment, not JSON, so `jq` doesn't apply.\n```\n\nFetching a message body *may* mark it read on some district configs\n(unconfirmed either way).\n\n## Fees (two endpoints in parallel; either can 404 independently)\n\n```sh\nic_get \"/campus/api/portal/fees/feeAssignments?personID=$PID\" | jq .\nic_get \"/campus/api/portal/fees/feeTransactionDetail/totalSurplus/-1?personID=$PID\" | jq .\n```\n\nBoth 404 → module is off for this district. One 404 → report the other\nside plus a note that the failing endpoint may be disabled.\n\n## Feature flags (displayOptions — check before assuming a 404 is a bug)\n\n```sh\nic_get \"/campus/api/portal/displayOptions/$STRUCTURE_ID?personID=$PID\" \\\n  | jq '{attendance, behavior, assessment, documents, foodService, grades, schedule}'\n```\n\n`false` = district has that module turned off for this enrollment; `true`\nor absent = available. ~90 flags total; the MCP caches this per\n`(district, structureID)` for the session lifetime since it rarely changes\nmid-session — worth doing the same if you're calling it more than once.\n\n---\n\n## Not covered here (out of scope for this skill)\n\n- **CUPS linked-district discovery/switching** — a parent with kids at 2+\n  IC-hosted districts under shared SSO. Multi-step: fetch\n  `cups/linkedAccounts`, `userAccountSwitch/originalDistrict`,\n  `districts/current`, then per linked account a `cups/loginToken` POST\n  followed by a second `verify.jsp`-style POST at the *linked* district's\n  own host with the CUPS token. Fully discoverable in `src/client.ts`\n  (`discoverLinkedDistricts`) but a lot of shell for a one-shot skill —\n  just re-run the Login block against the linked district's own\n  `IC_BASE_URL`/`IC_DISTRICT` instead.\n\nFile v3.1.3:skill-card.md\n\n## Description:\n\nQuery an Infinite Campus Campus Parent portal directly with curl to log in, capture a session cookie and XSRF token, and retrieve grades, attendance, assignments, schedules, messages, documents, and fees without running the MCP server.\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\nDevelopers, parents, guardians, and authorized users use this skill to retrieve their own Infinite Campus portal data with curl and jq when they need scriptable access without installing the MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Credentials, cookie jars, XSRF tokens, downloaded documents, and command output may contain sensitive student records.\n\nMitigation: Use only authorized Infinite Campus accounts, keep secrets and outputs out of logs, avoid sharing them, and delete temporary session files after use.\n\nRisk: Fetching a message body may mark it read on some district configurations.\n\nMitigation: Fetch message bodies only when the possible read-state change is acceptable.\n\nRisk: Some district modules or endpoints may be disabled or unavailable, and the behavior and food service paths are documented as unconfirmed live paths.\n\nMitigation: Check displayOptions feature flags first and treat documented module 404 responses as disabled or unavailable rather than as unexpected failures.\n\n## Reference(s):\n\n- [Infinite Campus endpoints for curl](references/endpoints.md)\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/infinitecampus-api)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Configuration, API Calls, Guidance, Markdown]\n\n**Output Format:** [Markdown with inline shell commands and curl/jq examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Read-only curl recipes using user-provided district URL, district app name, username, password, cookie jar, and XSRF token.]\n\n## Skill Version(s):\n\n3.1.3 (source: ClawHub release evidence)\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 v3.1.2: 4 files, 8100 bytes\n\nFiles: references/endpoints.md (8445b), skill-card.md (2403b), SKILL.md (5776b), _meta.json (137b)\n\nFile v3.1.2:SKILL.md\n\n---\nname: infinitecampus-api\ndescription: >-\n  Query an Infinite Campus (Campus Parent portal) district directly with curl\n  instead of running the infinitecampus-mcp server — log in with a real\n  username/password, capture the session cookie + XSRF token, and curl grades,\n  attendance, assignments, schedule, messages, documents, and fees. Use when\n  you want IC data without the MCP, in a script, or on a machine where the MCP\n  isn't installed. Infinite Campus is per-district: the base URL and district\n  app name are configurable, not hardcoded.\n---\n\n# Infinite Campus via curl (no MCP)\n\nInfinite Campus's Campus Parent portal is a classic server-rendered site\n(no public API, no bot wall). A plain form POST to `verify.jsp` logs a\nparent in and hands back a session cookie — no browser bridge needed. This\nis the same login `infinitecampus-mcp`'s `src/client.ts`/`src/auth.ts` do;\nthis skill reproduces it with `curl` + a cookie jar.\n\n**Infinite Campus is per-district.** Every parent's portal lives at their\nown district's host (`https://campus.<district>.k12.example.us` or similar)\nunder an `appName` path segment. There is no shared IC endpoint — carry the\nbase URL and district from the same config the MCP uses:\n\n```sh\n: \"${IC_BASE_URL:?set to your district's portal URL, e.g. https://campus.yourdistrict.org}\"\n: \"${IC_DISTRICT:?set to the appName path segment, e.g. yourdistrict}\"\n: \"${IC_USERNAME:?parent portal username}\"\n: \"${IC_PASSWORD:?parent portal password}\"\n```\n\n(Same 4 env vars the MCP reads — reuse the repo's `.env` or the host's\nsecret store; don't hardcode a district.)\n\n## One-time setup\n\nNone — `curl` and `jq` are the only tools needed (`brew install jq` if\nmissing). No profile/pairing step, no extension.\n\n## Login (capture cookies + XSRF token)\n\n`verify.jsp` accepts a urlencoded form POST and returns 200 with an\n`<AUTHENTICATION>state</AUTHENTICATION>` marker in the body — **not**\nvia HTTP status — plus ~20 `Set-Cookie` headers. Use a cookie jar so curl\ndoes the Set-Cookie parsing/deduping for you:\n\n```sh\nJAR=$(mktemp)\nBODY=$(curl -sS -c \"$JAR\" \\\n  \"$IC_BASE_URL/campus/verify.jsp?nonBrowser=true\" \\\n  --data-urlencode \"username=$IC_USERNAME\" \\\n  --data-urlencode \"password=$IC_PASSWORD\" \\\n  --data-urlencode \"appName=$IC_DISTRICT\" \\\n  --data-urlencode \"portalLoginPage=parents\")\n\necho \"$BODY\" | grep -o '<AUTHENTICATION>[^<]*' # AUTHENTICATIONsuccess = good\n```\n\n`password-error` / `account-locked` / any non-`success` state means the\nlogin failed — check that string before doing anything else. Credentials\ngo in the form body (never the query string — it lands in access logs).\n\nPull the XSRF token out of the jar (IC's JS reads it back and echoes it on\nevery request as `X-XSRF-TOKEN`; `JSESSIONID` alone is not enough):\n\n```sh\nXSRF=$(awk -F'\\t' '$6==\"XSRF-TOKEN\"{print $7}' \"$JAR\")\n```\n\n## Core call pattern\n\nEvery subsequent request rides the jar + the XSRF header:\n\n```sh\nic_get() { # ic_get <path-with-query>\n  curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \\\n    \"$IC_BASE_URL$1\"\n}\n\nic_get '/campus/api/portal/students' | jq .\n```\n\nA **401** means the session expired (IC sessions last ~5-6h) — re-run the\nLogin step to get a fresh `$JAR`/`$XSRF` and retry once. There is no retry\nbudget beyond that in this shell version (the MCP's `CookieSessionManager`\ndoes this automatically; here just redo the login block).\n\n## The one rule: resolve student + enrollment first\n\nAlmost every read is scoped to a `personID` (student), and several need the\nstudent's **enrollment** (`enrollmentID` / `calendarID` / `structureID`).\nAlways start with:\n\n```sh\nic_get '/campus/api/portal/students' | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nTake `personID` for `studentId`/`personID` params, and the first (or\nrelevant) `enrollments[]` entry's `enrollmentID`/`calendarID`/`structureID`\nfor the endpoints that need them (attendance, school days, assessments,\nfeatures). There is no name→id search — `ic_list_students` (i.e. this call)\n*is* the resolve step.\n\n## Endpoints\n\nAll 17 read endpoints (grades, attendance, assignments, schedule, messages,\ndocuments, fees, teachers, assessments, feature flags) with ready-to-run\n`curl`+`jq` recipes are in `references/endpoints.md`. Two (`behavior`,\n`food_service`) have real paths that were never confirmed live against a\ndistrict with that module enabled — try them, expect a 404 if the module\nis off or the district's shape differs.\n\n## Downloads\n\n`ic_download_document`'s `url` field (from the documents list) may be a\nrelative `/campus/...` path or an absolute URL. Same jar + header pattern,\njust write to a file instead of piping to `jq`:\n\n```sh\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -o report-card.pdf \"$IC_BASE_URL$DOC_URL\"\n```\n\n## FeatureDisabled districts\n\nSome districts turn whole modules off (behavior, food service, assessments,\ndocuments, attendance). The MCP checks `displayOptions` first and falls\nback to treating a 404 as \"disabled.\" Doing the same by hand: fetch\n`/campus/api/portal/displayOptions/{structureID}?personID=X` (see\nreferences) and check the flag before assuming a 404 is a bug.\n\n## Notes\n\n- Read-only in practice: every recipe here is a GET. `ic_get_message`\n  fetching a message body *may* mark it read on some district configs — the\n  MCP's own probe couldn't confirm the side effect either way.\n- No CUPS linked-district switching in this skill (a parent with kids in\n  2+ IC instances under shared SSO) — that's a multi-step token-exchange\n  flow (`src/client.ts`'s `discoverLinkedDistricts`); out of scope here.\n  Run the login flow again against the linked district's own\n  `IC_BASE_URL`/`IC_DISTRICT` if you need it.\n- This project is developed and maintained by AI (Claude).\n\nFile v3.1.2:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-api\",\n  \"version\": \"3.1.2\",\n  \"publishedAt\": 1790178320691\n}\n\nFile v3.1.2:references/endpoints.md\n\n# Infinite Campus endpoints for curl\n\nAssumes the Login block from `../SKILL.md` has already run (`$JAR`, `$XSRF`,\n`$IC_BASE_URL` set) and the `ic_get` helper is defined:\n\n```sh\nic_get() { curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \"$IC_BASE_URL$1\"; }\n```\n\nAll paths are transcribed from `infinitecampus-mcp`'s `src/tools/*.ts` and\n`docs/endpoints.md` — the same requests the `ic_*` MCP tools make. `$PID` =\na student's `personID` from the resolve-first step.\n\nSome IC/prism responses serialize a 1-item collection as a bare object\ninstead of a 1-element array — pipe through `jq 'if type==\"array\" then . else [.] end'`\nif a recipe below assumes an array and you get an object back.\n\n---\n\n## Students (resolve-first — always start here)\n\n```sh\nic_get '/campus/api/portal/students' \\\n  | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nEach `enrollments[]` entry carries `enrollmentID`, `calendarID`,\n`structureID`, `schoolName`, `endDate` — needed by several endpoints below.\n\n## Schedule\n\n```sh\nic_get \"/campus/resources/portal/roster?personID=$PID\" | jq .\n```\n\n(Only `personID` is honored server-side — no working date/term filter, even\nthough the MCP tool's schema accepts `date`/`termFilter` args.)\n\n## Assignments\n\n```sh\n# all assignments (full term history, ~hundreds of rows)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" | jq .\n\n# scoped to one course (sectionID from the schedule call above)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID&sectionID=$SECTION_ID\" | jq .\n\n# client-side filters (server ignores date/missing params entirely):\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.[] | select(.dueDate >= $since)]'\n```\n\n## Recent (recently-graded) assignments\n\nServer-side filtered by `modifiedDate` (defaults to 14 days ago in the MCP;\nreproduce with `date -u -v-14d '+%Y-%m-%dT00:00:00'` on macOS):\n\n```sh\nSINCE=$(date -u -v-14d '+%Y-%m-%dT00:00:00')\nic_get \"/campus/api/portal/assignment/recentlyScored?modifiedDate=$SINCE&personID=$PID\" | jq .\n```\n\n## Grades\n\n```sh\nic_get \"/campus/resources/portal/grades?personID=$PID\" | jq .\n# scoped to one term:\nic_get \"/campus/resources/portal/grades?personID=$PID&termID=$TERM_ID\" | jq .\n```\n\n## Attendance (per-course summary, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/$ENROLLMENT_ID?courseSummary=true&personID=$PID\" \\\n  | jq '.terms[]?.courses[]? | {absentList, tardyList, presentList, earlyReleaseList}'\n```\n\nFeature-gated: check `displayOptions.attendance` first (see Feature flags,\nbelow) — a 404 here on a district with attendance disabled is expected, not\na bug.\n\n## Attendance events (individual absences/tardies, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq '.events[]? | {date: .localDate, code, description, excuse, comments}'\n\n# excused-only, since a date (client-side filter):\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.events[]? | select((.localDate[0:10] >= $since) and (.excuse == \"E\"))]'\n```\n\n## School days / calendar (loop over each enrollment)\n\nTwo calls per enrollment, then join term windows to days client-side:\n\n```sh\nic_get \"/campus/resources/term?structureID=$STRUCTURE_ID\" | jq .          # term boundaries\nic_get \"/campus/resources/calendar/instructionalDay?calendarID=$CALENDAR_ID\" | jq .  # day list\n\n# days within term N's [startDate,endDate]:\njq --slurpfile terms terms.json --arg t \"$TERM_ID\" \\\n  '[.[] | select(.date >= $terms[0][0].startDate and .date <= $terms[0][0].endDate)]' days.json\n```\n\n## Assessments (standardized tests, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/prism/portal/assessments?personID=$PID&calendarID=$CALENDAR_ID\" \\\n  | jq '{stateTests, nationalTests, districtTests}'\n```\n\nFeature-gated on `displayOptions.assessment`; test-item shape varies by\ndistrict/test type — pass through unchanged, don't assume field names\nbeyond `stateTests`/`nationalTests`/`districtTests`.\n\n## Behavior *(path unconfirmed — never verified live against a district with the module on)*\n\n```sh\nic_get \"/campus/resources/portal/behavior?personID=$PID\" | jq .\n# with a date range:\nic_get \"/campus/resources/portal/behavior?personID=$PID&startDate=2026-01-01&endDate=2026-06-01\" | jq .\n```\n\nFeature-gated on `displayOptions.behavior` — a 404 is the expected result\non most districts (few enable this module for parent portal).\n\n## Food service *(path unconfirmed — same caveat as behavior)*\n\n```sh\nic_get \"/campus/resources/portal/foodService?personID=$PID\" | jq .\nic_get \"/campus/resources/portal/foodService?personID=$PID&startDate=2026-01-01&endDate=2026-06-01\" | jq .\n```\n\nFeature-gated on `displayOptions.foodService`.\n\n## Documents + download\n\n```sh\nic_get \"/campus/resources/portal/report/all?personID=$PID\" \\\n  | jq '.[] | {name, type, url, moduleLabel, endYear}'\n\n# download (url may be relative /campus/... or an absolute URL — handle both):\nDOC_URL='/campus/resources/portal/report/...'   # from the .url field above\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -o report-card.pdf \\\n  \"$( [[ \"$DOC_URL\" == http* ]] && echo \"$DOC_URL\" || echo \"$IC_BASE_URL$DOC_URL\" )\"\n```\n\nFeature-gated on `displayOptions.documents`.\n\n## Teachers + counselors\n\n```sh\nic_get \"/campus/resources/portal/section/contacts?personID=$PID\" | jq .\nic_get \"/campus/resources/portal/studentCounselor/byUser?personID=$PID\" | jq .\n```\n\nEither endpoint 404ing is treated as \"no results\" (not FeatureDisabled) by\nthe MCP — do the same.\n\n## Messages (three independent sources — combine client-side)\n\n```sh\n# 1. prism notifications (assignment/grade/attendance alerts)\nic_get '/campus/prism?x=notifications.Notification-retrieve&limitCount=20' \\\n  | jq '.data.NotificationList.Notification'\n\n# 2. Messenger 2.0 inbox (teacher messages, district announcements)\nic_get '/campus/api/portal/process-message' | jq .\n\n# 3. portal userNotice announcements\nic_get '/campus/resources/portal/userNotice' | jq .\n```\n\nEach inbox item's `url` field feeds `ic_get_message` below (normalize a bare\nor `portal/...`-relative URL to `/campus/portal/...` first).\n\n## Get one message body (HTML → text)\n\n```sh\n# normalize: bare \"portal/...\" → \"/campus/portal/...\"; already-absolute /campus/... stays as-is\nMSG_PATH='/campus/portal/messageView.xsl?x=messenger.MessengerEngine-getMessageRecipientView&messageID=...&messageRecipientID=...'\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: text/html' \"$IC_BASE_URL$MSG_PATH\"\n# then strip tags/entities yourself, or eyeball the rendered HTML — this is\n# an HTML fragment, not JSON, so `jq` doesn't apply.\n```\n\nFetching a message body *may* mark it read on some district configs\n(unconfirmed either way).\n\n## Fees (two endpoints in parallel; either can 404 independently)\n\n```sh\nic_get \"/campus/api/portal/fees/feeAssignments?personID=$PID\" | jq .\nic_get \"/campus/api/portal/fees/feeTransactionDetail/totalSurplus/-1?personID=$PID\" | jq .\n```\n\nBoth 404 → module is off for this district. One 404 → report the other\nside plus a note that the failing endpoint may be disabled.\n\n## Feature flags (displayOptions — check before assuming a 404 is a bug)\n\n```sh\nic_get \"/campus/api/portal/displayOptions/$STRUCTURE_ID?personID=$PID\" \\\n  | jq '{attendance, behavior, assessment, documents, foodService, grades, schedule}'\n```\n\n`false` = district has that module turned off for this enrollment; `true`\nor absent = available. ~90 flags total; the MCP caches this per\n`(district, structureID)` for the session lifetime since it rarely changes\nmid-session — worth doing the same if you're calling it more than once.\n\n---\n\n## Not covered here (out of scope for this skill)\n\n- **CUPS linked-district discovery/switching** — a parent with kids at 2+\n  IC-hosted districts under shared SSO. Multi-step: fetch\n  `cups/linkedAccounts`, `userAccountSwitch/originalDistrict`,\n  `districts/current`, then per linked account a `cups/loginToken` POST\n  followed by a second `verify.jsp`-style POST at the *linked* district's\n  own host with the CUPS token. Fully discoverable in `src/client.ts`\n  (`discoverLinkedDistricts`) but a lot of shell for a one-shot skill —\n  just re-run the Login block against the linked district's own\n  `IC_BASE_URL`/`IC_DISTRICT` instead.\n\nFile v3.1.2:skill-card.md\n\n## Description:\n\nQuery an Infinite Campus Campus Parent district directly with curl by logging in with a real username and password, capturing the session cookie and XSRF token, and fetching grades, attendance, assignments, schedules, messages, documents, and fees.\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\nDevelopers and technically capable users use this skill to script authorized access to their Infinite Campus Campus Parent portal without running the MCP server. It is intended for retrieving student records from district-specific portals with curl and jq.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill handles live school portal credentials, session cookies, XSRF tokens, and student records.\n\nMitigation: Use it only for accounts and records you are authorized to access, keep credentials and downloaded records private, and delete temporary cookie jars and session files when finished.\n\nRisk: Fetching a message body may change its read status in some district configurations.\n\nMitigation: Avoid fetching message bodies unless that side effect is acceptable, and warn users before running those commands.\n\nRisk: Some endpoints are district- or module-dependent, and behavior and food service paths are not confirmed live for every district.\n\nMitigation: Check displayOptions and treat expected 404 responses as disabled modules before reporting failures.\n\n## Reference(s):\n\n- [Infinite Campus endpoints for curl](references/endpoints.md)\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/infinitecampus-api)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown with inline shell commands and jq examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Guidance includes district-specific environment variables, curl login flow, endpoint recipes, and document download commands.]\n\n## Skill Version(s):\n\n3.1.2 (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 v3.1.1: 4 files, 8106 bytes\n\nFiles: references/endpoints.md (8445b), skill-card.md (2472b), SKILL.md (5776b), _meta.json (137b)\n\nFile v3.1.1:SKILL.md\n\n---\nname: infinitecampus-api\ndescription: >-\n  Query an Infinite Campus (Campus Parent portal) district directly with curl\n  instead of running the infinitecampus-mcp server — log in with a real\n  username/password, capture the session cookie + XSRF token, and curl grades,\n  attendance, assignments, schedule, messages, documents, and fees. Use when\n  you want IC data without the MCP, in a script, or on a machine where the MCP\n  isn't installed. Infinite Campus is per-district: the base URL and district\n  app name are configurable, not hardcoded.\n---\n\n# Infinite Campus via curl (no MCP)\n\nInfinite Campus's Campus Parent portal is a classic server-rendered site\n(no public API, no bot wall). A plain form POST to `verify.jsp` logs a\nparent in and hands back a session cookie — no browser bridge needed. This\nis the same login `infinitecampus-mcp`'s `src/client.ts`/`src/auth.ts` do;\nthis skill reproduces it with `curl` + a cookie jar.\n\n**Infinite Campus is per-district.** Every parent's portal lives at their\nown district's host (`https://campus.<district>.k12.example.us` or similar)\nunder an `appName` path segment. There is no shared IC endpoint — carry the\nbase URL and district from the same config the MCP uses:\n\n```sh\n: \"${IC_BASE_URL:?set to your district's portal URL, e.g. https://campus.yourdistrict.org}\"\n: \"${IC_DISTRICT:?set to the appName path segment, e.g. yourdistrict}\"\n: \"${IC_USERNAME:?parent portal username}\"\n: \"${IC_PASSWORD:?parent portal password}\"\n```\n\n(Same 4 env vars the MCP reads — reuse the repo's `.env` or the host's\nsecret store; don't hardcode a district.)\n\n## One-time setup\n\nNone — `curl` and `jq` are the only tools needed (`brew install jq` if\nmissing). No profile/pairing step, no extension.\n\n## Login (capture cookies + XSRF token)\n\n`verify.jsp` accepts a urlencoded form POST and returns 200 with an\n`<AUTHENTICATION>state</AUTHENTICATION>` marker in the body — **not**\nvia HTTP status — plus ~20 `Set-Cookie` headers. Use a cookie jar so curl\ndoes the Set-Cookie parsing/deduping for you:\n\n```sh\nJAR=$(mktemp)\nBODY=$(curl -sS -c \"$JAR\" \\\n  \"$IC_BASE_URL/campus/verify.jsp?nonBrowser=true\" \\\n  --data-urlencode \"username=$IC_USERNAME\" \\\n  --data-urlencode \"password=$IC_PASSWORD\" \\\n  --data-urlencode \"appName=$IC_DISTRICT\" \\\n  --data-urlencode \"portalLoginPage=parents\")\n\necho \"$BODY\" | grep -o '<AUTHENTICATION>[^<]*' # AUTHENTICATIONsuccess = good\n```\n\n`password-error` / `account-locked` / any non-`success` state means the\nlogin failed — check that string before doing anything else. Credentials\ngo in the form body (never the query string — it lands in access logs).\n\nPull the XSRF token out of the jar (IC's JS reads it back and echoes it on\nevery request as `X-XSRF-TOKEN`; `JSESSIONID` alone is not enough):\n\n```sh\nXSRF=$(awk -F'\\t' '$6==\"XSRF-TOKEN\"{print $7}' \"$JAR\")\n```\n\n## Core call pattern\n\nEvery subsequent request rides the jar + the XSRF header:\n\n```sh\nic_get() { # ic_get <path-with-query>\n  curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \\\n    \"$IC_BASE_URL$1\"\n}\n\nic_get '/campus/api/portal/students' | jq .\n```\n\nA **401** means the session expired (IC sessions last ~5-6h) — re-run the\nLogin step to get a fresh `$JAR`/`$XSRF` and retry once. There is no retry\nbudget beyond that in this shell version (the MCP's `CookieSessionManager`\ndoes this automatically; here just redo the login block).\n\n## The one rule: resolve student + enrollment first\n\nAlmost every read is scoped to a `personID` (student), and several need the\nstudent's **enrollment** (`enrollmentID` / `calendarID` / `structureID`).\nAlways start with:\n\n```sh\nic_get '/campus/api/portal/students' | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nTake `personID` for `studentId`/`personID` params, and the first (or\nrelevant) `enrollments[]` entry's `enrollmentID`/`calendarID`/`structureID`\nfor the endpoints that need them (attendance, school days, assessments,\nfeatures). There is no name→id search — `ic_list_students` (i.e. this call)\n*is* the resolve step.\n\n## Endpoints\n\nAll 17 read endpoints (grades, attendance, assignments, schedule, messages,\ndocuments, fees, teachers, assessments, feature flags) with ready-to-run\n`curl`+`jq` recipes are in `references/endpoints.md`. Two (`behavior`,\n`food_service`) have real paths that were never confirmed live against a\ndistrict with that module enabled — try them, expect a 404 if the module\nis off or the district's shape differs.\n\n## Downloads\n\n`ic_download_document`'s `url` field (from the documents list) may be a\nrelative `/campus/...` path or an absolute URL. Same jar + header pattern,\njust write to a file instead of piping to `jq`:\n\n```sh\ncurl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -o report-card.pdf \"$IC_BASE_URL$DOC_URL\"\n```\n\n## FeatureDisabled districts\n\nSome districts turn whole modules off (behavior, food service, assessments,\ndocuments, attendance). The MCP checks `displayOptions` first and falls\nback to treating a 404 as \"disabled.\" Doing the same by hand: fetch\n`/campus/api/portal/displayOptions/{structureID}?personID=X` (see\nreferences) and check the flag before assuming a 404 is a bug.\n\n## Notes\n\n- Read-only in practice: every recipe here is a GET. `ic_get_message`\n  fetching a message body *may* mark it read on some district configs — the\n  MCP's own probe couldn't confirm the side effect either way.\n- No CUPS linked-district switching in this skill (a parent with kids in\n  2+ IC instances under shared SSO) — that's a multi-step token-exchange\n  flow (`src/client.ts`'s `discoverLinkedDistricts`); out of scope here.\n  Run the login flow again against the linked district's own\n  `IC_BASE_URL`/`IC_DISTRICT` if you need it.\n- This project is developed and maintained by AI (Claude).\n\nFile v3.1.1:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-api\",\n  \"version\": \"3.1.1\",\n  \"publishedAt\": 1789964039230\n}\n\nFile v3.1.1:references/endpoints.md\n\n# Infinite Campus endpoints for curl\n\nAssumes the Login block from `../SKILL.md` has already run (`$JAR`, `$XSRF`,\n`$IC_BASE_URL` set) and the `ic_get` helper is defined:\n\n```sh\nic_get() { curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \"$IC_BASE_URL$1\"; }\n```\n\nAll paths are transcribed from `infinitecampus-mcp`'s `src/tools/*.ts` and\n`docs/endpoints.md` — the same requests the `ic_*` MCP tools make. `$PID` =\na student's `personID` from the resolve-first step.\n\nSome IC/prism responses serialize a 1-item collection as a bare object\ninstead of a 1-element array — pipe through `jq 'if type==\"array\" then . else [.] end'`\nif a recipe below assumes an array and you get an object back.\n\n---\n\n## Students (resolve-first — always start here)\n\n```sh\nic_get '/campus/api/portal/students' \\\n  | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nEach `enrollments[]` entry carries `enrollmentID`, `calendarID`,\n`structureID`, `schoolName`, `endDate` — needed by several endpoints below.\n\n## Schedule\n\n```sh\nic_get \"/campus/resources/portal/roster?personID=$PID\" | jq .\n```\n\n(Only `personID` is honored server-side — no working date/term filter, even\nthough the MCP tool's schema accepts `date`/`termFilter` args.)\n\n## Assignments\n\n```sh\n# all assignments (full term history, ~hundreds of rows)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" | jq .\n\n# scoped to one course (sectionID from the schedule call above)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID&sectionID=$SECTION_ID\" | jq .\n\n# client-side filters (server ignores date/missing params entirely):\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.[] | select(.dueDate >= $since)]'\n```\n\n## Recent (recently-graded) assignments\n\nServer-side filtered by `modifiedDate` (defaults to 14 days ago in the MCP;\nreproduce with `date -u -v-14d '+%Y-%m-%dT00:00:00'` on macOS):\n\n```sh\nSINCE=$(date -u -v-14d '+%Y-%m-%dT00:00:00')\nic_get \"/campus/api/portal/assignment/recentlyScored?modifiedDate=$SINCE&personID=$PID\" | jq .\n```\n\n## Grades\n\n```sh\nic_get \"/campus/resources/portal/grades?personID=$PID\" | jq .\n# scoped to one term:\nic_get \"/campus/resources/portal/grades?personID=$PID&termID=$TERM_ID\" | jq .\n```\n\n## Attendance (per-course summary, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/$ENROLLMENT_ID?courseSummary=true&personID=$PID\" \\\n  | jq '.terms[]?.courses[]? | {absentList, tardyList, presentList, earlyReleaseList}'\n```\n\nFeature-gated: check `displayOptions.attendance` first (see Feature flags,\nbelow) — a 404 here on a district with attendance disabled is expected, not\na bug.\n\n## Attendance events (individual absences/tardies, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq '.events[]? | {date: .localDate, code, description, excuse, comments}'\n\n# excused-only, since a date (client-side filter):\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.events[]? | select((.localDate[0:10] >= $since) and (.excuse == \"E\"))]'\n```\n\n## School days / calendar (loop over each enrollment)\n\nTwo calls per enrollment, then join term windows to days client-side:\n\n```sh\nic_get \"/campus/resources/term?structureID=$STRUCTURE_ID\" | jq .          # term boundaries\nic_get \"/campus/resources/calendar/instructionalDay?calendarID=$CALENDAR_ID\" | jq .  # day list\n\n# days within term N's [startDate,endDate]:\njq --slurpfile terms terms.json --arg t \"$TERM_ID\" \\\n  '[.[] | select(.date >= $terms[0][0].startDate and .date <= $terms[0][0].endDate)]' days.json\n```\n\n## Assessments (standardized tests, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/prism/portal/assessments?personID=$PID&calendarID=$CALENDAR_ID\" \\\n  | jq '{stateTests, nationalTests, districtTests}'\n```\n\nFeature-g\n\nArchive v3.1.0: 4 files, 8185 bytes\n\nFiles: references/endpoints.md (8445b), skill-card.md (2650b), SKILL.md (5776b), _meta.json (137b)","readmeExcerpt":"Skill: infinitecampus-api Owner: chrischall Summary: Query an Infinite Campus (Campus Parent portal) district directly with curl instead of running the infinitecampus-mcp server — log in with a real username/password, capture the session cookie + XSRF token, and curl grades, attendance, assignments, schedule, messages, documents, and fees. Use when you want IC data without the MCP, in a script, or on a machine where ","codeSnippets":[],"executableExamples":[{"language":"sh","snippet":": \"${IC_BASE_URL:?set to your district's portal URL, e.g. https://campus.yourdistrict.org}\"\n: \"${IC_DISTRICT:?set to the appName path segment, e.g. yourdistrict}\"\n: \"${IC_USERNAME:?parent portal username}\"\n: \"${IC_PASSWORD:?parent portal password}\""},{"language":"sh","snippet":"JAR=$(mktemp)\nBODY=$(curl -sS -c \"$JAR\" \\\n  \"$IC_BASE_URL/campus/verify.jsp?nonBrowser=true\" \\\n  --data-urlencode \"username=$IC_USERNAME\" \\\n  --data-urlencode \"password=$IC_PASSWORD\" \\\n  --data-urlencode \"appName=$IC_DISTRICT\" \\\n  --data-urlencode \"portalLoginPage=parents\")\n\necho \"$BODY\" | grep -o '<AUTHENTICATION>[^<]*' # AUTHENTICATIONsuccess = good"},{"language":"sh","snippet":"XSRF=$(awk -F'\\t' '$6==\"XSRF-TOKEN\"{print $7}' \"$JAR\")"},{"language":"sh","snippet":"curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \\"},{"language":"sh","snippet":"ic_get() { # ic_get <path-with-query>\n  curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \\\n    \"$IC_BASE_URL$1\"\n}\n\nic_get '/campus/api/portal/students' | jq ."},{"language":"sh","snippet":"ic_get '/campus/api/portal/students' | jq '.[] | {personID, firstName, lastName, enrollments}'"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: infinitecampus-api\ndescription: >-\n  Query an Infinite Campus (Campus Parent portal) district directly with curl\n  instead of running the infinitecampus-mcp server — log in with a real\n  username/password, capture the session cookie + XSRF token, and curl grades,\n  attendance, assignments, schedule, messages, documents, and fees. Use when\n  you want IC data without the MCP, in a script, or on a machine where the MCP\n  isn't installed. Infinite Campus is per-district: the base URL and district\n  app name are configurable, not hardcoded.\n---\n\n# Infinite Campus via curl (no MCP)\n\nInfinite Campus's Campus Parent portal is a classic server-rendered site\n(no public API, no bot wall). A plain form POST to `verify.jsp` logs a\nparent in and hands back a session cookie — no browser bridge needed. This\nis the same login `infinitecampus-mcp`'s `src/client.ts`/`src/auth.ts` do;\nthis skill reproduces it with `curl` + a cookie jar.\n\n**Infinite Campus is per-district.** Every parent's portal lives at their\nown district's host (`https://campus.<district>.k12.example.us` or similar)\nunder an `appName` path segment. There is no shared IC endpoint — carry the\nbase URL and district from the same config the MCP uses:\n\n```sh\n: \"${IC_BASE_URL:?set to your district's portal URL, e.g. https://campus.yourdistrict.org}\"\n: \"${IC_DISTRICT:?set to the appName path segment, e.g. yourdistrict}\"\n: \"${IC_USERNAME:?parent portal username}\"\n: \"${IC_PASSWORD:?parent portal password}\"\n```\n\n(Same 4 env vars the MCP reads — reuse the repo's `.env` or the host's\nsecret store; don't hardcode a district.)\n\n## One-time setup\n\nNone — `curl` and `jq` are the only tools needed (`brew install jq` if\nmissing). No profile/pairing step, no extension.\n\n## Login (capture cookies + XSRF token)\n\n`verify.jsp` accepts a urlencoded form POST and returns 200 with an\n`<AUTHENTICATION>state</AUTHENTICATION>` marker in the body — **not**\nvia HTTP status — plus ~20 `Set-Cookie` headers. Use a cookie jar so curl\ndoes the Set-Cookie parsing/deduping for you:\n\n```sh\nJAR=$(mktemp)\nBODY=$(curl -sS -c \"$JAR\" \\\n  \"$IC_BASE_URL/campus/verify.jsp?nonBrowser=true\" \\\n  --data-urlencode \"username=$IC_USERNAME\" \\\n  --data-urlencode \"password=$IC_PASSWORD\" \\\n  --data-urlencode \"appName=$IC_DISTRICT\" \\\n  --data-urlencode \"portalLoginPage=parents\")\n\necho \"$BODY\" | grep -o '<AUTHENTICATION>[^<]*' # AUTHENTICATIONsuccess = good\n```\n\n`password-error` / `account-locked` / any non-`success` state means the\nlogin failed — check that string before doing anything else. Credentials\ngo in the form body (never the query string — it lands in access logs).\n\nPull the XSRF token out of the jar (IC's JS reads it back and echoes it on\nevery request as `X-XSRF-TOKEN`; `JSESSIONID` alone is not enough):\n\n```sh\nXSRF=$(awk -F'\\t' '$6==\"XSRF-TOKEN\"{print $7}' \"$JAR\")\n```\n\n## Core call pattern\n\nEvery subsequent request rides the jar + the XSRF header:\n\n```sh\nic_get() { # ic_get <path-with-query>\n  curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XS"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"infinitecampus-api\",\n  \"version\": \"3.1.9\",\n  \"publishedAt\": 1791588394808\n}"},{"path":"references/endpoints.md","content":"# Infinite Campus endpoints for curl\n\nAssumes the Login block from `../SKILL.md` has already run (`$JAR`, `$XSRF`,\n`$IC_BASE_URL` set) and the `ic_get` helper is defined:\n\n```sh\nic_get() { curl -sS -b \"$JAR\" -H \"X-XSRF-TOKEN: $XSRF\" -H 'Accept: application/json' \"$IC_BASE_URL$1\"; }\n```\n\nAll paths are transcribed from `infinitecampus-mcp`'s `src/tools/*.ts` and\n`docs/endpoints.md` — the same requests the `ic_*` MCP tools make. `$PID` =\na student's `personID` from the resolve-first step.\n\nSome IC/prism responses serialize a 1-item collection as a bare object\ninstead of a 1-element array — pipe through `jq 'if type==\"array\" then . else [.] end'`\nif a recipe below assumes an array and you get an object back.\n\n---\n\n## Students (resolve-first — always start here)\n\n```sh\nic_get '/campus/api/portal/students' \\\n  | jq '.[] | {personID, firstName, lastName, enrollments}'\n```\n\nEach `enrollments[]` entry carries `enrollmentID`, `calendarID`,\n`structureID`, `schoolName`, `endDate` — needed by several endpoints below.\n\n## Schedule\n\n```sh\nic_get \"/campus/resources/portal/roster?personID=$PID\" | jq .\n```\n\n(Only `personID` is honored server-side — there is no date/term filter, so\nthe MCP tool takes none.)\n\n## Assignments\n\n```sh\n# all assignments (full term history, ~hundreds of rows)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" | jq .\n\n# scoped to one course (sectionID from the schedule call above)\nic_get \"/campus/api/portal/assignment/listView?personID=$PID&sectionID=$SECTION_ID\" | jq .\n\n# client-side filters (server ignores date/missing params entirely):\nic_get \"/campus/api/portal/assignment/listView?personID=$PID\" \\\n  | jq --arg since 2026-01-01 '[.[] | select(.dueDate >= $since)]'\n```\n\n## Recent (recently-graded) assignments\n\nServer-side filtered by `modifiedDate` (defaults to 14 days ago in the MCP;\nreproduce with `date -u -v-14d '+%Y-%m-%dT00:00:00'` on macOS):\n\n```sh\nSINCE=$(date -u -v-14d '+%Y-%m-%dT00:00:00')\nic_get \"/campus/api/portal/assignment/recentlyScored?modifiedDate=$SINCE&personID=$PID\" | jq .\n```\n\n## Grades\n\n```sh\nic_get \"/campus/resources/portal/grades?personID=$PID\" | jq .\n# scoped to one term:\nic_get \"/campus/resources/portal/grades?personID=$PID&termID=$TERM_ID\" | jq .\n```\n\n## Attendance (per-course summary, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/$ENROLLMENT_ID?courseSummary=true&personID=$PID\" \\\n  | jq '.terms[]?.courses[]? | {absentList, tardyList, presentList, earlyReleaseList}'\n```\n\nFeature-gated: check `displayOptions.attendance` first (see Feature flags,\nbelow) — a 404 here on a district with attendance disabled is expected, not\na bug.\n\n## Attendance events (individual absences/tardies, loop over each enrollment)\n\n```sh\nic_get \"/campus/resources/portal/attendance/events?enrollmentID=$ENROLLMENT_ID&personID=$PID\" \\\n  | jq '.events[]? | {date: .localDate, code, description, excuse, comments}'\n\n# excused-only, since a date (client-side filter):\nic_get \"/campus/resources/portal/atten"},{"path":"skill-card.md","content":"## Description:\n\nGuides authorized Campus Parent users through district-specific sign-in and read-only queries for grades, attendance, assignments, schedules, messages, documents, and fees using curl.\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\nAuthorized parents, guardians, and developers use this skill to retrieve student portal information from their own district without installing the MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Portal credentials and session tokens can expose a student's account if disclosed.\n\nMitigation: Use only an account you are authorized to access on a trusted private machine; keep credentials and cookie jars out of logs and shared or synced folders, and delete temporary cookies after use.\n\nRisk: Downloaded documents and query results can contain sensitive student records.\n\nMitigation: Limit access to authorized users, avoid logging record contents, and remove saved documents when finished.\n\n## Reference(s):\n\n- [Infinite Campus endpoint recipes](references/endpoints.md)\n- [ClawHub skill listing](https://clawhub.ai/chrischall/skills/infinitecampus-api)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Configuration guidance, Text]\n\n**Output Format:** [Markdown with curl and jq command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Responses depend on the district's enabled portal modules; some example endpoints are unconfirmed.]\n\n## Skill Version(s):\n\n3.1.9 (source: ClawHub release evidence)\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":"Query an Infinite Campus (Campus Parent portal) district directly with curl instead of running the infinitecampus-mcp server — log in with a real username/password, capture the session cookie + XSRF token, and curl grades, attendance, assignments, schedule, messages, documents, and fees. Use when you want IC data without the MCP, in a script, or on a machine where the MCP isn't installed. Infinite Campus is per-district: the base URL and district app name are configurable, not hardcoded. Skill: infinitecampus-api Owner: chrischall Summary: Query an Infinite Campus (Campus Parent portal) district directly with curl instead of running the infinitecampus-mcp server — log in with a real username/password, capture the session cookie + XSRF token, and curl grades, attendance, assignments, schedule, messages, documents, and fees. Use when you want IC data without the MCP, in a script, or on a machine where","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1392,"uniquenessScore":46,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T05:17:49.076Z","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-10T05:17:49.076Z","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-10T10:43:17.264Z","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"}]}}}