UofT Timetable Builder MCP
A small Python MCP server for the public UofT Timetable Builder API. It exposes ten tools over local stdio using the official MCP Python SDK: seven course-lookup tools, a schedule solver wrapper, and share save/retrieve tools.
Work in progress: ACORN and Degree Explorer authentication is available, but their student-data and planning tools are still under development. The current ACORN and Degree Explorer controls only connect, check, and forget local sessions.
Public timetable tools require no login, API key, database, web server, or environment variables. This is an No API key, database, web server, or environment variables are required. This is an unofficial wrapper; it does not enroll students or write to ACORN. Saving a timetable creates an anonymous public share link on the Timetable Builder, not a personal account record.
Connect an MCP Client
Install uv, then add this
configuration to any client that supports mcpServers:
{
"mcpServers": {
"uoft-timetable": {
"command": "uvx",
"args": ["uoft-mcp@latest"],
"env": {
"UV_HTTP_TIMEOUT": "300"
}
}
}
}
Restart the client after saving its configuration. On Windows, if the client cannot
find uvx, restart it after installing uv or replace "uvx" with the absolute path
reported by where.exe uvx.
uvx downloads the published package into an isolated environment and starts the
uoft-mcp command. No repository clone, virtual environment setup, API key, server
URL, or listening port is needed. The first start can take longer while uv downloads
Python and the dependencies; later starts use its cache.
To pin a release instead of following the newest release, use
"args": ["uoft-mcp==0.3.1"].
Local Development
From a clone of this repository:
uv python install 3.13
uv sync --locked --managed-python
uv sync creates .venv, installs the package and development tools, and uses the
committed uv.lock. .python-version selects Python 3.13; the package supports
Python 3.13 and newer.
Run the local checkout with:
uv run --locked python -m uoft_mcp
The installed uoft-mcp command is another entry point. The process waits for an
MCP client on stdin; a blank terminal is expected. Use Ctrl+C to stop a manual run.
Stdout carries protocol messages only, and logging goes to stderr.
Tools
| Tool | Arguments and purpose |
|---|---|
get_current_sessions |
No arguments. Get current session IDs; skip entries with header: true. |
get_reference_data |
No arguments. Get campus, division, delivery-mode, and sorting values. |
get_divisions |
No arguments. List recognized faculty/division codes. |
search_departments |
Required term keyword and divisions code. |
search_course_titles |
Required term, divisions, and sessions strings. Optional lower_threshold=50, upper_threshold=200. |
get_course_details |
Required course_code; optional section_code of F, S, or Y. The returned course id is required by generate_timetable. |
search_courses |
Optional code/title, section, description, division, session, campus, delivery, and pagination filters. |
generate_timetable |
Required plans array. Each plan has courses (course_id plus activity_types), optional preference of early, balanced, or late, and optional blocked_times. |
save_timetable |
Required timetable object with sessions, timetables, and plans. Returns the share id plus a share_url. |
retrieve_timetable |
Required share_id from save_timetable. |
uoft_login |
Optional service of degree_explorer, acorn, or both (default), plus remember=true. Starts or reuses official browser login and returns while you complete Duo. |
uoft_auth_status |
Optional refresh=false. Reports connection and login progress; refresh=true checks both services without opening a browser. |
uoft_forget_session |
No arguments. Cancels login and removes locally saved UofT session state and its encryption key. |
Each successful lookup, generation, and retrieve call returns one text block
containing the complete upstream JSON. The wrapper preserves fields and arrays,
including upstream payload and status envelopes. It does not summarize or
truncate course data. save_timetable keeps the upstream share object and adds
share_url.
Example Workflow
- Call
get_current_sessionswith{}and select a non-header entry'svalue. - Call
get_divisionsorget_reference_datafor valid filter codes. - Call
search_course_titleswith these arguments, substituting the session value:
{"term": "CSC108", "divisions": "ARTSC", "sessions": "SESSION_ID_FROM_STEP_1"}
- Use the returned exact course code in
get_course_details:
{"course_code": "CSC108H1", "section_code": "F"}
- For filtered, paginated results, call
search_courses:
{
"course_code": "CSC108H1",
"divisions": ["ARTSC"],
"sessions": ["SESSION_ID_FROM_STEP_1"],
"page": 1,
"page_size": 2
}
search_courses also accepts course_title, course_section_code,
search_course_description, campuses, delivery_modes, and direction (asc or
desc). Pages start at 1, page size defaults to 20, and sorting defaults to
asc. Omitted collection filters become empty arrays. Course codes should be exact;
use autocomplete for prefixes or course_title for keyword searches.
- Copy each selected offering's
idfromget_course_detailsintogenerate_timetable. Activity types are the section types to fill, such asLecture,Tutorial, orPractical. Preference defaults tobalanced. Optional blocked intervals use weekday names and 24-hourHH:MMtimes:
{
"plans": [
{
"courses": [
{
"course_id": "COURSE_ID_FROM_STEP_4",
"activity_types": ["Lecture", "Tutorial"]
}
],
"preference": "early",
"blocked_times": [{"day": "Monday", "start": "8:00", "end": "10:00"}]
}
]
}
Use one plan per term. A Fall and Winter year is two plans. The solver returns chosen sections; it does not enroll students.
-
Store a Timetable Builder state with
save_timetable. Thetimetableobject must includesessions,timetables, andplansin the frontend's serialized shape. The tool returnsidandshare_url(https://ttb.utoronto.ca/#!/?t=...). -
Reload that share later with
retrieve_timetable:
{"share_id": "SHARE_ID_FROM_STEP_7"}
Checks
uv run --locked pytest -q
uv run --locked ruff check .
uv run --locked ruff format --check .
The tests run offline. They cover all ten tools, request mapping, raw JSON preservation, validation, HTTP errors, timeouts, connection errors, invalid JSON, shared-client cleanup, MCP discovery, and actual stdio subprocesses.
Verification: 50 tests passed. Live checks of lookup, generateYear, tiny/shorten,
and tiny/retrieve succeeded, including generating CSC258H1 and CSC311H1, saving an
anonymous share, and retrieving that share. Live requests are deliberately not part
of the test suite, so tests remain reproducible.
API Notes
- The supplied timetable_builder.json remains the original
reference. Live checks found two missing details: pagination starts at 1, and
paginated search requires an empty
departmentPropsarray when not filtering by department. The wrapper supplies it. generateYearaccepts an array of plans. Each plan needs courseidvalues fromget_course_details,sectionswithname: "*"and a type,fitnessFunctionOption(MORNING_WEIGHTED,BALANCED, orAFTERNOON_WEIGHTED), andblockedOffintervals using weekday numbers 1-5 (Monday-Friday) and milliseconds since midnight.tiny/shortenstores the officialhttps://ttb.utoronto.ca/#!/?URL for a serialized timetable and returns{ "id": "..." }.tiny/retrieve?id=loads it. These are anonymous share records, not ACORN enrolment.- Get division codes from the API. For example, the live API uses
ERINandSCARfor Mississauga and Scarborough, rather than the reference'sUTMandUTSCexamples. - Even one course can have a large response because all its sections are included. Choose narrow filters and small page sizes. The wrapper never fetches extra pages.
- HTTP failures become MCP tool errors containing the endpoint and status code. UofT may return HTTP 404 for no matching courses. Timeouts, connection failures, and malformed JSON get their own readable errors. No automatic retries occur.
- This API is not covered by an official support guarantee. Changes upstream may require updating the mappings. Successful HTTP responses are preserved as supplied, including any application-level status messages inside their JSON.
For a walkthrough of the code and how to extend it, read EXPLAINED.md.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file uoft_mcp-0.3.1.tar.gz.
File metadata
- Download URL: uoft_mcp-0.3.1.tar.gz
- Upload date:
- Size: 54.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
73258c6d6774d87514130af162f77fd426b4c94e6529c7888e1ff37f21be3071
|
|
| MD5 |
e318277ad8e6a8b1f474de5f23c7091c
|
|
| BLAKE2b-256 |
46643197d61aeab81d8e8d622142fb13b21bfdd80d061059e8bf4d6c4460b11e
|
File details
Details for the file uoft_mcp-0.3.1-py3-none-any.whl.
File metadata
- Download URL: uoft_mcp-0.3.1-py3-none-any.whl
- Upload date:
- Size: 12.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec8312cc8918c0c2601f33b8d9e343cebba409015906af2ad5f8da6328cfc621
|
|
| MD5 |
549cac532e03a41ee7a9406c2c14e3f3
|
|
| BLAKE2b-256 |
626a255acabe52ac98d14d55abd9648f5d6eb37b63d95134018f29301e5495b4
|