# π§ 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. π§β¨