MAWB's picture
Update services/mcp.py
43149e3 verified
"""
MCP service wrapper for MCP server lifecycle management.
Per @specs/001-chatbot-mcp/plan.md Section VIII - AI Chatbot Architecture
MCP First: All task operations go through MCP SDK for OpenAI Agents integration.
This service manages the MCP server lifecycle including startup, shutdown,
and tool registration.
"""
from typing import Optional, Any, Dict
from contextlib import asynccontextmanager
import logging
from mcp.server import get_mcp_server
logger = logging.getLogger(__name__)
class MCPService:
"""
Service wrapper for MCP server lifecycle management.
This class provides a singleton interface to the MCP server,
managing its lifecycle and providing access to tool execution.
"""
_instance: Optional["MCPService"] = None
_server: Optional[Any] = None
def __new__(cls) -> "MCPService":
"""Singleton pattern to ensure only one MCP server instance."""
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
async def initialize(self) -> None:
"""
Initialize the MCP server and register all tools.
This should be called during FastAPI application startup.
Per @specs/001-chatbot-mcp/plan.md, MCP server starts with the FastAPI app.
"""
if self._server is None:
try:
self._server = get_mcp_server()
logger.info("MCP server initialized successfully with todo-mcp-server")
except Exception as e:
logger.error(f"Failed to initialize MCP server: {e}")
raise
async def shutdown(self) -> None:
"""
Shutdown the MCP server gracefully.
This should be called during FastAPI application shutdown.
"""
if self._server is not None:
try:
# FastMCP handles cleanup automatically
logger.info("MCP server shut down successfully")
except Exception as e:
logger.error(f"Error during MCP server shutdown: {e}")
def get_server(self) -> Any:
"""
Get the MCP server instance.
Returns:
The FastMCP server instance
Raises:
RuntimeError: If server has not been initialized
"""
if self._server is None:
raise RuntimeError("MCP server not initialized. Call initialize() first.")
return self._server
async def list_tools(self) -> list[Dict[str, Any]]:
"""
List all available MCP tools.
Returns:
List of tool definitions with name, description, and schemas
Per @specs/001-chatbot-mcp/contracts/mcp-tools.json
"""
server = self.get_server()
# FastMCP stores tools in _tool_manager
if hasattr(server, '_tool_manager'):
tools = server._tool_manager.list_tools()
return [
{
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema,
"output_schema": tool.outputSchema if hasattr(tool, 'outputSchema') else None
}
for tool in tools
]
return []
def get_tool_info(self) -> Dict[str, Any]:
"""
Get information about available MCP tools.
Returns:
Dict with tool count, server info, and tool names
Useful for debugging and monitoring.
"""
server = self.get_server()
return {
"server_name": getattr(server, 'name', 'todo-mcp-server'),
"initialized": self._server is not None,
"tool_count": len(server.list_tools()) if hasattr(server, 'list_tools') else 0
}
def _get_tool_names(self) -> list[str]:
"""Get list of registered tool names."""
server = self.get_server()
if hasattr(server, 'list_tools'):
tools = server.list_tools()
return [tool.name for tool in tools]
return []
# Singleton instance
mcp_service = MCPService()
@asynccontextmanager
async def mcp_lifespan():
"""
Async context manager for MCP server lifecycle.
Usage in FastAPI:
@asynccontextmanager
async def lifespan(app: FastAPI):
async with mcp_lifespan():
yield
Per @specs/001-chatbot-mcp/plan.md, MCP server lifecycle tied to FastAPI app.
"""
try:
await mcp_service.initialize()
yield
finally:
await mcp_service.shutdown()