Skip to main content
Glama

eClass MCP Server

server.py8.67 kB
""" eClass MCP Server - MCP Integration for Open eClass Platform This module provides an MCP server for interacting with an eClass platform instance. It handles authentication, session management, and course access for eClass resources. Specifically tailored for UoA's SSO authentication system. """ import asyncio import os import logging import re from typing import Any, Dict, List, Optional import requests from bs4 import BeautifulSoup from dotenv import load_dotenv from mcp.server.models import InitializationOptions import mcp.types as types from mcp.server import NotificationOptions, Server from pydantic import AnyUrl import mcp.server.stdio # Import from modularized components from . import authentication from . import course_management from . import html_parsing # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger('eclass_mcp_server') # Initialize the MCP server server = Server("eclass-mcp") # Global session state - maintains authentication state between calls class SessionState: def __init__(self): # Load environment variables load_dotenv() # Initialize session and state self.session = requests.Session() self.logged_in = False # Set base URL self.base_url = os.getenv('ECLASS_URL') if not self.base_url: self.base_url = "https://eclass.uoa.gr" logger.warning(f"ECLASS_URL not set in environment, using default: {self.base_url}") # Remove trailing slash if present self.base_url = self.base_url.rstrip('/') # Set SSO URLs self.login_form_url = f"{self.base_url}/main/login_form.php" self.portfolio_url = f"{self.base_url}/main/portfolio.php" self.logout_url = f"{self.base_url}/index.php?logout=yes" # Store user information self.username = None self.courses = [] logger.info(f"Initialized eClass session for {self.base_url}") def is_session_valid(self) -> bool: """Check if the current session is still valid without full re-auth.""" if not self.logged_in: return False try: # Try accessing a protected resource that requires authentication response = self.session.get(self.portfolio_url, allow_redirects=False) # If we get redirected to login page, session is invalid if response.status_code == 302 and 'login' in response.headers.get('Location', ''): self.logged_in = False return False # Check for successful access to the portfolio page if response.status_code == 200: if html_parsing.verify_login_success(response.text): return True # Session is invalid self.logged_in = False return False except: self.logged_in = False return False def reset(self): """Reset the session state.""" self.session = requests.Session() self.logged_in = False self.username = None self.courses = [] # Initialize global session state session_state = SessionState() @server.list_tools() async def handle_list_tools() -> list[types.Tool]: """ List available eClass tools. """ return [ types.Tool( name="login", description="Log in to eClass using username/password from your .env file through UoA's SSO. Configure ECLASS_USERNAME and ECLASS_PASSWORD in your .env file.", inputSchema={ "type": "object", "properties": { "random_string": {"type": "string", "description": "Dummy parameter for no-parameter tools"}, }, "required": ["random_string"], }, ), types.Tool( name="get_courses", description="Get list of enrolled courses from eClass", inputSchema={ "type": "object", "properties": { "random_string": {"type": "string", "description": "Dummy parameter for no-parameter tools"}, }, "required": ["random_string"], }, ), types.Tool( name="logout", description="Log out from eClass", inputSchema={ "type": "object", "properties": { "random_string": {"type": "string", "description": "Dummy parameter for no-parameter tools"}, }, "required": ["random_string"], }, ), types.Tool( name="authstatus", description="Check authentication status with eClass", inputSchema={ "type": "object", "properties": { "random_string": {"type": "string", "description": "Dummy parameter for no-parameter tools"}, }, "required": ["random_string"], }, ), ] @server.call_tool() async def handle_call_tool( name: str, arguments: Dict[str, Any] | None ) -> List[types.TextContent | types.ImageContent | types.EmbeddedResource]: """ Handle eClass tool execution requests. """ if name == "login": return await handle_login({}) elif name == "get_courses": return await handle_get_courses() elif name == "logout": return await handle_logout() elif name == "authstatus": return await handle_authstatus() else: raise ValueError(f"Unknown tool: {name}") async def handle_login(arguments: Dict[str, Any]) -> List[types.TextContent]: """Handle login to eClass.""" # Check if already logged in if session_state.logged_in and session_state.is_session_valid(): return [ types.TextContent( type="text", text=f"Already logged in as {session_state.username}", ) ] # Reset session if needed if session_state.logged_in and not session_state.is_session_valid(): session_state.reset() # Get credentials from environment variables username = os.getenv('ECLASS_USERNAME') password = os.getenv('ECLASS_PASSWORD') if not username or not password: return [ types.TextContent( type="text", text="Error: Username and password must be provided in the .env file. Please set ECLASS_USERNAME and ECLASS_PASSWORD in your .env file.", ) ] logger.info(f"Attempting to log in as {username}") # Attempt login using the authentication module success, message = authentication.attempt_login(session_state, username, password) # Format and return the response return [authentication.format_login_response(success, message, username if success else None)] async def handle_get_courses() -> List[types.TextContent]: """Handle getting the list of enrolled courses.""" # Use the course_management module to get courses success, message, courses = course_management.get_courses(session_state) # Format and return the response return [course_management.format_courses_response(success, message, courses)] async def handle_logout() -> List[types.TextContent]: """Handle logout from eClass.""" # Use the authentication module to perform logout success, username_or_error = authentication.perform_logout(session_state) # Format and return the response return [authentication.format_logout_response(success, username_or_error)] async def handle_authstatus() -> List[types.TextContent]: """Handle checking authentication status.""" # Use the authentication module to format the status response return [authentication.format_authstatus_response(session_state)] async def main(): """Run the MCP server.""" # Run the server using stdin/stdout streams async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): await server.run( read_stream, write_stream, InitializationOptions( server_name="eclass-mcp", server_version="0.1.0", capabilities=server.get_capabilities( notification_options=NotificationOptions(), experimental_capabilities={}, ), ), ) if __name__ == "__main__": asyncio.run(main())

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sdi2200262/eclass-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server