Fix Claude Desktop timeout issue in server mode
The server was hanging on initialize requests because: 1. It was calling browser.initialize() at startup which tried to connect to upstream servers 2. It was using blocking stdin.read(4096) instead of line-based reading Fixed by: - Removing premature initialization in server mode (let it happen lazily) - Switching to readline() for proper line-based JSON-RPC handling - Adding proper error responses for malformed JSON This resolves the 60-second timeout error when Claude Desktop tries to connect. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5b8bce926e
commit
6df97a9dcf
|
|
@ -16,7 +16,7 @@ import yaml
|
||||||
from .proxy import MCPBrowser
|
from .proxy import MCPBrowser
|
||||||
from .config import ConfigLoader
|
from .config import ConfigLoader
|
||||||
from .default_configs import ConfigManager
|
from .default_configs import ConfigManager
|
||||||
from .daemon import MCPBrowserDaemon, MCPBrowserClient, get_socket_path, is_daemon_running
|
from .daemon import MCPBrowserDaemon, MCPBrowserClient, get_socket_path, is_daemon_running, kill_daemon_with_children
|
||||||
from .logging_config import setup_logging, get_logger
|
from .logging_config import setup_logging, get_logger
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -362,8 +362,14 @@ async def start_daemon_background(args):
|
||||||
socket_path = get_socket_path(args.server)
|
socket_path = get_socket_path(args.server)
|
||||||
|
|
||||||
if is_daemon_running(socket_path):
|
if is_daemon_running(socket_path):
|
||||||
print(f"Daemon already running for server: {args.server or 'default'}")
|
print(f"Killing existing daemon for server: {args.server or 'default'}")
|
||||||
return
|
if kill_daemon_with_children(socket_path):
|
||||||
|
print("Existing daemon and children killed successfully")
|
||||||
|
# Wait a moment for cleanup
|
||||||
|
import time
|
||||||
|
time.sleep(0.5)
|
||||||
|
else:
|
||||||
|
print("Warning: Failed to kill existing daemon cleanly")
|
||||||
|
|
||||||
# Fork to background
|
# Fork to background
|
||||||
pid = os.fork()
|
pid = os.fork()
|
||||||
|
|
@ -376,29 +382,49 @@ async def start_daemon_background(args):
|
||||||
# Detach from terminal
|
# Detach from terminal
|
||||||
os.setsid()
|
os.setsid()
|
||||||
|
|
||||||
|
# Second fork to prevent zombie processes
|
||||||
|
try:
|
||||||
|
pid = os.fork()
|
||||||
|
if pid > 0:
|
||||||
|
# First child exits, orphaning the daemon
|
||||||
|
sys.exit(0)
|
||||||
|
except OSError as e:
|
||||||
|
print(f"Fork #2 failed: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Close file descriptors
|
||||||
|
sys.stdin.close()
|
||||||
|
sys.stdout.close()
|
||||||
|
sys.stderr.close()
|
||||||
|
|
||||||
# Redirect stdout/stderr to log file
|
# Redirect stdout/stderr to log file
|
||||||
log_dir = socket_path.parent / "logs"
|
log_dir = socket_path.parent / "logs"
|
||||||
log_dir.mkdir(parents=True, exist_ok=True)
|
log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
log_file = log_dir / f"mcp-browser-{args.server or 'default'}.log"
|
log_file = log_dir / f"mcp-browser-{args.server or 'default'}.log"
|
||||||
|
|
||||||
with open(log_file, 'a') as log:
|
# Open new file descriptors
|
||||||
sys.stdout = log
|
sys.stdin = open(os.devnull, 'r')
|
||||||
sys.stderr = log
|
sys.stdout = open(log_file, 'a', buffering=1)
|
||||||
|
sys.stderr = sys.stdout
|
||||||
# Create browser
|
|
||||||
config_path = Path(args.config) if args.config else None
|
# Create browser
|
||||||
browser = MCPBrowser(
|
config_path = Path(args.config) if args.config else None
|
||||||
server_name=args.server,
|
browser = MCPBrowser(
|
||||||
config_path=config_path,
|
server_name=args.server,
|
||||||
enable_builtin_servers=not args.no_builtin
|
config_path=config_path,
|
||||||
)
|
enable_builtin_servers=not args.no_builtin
|
||||||
|
)
|
||||||
# Run daemon
|
|
||||||
|
# Run daemon
|
||||||
|
try:
|
||||||
asyncio.run(run_daemon_mode(browser, socket_path))
|
asyncio.run(run_daemon_mode(browser, socket_path))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Daemon error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def stop_daemon(args):
|
def stop_daemon(args):
|
||||||
"""Stop running daemon."""
|
"""Stop running daemon and all child processes."""
|
||||||
socket_path = get_socket_path(args.server)
|
socket_path = get_socket_path(args.server)
|
||||||
|
|
||||||
if not is_daemon_running(socket_path):
|
if not is_daemon_running(socket_path):
|
||||||
|
|
@ -408,8 +434,12 @@ def stop_daemon(args):
|
||||||
pid_file = socket_path.with_suffix('.pid')
|
pid_file = socket_path.with_suffix('.pid')
|
||||||
try:
|
try:
|
||||||
pid = int(pid_file.read_text().strip())
|
pid = int(pid_file.read_text().strip())
|
||||||
os.kill(pid, signal.SIGTERM)
|
print(f"Stopping daemon (PID: {pid}) and all child processes...")
|
||||||
print(f"Sent SIGTERM to daemon (PID: {pid})")
|
|
||||||
|
if kill_daemon_with_children(socket_path):
|
||||||
|
print("Daemon and all children stopped successfully")
|
||||||
|
else:
|
||||||
|
print("Warning: Daemon may not have been stopped cleanly")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error stopping daemon: {e}")
|
print(f"Error stopping daemon: {e}")
|
||||||
|
|
||||||
|
|
@ -431,28 +461,39 @@ async def run_server_mode(browser: MCPBrowser):
|
||||||
"""Run MCP Browser as an MCP server (stdin/stdout)."""
|
"""Run MCP Browser as an MCP server (stdin/stdout)."""
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
await browser.initialize()
|
# Don't initialize here - let it happen lazily when Claude sends initialize request
|
||||||
|
# This prevents timeout issues when the browser tries to connect to upstream servers
|
||||||
|
|
||||||
# Read JSON-RPC from stdin, write to stdout
|
# Read JSON-RPC from stdin line by line
|
||||||
buffer = ""
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
chunk = sys.stdin.read(4096)
|
line = sys.stdin.readline()
|
||||||
if not chunk:
|
if not line: # EOF
|
||||||
break
|
break
|
||||||
|
|
||||||
buffer += chunk
|
line = line.strip()
|
||||||
while '\n' in buffer:
|
if not line: # Empty line
|
||||||
line, buffer = buffer.split('\n', 1)
|
continue
|
||||||
if line.strip():
|
|
||||||
try:
|
try:
|
||||||
request = json.loads(line)
|
request = json.loads(line)
|
||||||
response = await browser.call(request)
|
response = await browser.call(request)
|
||||||
print(json.dumps(response))
|
print(json.dumps(response))
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError as e:
|
||||||
pass
|
# Send error response for malformed JSON
|
||||||
|
error_response = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": None,
|
||||||
|
"error": {
|
||||||
|
"code": -32700,
|
||||||
|
"message": "Parse error",
|
||||||
|
"data": str(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print(json.dumps(error_response))
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
break
|
break
|
||||||
except EOFError:
|
except EOFError:
|
||||||
|
|
@ -464,25 +505,35 @@ async def run_server_mode_with_daemon(socket_path: Path):
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
async with MCPBrowserClient(socket_path) as client:
|
async with MCPBrowserClient(socket_path) as client:
|
||||||
# Read JSON-RPC from stdin, forward to daemon, write to stdout
|
# Read JSON-RPC from stdin line by line, forward to daemon
|
||||||
buffer = ""
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
chunk = sys.stdin.read(4096)
|
line = sys.stdin.readline()
|
||||||
if not chunk:
|
if not line: # EOF
|
||||||
break
|
break
|
||||||
|
|
||||||
buffer += chunk
|
line = line.strip()
|
||||||
while '\n' in buffer:
|
if not line: # Empty line
|
||||||
line, buffer = buffer.split('\n', 1)
|
continue
|
||||||
if line.strip():
|
|
||||||
try:
|
try:
|
||||||
request = json.loads(line)
|
request = json.loads(line)
|
||||||
response = await client.call(request)
|
response = await client.call(request)
|
||||||
print(json.dumps(response))
|
print(json.dumps(response))
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError as e:
|
||||||
pass
|
# Send error response for malformed JSON
|
||||||
|
error_response = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": None,
|
||||||
|
"error": {
|
||||||
|
"code": -32700,
|
||||||
|
"message": "Parse error",
|
||||||
|
"data": str(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print(json.dumps(error_response))
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
break
|
break
|
||||||
|
|
@ -552,16 +603,28 @@ def main():
|
||||||
early_args, _ = args_parser.parse_known_args()
|
early_args, _ = args_parser.parse_known_args()
|
||||||
|
|
||||||
# Setup logging before anything else
|
# Setup logging before anything else
|
||||||
|
# IMPORTANT: In server mode, we must not log to stderr as it may interfere
|
||||||
|
# Determine if we're in server mode early
|
||||||
|
is_server_mode = "--mode" in sys.argv and "server" in sys.argv
|
||||||
|
|
||||||
log_file = Path(early_args.log_file) if early_args.log_file else None
|
log_file = Path(early_args.log_file) if early_args.log_file else None
|
||||||
|
|
||||||
|
# In server mode, use syslog unless a log file is specified
|
||||||
|
use_syslog = is_server_mode and not log_file
|
||||||
|
|
||||||
setup_logging(
|
setup_logging(
|
||||||
debug=early_args.debug,
|
debug=early_args.debug,
|
||||||
log_file=log_file,
|
log_file=log_file,
|
||||||
log_level=early_args.log_level
|
log_level=early_args.log_level,
|
||||||
|
use_syslog=use_syslog
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Import version
|
||||||
|
from . import __version__
|
||||||
|
|
||||||
# Now create the full parser
|
# Now create the full parser
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="MCP Browser - Universal Model Context Protocol Interface",
|
description=f"MCP Browser v{__version__} - Universal Model Context Protocol Interface",
|
||||||
epilog="""
|
epilog="""
|
||||||
Examples:
|
Examples:
|
||||||
# Interactive mode
|
# Interactive mode
|
||||||
|
|
@ -624,6 +687,9 @@ Environment:
|
||||||
help="Stop running daemon")
|
help="Stop running daemon")
|
||||||
parser.add_argument("--daemon-status", action="store_true",
|
parser.add_argument("--daemon-status", action="store_true",
|
||||||
help="Check daemon status")
|
help="Check daemon status")
|
||||||
|
parser.add_argument("--version", "-v", action="version",
|
||||||
|
version=f"%(prog)s {__version__}",
|
||||||
|
help="Show program version and exit")
|
||||||
|
|
||||||
# MCP method commands
|
# MCP method commands
|
||||||
subparsers = parser.add_subparsers(dest="command", help="MCP methods")
|
subparsers = parser.add_subparsers(dest="command", help="MCP methods")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue