#!/usr/bin/env python3 """ PIM MCP Server - Personal Information Management via Model Context Protocol A self-hosted MCP server that provides tools for managing: - Email (IMAP/SMTP) - Calendar (CalDAV) - Contacts (CardDAV) """ import logging import os import sys # Add src directory to path for imports sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from fastmcp import FastMCP from config import settings from database import init_db, close_db # Configure logging logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) # Initialize MCP server mcp = FastMCP( name=settings.server_name, instructions="Personal Information Management MCP Server for Email, Calendar, and Contacts", ) # Initialize services based on configuration email_service = None calendar_service = None contacts_service = None email_monitor = None def setup_services(): """Initialize services based on configuration.""" global email_service, calendar_service, contacts_service if settings.is_email_configured(): from services.email_service import EmailService email_service = EmailService(settings) print(f" Email service: enabled (IMAP: {settings.imap_host})") else: print(" Email service: disabled (not configured)") if settings.is_calendar_configured(): from services.calendar_service import CalendarService calendar_service = CalendarService(settings) if settings.is_caldav_configured(): print(f" Calendar service: enabled (CalDAV: {settings.caldav_url})") else: print(" Calendar service: enabled (ICS calendars only)") else: print(" Calendar service: disabled (not configured)") if settings.is_contacts_configured(): from services.contacts_service import ContactsService contacts_service = ContactsService(settings) print(f" Contacts service: enabled (CardDAV: {settings.carddav_url})") else: print(" Contacts service: disabled (not configured)") async def setup_email_monitor(): """Initialize email notification monitoring.""" global email_monitor if settings.is_notification_configured() and email_service: from services.webhook_service import WebhookService from services.email_monitor import EmailMonitor webhook_service = WebhookService(settings) email_monitor = EmailMonitor(email_service, webhook_service, settings) await email_monitor.start() print(f" Email notifications: enabled (monitoring: {settings.notification_mailboxes})") else: print(" Email notifications: disabled (not configured)") def register_tools(): """Register MCP tools based on enabled services.""" if email_service: from tools.email_tools import register_email_tools register_email_tools(mcp, email_service) print(" Registered email tools") if calendar_service: from tools.calendar_tools import register_calendar_tools register_calendar_tools(mcp, calendar_service) print(" Registered calendar tools") if contacts_service: from tools.contacts_tools import register_contacts_tools register_contacts_tools(mcp, contacts_service) print(" Registered contacts tools") # Server info tool (always available) @mcp.tool(description="Get information about this PIM MCP server including enabled services and version.") def get_server_info() -> dict: """Get server information and status.""" return { "server_name": settings.server_name, "version": "1.0.0", "environment": settings.environment, "services": { "email": { "enabled": email_service is not None, "imap_host": settings.imap_host if email_service else None, "smtp_configured": settings.is_smtp_configured() if email_service else False, }, "calendar": { "enabled": calendar_service is not None, "caldav_url": settings.caldav_url if calendar_service else None, "ics_calendars": [c[1] for c in settings.get_ics_calendars()] if calendar_service else [], }, "contacts": { "enabled": contacts_service is not None, "carddav_url": settings.carddav_url if contacts_service else None, }, }, } async def initialize(): """Initialize the server.""" print(f"\n{'='*60}") print(f" {settings.server_name}") print(f"{'='*60}") print(f"\nInitializing database...") await init_db(settings.sqlite_path) print(f" Database: {settings.sqlite_path}") print(" Using SQLModel with Alembic migrations") print(f"\nConfiguring services...") setup_services() print(f"\nRegistering tools...") register_tools() print(f"\nStarting background services...") await setup_email_monitor() print(f"\n{'='*60}") async def shutdown(): """Shutdown the server gracefully.""" global email_monitor print("\nShutting down...") if email_monitor: await email_monitor.stop() email_monitor = None await close_db() print("Shutdown complete") if __name__ == "__main__": import asyncio import signal import uvicorn from starlette.responses import JSONResponse from starlette.routing import Route from middleware import APIKeyAuthMiddleware async def health_check(request): """Health check endpoint for Docker/Traefik.""" return JSONResponse({"status": "healthy"}) async def main(): await initialize() port = settings.server_port host = settings.server_host # Get the underlying ASGI app from FastMCP app = mcp.http_app(path="/mcp") # Add health check route app.routes.append(Route("/health", health_check)) # Add authentication middleware if API key is configured if settings.mcp_api_key: app.add_middleware( APIKeyAuthMiddleware, api_key=settings.mcp_api_key.get_secret_value(), ) print(f"\n Authentication: enabled (API key required)") else: print(f"\n Authentication: disabled (no MCP_API_KEY set)") print(f"\nStarting server on {host}:{port}") print(f"MCP endpoint: http://{host}:{port}/mcp") print(f"{'='*60}\n") # Setup signal handlers for graceful shutdown loop = asyncio.get_running_loop() def signal_handler(): asyncio.create_task(shutdown()) for sig in (signal.SIGTERM, signal.SIGINT): loop.add_signal_handler(sig, signal_handler) # Run with uvicorn config = uvicorn.Config( app, host=host, port=port, log_level="info", ) server = uvicorn.Server(config) try: await server.serve() finally: await shutdown() asyncio.run(main())