Skip to main content
Glama

OpenEdu MCP Server

main.pyโ€ข31.2 kB
""" OpenEdu MCP Server - Main Entry Point This module provides the main entry point for the OpenEdu MCP Server, setting up the FastMCP server with all educational tools and services. """ import asyncio import logging import sys from pathlib import Path from typing import Optional, List, Dict, Any # Add src to path for imports sys.path.insert(0, str(Path(__file__).parent)) import asyncio import json import logging import contextlib import sys from pathlib import Path from typing import Optional, List, Dict, Any # Add src to path for imports sys.path.insert(0, str(Path(__file__).parent)) from starlette.requests import Request from starlette.responses import StreamingResponse from mcp.server.fastmcp import FastMCP, Context from mcp.server.fastmcp import FastMCP, Context # Import with absolute paths import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) import config from config import load_config, Config import exceptions from exceptions import OpenEduMCPError import services.cache_service from services.cache_service import CacheService import services.rate_limiting_service from services.rate_limiting_service import RateLimitingService import services.usage_service from services.usage_service import UsageService import tools.openlibrary_tools from tools.openlibrary_tools import OpenLibraryTool import tools.wikipedia_tools from tools.wikipedia_tools import WikipediaTool import tools.dictionary_tools from tools.dictionary_tools import DictionaryTool import tools.arxiv_tools from tools.arxiv_tools import ArxivTool # Initialize logger logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # Create FastMCP server instance mcp = FastMCP("openedu-mcp-server") # Global services cache_service: Optional[CacheService] = None rate_limiting_service: Optional[RateLimitingService] = None usage_service: Optional[UsageService] = None config: Optional[Config] = None # Global tools openlibrary_tool: Optional[OpenLibraryTool] = None wikipedia_tool: Optional[WikipediaTool] = None dictionary_tool: Optional[DictionaryTool] = None arxiv_tool: Optional[ArxivTool] = None async def initialize_services() -> None: """Initialize all server services and dependencies.""" global cache_service, rate_limiting_service, usage_service, config, openlibrary_tool, wikipedia_tool, dictionary_tool, arxiv_tool try: # Load configuration config = load_config() logger.info(f"Loaded configuration for {config.server.name}") # Initialize cache service cache_service = CacheService(config.cache) await cache_service.initialize() logger.info("Cache service initialized") # Initialize rate limiting service rate_limiting_service = RateLimitingService(config.apis) logger.info("Rate limiting service initialized") # Initialize usage service usage_service = UsageService(config.cache) # Uses same DB as cache await usage_service.initialize() logger.info("Usage service initialized") # Initialize Open Library tool openlibrary_tool = OpenLibraryTool( config=config, cache_service=cache_service, rate_limiting_service=rate_limiting_service, usage_service=usage_service ) logger.info("Open Library tool initialized") # Initialize Wikipedia tool wikipedia_tool = WikipediaTool( config=config, cache_service=cache_service, rate_limiting_service=rate_limiting_service, usage_service=usage_service ) logger.info("Wikipedia tool initialized") # Initialize Dictionary tool dictionary_tool = DictionaryTool( config=config, cache_service=cache_service, rate_limiting_service=rate_limiting_service, usage_service=usage_service ) logger.info("Dictionary tool initialized") # Initialize arXiv tool arxiv_tool = ArxivTool( config=config, cache_service=cache_service, rate_limiting_service=rate_limiting_service, usage_service=usage_service ) logger.info("arXiv tool initialized") logger.info("OpenEdu MCP Server services initialized successfully") except Exception as e: logger.error(f"Failed to initialize server services: {e}") raise OpenEduMCPError(f"Server initialization failed: {e}") @mcp.tool() async def search_educational_books( ctx: Context, query: str, subject: Optional[str] = None, grade_level: Optional[str] = None, limit: int = 10 ) -> List[Dict[str, Any]]: """ Search for educational books using Open Library API. Args: query: Search query for books subject: Educational subject filter (optional) grade_level: Target grade level (K-2, 3-5, 6-8, 9-12, College) limit: Maximum number of results (1-50) Returns: List of educational books with metadata """ if not openlibrary_tool: raise OpenEduMCPError("Open Library tool not properly initialized") # Validate parameters if not query or not query.strip(): raise OpenEduMCPError("Query cannot be empty") if grade_level and grade_level not in ["K-2", "3-5", "6-8", "9-12", "College"]: raise OpenEduMCPError(f"Invalid grade level: {grade_level}") if limit < 1 or limit > 50: raise OpenEduMCPError("Limit must be between 1 and 50") try: return await openlibrary_tool.search_educational_books( query=query, subject=subject, grade_level=grade_level, limit=limit, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in search_educational_books: {e}") raise OpenEduMCPError(f"Book search failed: {str(e)}") @mcp.tool() async def get_book_details_by_isbn( ctx: Context, isbn: str, include_cover: bool = True ) -> Dict[str, Any]: """ Get detailed book information by ISBN. Args: isbn: ISBN-10 or ISBN-13 include_cover: Whether to include cover image URL Returns: Detailed book information with educational metadata """ if not openlibrary_tool: raise OpenEduMCPError("Open Library tool not properly initialized") try: return await openlibrary_tool.get_book_details_by_isbn( isbn=isbn, include_cover=include_cover, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in get_book_details_by_isbn: {e}") raise OpenEduMCPError(f"Book details retrieval failed: {str(e)}") @mcp.tool() async def search_books_by_subject( ctx: Context, subject: str, grade_level: Optional[str] = None, limit: int = 10 ) -> List[Dict[str, Any]]: """ Search books by educational subject. Args: subject: Educational subject grade_level: Target grade level (optional) limit: Maximum number of results (1-50) Returns: List of books in the subject area """ if not openlibrary_tool: raise OpenEduMCPError("Open Library tool not properly initialized") try: return await openlibrary_tool.search_books_by_subject( subject=subject, grade_level=grade_level, limit=limit, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in search_books_by_subject: {e}") raise OpenEduMCPError(f"Subject search failed: {str(e)}") @mcp.tool() async def get_book_recommendations( ctx: Context, grade_level: str, subject: Optional[str] = None, limit: int = 10 ) -> List[Dict[str, Any]]: """ Get book recommendations for a specific grade level and subject. Args: grade_level: Target grade level (K-2, 3-5, 6-8, 9-12, College) subject: Educational subject (optional) limit: Maximum number of results (1-50) Returns: List of recommended books """ if not openlibrary_tool: raise OpenEduMCPError("Open Library tool not properly initialized") try: return await openlibrary_tool.get_book_recommendations( grade_level=grade_level, subject=subject, limit=limit, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in get_book_recommendations: {e}") raise OpenEduMCPError(f"Book recommendations failed: {str(e)}") @mcp.tool() async def search_educational_articles( ctx: Context, query: str, subject: Optional[str] = None, grade_level: Optional[str] = None, language: str = 'en', limit: int = 10 ) -> List[Dict[str, Any]]: """ Search for educational articles using Wikipedia API. Args: query: Search query for articles subject: Educational subject filter (optional) grade_level: Target grade level (K-2, 3-5, 6-8, 9-12, College) language: Language code (default: 'en') limit: Maximum number of results (1-50) Returns: List of educational articles with summaries """ if not wikipedia_tool: raise OpenEduMCPError("Wikipedia tool not properly initialized") try: return await wikipedia_tool.search_educational_articles( query=query, subject=subject, grade_level=grade_level, language=language, limit=limit, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in search_educational_articles: {e}") raise OpenEduMCPError(f"Article search failed: {str(e)}") @mcp.tool() async def get_article_summary( ctx: Context, title: str, language: str = 'en', include_educational_analysis: bool = True ) -> Dict[str, Any]: """ Get Wikipedia article summary with educational analysis. Args: title: Article title language: Language code (default: 'en') include_educational_analysis: Whether to include educational metadata Returns: Article summary with educational metadata """ if not wikipedia_tool: raise OpenEduMCPError("Wikipedia tool not properly initialized") try: return await wikipedia_tool.get_article_summary( title=title, language=language, include_educational_analysis=include_educational_analysis, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in get_article_summary: {e}") raise OpenEduMCPError(f"Article summary retrieval failed: {str(e)}") @mcp.tool() async def get_article_content( ctx: Context, title: str, language: str = 'en', include_images: bool = False ) -> Dict[str, Any]: """ Get full Wikipedia article content with educational enrichment. Args: title: Article title language: Language code (default: 'en') include_images: Whether to include article images Returns: Full article content with educational metadata """ if not wikipedia_tool: raise OpenEduMCPError("Wikipedia tool not properly initialized") try: return await wikipedia_tool.get_article_content( title=title, language=language, include_images=include_images, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in get_article_content: {e}") raise OpenEduMCPError(f"Article content retrieval failed: {str(e)}") @mcp.tool() async def get_featured_article( ctx: Context, date: Optional[str] = None, language: str = 'en' ) -> Dict[str, Any]: """ Get Wikipedia featured article of the day with educational analysis. Args: date: Date in YYYY/MM/DD format (optional, defaults to today) language: Language code (default: 'en') Returns: Featured article with educational metadata """ if not wikipedia_tool: raise OpenEduMCPError("Wikipedia tool not properly initialized") try: return await wikipedia_tool.get_featured_article( date_param=date, language=language, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in get_featured_article: {e}") raise OpenEduMCPError(f"Featured article retrieval failed: {str(e)}") @mcp.tool() async def get_articles_by_subject( ctx: Context, subject: str, grade_level: Optional[str] = None, language: str = 'en', limit: int = 10 ) -> List[Dict[str, Any]]: """ Get Wikipedia articles by educational subject with grade level filtering. Args: subject: Educational subject grade_level: Target grade level (optional) language: Language code (default: 'en') limit: Maximum number of results Returns: List of articles in the subject area """ if not wikipedia_tool: raise OpenEduMCPError("Wikipedia tool not properly initialized") try: return await wikipedia_tool.get_articles_by_subject( subject=subject, grade_level=grade_level, language=language, limit=limit, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in get_articles_by_subject: {e}") raise OpenEduMCPError(f"Subject articles retrieval failed: {str(e)}") @mcp.tool() async def get_word_definition( ctx: Context, word: str, grade_level: Optional[str] = None, include_pronunciation: bool = True ) -> Dict[str, Any]: """ Get educational word definition from dictionary API. Args: word: Word to define grade_level: Target grade level for appropriate complexity include_pronunciation: Whether to include pronunciation information Returns: Word definition with educational metadata """ if not dictionary_tool: raise OpenEduMCPError("Dictionary tool not properly initialized") # Validate parameters if not word or not word.strip(): raise OpenEduMCPError("Word cannot be empty") if grade_level and grade_level not in ["K-2", "3-5", "6-8", "9-12", "College"]: raise OpenEduMCPError(f"Invalid grade level: {grade_level}") try: return await dictionary_tool.get_word_definition( word=word, grade_level=grade_level, include_pronunciation=include_pronunciation, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in get_word_definition: {e}") raise OpenEduMCPError(f"Word definition retrieval failed: {str(e)}") @mcp.tool() async def get_vocabulary_analysis( ctx: Context, word: str, context: Optional[str] = None ) -> Dict[str, Any]: """ Analyze word complexity and educational value. Args: word: Word to analyze context: Optional context for better analysis Returns: Vocabulary analysis with educational insights """ if not dictionary_tool: raise OpenEduMCPError("Dictionary tool not properly initialized") try: return await dictionary_tool.get_vocabulary_analysis( word=word, context=context, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in get_vocabulary_analysis: {e}") raise OpenEduMCPError(f"Vocabulary analysis failed: {str(e)}") @mcp.tool() async def get_word_examples( ctx: Context, word: str, grade_level: Optional[str] = None, subject: Optional[str] = None ) -> Dict[str, Any]: """ Get educational examples and usage contexts for a word. Args: word: Word to find examples for grade_level: Target grade level for appropriate examples subject: Subject area for context-specific examples Returns: Educational examples with context """ if not dictionary_tool: raise OpenEduMCPError("Dictionary tool not properly initialized") try: return await dictionary_tool.get_word_examples( word=word, grade_level=grade_level, subject=subject, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in get_word_examples: {e}") raise OpenEduMCPError(f"Word examples retrieval failed: {str(e)}") @mcp.tool() async def get_pronunciation_guide( ctx: Context, word: str, include_audio: bool = True ) -> Dict[str, Any]: """ Get phonetic information for language learning. Args: word: Word to get pronunciation for include_audio: Whether to include audio URL Returns: Pronunciation guide with phonetic information """ if not dictionary_tool: raise OpenEduMCPError("Dictionary tool not properly initialized") try: return await dictionary_tool.get_pronunciation_guide( word=word, include_audio=include_audio, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in get_pronunciation_guide: {e}") raise OpenEduMCPError(f"Pronunciation guide retrieval failed: {str(e)}") @mcp.tool() async def get_related_vocabulary( ctx: Context, word: str, relationship_type: str = "all", grade_level: Optional[str] = None, limit: int = 10 ) -> Dict[str, Any]: """ Get synonyms, antonyms, and related educational terms. Args: word: Base word relationship_type: Type of relationship (synonyms, antonyms, related, all) grade_level: Target grade level for appropriate vocabulary limit: Maximum number of related words Returns: Related vocabulary with educational context """ if not dictionary_tool: raise OpenEduMCPError("Dictionary tool not properly initialized") try: return await dictionary_tool.get_related_vocabulary( word=word, relationship_type=relationship_type, grade_level=grade_level, limit=limit, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in get_related_vocabulary: {e}") raise OpenEduMCPError(f"Related vocabulary retrieval failed: {str(e)}") @mcp.tool() async def search_academic_papers( ctx: Context, query: str, subject: Optional[str] = None, academic_level: Optional[str] = None, max_results: int = 10, include_educational_analysis: bool = True ) -> List[Dict[str, Any]]: """ Search for academic papers with educational filtering using arXiv API. Args: query: Search query for papers subject: Educational subject filter (optional) academic_level: Target academic level (High School, Undergraduate, Graduate, Research) max_results: Maximum number of results (1-50) include_educational_analysis: Whether to include educational metadata Returns: List of academic papers with educational metadata """ if not arxiv_tool: raise OpenEduMCPError("arXiv tool not properly initialized") try: return await arxiv_tool.search_academic_papers( query=query, subject=subject, academic_level=academic_level, max_results=max_results, include_educational_analysis=include_educational_analysis, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in search_academic_papers: {e}") raise OpenEduMCPError(f"Academic paper search failed: {str(e)}") @mcp.tool() async def get_paper_summary( ctx: Context, paper_id: str, include_educational_analysis: bool = True ) -> Dict[str, Any]: """ Get paper summary with educational analysis using arXiv API. Args: paper_id: arXiv paper ID (e.g., '2301.00001') include_educational_analysis: Whether to include educational metadata Returns: Paper summary with educational metadata """ if not arxiv_tool: raise OpenEduMCPError("arXiv tool not properly initialized") try: return await arxiv_tool.get_paper_summary( paper_id=paper_id, include_educational_analysis=include_educational_analysis, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in get_paper_summary: {e}") raise OpenEduMCPError(f"Paper summary retrieval failed: {str(e)}") @mcp.tool() async def get_recent_research( ctx: Context, subject: str, days: int = 7, academic_level: Optional[str] = None, max_results: int = 10 ) -> List[Dict[str, Any]]: """ Get recent research papers by educational subject using arXiv API. Args: subject: Educational subject days: Number of days back to search (1-30) academic_level: Target academic level (optional) max_results: Maximum number of results Returns: List of recent papers in the subject area """ if not arxiv_tool: raise OpenEduMCPError("arXiv tool not properly initialized") try: return await arxiv_tool.get_recent_research( subject=subject, days=days, academic_level=academic_level, max_results=max_results, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in get_recent_research: {e}") raise OpenEduMCPError(f"Recent research retrieval failed: {str(e)}") @mcp.tool() async def get_research_by_level( ctx: Context, academic_level: str, subject: Optional[str] = None, max_results: int = 10 ) -> List[Dict[str, Any]]: """ Get research papers appropriate for specific academic levels using arXiv API. Args: academic_level: Target academic level (High School, Undergraduate, Graduate, Research) subject: Subject area filter (optional) max_results: Maximum number of results Returns: List of papers appropriate for the academic level """ if not arxiv_tool: raise OpenEduMCPError("arXiv tool not properly initialized") try: return await arxiv_tool.get_research_by_level( academic_level=academic_level, subject=subject, max_results=max_results, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in get_research_by_level: {e}") raise OpenEduMCPError(f"Research by level retrieval failed: {str(e)}") @mcp.tool() async def analyze_research_trends( ctx: Context, subject: str, days: int = 30 ) -> Dict[str, Any]: """ Analyze research trends for educational insights using arXiv API. Args: subject: Educational subject to analyze days: Number of days to analyze (7-90) Returns: Research trend analysis with educational insights """ if not arxiv_tool: raise OpenEduMCPError("arXiv tool not properly initialized") try: return await arxiv_tool.analyze_research_trends( subject=subject, days=days, user_session=getattr(ctx, 'session_id', None) ) except Exception as e: logger.error(f"Error in analyze_research_trends: {e}") raise OpenEduMCPError(f"Research trend analysis failed: {str(e)}") @mcp.tool() async def handle_stdio_input(ctx: Context, input_string: str) -> str: """ Handles a line of input from stdin and returns a processed string. Args: ctx: The context object. input_string: The string read from stdin. Returns: The processed string. """ if not input_string: raise OpenEduMCPError("Input string cannot be empty") try: # Simple processing: prepend "Processed: " and convert to uppercase processed_string = f"Processed: {input_string.upper()}" logger.info(f"Processed stdin input: {input_string} -> {processed_string}") return processed_string except Exception as e: logger.error(f"Error processing stdio input: {e}") raise OpenEduMCPError(f"Failed to process stdio input: {str(e)}") async def sse_event_generator(request: Request): """ Asynchronous generator that streams Server-Sent Events (SSE) to the client. Yields an initial "connected" event, followed by periodic "ping" events every 5 seconds. If an error occurs, attempts to yield an "error" event before terminating. """ try: yield f"event: connected\ndata: {json.dumps({'message': 'Successfully connected to SSE stream'})}\n\n" loop_count = 0 while True: # Check if client is still connected if await request.is_disconnected(): logger.info("SSE client disconnected.") break loop_count += 1 yield f"event: ping\ndata: {json.dumps({'heartbeat': loop_count, 'message': 'ping'})}\n\n" await asyncio.sleep(5) # Send a ping every 5 seconds except asyncio.CancelledError: logger.info("SSE event generator cancelled.") # Handle cleanup if necessary except Exception as e: logger.error(f"Error in SSE event generator: {e}") # Yield an error event if possible, or just log and exit # Try to yield error event - if connection is closed, this will fail silently with contextlib.suppress(ConnectionError, RuntimeError): yield f"event: error\ndata: {json.dumps({'error': str(e)})}\n\n" finally: logger.info("SSE event generator finished.") @mcp.tool(route="/events", methods=["GET"]) # Assuming a route decorator might exist or be added to FastMCP async def stream_events(request: Request) -> StreamingResponse: """ SSE endpoint to stream events. Uses an async generator to produce events. """ # Note: The 'request: Request' parameter might not be directly supported by @mcp.tool # if it only expects Context and tool-specific arguments. # This is an attempt based on common ASGI framework patterns. # If FastMCP uses a different mechanism for raw requests or streaming, this will need adjustment. logger.info(f"SSE connection request received: {request}") # Check if FastMCP passes the raw request object. # If not, this 'request.is_disconnected()' will fail. # This part is speculative. if not isinstance(request, Request): logger.warning("Request object not available as expected for SSE. Client disconnection check might not work.") # Fallback: create a dummy request object if needed by sse_event_generator, # but is_disconnected will not work. This is a significant limitation. # For now, let's assume 'request' is passed correctly or sse_event_generator handles it. generator = sse_event_generator(request) return StreamingResponse(generator, media_type="text/event-stream") @mcp.tool() async def get_server_status(ctx: Context) -> Dict[str, Any]: """ Get OpenEdu MCP Server status and statistics. Returns: Server status information including cache and usage statistics """ if not cache_service or not rate_limiting_service or not usage_service: return { "status": "error", "message": "Server services not properly initialized" } try: # Get cache statistics cache_stats = await cache_service.get_stats() # Get rate limiting status rate_limit_status = await rate_limiting_service.get_all_rate_limit_status() # Get usage statistics usage_stats = await usage_service.get_usage_stats() return { "status": "healthy", "server": { "name": config.server.name if config else "openedu-mcp-server", "version": config.server.version if config else "1.0.0" }, "cache": cache_stats, "rate_limits": rate_limit_status, "usage": usage_stats, "message": "OpenEdu MCP Server is running with core infrastructure ready" } except Exception as e: logger.error(f"Error getting server status: {e}") return { "status": "error", "message": f"Failed to get server status: {str(e)}" } async def cleanup_services() -> None: """Clean up services on shutdown.""" global cache_service, usage_service, openlibrary_tool, wikipedia_tool, dictionary_tool, arxiv_tool try: if arxiv_tool: await arxiv_tool.client.close() logger.info("arXiv tool closed") if dictionary_tool: await dictionary_tool.client.close() logger.info("Dictionary tool closed") if wikipedia_tool: await wikipedia_tool.client.close() logger.info("Wikipedia tool closed") if openlibrary_tool: await openlibrary_tool.client.close() logger.info("Open Library tool closed") if usage_service: await usage_service.close() logger.info("Usage service closed") if cache_service: await cache_service.close() logger.info("Cache service closed") except Exception as e: logger.error(f"Error during cleanup: {e}") def main(): """Main entry point for the OpenEdu MCP Server.""" try: # Initialize services asyncio.run(initialize_services()) # Set up cleanup on exit import atexit atexit.register(lambda: asyncio.run(cleanup_services())) logger.info("Starting OpenEdu MCP Server...") # Run the MCP server mcp.run() except KeyboardInterrupt: logger.info("Server shutdown requested") except Exception as e: logger.error(f"Server startup failed: {e}") sys.exit(1) finally: logger.info("OpenEdu MCP Server stopped") if __name__ == "__main__": 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/Cicatriiz/openedu-mcp'

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