FastMCP Todo Server
by DanEdens
- src
- fastmcp_todo_server
import logging
import os
import signal
import sys
from typing import Callable
from fastmcp.server import FastMCP
import uvicorn
class Omnispindle(FastMCP):
def __init__(self, name: str = "todo-server", server_type: str = "sse"):
super().__init__(name, server_type)
# We don't need to create another server instance since we inherit from FastMCP
async def run_server(self, publish_mqtt_status: Callable):
print("Starting FastMCP server")
try:
hostname = os.getenv("DeNa", os.uname().nodename)
topic = f"status/{hostname}-mcp/alive"
publish_mqtt_status(topic, "1")
def signal_handler(sig, frame):
print(f"Received signal {sig}, shutting down gracefully...")
publish_mqtt_status(topic, "0", retain=True)
sys.exit(0)
# Register signal handlers
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# Custom exception handler to silence specific SSE-related errors
original_excepthook = sys.excepthook
def custom_excepthook(exctype, value, traceback):
# Handle NoneType errors from Starlette more broadly
if exctype is TypeError and "'NoneType' object is not callable" in str(value):
# Log minimally instead of full stack trace
print(f"Suppressed NoneType error: {str(value)}")
return
# For all other errors, use the original exception handler
original_excepthook(exctype, value, traceback)
# Replace the default exception handler
sys.excepthook = custom_excepthook
# Configure uvicorn to suppress specific access logs for /messages endpoint
log_config = uvicorn.config.LOGGING_CONFIG
if "formatters" in log_config:
log_config["formatters"]["access"]["fmt"] = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
# Configure logging instead
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Run the server
await self.run_sse_async() # Use self instead of server
except Exception as e:
print(f"Error in server: {str(e)}")
# Publish offline status with retain flag in case of error
try:
hostname = os.getenv("HOSTNAME", os.uname().nodename)
topic = f"status/{hostname}/alive"
publish_mqtt_status(topic, "0", retain=True)
print(f"Published offline status to {topic} (retained)")
except Exception as ex:
print(f"Failed to publish offline status: {str(ex)}")
raise
# Add method to register tools
def register_tool(self, tool_func):
"""Register a single tool with the server"""
if not hasattr(self, '_registered_tools'):
self._registered_tools = set()
if tool_func.__name__ not in self._registered_tools:
# Use the original FastMCP tool registration method
self.tool()(tool_func)
self._registered_tools.add(tool_func.__name__)
print(f"Registered tool: {tool_func.__name__}")
return tool_func
def register_tools(self, tools_list):
"""Register multiple tools with the server"""
for tool in tools_list:
self.register_tool(tool)
# Create a singleton instance
server = Omnispindle()