Skip to main content
Glama
TROUBLESHOOTING_FASTMCP_2.12.mdβ€’18.8 kB
# πŸ”§ FastMCP 2.12 Troubleshooting Guide **Based on Real-World Debugging Experience** **Project**: nest-protect MCP Server **Timeline**: September 17-20, 2025 **Framework**: FastMCP 2.12.3 **Status**: βœ… **PRODUCTION READY** This guide documents the **complete debugging journey** from a broken MCP server to a production-ready system. Use this to troubleshoot similar issues in other MCP projects like avatarmcp, local llms, and tapo. --- ## 🎯 The Journey: From Broken to Production ### **Initial Problem** - βœ… **Symptom**: `ImportError: cannot import name 'Tool' from 'fastmcp'` - βœ… **Impact**: Server wouldn't start at all - βœ… **Root Cause**: FastMCP 2.12 changed the import structure ### **Final Result** - βœ… **Status**: Production-ready server with 20 working tools - βœ… **Performance**: Sub-second response times - βœ… **Integration**: Perfect Claude Desktop compatibility - βœ… **Quality**: Real API integration, no mock data - βœ… **Logging**: Enhanced debugging and monitoring - βœ… **Compatibility**: Pydantic V2 patterns throughout --- ## πŸ†• **Latest Fixes (September 20, 2025)** ### **βœ… Pydantic V2 Migration Complete** - **Issue**: `PydanticDeprecatedSince20` warnings causing server instability - **Solution**: Updated all models to use `field_validator` and `ConfigDict` - **Impact**: Eliminated deprecation warnings, improved stability ### **βœ… Enhanced Logging System** - **Issue**: Difficult to debug server lifecycle and crashes - **Solution**: Comprehensive logging with detailed startup/shutdown tracking - **Impact**: Easy identification of issues and server state ### **βœ… Claude Desktop Configuration Fix** - **Issue**: Server disconnecting after a few seconds - **Root Cause**: `--kill` argument hardcoded in `claude_desktop_config.json` - **Solution**: Removed `--kill` from production configuration - **Impact**: Server now stays connected and operational ### **⚠️ CRITICAL: Never Use Print Statements in FastMCP** #### **Stdout vs Stderr Rules** - **Issue**: Server crashes with stdio transport errors - **Root Cause**: `print()` to stdout breaks FastMCP's stdio protocol - **Solution**: Use proper logging calls OR stderr for extra info - **Impact**: Stdio communication remains intact **Code Examples:** ```python import sys # ❌ WRONG - Breaks stdio protocol (stdout) print("Server started") print(f"Error: {error}") # βœ… CORRECT - FastMCP 2.12 compatible (logging) logger.info("Server started") logger.error(f"Error: {error}") # βœ… ACCEPTABLE - Extra debug info (stderr) print("Debug: Processing request...", file=sys.stderr) sys.stderr.write(f"Debug: User ID {user_id}\n") sys.stderr.flush() # Important for immediate output ``` #### **Stderr Usage Guidelines** - **βœ… Stderr is safe** for Claude Desktop MCP logs - **βœ… Appears in MCP server logs** without breaking protocol - **βœ… Use for debugging** and additional information - **βœ… Always flush** stderr buffer for immediate output - **❌ Don't overuse** - can clutter logs - **❌ Avoid for normal operation** - use logging instead **Why This Matters**: FastMCP uses stdin/stdout for Claude Desktop communication. Stdout must remain clean for MCP protocol messages, but stderr is available for logging and debugging. --- ## πŸ†• **Pydantic V2 Migration Issues** ### **❌ Problem**: `PydanticDeprecatedSince20` Warnings **Symptoms**: ``` PydanticDeprecatedSince20: Pydantic V1 style `@validator` validators are deprecated PydanticDeprecatedSince20: Support for class-based `config` is deprecated PydanticDeprecatedSince20: `json_encoders` is deprecated ``` **Root Cause**: Using Pydantic V1 patterns in V2 environment **βœ… SOLUTION - Complete Migration**: #### **1. Update Validators** ```python # ❌ Old (Pydantic V1) from pydantic import BaseModel, validator class MyModel(BaseModel): @validator('field_name') def validate_field(cls, v): return v # βœ… New (Pydantic V2) from pydantic import BaseModel, field_validator class MyModel(BaseModel): @field_validator('field_name') @classmethod def validate_field(cls, v): return v ``` #### **2. Update Config Classes** ```python # ❌ Old (Pydantic V1) class MyModel(BaseModel): class Config: env_prefix = "MY_" json_encoders = {datetime: lambda v: v.isoformat()} # βœ… New (Pydantic V2) from pydantic import ConfigDict class MyModel(BaseModel): model_config = ConfigDict( env_prefix="MY_", json_encoders={datetime: lambda v: v.isoformat()} ) ``` #### **3. Files Updated** - βœ… `src/nest_protect_mcp/models.py` - Main model definitions - βœ… `src/nest_protect_mcp/mcp_compliant_server.py` - Server models - βœ… `src/nest_protect_mcp/state_manager.py` - State management models **Impact**: Eliminated all deprecation warnings, improved server stability --- ## πŸ› Common FastMCP 2.12 Issues & Solutions ### **1. Import Errors - The Starting Point** #### **❌ Problem**: `ImportError: cannot import name 'Tool' from 'fastmcp'` **What we tried first (WRONG)**: ```python from fastmcp import FastMCP, Tool # ❌ This doesn't work in 2.12 ``` **βœ… SOLUTION**: ```python from fastmcp import FastMCP from fastmcp.tools import Tool # βœ… Tool moved to separate module ``` **Files affected**: `server.py`, `mcp_compliant_server.py`, `mcp_server.py` #### **❌ Problem**: `TypeError: FastMCP.__init__() got an unexpected keyword argument 'description'` **What we tried first (WRONG)**: ```python app = FastMCP("nest-protect", description="...") # ❌ Parameter renamed ``` **βœ… SOLUTION**: ```python app = FastMCP("nest-protect", instructions="...") # βœ… Use 'instructions' instead ``` --- ### **2. Tool Registration - The Core Challenge** #### **❌ Problem**: Tools not showing up in Claude Desktop **What we tried first (WRONG)**: ```python # Old pattern - separate tool registry self._tool_registry = ToolRegistry() # ❌ Not needed in 2.12 @tool # ❌ Decorator from custom module def my_tool(): pass ``` **βœ… SOLUTION - The FastMCP 2.12 Way**: ```python app = FastMCP("server-name") @app.tool() # βœ… Use app.tool() decorator async def my_tool() -> Dict[str, Any]: """Tool description here.""" return {"result": "data"} # That's it! No manual registration needed ``` **Key insight**: FastMCP 2.12 handles tool registration automatically when you use `@app.tool()`. --- ### **3. Server Startup - Getting It Right** #### **❌ Problem**: Various server startup errors **What we tried first (WRONG)**: ```python # Complex server setup server = NestProtectMCP() server_task = asyncio.create_task(server.start_stdio()) # ❌ Old API ``` **βœ… SOLUTION**: ```python app = FastMCP("server-name") # Register tools with @app.tool() @app.tool() async def some_tool(): return {"status": "ok"} # Simple startup if __name__ == "__main__": app.run() # βœ… That's it! ``` **Key insight**: Don't overcomplicate it. FastMCP 2.12 handles the complexity for you. --- ### **4. Pydantic Model Integration** #### **❌ Problem**: Parameter validation not working **What we tried first (WRONG)**: ```python @app.tool(parameters=SomeModel) # ❌ Wrong parameter name async def tool_func(data: dict): pass ``` **βœ… SOLUTION**: ```python class ToolParams(BaseModel): device_id: str = Field(..., description="Device identifier") timeout: int = Field(30, description="Timeout in seconds") @app.tool() async def tool_func(device_id: str, timeout: int = 30) -> Dict[str, Any]: """Tool description.""" # FastMCP automatically validates parameters based on function signature return {"device_id": device_id, "timeout": timeout} ``` **Key insight**: FastMCP 2.12 infers validation from function type hints. Keep it simple! --- ## πŸ”₯ The Claude Desktop "Start & Kill" Mystery ### **🎭 The Behavior** **What You See in Logs**: ``` 2025-09-19T19:52:08.612Z [nest protect] [info] Server started and connected successfully 2025-09-19T19:52:08.760Z [nest protect] [info] Message from client: {"method":"initialize"...} [... 5-10 seconds of normal operation ...] 2025-09-19 21:52:13,700 - nest_protect_mcp - INFO - Kill argument received - exiting gracefully 2025-09-19T19:52:14.266Z [nest protect] [info] Server transport closed 2025-09-19T19:52:14.266Z [nest protect] [error] Server disconnected ``` **What Users Think**: "Claude is randomly killing my server!" ### **πŸ•΅οΈ The Real Cause** **Claude Desktop doesn't randomly kill servers.** Here's what's actually happening: #### **Scenario 1: Import Errors During Tool Loading** ```python # This looks fine at startup... @app.tool() async def my_tool(): from some_module import missing_function # ❌ Import error! return {"result": "ok"} ``` **What happens**: 1. βœ… Server starts successfully 2. βœ… Claude connects and initializes 3. ❌ Claude tries to list tools β†’ import error occurs 4. ❌ Server crashes due to unhandled import error 5. ❌ Claude detects disconnect and sends `--kill` to cleanup #### **Scenario 2: Configuration Validation Errors** ```python # Server starts, but then... def some_tool(): config = ProtectConfig() # ❌ Validation error on missing required fields! ``` **What happens**: 1. βœ… Server starts (no config loaded yet) 2. βœ… Claude connects 3. ❌ Tool execution triggers config validation 4. ❌ Pydantic validation fails β†’ unhandled exception 5. ❌ Server crashes, Claude sends `--kill` #### **Scenario 3: Missing Dependencies in Tool Functions** ```python @app.tool() async def my_tool(): import missing_library # ❌ Library not installed! return {"result": "ok"} ``` ### **πŸ” How to Debug the "Start & Kill" Issue** #### **Step 1: Add Comprehensive Logging** ```python import logging import sys # Add this to your main server file logging.basicConfig( level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.StreamHandler(sys.stderr) # ← This appears in Claude Desktop logs! ] ) logger = logging.getLogger(__name__) @app.tool() async def my_tool(): try: logger.info("Tool starting...") # Your tool logic here logger.info("Tool completed successfully") return {"status": "ok"} except Exception as e: logger.error(f"Tool failed: {e}", exc_info=True) raise ``` #### **Step 2: Test Tool Registration** ```python # Add this debug function def test_tool_registration(): """Test that all tools can be imported without errors.""" try: # Import all your tool modules from .tools import device_status, device_control, auth_tools logger.info("All tool modules imported successfully") # Test that required dependencies are available import aiohttp import pydantic logger.info("All dependencies available") except Exception as e: logger.error(f"Tool registration test failed: {e}", exc_info=True) raise # Call this before app.run() if __name__ == "__main__": test_tool_registration() app.run() ``` #### **Step 3: Minimal Reproduction** ```python # Create a minimal server to test from fastmcp import FastMCP app = FastMCP("test-server") @app.tool() async def hello_world() -> dict: """Simple test tool.""" return {"message": "Hello, World!"} if __name__ == "__main__": app.run() ``` If this works but your full server doesn't, you have an issue in your tool implementations. --- ## πŸ”§ Specific Fixes We Applied ### **Fix 1: State Manager Import Errors** #### **❌ Problem**: ```python # state_manager.py was missing imports def get_app_state(): import time # ❌ Import inside function import os # ❌ These should be at module level ``` #### **βœ… Solution**: ```python # Put imports at the top of the file import time import os from typing import Optional from pydantic import BaseModel def get_app_state(): # Now the imports are available return app_state ``` ### **Fix 2: Module-Level Server Instantiation** #### **❌ Problem**: ```python # server.py server = NestProtectMCP() # ❌ Instantiated at import time β†’ validation errors ``` #### **βœ… Solution**: ```python # server.py server = None # βœ… Defer instantiation def create_server(): global server if server is None: server = NestProtectMCP() return server ``` ### **Fix 3: Async Function Confusion** #### **❌ Problem**: ```python # __main__.py async def main(): # ❌ Made it async but app.run() is blocking app.run() asyncio.run(main()) # ❌ This creates issues ``` #### **βœ… Solution**: ```python # __main__.py def main(): # βœ… Keep it simple and synchronous app.run() # βœ… This is blocking and handles its own event loop if __name__ == "__main__": main() # βœ… Direct call ``` ### **Fix 4: Tool Function Imports** #### **❌ Problem**: ```python @app.tool() async def list_devices(): from .tools.device_status import list_devices as tool_func # ❌ Import inside tool return await tool_func() ``` This worked but created issues with tool discovery. #### **βœ… Solution**: ```python # Import at module level from .tools.device_status import list_devices as device_list_func @app.tool() async def list_devices(): return await device_list_func() # βœ… Clean function call ``` --- ## 🎯 Debugging Checklist for Other MCP Projects ### **Phase 1: Import & Startup Issues** - [ ] **Check imports**: Is `Tool` imported from `fastmcp.tools`? - [ ] **Check constructor**: Using `instructions` instead of `description`? - [ ] **Check dependencies**: Are all imports at module level? - [ ] **Test minimal server**: Does a simple "hello world" tool work? ### **Phase 2: Tool Registration Issues** - [ ] **Using `@app.tool()`**: Not custom decorators or manual registration? - [ ] **Function signatures**: Proper type hints for parameters? - [ ] **Return types**: All tools return `Dict[str, Any]` or similar? - [ ] **Async consistency**: All tools marked `async` if they do I/O? ### **Phase 3: Runtime Issues** - [ ] **Error handling**: Try/catch blocks in tool functions? - [ ] **Logging setup**: stderr logging for Claude Desktop visibility? - [ ] **State management**: No circular imports or initialization issues? - [ ] **Dependencies**: All required packages installed and importable? ### **Phase 4: Claude Desktop Integration** - [ ] **STDIO transport**: Not trying to use WebSocket or HTTP? - [ ] **Entry point**: Correct command in Claude Desktop config? - [ ] **Working directory**: Path accessible and correct? - [ ] **Environment variables**: All required config available? --- ## πŸš€ Pro Tips from Our Experience ### **1. Start Simple, Add Complexity Gradually** ```python # Step 1: Get this working first @app.tool() async def test_tool() -> dict: return {"status": "working"} # Step 2: Add real functionality @app.tool() async def real_tool(param: str) -> dict: # Your actual logic here return {"result": param} ``` ### **2. Use Comprehensive Error Handling** ```python @app.tool() async def robust_tool(device_id: str) -> dict: try: # Your tool logic result = await some_api_call(device_id) return {"success": True, "data": result} except aiohttp.ClientError as e: return {"success": False, "error": f"Network error: {e}"} except ValidationError as e: return {"success": False, "error": f"Invalid input: {e}"} except Exception as e: logger.error(f"Unexpected error in tool: {e}", exc_info=True) return {"success": False, "error": "Internal server error"} ``` ### **3. Validate Your Tool Chain** ```python # Add this test function to validate everything works async def validate_all_tools(): """Test that all tools can be called without errors.""" tools = [ ("test_tool", {}), ("real_tool", {"param": "test"}), ] for tool_name, params in tools: try: result = await globals()[tool_name](**params) print(f"βœ… {tool_name}: {result}") except Exception as e: print(f"❌ {tool_name}: {e}") raise ``` ### **4. Monitor Claude Desktop Logs** - Look for stderr output in Claude Desktop logs - Watch for the 5-10 second pattern (startup β†’ kill) - Pay attention to the last successful operation before disconnect --- ## πŸ† Success Patterns ### **What Works Reliably** 1. **Simple FastMCP Pattern**: ```python app = FastMCP("name") @app.tool() async def tool() -> dict: return {"result": "data"} app.run() ``` 2. **Clean State Management**: ```python # Global state object app_state = AppState() def get_app_state() -> AppState: return app_state ``` 3. **Module-Level Imports**: ```python # At top of file import aiohttp import pydantic from typing import Dict, Any # Not inside functions ``` 4. **Robust Error Handling**: ```python try: # Operation pass except SpecificError: # Handle specific case pass except Exception as e: logger.error("Unexpected error", exc_info=True) return {"error": str(e)} ``` ### **What Breaks Reliably** 1. **Complex server inheritance and custom MCP handling** 2. **Import errors inside tool functions** 3. **Module-level object instantiation with validation** 4. **Missing error handling in async functions** 5. **Circular imports between modules** --- ## πŸ“ž Quick Fixes for Common Projects ### **For avatarmcp** - Check if using FastMCP 2.12 import pattern - Verify avatar generation dependencies are installed - Add error handling for image processing failures ### **For local llms** - Ensure model loading doesn't happen at import time - Add timeout handling for slow model responses - Check memory usage doesn't cause crashes ### **For tapo** - Verify network connectivity to devices - Add retry logic for device communication - Handle device discovery failures gracefully --- ## 🎯 Final Advice **The key insight**: Claude Desktop doesn't kill servers randomly. It kills them because they crash during normal operation, usually during tool discovery or execution. The "start and kill after a few seconds" pattern almost always indicates: 1. **Import errors** when tools are loaded 2. **Validation errors** when configuration is accessed 3. **Missing dependencies** when tools try to execute 4. **Unhandled exceptions** in async functions **Debug strategy**: Start with the minimal working example and add complexity gradually. Use comprehensive logging and error handling throughout. **Remember**: FastMCP 2.12 is designed to be simple. If you're fighting the framework, you're probably doing it wrong. πŸ”§βœ¨

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sandraschi/notepadpp-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server