All checks were successful
Build And Test / publish (push) Successful in 48s
231 lines
6.9 KiB
Python
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())
|