#!/usr/bin/env python3
"""
UofT Student Helper MCP Server
A Model Context Protocol server that provides tools for University of Toronto students
to access their ACORN course information and Quercus syllabi.
Author: Alice
Date: January 2026
"""
import os
from typing import Dict, List, Any, Optional
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Import MCP SDK (adjust import based on actual SDK structure)
try:
from north_mcp_python_sdk import NorthMCPServer
except ImportError:
print("ERROR: north_mcp_python_sdk not found.")
print("Please install it from: https://github.com/cohere-ai/north-mcp-python-sdk")
print("Run: pip install -e path/to/north-mcp-python-sdk")
exit(1)
# Import our custom clients
from acorn_client import AcornClient
from quercus_client import QuercusClient
from google_calendar_client import GoogleCalendarClient
from deadline_extractor import DeadlineExtractor
# Server configuration
_default_port = int(os.getenv("SERVER_PORT", "3002"))
# Initialize MCP server
mcp = NorthMCPServer(
name="uoft-student-helper",
version="1.0.0",
description="University of Toronto Student Helper - Access ACORN courses and Quercus syllabi"
)
# Initialize clients
acorn_client = None
quercus_client = None
google_calendar_client = None
deadline_extractor = None
def initialize_clients():
"""Initialize ACORN and Quercus clients with credentials from environment"""
global acorn_client, quercus_client, google_calendar_client, deadline_extractor
# Initialize ACORN client
acorn_username = os.getenv("ACORN_USERNAME")
acorn_password = os.getenv("ACORN_PASSWORD")
if acorn_username and acorn_password:
acorn_client = AcornClient(acorn_username, acorn_password)
print("✅ ACORN client initialized")
else:
print("⚠️ ACORN credentials not found in .env file")
# Initialize Quercus client
quercus_token = os.getenv("QUERCUS_API_TOKEN")
if quercus_token:
quercus_client = QuercusClient(quercus_token)
print("✅ Quercus client initialized")
else:
print("⚠️ Quercus API token not found in .env file")
# Initialize Google Calendar client
try:
google_calendar_client = GoogleCalendarClient()
if google_calendar_client.authenticate():
print("✅ Google Calendar client initialized")
else:
print("⚠️ Google Calendar authentication failed")
google_calendar_client = None
except Exception as e:
print(f"⚠️ Google Calendar client not available: {str(e)}")
google_calendar_client = None
# Initialize deadline extractor
deadline_extractor = DeadlineExtractor()
print("✅ Deadline extractor initialized")
# ============================================================================
# ACORN Tools
# ============================================================================
@mcp.tool()
def get_enrolled_courses() -> Dict[str, Any]:
"""
Get all courses the student is currently enrolled in from ACORN.
Returns:
A dictionary containing a list of enrolled courses with their details:
- course_code: Course code (e.g., "CSC148H1")
- course_name: Full course name
- section: Section code
- credits: Number of credits
- term: Academic term (e.g., "Fall 2025", "Winter 2026")
Example:
{
"courses": [
{
"course_code": "CSC148H1",
"course_name": "Introduction to Computer Science",
"section": "LEC0101",
"credits": 0.5,
"term": "Fall 2025"
},
...
],
"total_courses": 5,
"total_credits": 2.5
}
"""
if not acorn_client:
return {
"error": "ACORN client not initialized. Please check your credentials in .env file."
}
try:
courses = acorn_client.get_enrolled_courses()
return {
"courses": courses,
"total_courses": len(courses),
"total_credits": sum(c.get("credits", 0) for c in courses)
}
except Exception as e:
return {"error": f"Failed to retrieve courses: {str(e)}"}
@mcp.tool()
def get_course_details(course_code: str) -> Dict[str, Any]:
"""
Get detailed information about a specific course from ACORN.
Args:
course_code: The course code (e.g., "CSC148H1", "MAT137Y1")
Returns:
A dictionary containing detailed course information:
- course_code: Course code
- course_name: Full course name
- description: Course description
- prerequisites: List of prerequisite courses
- corequisites: List of corequisite courses
- exclusions: List of excluded courses
- instructor: Instructor name(s)
- meeting_times: List of class meeting times
- location: Classroom location(s)
- delivery_mode: In-person, online, or hybrid
Example:
{
"course_code": "CSC148H1",
"course_name": "Introduction to Computer Science",
"description": "Abstract data types and data structures...",
"prerequisites": ["CSC108H1"],
"instructor": "Dr. Smith",
"meeting_times": [
{"day": "Monday", "time": "10:00-11:00", "type": "Lecture"},
{"day": "Wednesday", "time": "10:00-11:00", "type": "Lecture"}
],
"location": "BA1170"
}
"""
if not acorn_client:
return {
"error": "ACORN client not initialized. Please check your credentials in .env file."
}
try:
details = acorn_client.get_course_details(course_code)
return details
except Exception as e:
return {"error": f"Failed to retrieve course details: {str(e)}"}
@mcp.tool()
def get_course_schedule() -> Dict[str, Any]:
"""
Get the student's weekly class schedule from ACORN.
Returns:
A dictionary containing the weekly schedule organized by day:
- monday: List of classes on Monday
- tuesday: List of classes on Tuesday
- ... (for each day of the week)
Each class entry contains:
- course_code: Course code
- course_name: Course name
- time: Class time (e.g., "10:00-11:00")
- location: Classroom location
- type: Class type (Lecture, Tutorial, Lab, etc.)
Example:
{
"schedule": {
"monday": [
{
"course_code": "CSC148H1",
"course_name": "Intro to CS",
"time": "10:00-11:00",
"location": "BA1170",
"type": "Lecture"
}
],
"tuesday": [],
...
},
"total_hours_per_week": 15
}
"""
if not acorn_client:
return {
"error": "ACORN client not initialized. Please check your credentials in .env file."
}
try:
schedule = acorn_client.get_schedule()
return schedule
except Exception as e:
return {"error": f"Failed to retrieve schedule: {str(e)}"}
# ============================================================================
# Quercus Tools
# ============================================================================
@mcp.tool()
def get_syllabus(course_code: str) -> Dict[str, Any]:
"""
Fetch the syllabus for a specific course from Quercus.
Args:
course_code: The course code (e.g., "CSC148H1")
Returns:
A dictionary containing the syllabus information:
- course_code: Course code
- course_name: Course name
- syllabus_text: Text content of the syllabus
- syllabus_url: URL to the syllabus (if available)
- last_updated: Last update date
Example:
{
"course_code": "CSC148H1",
"course_name": "Introduction to Computer Science",
"syllabus_text": "Course Description: This course introduces...",
"syllabus_url": "https://q.utoronto.ca/courses/12345/assignments/syllabus",
"last_updated": "2025-09-01"
}
"""
if not quercus_client:
return {
"error": "Quercus client not initialized. Please check your API token in .env file."
}
try:
syllabus = quercus_client.get_syllabus(course_code)
return syllabus
except Exception as e:
return {"error": f"Failed to retrieve syllabus: {str(e)}"}
@mcp.tool()
def get_course_assignments(course_code: str) -> Dict[str, Any]:
"""
List all assignments for a specific course from Quercus.
Args:
course_code: The course code (e.g., "CSC148H1")
Returns:
A dictionary containing assignment information:
- course_code: Course code
- assignments: List of assignments
Each assignment contains:
- name: Assignment name
- due_date: Due date and time
- points_possible: Maximum points
- submission_status: "submitted", "not_submitted", or "graded"
- grade: Grade received (if graded)
Example:
{
"course_code": "CSC148H1",
"assignments": [
{
"name": "Assignment 1: Recursion",
"due_date": "2025-10-15 23:59:00",
"points_possible": 100,
"submission_status": "submitted",
"grade": 95
},
...
],
"total_assignments": 5,
"completed": 3,
"pending": 2
}
"""
if not quercus_client:
return {
"error": "Quercus client not initialized. Please check your API token in .env file."
}
try:
assignments = quercus_client.get_assignments(course_code)
return assignments
except Exception as e:
return {"error": f"Failed to retrieve assignments: {str(e)}"}
@mcp.tool()
def get_course_announcements(course_code: str, limit: int = 5) -> Dict[str, Any]:
"""
Get recent announcements from a specific course on Quercus.
Args:
course_code: The course code (e.g., "CSC148H1")
limit: Maximum number of announcements to retrieve (default: 5)
Returns:
A dictionary containing announcement information:
- course_code: Course code
- announcements: List of announcements
Each announcement contains:
- title: Announcement title
- message: Full announcement text
- posted_at: Date and time posted
- author: Name of the person who posted
Example:
{
"course_code": "CSC148H1",
"announcements": [
{
"title": "Midterm Reminder",
"message": "Don't forget the midterm is next week...",
"posted_at": "2025-10-01 14:30:00",
"author": "Dr. Smith"
},
...
],
"total_count": 3
}
"""
if not quercus_client:
return {
"error": "Quercus client not initialized. Please check your API token in .env file."
}
try:
announcements = quercus_client.get_announcements(course_code, limit)
return announcements
except Exception as e:
return {"error": f"Failed to retrieve announcements: {str(e)}"}
@mcp.tool()
def get_all_quercus_courses() -> Dict[str, Any]:
"""
Get a list of all courses available on Quercus for the student.
Returns:
A dictionary containing all Quercus courses:
- courses: List of courses
Each course contains:
- course_code: Course code
- course_name: Course name
- course_id: Quercus course ID
- term: Academic term
- enrollment_status: "active" or "completed"
Example:
{
"courses": [
{
"course_code": "CSC148H1",
"course_name": "Introduction to Computer Science",
"course_id": 12345,
"term": "Fall 2025",
"enrollment_status": "active"
},
...
],
"total_courses": 5
}
"""
if not quercus_client:
return {
"error": "Quercus client not initialized. Please check your API token in .env file."
}
try:
courses = quercus_client.get_courses()
return {
"courses": courses,
"total_courses": len(courses)
}
except Exception as e:
return {"error": f"Failed to retrieve Quercus courses: {str(e)}"}
# ============================================================================
# Deadline Management Tools
# ============================================================================
@mcp.tool()
def extract_deadlines_from_syllabus(course_code: str) -> Dict[str, Any]:
"""
Extract all deadlines from a course syllabus and return them.
Args:
course_code: The course code (e.g., "CSC148H1")
Returns:
A dictionary containing extracted deadlines:
- course_code: Course code
- deadlines: List of deadlines found
Each deadline contains:
- assignment_name: Name of the assignment
- due_date: Due date and time (ISO format)
- description: Description or context
- confidence: Confidence score (0-1) of the extraction
Example:
{
"course_code": "CSC148H1",
"deadlines": [
{
"assignment_name": "Assignment 1",
"due_date": "2026-02-15T23:59:00",
"description": "Assignment 1 due February 15",
"confidence": 0.9
}
],
"total_deadlines": 1
}
"""
if not quercus_client or not deadline_extractor:
return {
"error": "Required clients not initialized."
}
try:
# Get syllabus
syllabus = quercus_client.get_syllabus(course_code)
if "error" in syllabus:
return syllabus
# Extract deadlines
syllabus_text = syllabus.get('syllabus_text', '')
deadlines = deadline_extractor.extract_from_syllabus(syllabus_text, course_code)
# Convert datetime objects to ISO strings
for deadline in deadlines:
if 'due_date' in deadline:
deadline['due_date'] = deadline['due_date'].isoformat()
return {
"course_code": course_code,
"deadlines": deadlines,
"total_deadlines": len(deadlines)
}
except Exception as e:
return {"error": f"Failed to extract deadlines: {str(e)}"}
@mcp.tool()
def add_deadline_to_calendar(course_code: str, assignment_name: str, due_date: str, description: str = "") -> Dict[str, Any]:
"""
Add a single deadline to Google Calendar.
Args:
course_code: Course code (e.g., "CSC148H1")
assignment_name: Name of the assignment
due_date: Due date in ISO format (e.g., "2026-02-15T23:59:00")
description: Optional description
Returns:
Dictionary with event information:
- success: True if added successfully
- event_id: Google Calendar event ID
- event_link: Link to view the event
- title: Event title
Example:
{
"success": true,
"event_id": "abc123",
"event_link": "https://calendar.google.com/...",
"title": "[CSC148H1] Assignment 1 DUE"
}
"""
if not google_calendar_client:
return {
"error": "Google Calendar client not initialized. Please set up Google Calendar authentication."
}
try:
from datetime import datetime
# Parse due date
due_datetime = datetime.fromisoformat(due_date.replace('Z', '+00:00'))
# Add to calendar
result = google_calendar_client.add_deadline(
course_code=course_code,
assignment_name=assignment_name,
due_date=due_datetime,
description=description
)
return result
except Exception as e:
return {"error": f"Failed to add deadline to calendar: {str(e)}"}
@mcp.tool()
def add_all_course_deadlines_to_calendar(course_code: str) -> Dict[str, Any]:
"""
Extract all deadlines from a course syllabus and add them to Google Calendar.
This is a convenience tool that combines extract_deadlines_from_syllabus
and add_deadline_to_calendar.
Args:
course_code: The course code (e.g., "CSC148H1")
Returns:
Dictionary with results:
- total_deadlines: Total number of deadlines found
- successful: Number successfully added to calendar
- failed: Number that failed to add
- events: List of event details
Example:
{
"total_deadlines": 5,
"successful": 5,
"failed": 0,
"events": [
{
"success": true,
"event_id": "abc123",
"title": "[CSC148H1] Assignment 1 DUE"
},
...
]
}
"""
if not quercus_client or not deadline_extractor or not google_calendar_client:
return {
"error": "Required clients not initialized."
}
try:
# Extract deadlines
extraction_result = extract_deadlines_from_syllabus(course_code)
if "error" in extraction_result:
return extraction_result
deadlines = extraction_result.get('deadlines', [])
if not deadlines:
return {
"message": "No deadlines found in syllabus",
"total_deadlines": 0,
"successful": 0,
"failed": 0,
"events": []
}
# Add to calendar
result = google_calendar_client.batch_add_deadlines(deadlines)
return result
except Exception as e:
return {"error": f"Failed to add deadlines to calendar: {str(e)}"}
@mcp.tool()
def sync_all_deadlines_to_calendar() -> Dict[str, Any]:
"""
Sync all deadlines from all enrolled courses to Google Calendar.
This tool:
1. Gets all enrolled courses from ACORN
2. For each course, extracts deadlines from syllabus
3. Adds all deadlines to Google Calendar
Returns:
Dictionary with sync results:
- total_courses: Total number of courses processed
- total_deadlines: Total deadlines found
- successful: Number successfully added
- failed: Number that failed
- courses: List of results per course
Example:
{
"total_courses": 5,
"total_deadlines": 25,
"successful": 24,
"failed": 1,
"courses": [
{
"course_code": "CSC148H1",
"deadlines_found": 5,
"added": 5
},
...
]
}
"""
if not acorn_client or not quercus_client or not deadline_extractor or not google_calendar_client:
return {
"error": "Required clients not initialized."
}
try:
# Get all enrolled courses
courses_result = get_enrolled_courses()
if "error" in courses_result:
return courses_result
courses = courses_result.get('courses', [])
total_deadlines = 0
total_successful = 0
total_failed = 0
course_results = []
for course in courses:
course_code = course['course_code']
# Add deadlines for this course
result = add_all_course_deadlines_to_calendar(course_code)
if "error" not in result:
deadlines_found = result.get('total_deadlines', 0)
successful = result.get('successful', 0)
failed = result.get('failed', 0)
total_deadlines += deadlines_found
total_successful += successful
total_failed += failed
course_results.append({
"course_code": course_code,
"deadlines_found": deadlines_found,
"added": successful,
"failed": failed
})
else:
course_results.append({
"course_code": course_code,
"error": result['error']
})
return {
"total_courses": len(courses),
"total_deadlines": total_deadlines,
"successful": total_successful,
"failed": total_failed,
"courses": course_results
}
except Exception as e:
return {"error": f"Failed to sync deadlines: {str(e)}"}
# ============================================================================
# Server Startup
# ============================================================================
if __name__ == "__main__":
print("=" * 60)
print(" UofT Student Helper MCP Server")
print("=" * 60)
print()
# Initialize clients
initialize_clients()
print()
print("Available tools:")
print(" 📚 ACORN Tools:")
print(" - get_enrolled_courses: View all enrolled courses")
print(" - get_course_details: Get detailed course information")
print(" - get_course_schedule: View weekly class schedule")
print()
print(" 📝 Quercus Tools:")
print(" - get_syllabus: Fetch course syllabus")
print(" - get_course_assignments: List course assignments")
print(" - get_course_announcements: Get course announcements")
print(" - get_all_quercus_courses: List all Quercus courses")
print()
print(" 📅 Deadline Management Tools:")
print(" - extract_deadlines_from_syllabus: Extract deadlines from syllabus")
print(" - add_deadline_to_calendar: Add a deadline to Google Calendar")
print(" - add_all_course_deadlines_to_calendar: Add all course deadlines")
print(" - sync_all_deadlines_to_calendar: Sync all deadlines from all courses")
print()
print("=" * 60)
print()
# Start the server
print(f"Starting server on port {_default_port}...")
mcp.run(port=_default_port)