Hi I'm Claude too! Here are the detailed notes that you might need when building MCP servers for reference:
MCP Development Reference Notes
Based on YouTube MCP Project Analysis
๐ฏ Critical MCP-Specific Architectural Decisions
FastMCP Framework vs Custom Implementation
Decision Made: FastMCP from the official MCP Python SDK
- Why: Official framework with excellent documentation and active development
- Alternative Considered: Building custom MCP server from scratch
- Key Benefits:
- Automatic tool registration with @mcp.tool() decorators
- Built-in JSON-RPC handling
- Automatic schema generation from type hints
- Error handling and protocol compliance
# FastMCP Implementation Pattern:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Server Name")
@mcp.tool()
def my_function(param: str, optional_param: str = "default") -> dict:
"""Tool description goes here - becomes MCP tool description."""
return {"result": "success"}
# Run with stdio transport (critical for Claude Desktop)
mcp.run(transport="stdio")
Transport Choice: stdio vs HTTP
Decision Made: stdio transport
- Why: Required for Claude Desktop integration
- Critical Insight: HTTP transport creates network servers, stdio creates pipe-based communication
- Security Implication: stdio ensures only parent process (Claude Desktop) can communicate
# โ
Correct for Claude Desktop:
mcp.run(transport="stdio")
# โ Won't work with Claude Desktop:
mcp.run(transport="http", port=8000)
๐จ Critical Integration Issues & Solutions
Issue 1: stdout Contamination (MCP Protocol Violation)
Problem: MCP expects ONLY JSON-RPC messages on stdout
Symptoms:
- "spawn uv ENOENT" errors
- Unexpected token 'd', "[download]"... is not valid JSON
- Unexpected token 'A', "Available tools:" is not valid JSON
Root Causes & Solutions:
# โ NEVER do this in MCP servers:
print("Server starting...") # Violates MCP protocol
print("Available tools:") # Contaminates stdout
# โ
Use stderr for debugging:
import sys
print("Debug info", file=sys.stderr)
# โ
Or use logging (goes to stderr by default):
import logging
logger = logging.getLogger(__name__)
logger.info("Server starting") # Safe for MCP
Specific yt-dlp Configuration (if using):
ydl_opts = {
"quiet": True,
"no_warnings": True,
"noprogress": True, # โ Critical: Prevents [download] messages
"no_color": True, # โ Removes color codes from output
"extract_flat": False
}
Issue 2: PATH Resolution in GUI Applications
Problem: Claude Desktop can't find uv command
Symptom: "spawn uv ENOENT" error
Root Cause: GUI applications on macOS don't inherit shell PATH variables
Solution: Use absolute paths in Claude Desktop config
{
"mcpServers": {
"your-mcp": {
"command": "/Users/username/.local/bin/uv", // โ Absolute path required
"args": [
"--directory",
"/absolute/path/to/project", // โ Absolute path required
"run", "python", "-m", "your_module.server"
]
}
}
}
How to find paths:
which uv # Get uv absolute path
pwd # Get current directory absolute path
๐๏ธ MCP Development Best Practices
Module Initialization Patterns
Avoid Circular Imports:
# โ Don't do this in __init__.py:
from .server import main # Can cause RuntimeWarning
# โ
Keep __init__.py minimal:
"""Package exports only."""
# Empty or just version/metadata
Server Module Pattern:
# server.py structure:
import logging
from mcp.server.fastmcp import FastMCP
# Configure logging to stderr
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[logging.StreamHandler()] # Goes to stderr by default
)
# Initialize FastMCP
mcp = FastMCP("Your Server Name")
# Global client pattern (if needed)
api_client = None
def get_api_client():
global api_client
if api_client is None:
api_client = YourAPIClient()
return api_client
@mcp.tool()
def your_tool(param: str) -> dict:
"""Tool description - this becomes the MCP tool description."""
try:
client = get_api_client()
result = client.do_something(param)
return {"result": result}
except Exception as e:
# Return structured errors, don't raise
return {"error": str(e), "message": "Failed to process request"}
def main():
"""Main entry point."""
# Check required environment variables
if not os.getenv("REQUIRED_API_KEY"):
logger.error("REQUIRED_API_KEY environment variable is required")
return
# No print statements - they contaminate stdout!
logger.info("Server starting...")
mcp.run(transport="stdio")
if __name__ == "__main__":
main()
Error Handling Strategy
Multi-layered Approach:
1. Input validation at tool level
2. API error handling in client classes
3. Structured error responses (don't raise exceptions to MCP)
@mcp.tool()
def extract_data(url: str) -> dict:
# Layer 1: Input validation
if not is_valid_url(url):
return {
"error": "Invalid URL provided",
"message": "Please provide a valid URL format"
}
try:
# Layer 2: API operation with specific error handling
client = get_api_client()
result = client.extract(url)
# Layer 3: Validate result
if "error" in result:
logger.error(f"Extraction failed: {result.get('error')}")
return result # Pass through structured error
logger.info(f"Successfully processed: {url}")
return result
except Exception as e:
# Layer 4: Catch-all with structured response
logger.exception("Exception in extract_data")
return {
"error": str(e),
"message": "Failed to extract data"
}
Tool Schema Generation
Leverage Type Hints and Docstrings:
@mcp.tool()
def process_data(
required_param: str,
optional_param: str = "default",
number_param: int = 42
) -> dict[str, Any]:
"""Process data with specified parameters.
Args:
required_param: Description of required parameter
optional_param: Description with default value
number_param: Numeric parameter with default
Returns:
Dictionary containing processing results.
"""
# FastMCP automatically generates:
# - Tool name: "process_data"
# - Description: First line of docstring
# - Required params: ["required_param"]
# - Optional params with defaults
# - Type validation from hints
โ๏ธ Project Configuration Insights
pyproject.toml Patterns
[project]
name = "your-mcp"
version = "0.1.0"
description = "MCP server description"
requires-python = ">=3.11"
dependencies = [
"mcp>=1.9.4", # Official MCP SDK
"python-dotenv>=1.1.0", # Environment variable management
# Your specific dependencies
]
# Critical: Entry point for distribution
[project.scripts]
your-mcp-server = "your_package.server:main"
# Development tools
[dependency-groups]
dev = [
"black>=25.1.0", # Code formatting
"pytest>=8.4.1", # Testing
"ruff>=0.12.0", # Fast linting
]
# Tool configurations
[tool.black]
line-length = 100
target-version = ['py311']
[tool.ruff]
line-length = 100
target-version = "py311"
Environment Management
# Use python-dotenv for development:
from dotenv import load_dotenv
import os
load_dotenv() # Loads .env file
# Always check for required variables:
def main():
api_key = os.getenv("REQUIRED_API_KEY")
if not api_key:
logger.error("REQUIRED_API_KEY environment variable is required")
return # Don't raise exceptions in main()
๐งช Testing & Debugging Strategies
Local Testing Before MCP Integration
# tests/test_functions.py - Test functions independently
import pytest
from your_package.client import YourClient
def test_basic_functionality():
client = YourClient()
result = client.extract_data("test_input")
assert "result" in result
assert result["result"] is not None
# Run with: uv run python tests/test_functions.py
MCP Server Testing
# Test server startup:
uv run python -m your_package.server
# Should see logging output to stderr, no stdout contamination
# Process should stay running waiting for JSON-RPC input
Claude Desktop Integration Testing
Configuration Location:
- macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
- Windows: %APPDATA%/Claude/claude_desktop_config.json
Testing Process:
1. Configure with absolute paths
2. Restart Claude Desktop completely
3. Test with simple requests first
4. Check logs in Claude Desktop settings
Debugging MCP Protocol Issues
Use stderr for all debugging:
import sys
# Debug MCP message flow:
def debug_json_rpc(message):
print(f"MCP Message: {message}", file=sys.stderr)
sys.stderr.flush()
Check stdout cleanliness:
# Your server should output ONLY JSON on stdout:
echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | uv run python -m your_package.server
# Should return ONLY: {"jsonrpc":"2.0","result":{"tools":[...]},"id":1}
๐ Client Lifecycle Management
MCP Protocol Handshake
# Claude Desktop automatically handles:
# 1. initialize request/response
# 2. tools/list discovery
# 3. tools/call execution
# 4. Error handling and retry logic
# Your server just needs to implement the handlers via FastMCP
Connection Management
- Persistent: Unlike HTTP, MCP connections stay open
- Stateful: You can maintain state between tool calls
- Process-based: Server dies when parent process (Claude) exits
๐ Key Takeaways for MCP Development
1. stdout is sacred - Only JSON-RPC messages, never debug output
2. Absolute paths required - GUI apps don't inherit shell environment
3. FastMCP handles complexity - Focus on your business logic
4. Type hints = API schema - Well-typed functions become great tools
5. Error handling is critical - Return structured errors, don't raise
6. Testing strategy - Function-level first, then MCP integration
7. Logging to stderr - Essential for debugging MCP issues
These patterns will help you avoid the major pitfalls and build robust MCP servers that integrate seamlessly with Claude Desktop!