Josh Ramirez
← Back to the journal

Departed Fall 2025

Schoology-Pro-MCP

Schoology · AI agent · Shipped

The end state — a tool result that comes back as UI my server controls, rendered inside someone else's chat client.

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

  1. 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.

    Two demos that reframed what a chat tool could return.
  2. 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.

  3. 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.

  4. 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.

  5. Receipts

    Took screenshots so I'd remember why I was doing this.

    The 'pain point' slide. None of this needed to be in my eyes.
  6. 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.

  7. 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.

  8. 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.

  9. 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.

  10. 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.

  11. 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.

  12. 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.

  13. 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.

From the gallery

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