Files
dav-imap-mcp/src/server.py
Yigit Colakoglu 7966a4302d
All checks were successful
Build And Test / publish (push) Successful in 48s
Add ICS calendar support
2026-01-01 15:06:44 -08:00

231 lines
6.9 KiB
Python

#!/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())