Departed Fall 2025
Schoology-Pro-MCP
Schoology · AI agent · Shipped
[image: Daily Briefing widget rendered inside ChatGPT — large '41' over 'assignments due the next 48h', categorized into Today (Unit 3 HW 7 Quiz, AP Calculus BC) and Tomorrow (Unit 5 Test for AP US Gvmt, Reading for 4.3A for AP Macroeconomics, Questions over the video for Unit 3 Day 9 Notes), every title a direct link back into Schoology]
Departure
Schoology is a firehose. Grades live in one tab, assignments in another, course materials in a third, and the main feed buries anything I cared about within a day. The workflow is reactive — open the site, dig through six filters per course, hope I didn't miss something due tonight. Everything is pull, nothing is pushed. The end state I wanted was simple: never open Schoology unless I'm submitting.
Approach
- MCP
- ChatGPT Apps SDK
- Python
- FastAPI
- SQLite
- SQLAlchemy
No public Schoology API — cookie-session auth and AJAX scraping with randomized backoff to stay under rate limits.
Field log
DevDay drop
Watched OpenAI ship the Apps SDK. Figma turned a sketch into a workable diagram inside ChatGPT; Spotify rendered an interactive playlist widget mid-conversation. The interesting move wasn't the model — it was the chat itself becoming a host for arbitrary tool UIs.
[image: DevDay keynote panel — Figma integration generating a flowchart from a sketch on the left, Spotify integration rendering a 'Latin Chill Mix' playlist widget inside ChatGPT on the right, partner logo strip below (AllTrails, DoorDash, Khan Academy, Instacart, Peloton, OpenTable, Target, TheFork, Tripadvisor, Thumbtack, Uber)]
Two demos that reframed what a chat tool could return. Reading the protocol
Pulled up the MCP spec. A host (Claude Desktop, ChatGPT) speaks one protocol; servers expose resources and tools; the host doesn't care what's behind the server. File system, GitHub, Slack, Bluesky, Maps — same shape. Felt like the moment USB hubs made sense.
[image: MCP architecture diagram from slide 3 — Claude Desktop labeled MCP Host at center, MCP Protocol arrows fanning out to File System / GitHub / Slack / Bluesky / Google Maps servers, each routing through Web APIs to remote data cylinders on the right]
Pattern catalog
The Apps SDK shipped with a 'Pizzaz' demo that's basically a layout cookbook: list, map, carousel, album. Saw it and immediately knew the shape my Schoology widget wanted — a list with a count on top.
[image: Apps SDK 'Pizzaz' mockups — four widget layouts of the same pizzeria data: ranked list with Save List button (top-left), SF map of pizza-slice pins with horizontal scroll (bottom-left), horizontal carousel of cards with Order Now (top-right), and stacked album view of photo collections Summer Slice / Pepperoni Nights / Truffle Forest (bottom-right)]
The pain, named
Wrote down what Schoology actually costs me daily. Three bullets fit it: information filtering (grades, assignments, materials all separate), too much noise (the main feed eats anything that mattered yesterday), reactive workflow (everything pulled, nothing pushed). End state: stop opening Schoology unless I'm submitting.
Receipts
Took screenshots so I'd remember why I was doing this.
[image: Cluttered Schoology dashboard collage — main feed firehose of group posts (US Student Notices, Mu Alpha Theta, Latinos Unidos), massive Overdue column with missing assignments, packed Upcoming column, plus an AP Calc BC PDF folder stretching off-screen with dozens of repetitive worksheets and solutions; jagged speech-bubble graphic pointing at it]
The 'pain point' slide. None of this needed to be in my eyes. Architecture sketch
Drew the pipeline before writing code. Two big moves: the synchronizer keeps a local mirror warm in the background, and the MCP server only ever reads from that mirror. Schoology becomes a write source I never have to talk to live.
[image: Architecture flowchart from slide 7 — color-coded boxes left to right: Schoology → Synchronizer → DB (SQLite cylinder) → MCP Server → ChatGPT, with captions calling out the Synchronizer as a background fetch job, the local mirror as fast SQLite, and the MCP server as Python serving only from the mirror]
First scrape
No public Schoology API, so I logged in, lifted the session cookie into a requests.Session, and walked /course/{id}/materials with list_filter cycled through six values. Endpoints return HTML wrapped in JSON; parsed each chunk back into rows of titles, URLs, and parent folders.
[image: Python code block of SchoologyClient.get_course_materials — FILTERS_TO_FETCH list with six strings (assignments, assessments, documents_files, documents_links, discussion, pages) being looped through ajax=1 list_filter requests, each response unwrapped from JSON-wrapped HTML and parsed into items]
Rate-limit backoff
Hammered the materials endpoint and the edge pushed back. Slid a random 0.75–1.75s sleep between requests — averages about 4 requests per 5 seconds, sits under whatever their threshold is, and looks enough like a person clicking around. The sync got slower; nothing else got worse.
Local mirror
Two SQLAlchemy tables. Resource holds every material (assignment, document, link, discussion, page) keyed on schoology_id with parent_folder and resource_type indexed. Assignment is the due-date subset, with due_at_utc indexed for windowed queries. Both carry last_seen_at_utc so the synchronizer can reason about what's new and what's stale.
[image: Two-pane SQLAlchemy schema slide — Resource model on top with Mapped[int] schoology_id (unique, indexed), course_id, title, url, resource_type, parent_folder, last_seen_at_utc; Assignment model below with course_id, course_name, title, due_at_utc (indexed), url, status, last_seen_at_utc — all using mapped_column type annotations]
MCP wiring
Wrote a FastAPI MCP server with a summary.get tool that takes range = today | 48h | week. The handler maps those to {24, 48, 168} hours, queries upcoming_assignments() against the local mirror, and returns a structured payload — no live scrape, no Schoology round-trip on the user's request.
[image: Python code block from slide 10 — _call_summary_get(args, db) parsing args.get('range','today') against hours_map {today:24, 48h:48, week:168} and a label_map for human-readable strings, then calling crud.upcoming_assignments(db, window_hours=hours, limit=50) and shaping ui_items plus a structured summary payload]
Widget render
Built a meta_for_ui payload (assignments, count, range, generatedAt), embedded the widget resource, and shipped both back as _meta["openai.com/widget"] on the CallToolResult. ChatGPT picked it up and rendered 'Daily Briefing' inline.
[image: Split-screen slide — left: Python code constructing a CallToolResult with TextContent, structuredContent, and _meta carrying the openai.com/widget resource alongside the meta_for_ui payload; right: a ChatGPT desktop screenshot where 'fetch my schoology briefing 48 hours' triggers the Schoology Pro tool and renders a Daily Briefing widget showing 41 assignments due in the next 48h]
Demo day
Two prompts on stage. 'Get my Schoology summary today and next 48 hours' → the Daily Briefing widget, every title clickable straight to Schoology. 'Get all my English class assignments and give me all the links to the poems' → 30 seconds of tool thinking, then a sectioned list of every AP English IV assignment plus every reading under the Daily Poetry folder.
[image: Two ChatGPT screenshots from slide 12 — left: 'Get my Schoology summary for today, and tell me what I have next 48 hours' yields the Daily Briefing widget with 5 assignments due today and tests/readings under Tomorrow; right: 'Get all my English class assignments and give me all the links to the poems' returns a long, sectioned response for AP English IV Literature & Composition with Dystopian Literature and Aeschylus' Oresteia headers and clickable direct Schoology links]
What's next
The mirror already has the data — each future card is just a new tool plus a new widget shape. Performance Dashboard reads grade history off the same Assignment table. Interactive Planner is a Kanban over the same query. Proactive Alerts is the synchronizer noticing a delta and pushing.
[image: Future roadmap from slide 13 — three white UI cards on a pinkish background: Performance Dashboard (blue upward line chart icon, 'Grade tracking'), Interactive Planner (blue Kanban board icon), Proactive Alerts (blue notification bell icon, 'Notifications for new grades')]
From the gallery
[image: Close-up of the Pizzaz album-view mockup — stacked photo galleries labeled Summer Slice, Pepperoni Nights, Truffle Forest, the layout I keep returning to as a reference for a future per-course detail widget]
[image: Zoom on the right-hand demo from slide 12 — ChatGPT response 'Done — I pulled the materials for your AP English IV Literature & Composition course' followed by 'All assignments & assessments (title -> link)' with sectioned Schoology links, including everything under the Daily Poetry folder]
What I came back with
Never open Schoology
Lesson from the terrain
The leverage was the local mirror, not the model. Once Schoology's data lived in my own SQLite, the MCP server stopped being a scraper and became a query layer — the agent could ask anything across courses without paying the round-trip cost or risking the rate limit. MCP turned out to be the cleanest pattern I've found for exposing 'my stuff' to an agent, and the widget payload meant a tool result could come back as actual UI instead of a wall of text.
Cross-links
Other expeditions in Schoology
This fed into / from