Skip to main content
Glama
MCP_TIMEOUT_SOLUTION.md6.32 kB
# MCP Timeout Solution: Direct Tool Handler with Progress Notifications **Date:** October 22, 2025 **Problem:** Claude Code disconnects after ~11 minutes, even with keep-alive logging **Root Cause:** Python logging doesn't send data over SSE connection - only MCP messages do --- ## Problem Analysis ### What We Discovered 1. **Logging doesn't help**: `logging.info()` writes to log file, NOT to SSE connection 2. **SSE pings aren't enough**: The connection has pings every 15s, but Claude Code still times out 3. **Client-side timeout**: Claude Code has a hard ~660 second (11 minute) timeout for tool calls 4. **Architecture issue**: FastApiMCP uses internal HTTP requests, so we can't access MCP session ### Test Results - Script duration: 650.9 seconds (10.8 minutes) - Progress logs: Every 60 seconds (working correctly in log file) - SSE pings: Every 15 seconds (working correctly) - Result: **Still disconnects** at 10m51s - just before completion **Conclusion**: Keep-alive logging approach doesn't work because logs don't go over the wire! --- ## Solution: Direct MCP Tool Handler ### Architecture Change **Before (Current):** ``` Claude Code → MCP → fastapi-mcp → HTTP request → FastAPI endpoint → run_stata_file() ↑ No session access! ``` **After (Proposed):** ``` Claude Code → MCP → Custom tool handler → run_stata_file_async() ↑ ↓ Has session access! Send progress notifications ``` ### Implementation Register `stata_run_file` as a direct MCP tool instead of going through FastAPI endpoint: ```python # After creating FastApiMCP mcp = FastApiMCP(app, ...) mcp.mount() # Access the underlying MCP server mcp_server = mcp.server # Register custom handler for stata_run_file with progress support @mcp_server.call_tool() async def handle_stata_run_file_with_progress( name: str, arguments: dict ) -> list[types.TextContent]: if name != "stata_run_file": # Fallback to fastapi-mcp for other tools return await mcp._execute_api_tool(...) # Get session from request context ctx = mcp_server.request_context session = ctx.session request_id = ctx.request_id # Extract parameters file_path = arguments["file_path"] timeout = arguments.get("timeout", 600) # Run Stata in background import asyncio from concurrent.futures import ThreadPoolExecutor executor = ThreadPoolExecutor(max_workers=1) task = asyncio.get_event_loop().run_in_executor( executor, run_stata_file, file_path, timeout, True # auto_name_graphs ) # Send progress notifications every 60 seconds start_time = time.time() while not task.done(): await asyncio.sleep(60) elapsed = time.time() - start_time # THIS IS THE KEY: Send progress over MCP connection! await session.send_progress_notification( progress_token=str(request_id), progress=elapsed, total=timeout ) logging.info(f"📡 Sent progress notification: {elapsed:.0f}s / {timeout}s") # Get final result result = await task return [types.TextContent(type="text", text=result)] ``` --- ## Alternative: Monkey-Patch fastapi-mcp If we don't want to bypass fastapi-mcp entirely, we could monkey-patch its `_execute_api_tool` method to send progress notifications while waiting for long-running requests: ```python # After mcp.mount() original_execute = mcp._execute_api_tool async def execute_with_progress(client, base_url, tool_name, arguments, operation_map): if tool_name == "stata_run_file": # Get session ctx = mcp.server.request_context session = ctx.session request_id = ctx.request_id # Start the request in background task = asyncio.create_task( original_execute(client, base_url, tool_name, arguments, operation_map) ) # Send progress while waiting start_time = time.time() timeout = arguments.get("timeout", 600) while not task.done(): await asyncio.sleep(60) elapsed = time.time() - start_time await session.send_progress_notification( progress_token=str(request_id), progress=elapsed, total=timeout ) return await task else: return await original_execute(client, base_url, tool_name, arguments, operation_map) mcp._execute_api_tool = execute_with_progress ``` --- ## Recommended Approach ### Option 1: Monkey-Patch (Quickest - 1 hour) **Pros:** - Minimal code changes - Keeps using FastAPI endpoints - Easy to test **Cons:** - Relies on internals of fastapi-mcp - Might break with updates ### Option 2: Custom Tool Handler (Clean - 3 hours) **Pros:** - Proper MCP implementation - Full control over tool behavior - Future-proof **Cons:** - More code to write - Need to duplicate FastAPI endpoint logic ### Option 3: Fork fastapi-mcp (Long-term - 8+ hours) **Pros:** - Fix the root cause - Can contribute back to project - Benefits everyone **Cons:** - Time-consuming - Need to maintain fork --- ## Next Steps 1. **Try Option 1 (Monkey-Patch)** first - quickest to implement and test 2. If it works, document it and use in production 3. If issues arise, move to Option 2 (Custom Handler) 4. Long-term: Consider contributing fix to fastapi-mcp --- ## Success Criteria ✅ Scripts running > 11 minutes complete successfully ✅ Claude Code receives final result ✅ Progress notifications sent every 60 seconds ✅ No "Jitterbugging..." forever ✅ Connection stays alive for duration of execution --- ## Code Location **File to modify:** `src/stata_mcp_server.py` **Line to add code after:** 2678 (after `mcp.mount()`) **Estimated new lines:** 40-50 lines --- ## Testing Plan 1. Add monkey-patch code 2. Restart server 3. Run long script (run_LP_analysis.do, ~11 minutes) 4. Monitor server logs for "📡 Sent progress notification" 5. Verify Claude Code doesn't disconnect 6. Confirm final result is received --- **Status:** Ready to implement Option 1 (Monkey-Patch)

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/hanlulong/stata-mcp'

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