Things MCP Server
by excelsier
from typing import Any, List, Optional, Dict
import logging
import asyncio
import sys
import re
import traceback
from mcp.server.models import InitializationOptions
import mcp.types as types
from mcp.server import NotificationOptions, Server
import mcp.server.stdio
# Import our direct MCP tool definitions for Windsurf compatibility
from mcp_tools import get_mcp_tools_list
from handlers import handle_tool_call
from utils import validate_tool_registration, app_state
import url_scheme
# Configure logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
server = Server("things")
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""List available tools for Things integration with Windsurf compatibility."""
return get_mcp_tools_list()
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""Handle tool execution requests with Windsurf compatibility."""
try:
# Handle both prefixed and non-prefixed tool names consistently
# If name has mcp2_ prefix, remove it for handler compatibility
# If name doesn't have prefix, use it directly
original_name = name
base_name = name
# Check if the name has the 'mcp2_' prefix and remove it if present
if name.startswith("mcp2_"):
base_name = name[5:] # Remove the 'mcp2_' prefix
logger.info(f"Received prefixed tool call: {name} -> mapping to {base_name}")
else:
# No prefix, check if the name is one of our supported tools
# This allows both prefixed and direct calls to work
logger.info(f"Received non-prefixed tool call: {name}")
# Log the incoming arguments for debugging
argument_summary = str(arguments)[:100] + "..." if arguments and len(str(arguments)) > 100 else str(arguments)
logger.info(f"MCP tool call received: {original_name} (handling as: {base_name}) with arguments: {argument_summary}")
# Call the appropriate handler with robust error handling
try:
return await handle_tool_call(base_name, arguments)
except Exception as e:
error_message = f"Error executing tool {name}: {str(e)}"
logger.error(error_message)
logger.error(traceback.format_exc())
return [types.TextContent(type="text", text=f"⚠️ {error_message}")]
except Exception as outer_e:
# Catch-all to prevent server crashes
logger.error(f"Critical error in tool call handler: {str(outer_e)}")
logger.error(traceback.format_exc())
return [types.TextContent(type="text", text=f"⚠️ Critical error: {str(outer_e)}")]
async def main():
# Get our MCP tools with proper naming for Windsurf
mcp_tools = get_mcp_tools_list()
# Log successful registration
logger.info(f"Registered {len(mcp_tools)} MCP-compatible tools for Things")
# Check if Things app is available
if not app_state.update_app_state():
logger.warning("Things app is not running at startup. MCP will attempt to launch it when needed.")
try:
# Try to launch Things
if url_scheme.launch_things():
logger.info("Successfully launched Things app")
else:
logger.error("Unable to launch Things app. Some operations may fail.")
except Exception as e:
logger.error(f"Error launching Things app: {str(e)}")
else:
logger.info("Things app is running and ready for operations")
# Run the server using stdin/stdout streams
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="things",
server_version="0.1.1", # Updated version with reliability enhancements
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)
if __name__ == "__main__":
asyncio.run(main())