Skip to main content
Glama
server.py8.12 kB
""" eClass MCP Server - MCP Integration for Open eClass Platform Provides an MCP server for interacting with eClass through UoA's SSO authentication. """ import asyncio import logging import os from typing import Any, Dict, List from urllib.parse import urlparse import requests from dotenv import load_dotenv from mcp.server.lowlevel import NotificationOptions, Server from mcp.server.models import InitializationOptions import mcp.server.stdio import mcp.types as types from . import authentication from . import course_management from . import html_parsing logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger('eclass_mcp_server') server = Server("eclass-mcp") class SessionState: """Maintains authentication state between MCP tool calls.""" def __init__(self) -> None: # Load .env from project root script_dir = os.path.dirname(os.path.abspath(__file__)) project_root = os.path.dirname(os.path.dirname(script_dir)) env_path = os.path.join(project_root, '.env') load_dotenv(env_path, override=False) self.session = requests.Session() self.logged_in = False # Base URL configuration self.base_url = os.getenv('ECLASS_URL', 'https://eclass.uoa.gr').rstrip('/') self.eclass_domain = urlparse(self.base_url).netloc # SSO configuration self.sso_domain = os.getenv('ECLASS_SSO_DOMAIN', 'sso.uoa.gr') sso_protocol = os.getenv('ECLASS_SSO_PROTOCOL', 'https') self.sso_base_url = f"{sso_protocol}://{self.sso_domain}" # eClass endpoint 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" self.username: str | None = None self.courses: List[Dict[str, str]] = [] logger.info(f"Initialized eClass session for {self.base_url} (SSO: {self.sso_domain})") def is_session_valid(self) -> bool: """Check if the current session is still valid.""" if not self.logged_in: return False try: response = self.session.get(self.portfolio_url, allow_redirects=False) if response.status_code == 302 and 'login' in response.headers.get('Location', ''): self.logged_in = False return False if response.status_code == 200 and html_parsing.verify_login_success(response.text): return True self.logged_in = False return False except Exception: self.logged_in = False return False def reset(self) -> None: """Reset the session state.""" self.session = requests.Session() self.logged_in = False self.username = None self.courses = [] 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() -> List[types.TextContent]: """Handle login to eClass.""" 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}", ) ] if session_state.logged_in and not session_state.is_session_valid(): session_state.reset() 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}") success, message = authentication.attempt_login(session_state, username, password) 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.""" success, message, courses = course_management.get_courses(session_state) return [course_management.format_courses_response(success, message, courses)] async def handle_logout() -> List[types.TextContent]: """Handle logout from eClass.""" success, username_or_error = authentication.perform_logout(session_state) return [authentication.format_logout_response(success, username_or_error)] async def handle_authstatus() -> List[types.TextContent]: """Handle checking authentication status.""" return [authentication.format_authstatus_response(session_state)] async def main() -> None: """Run the MCP server.""" 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())

Latest Blog Posts

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