Add daemon mode and improve debug output

- Implement socket-based daemon mode using Unix domain sockets
  - Allows multiple clients to connect to a single MCP Browser instance
  - Supports automatic daemon detection and connection
  - Added --daemon-start, --daemon-stop, --daemon-status commands

- Improve debug output handling
  - All debug output now goes to stderr instead of stdout
  - Added debug_print() and debug_json() utilities
  - Debug messages show before and after server communication

- Fix builtin-only mode handling
  - Properly route tools/list and tools/call when no external server
  - Apply sparse mode filtering to builtin server tools

- Improve JSON-RPC command output
  - Raw jsonrpc command now outputs proper JSON for piping
  - Other commands use human-friendly formatting

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude4Ξlope 2025-06-27 15:56:05 +02:00
parent 33ff1c33f5
commit 4a7d3cdc7f
5 changed files with 797 additions and 10 deletions

View File

@ -3,17 +3,158 @@
MCP Browser command-line interface. MCP Browser command-line interface.
""" """
import os
import sys import sys
import asyncio import asyncio
import argparse import argparse
import json import json
import signal
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional, Dict, Any
import yaml 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 .utils import debug_print, debug_json
def build_mcp_request(args) -> Dict[str, Any]:
"""Build JSON-RPC request from command line arguments."""
if args.command == "tools-list":
return {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
elif args.command == "tools-call":
return {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": args.name,
"arguments": json.loads(args.arguments)
}
}
elif args.command == "resources-list":
return {
"jsonrpc": "2.0",
"id": 1,
"method": "resources/list",
"params": {}
}
elif args.command == "resources-read":
return {
"jsonrpc": "2.0",
"id": 1,
"method": "resources/read",
"params": {
"uri": args.uri
}
}
elif args.command == "prompts-list":
return {
"jsonrpc": "2.0",
"id": 1,
"method": "prompts/list",
"params": {}
}
elif args.command == "prompts-get":
return {
"jsonrpc": "2.0",
"id": 1,
"method": "prompts/get",
"params": {
"name": args.name,
"arguments": json.loads(args.arguments)
}
}
elif args.command == "completion":
params = {}
if args.ref:
params["ref"] = {"type": "ref/resource", "uri": args.ref}
if args.argument:
params["argument"] = args.argument
return {
"jsonrpc": "2.0",
"id": 1,
"method": "completion/complete",
"params": params
}
elif args.command == "jsonrpc":
request = json.loads(args.request)
if "jsonrpc" not in request:
request["jsonrpc"] = "2.0"
if "id" not in request:
request["id"] = 1
return request
else:
raise ValueError(f"Unknown command: {args.command}")
def format_mcp_response(args, request: Dict[str, Any], response: Dict[str, Any]):
"""Format and print MCP response based on command."""
if args.debug:
debug_json("Request", request)
debug_json("Response", response)
# Format output based on command
if args.command == "tools-list" and "result" in response:
tools = response["result"].get("tools", [])
print(f"Found {len(tools)} tools:")
for tool in tools:
print(f" - {tool['name']}: {tool.get('description', 'No description')}")
elif args.command == "tools-call" and "result" in response:
# Format tool response
result = response["result"]
if "content" in result:
for content in result["content"]:
if content.get("type") == "text":
print(content.get("text", ""))
else:
print(json.dumps(content, indent=2))
else:
print(json.dumps(result, indent=2))
elif args.command == "resources-list" and "result" in response:
resources = response["result"].get("resources", [])
print(f"Found {len(resources)} resources:")
for res in resources:
print(f" - {res['uri']}: {res.get('name', 'Unnamed')}")
elif args.command == "prompts-list" and "result" in response:
prompts = response["result"].get("prompts", [])
print(f"Found {len(prompts)} prompts:")
for prompt in prompts:
print(f" - {prompt['name']}: {prompt.get('description', 'No description')}")
elif args.command == "jsonrpc":
# For raw JSON-RPC, output the full response as JSON
print(json.dumps(response))
else:
# Default: pretty print result
if "result" in response:
print(json.dumps(response["result"], indent=2))
elif "error" in response:
print(f"Error: {response['error'].get('message', 'Unknown error')}")
if args.debug:
debug_json("Error details", response["error"])
else:
print(json.dumps(response, indent=2))
async def interactive_mode(browser: MCPBrowser): async def interactive_mode(browser: MCPBrowser):
@ -161,6 +302,130 @@ async def test_server_connection(browser: MCPBrowser, server_name: Optional[str]
print("\nConnection test completed successfully!") print("\nConnection test completed successfully!")
async def handle_mcp_command(args):
"""Handle MCP method commands by simulating JSON-RPC calls."""
# Check if we should use daemon mode
socket_path = get_socket_path(args.server)
if hasattr(args, 'use_daemon') and args.use_daemon and is_daemon_running(socket_path):
# Use daemon client
async with MCPBrowserClient(socket_path) as client:
request = build_mcp_request(args)
response = await client.call(request)
format_mcp_response(args, request, response)
return
# Create browser directly
config_path = Path(args.config) if args.config else None
# Set debug in config if requested
if args.debug and config_path is None:
from .config import ConfigLoader
loader = ConfigLoader()
config = loader.load()
config.debug = True
browser = MCPBrowser(
server_name=args.server,
config_path=config_path,
enable_builtin_servers=not args.no_builtin
)
try:
await browser.initialize()
# Build and send request
request = build_mcp_request(args)
response = await browser.call(request)
# Format response
format_mcp_response(args, request, response)
except Exception as e:
print(f"Error: {e}")
if args.debug:
import traceback
traceback.print_exc(file=sys.stderr)
finally:
await browser.close()
async def run_daemon_mode(browser: MCPBrowser, socket_path: Path):
"""Run MCP Browser in daemon mode."""
daemon = MCPBrowserDaemon(browser, socket_path)
await daemon.start()
async def start_daemon_background(args):
"""Start daemon in background."""
socket_path = get_socket_path(args.server)
if is_daemon_running(socket_path):
print(f"Daemon already running for server: {args.server or 'default'}")
return
# Fork to background
pid = os.fork()
if pid > 0:
# Parent process
print(f"Starting daemon in background (PID: {pid})")
return
# Child process
# Detach from terminal
os.setsid()
# Redirect stdout/stderr to log file
log_dir = socket_path.parent / "logs"
log_dir.mkdir(exist_ok=True)
log_file = log_dir / f"mcp-browser-{args.server or 'default'}.log"
with open(log_file, 'a') as log:
sys.stdout = log
sys.stderr = log
# Create browser
config_path = Path(args.config) if args.config else None
browser = MCPBrowser(
server_name=args.server,
config_path=config_path,
enable_builtin_servers=not args.no_builtin
)
# Run daemon
asyncio.run(run_daemon_mode(browser, socket_path))
def stop_daemon(args):
"""Stop running daemon."""
socket_path = get_socket_path(args.server)
if not is_daemon_running(socket_path):
print(f"No daemon running for server: {args.server or 'default'}")
return
pid_file = socket_path.with_suffix('.pid')
try:
pid = int(pid_file.read_text().strip())
os.kill(pid, signal.SIGTERM)
print(f"Sent SIGTERM to daemon (PID: {pid})")
except Exception as e:
print(f"Error stopping daemon: {e}")
def show_daemon_status(args):
"""Show daemon status."""
socket_path = get_socket_path(args.server)
if is_daemon_running(socket_path):
pid_file = socket_path.with_suffix('.pid')
pid = pid_file.read_text().strip()
print(f"Daemon running (PID: {pid})")
print(f"Socket: {socket_path}")
else:
print(f"No daemon running for server: {args.server or 'default'}")
async def run_server_mode(browser: MCPBrowser): 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
@ -193,16 +458,116 @@ async def run_server_mode(browser: MCPBrowser):
break break
async def run_server_mode_with_daemon(socket_path: Path):
"""Run as MCP server but forward to daemon."""
import sys
async with MCPBrowserClient(socket_path) as client:
# Read JSON-RPC from stdin, forward to daemon, write to stdout
buffer = ""
while True:
try:
chunk = sys.stdin.read(4096)
if not chunk:
break
buffer += chunk
while '\n' in buffer:
line, buffer = buffer.split('\n', 1)
if line.strip():
try:
request = json.loads(line)
response = await client.call(request)
print(json.dumps(response))
sys.stdout.flush()
except json.JSONDecodeError:
pass
except KeyboardInterrupt:
break
except EOFError:
break
async def interactive_mode_with_daemon(socket_path: Path):
"""Run interactive mode connected to daemon."""
async with MCPBrowserClient(socket_path) as client:
print("MCP Browser Interactive Mode (Daemon)")
print("=" * 50)
print(f"Connected to daemon at: {socket_path}")
print("Type 'help' for commands, 'exit' to quit\n")
while True:
try:
command = input("> ").strip()
if command == "exit":
break
elif command == "help":
print("\nCommands:")
print(" list - List available tools (sparse mode)")
print(" discover <path> - Discover tools using JSONPath")
print(" call <json> - Execute JSON-RPC call")
print(" exit - Exit interactive mode")
continue
elif command.startswith("call "):
json_str = command[5:]
request = json.loads(json_str)
if "jsonrpc" not in request:
request["jsonrpc"] = "2.0"
if "id" not in request:
request["id"] = 1
response = await client.call(request)
print(json.dumps(response, indent=2))
elif command == "list":
response = await client.call({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
})
if "result" in response:
tools = response["result"]["tools"]
for tool in tools:
print(f"- {tool['name']}: {tool['description']}")
else:
print("Unknown command. Type 'help' for available commands.")
except KeyboardInterrupt:
print("\nUse 'exit' to quit")
except Exception as e:
print(f"Error: {e}")
def main(): def main():
"""Main entry point.""" """Main entry point."""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="MCP Browser - Universal Model Context Protocol Interface", description="MCP Browser - Universal Model Context Protocol Interface",
epilog=""" epilog="""
Examples: Examples:
# Interactive mode
mcp-browser # Start interactive mode with default server mcp-browser # Start interactive mode with default server
mcp-browser --server brave-search # Use Brave Search server mcp-browser --server brave-search # Use Brave Search server
# Configuration
mcp-browser --list-servers # List configured servers mcp-browser --list-servers # List configured servers
mcp-browser --show-config # Show current configuration mcp-browser --show-config # Show current configuration
mcp-browser --test # Test server connection
# MCP method commands
mcp-browser tools-list # List available tools
mcp-browser tools-call brave_web_search '{"query": "MCP protocol"}'
mcp-browser resources-list # List available resources
mcp-browser resources-read "file:///path/to/file"
mcp-browser prompts-list # List available prompts
mcp-browser prompts-get "greeting" --arguments '{"name": "Alice"}'
# Raw JSON-RPC
mcp-browser jsonrpc '{"method": "tools/list", "params": {}}'
# Server mode
mcp-browser --mode server # Run as MCP server (stdin/stdout) mcp-browser --mode server # Run as MCP server (stdin/stdout)
Configuration: Configuration:
@ -217,7 +582,7 @@ Environment:
) )
parser.add_argument("--server", "-s", help="Target MCP server name (see --list-servers)") parser.add_argument("--server", "-s", help="Target MCP server name (see --list-servers)")
parser.add_argument("--config", "-c", help="Custom configuration file path") parser.add_argument("--config", "-c", help="Custom configuration file path")
parser.add_argument("--mode", choices=["interactive", "server"], parser.add_argument("--mode", choices=["interactive", "server", "daemon"],
default="interactive", help="Operating mode (default: interactive)") default="interactive", help="Operating mode (default: interactive)")
parser.add_argument("--no-sparse", action="store_true", parser.add_argument("--no-sparse", action="store_true",
help="Disable sparse mode (show all tools)") help="Disable sparse mode (show all tools)")
@ -231,6 +596,49 @@ Environment:
help="Test connection to specified server") help="Test connection to specified server")
parser.add_argument("--debug", action="store_true", parser.add_argument("--debug", action="store_true",
help="Enable debug output") help="Enable debug output")
parser.add_argument("--use-daemon", action="store_true",
help="Automatically use daemon if available")
parser.add_argument("--daemon-start", action="store_true",
help="Start daemon in background")
parser.add_argument("--daemon-stop", action="store_true",
help="Stop running daemon")
parser.add_argument("--daemon-status", action="store_true",
help="Check daemon status")
# MCP method commands
subparsers = parser.add_subparsers(dest="command", help="MCP methods")
# tools/list command
list_tools = subparsers.add_parser("tools-list", help="List available tools")
# tools/call command
call_tool = subparsers.add_parser("tools-call", help="Call a tool")
call_tool.add_argument("name", help="Tool name")
call_tool.add_argument("arguments", help="Tool arguments as JSON")
# resources/list command
list_resources = subparsers.add_parser("resources-list", help="List available resources")
# resources/read command
read_resource = subparsers.add_parser("resources-read", help="Read a resource")
read_resource.add_argument("uri", help="Resource URI")
# prompts/list command
list_prompts = subparsers.add_parser("prompts-list", help="List available prompts")
# prompts/get command
get_prompt = subparsers.add_parser("prompts-get", help="Get a prompt")
get_prompt.add_argument("name", help="Prompt name")
get_prompt.add_argument("--arguments", "-a", help="Prompt arguments as JSON", default="{}")
# completion command
completion = subparsers.add_parser("completion", help="Get completion")
completion.add_argument("--ref", help="Reference for completion")
completion.add_argument("--argument", help="Argument name")
# Generic JSON-RPC command
jsonrpc = subparsers.add_parser("jsonrpc", help="Send raw JSON-RPC request")
jsonrpc.add_argument("request", help="JSON-RPC request")
args = parser.parse_args() args = parser.parse_args()
@ -243,6 +651,24 @@ Environment:
show_configuration(args.config) show_configuration(args.config)
return return
# Handle daemon management commands
if args.daemon_start:
asyncio.run(start_daemon_background(args))
return
if args.daemon_stop:
stop_daemon(args)
return
if args.daemon_status:
show_daemon_status(args)
return
# Handle MCP method commands
if args.command:
asyncio.run(handle_mcp_command(args))
return
# Create browser # Create browser
config_path = Path(args.config) if args.config else None config_path = Path(args.config) if args.config else None
browser = MCPBrowser( browser = MCPBrowser(
@ -258,7 +684,22 @@ Environment:
# Run in appropriate mode # Run in appropriate mode
if args.mode == "server": if args.mode == "server":
# Run as MCP server (stdin/stdout) - can connect to daemon
socket_path = get_socket_path(args.server)
if args.use_daemon and is_daemon_running(socket_path):
# Use daemon as backend
asyncio.run(run_server_mode_with_daemon(socket_path))
else:
asyncio.run(run_server_mode(browser)) asyncio.run(run_server_mode(browser))
elif args.mode == "daemon":
# Run as daemon
socket_path = get_socket_path(args.server)
asyncio.run(run_daemon_mode(browser, socket_path))
else:
# Interactive mode - can use daemon if available
socket_path = get_socket_path(args.server)
if args.use_daemon and is_daemon_running(socket_path):
asyncio.run(interactive_mode_with_daemon(socket_path))
else: else:
asyncio.run(async_main(browser)) asyncio.run(async_main(browser))

276
mcp_browser/daemon.py Normal file
View File

@ -0,0 +1,276 @@
"""
MCP Browser daemon implementation using Unix domain sockets.
Provides a persistent MCP Browser instance that multiple clients can connect to,
allowing shared state and better performance.
"""
import os
import json
import asyncio
import socket
from pathlib import Path
from typing import Optional, Dict, Any
import signal
import sys
try:
import psutil
except ImportError:
# Fallback if psutil is not available
class psutil:
@staticmethod
def pid_exists(pid):
try:
os.kill(pid, 0)
return True
except OSError:
return False
from .proxy import MCPBrowser
from .utils import debug_print, debug_json
class MCPBrowserDaemon:
"""Daemon mode for MCP Browser using Unix domain sockets."""
def __init__(self, browser: MCPBrowser, socket_path: Path):
self.browser = browser
self.socket_path = socket_path
self.server: Optional[asyncio.Server] = None
self._running = False
self._clients: set = set()
async def start(self):
"""Start the daemon server."""
# Ensure socket directory exists
self.socket_path.parent.mkdir(parents=True, exist_ok=True)
# Remove existing socket if present
if self.socket_path.exists():
self.socket_path.unlink()
# Create Unix domain socket server
self.server = await asyncio.start_unix_server(
self._handle_client,
path=str(self.socket_path)
)
# Set permissions
os.chmod(self.socket_path, 0o600)
# Write PID file
pid_file = self.socket_path.with_suffix('.pid')
pid_file.write_text(str(os.getpid()))
self._running = True
# Set up signal handlers
signal.signal(signal.SIGTERM, self._signal_handler)
signal.signal(signal.SIGINT, self._signal_handler)
debug_print(f"MCP Browser daemon started on {self.socket_path}")
debug_print(f"PID: {os.getpid()}")
# Initialize browser
await self.browser.initialize()
# Run server
async with self.server:
await self.server.serve_forever()
def _signal_handler(self, signum, frame):
"""Handle shutdown signals."""
debug_print(f"\nReceived signal {signum}, shutting down...")
self._running = False
if self.server:
self.server.close()
sys.exit(0)
async def _handle_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
"""Handle a client connection."""
client_addr = writer.get_extra_info('peername')
debug_print(f"Client connected: {client_addr}")
self._clients.add(writer)
try:
buffer = ""
while self._running:
# Read data from client
data = await reader.read(4096)
if not data:
break
buffer += data.decode('utf-8')
# Process complete JSON objects
while '\n' in buffer:
line, buffer = buffer.split('\n', 1)
if line.strip():
await self._process_request(line, writer)
except asyncio.CancelledError:
pass
except Exception as e:
debug_print(f"Client error: {e}")
finally:
self._clients.discard(writer)
writer.close()
await writer.wait_closed()
debug_print(f"Client disconnected: {client_addr}")
async def _process_request(self, line: str, writer: asyncio.StreamWriter):
"""Process a JSON-RPC request from client."""
try:
request = json.loads(line)
# Add debug output if configured
if self.browser.config and self.browser.config.debug:
debug_json("Daemon received", request)
# Forward to browser
response = await self.browser.call(request)
# Send response back to client
response_str = json.dumps(response) + '\n'
writer.write(response_str.encode('utf-8'))
await writer.drain()
if self.browser.config and self.browser.config.debug:
debug_print(f"Daemon sent: {response_str.strip()}")
except json.JSONDecodeError as e:
error_response = {
"jsonrpc": "2.0",
"id": None,
"error": {
"code": -32700,
"message": f"Parse error: {e}"
}
}
writer.write((json.dumps(error_response) + '\n').encode('utf-8'))
await writer.drain()
except Exception as e:
error_response = {
"jsonrpc": "2.0",
"id": request.get("id") if 'request' in locals() else None,
"error": {
"code": -32603,
"message": f"Internal error: {e}"
}
}
writer.write((json.dumps(error_response) + '\n').encode('utf-8'))
await writer.drain()
async def stop(self):
"""Stop the daemon server."""
self._running = False
# Close all client connections
for writer in list(self._clients):
writer.close()
await writer.wait_closed()
# Close server
if self.server:
self.server.close()
await self.server.wait_closed()
# Clean up socket and PID files
if self.socket_path.exists():
self.socket_path.unlink()
pid_file = self.socket_path.with_suffix('.pid')
if pid_file.exists():
pid_file.unlink()
# Close browser
await self.browser.close()
class MCPBrowserClient:
"""Client for connecting to MCP Browser daemon."""
def __init__(self, socket_path: Path):
self.socket_path = socket_path
self.reader: Optional[asyncio.StreamReader] = None
self.writer: Optional[asyncio.StreamWriter] = None
async def connect(self):
"""Connect to the daemon."""
if not self.socket_path.exists():
raise ConnectionError(f"Daemon socket not found: {self.socket_path}")
# Check if daemon is alive
pid_file = self.socket_path.with_suffix('.pid')
if pid_file.exists():
try:
pid = int(pid_file.read_text().strip())
if not psutil.pid_exists(pid):
# Stale socket/PID file
self.socket_path.unlink()
pid_file.unlink()
raise ConnectionError("Daemon not running (stale PID file)")
except (ValueError, psutil.Error):
pass
# Connect to socket
self.reader, self.writer = await asyncio.open_unix_connection(str(self.socket_path))
async def call(self, request: Dict[str, Any]) -> Dict[str, Any]:
"""Send a JSON-RPC request and get response."""
if not self.writer:
await self.connect()
# Send request
request_str = json.dumps(request) + '\n'
self.writer.write(request_str.encode('utf-8'))
await self.writer.drain()
# Read response
response_line = await self.reader.readline()
if not response_line:
raise ConnectionError("Connection closed by daemon")
return json.loads(response_line.decode('utf-8'))
async def close(self):
"""Close the connection."""
if self.writer:
self.writer.close()
await self.writer.wait_closed()
async def __aenter__(self):
"""Async context manager entry."""
await self.connect()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Async context manager exit."""
await self.close()
def get_socket_path(server_name: Optional[str] = None) -> Path:
"""Get the socket path for a given server name."""
runtime_dir = os.environ.get('XDG_RUNTIME_DIR')
if not runtime_dir:
runtime_dir = f"/tmp/mcp-browser-{os.getuid()}"
socket_name = f"mcp-browser-{server_name}.sock" if server_name else "mcp-browser.sock"
return Path(runtime_dir) / socket_name
def is_daemon_running(socket_path: Path) -> bool:
"""Check if a daemon is running for the given socket."""
if not socket_path.exists():
return False
pid_file = socket_path.with_suffix('.pid')
if not pid_file.exists():
return False
try:
pid = int(pid_file.read_text().strip())
return psutil.pid_exists(pid)
except (ValueError, psutil.Error):
return False

View File

@ -16,6 +16,7 @@ from .multi_server import MultiServerManager
from .registry import ToolRegistry from .registry import ToolRegistry
from .filter import MessageFilter, VirtualToolHandler from .filter import MessageFilter, VirtualToolHandler
from .buffer import JsonRpcBuffer from .buffer import JsonRpcBuffer
from .utils import debug_print, debug_json
class MCPBrowser: class MCPBrowser:
@ -166,6 +167,52 @@ class MCPBrowser:
"error": {"code": -32603, "message": str(e)} "error": {"code": -32603, "message": str(e)}
} }
# Check if we have a server
if not self.server:
# In builtin-only mode, try to route to multi-server
if self.multi_server:
# Try to route based on method
if jsonrpc_object.get("method") == "tools/list":
# Apply filter to multi-server tools
tools = await self.multi_server.get_all_tools()
filtered_tools = self.filter.filter_outgoing({
"jsonrpc": "2.0",
"id": request_id,
"result": {"tools": tools}
})
return filtered_tools
elif jsonrpc_object.get("method") == "tools/call":
# Route tool call to multi-server
tool_name = jsonrpc_object.get("params", {}).get("name")
args = jsonrpc_object.get("params", {}).get("arguments", {})
try:
result = await self.multi_server.route_tool_call(tool_name, args)
return {
"jsonrpc": "2.0",
"id": request_id,
"result": result
}
except Exception as e:
return {
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32603, "message": str(e)}
}
# No server available
return {
"jsonrpc": "2.0",
"id": request_id,
"error": {
"code": -32603,
"message": "No MCP server available"
}
}
# Debug output
if self.config.debug:
debug_json("MCP Browser sending", jsonrpc_object)
# Create future for response # Create future for response
future = asyncio.Future() future = asyncio.Future()
self._response_buffer[request_id] = future self._response_buffer[request_id] = future
@ -176,6 +223,8 @@ class MCPBrowser:
# Wait for response # Wait for response
try: try:
response = await asyncio.wait_for(future, timeout=self.config.timeout) response = await asyncio.wait_for(future, timeout=self.config.timeout)
if self.config.debug:
debug_json("MCP Browser received", response)
return response return response
except asyncio.TimeoutError: except asyncio.TimeoutError:
del self._response_buffer[request_id] del self._response_buffer[request_id]

View File

@ -14,6 +14,7 @@ from pathlib import Path
from .buffer import JsonRpcBuffer from .buffer import JsonRpcBuffer
from .config import MCPServerConfig from .config import MCPServerConfig
from .utils import debug_print, debug_json
class MCPServer: class MCPServer:
@ -46,7 +47,7 @@ class MCPServer:
cmd = self.config.command + self.config.args cmd = self.config.command + self.config.args
if self.debug: if self.debug:
print(f"Starting MCP server: {' '.join(cmd)}") debug_print(f"Starting MCP server: {' '.join(cmd)}")
# Start process # Start process
self.process = subprocess.Popen( self.process = subprocess.Popen(
@ -116,7 +117,7 @@ class MCPServer:
self.process.stdin.flush() self.process.stdin.flush()
if self.debug: if self.debug:
print(f"Sent: {request_str.strip()}") debug_print(f"Sent: {request_str.strip()}")
# Wait for response # Wait for response
try: try:
@ -134,6 +135,9 @@ class MCPServer:
if not message.endswith('\n'): if not message.endswith('\n'):
message += '\n' message += '\n'
if self.debug:
debug_print(f"MCP Server sending: {message.strip()}")
self.process.stdin.write(message) self.process.stdin.write(message)
self.process.stdin.flush() self.process.stdin.flush()
@ -155,7 +159,7 @@ class MCPServer:
except Exception as e: except Exception as e:
if self.debug: if self.debug:
print(f"Error reading stdout: {e}") debug_print(f"Error reading stdout: {e}")
break break
async def _read_stderr(self): async def _read_stderr(self):
@ -166,7 +170,7 @@ class MCPServer:
if not line: if not line:
break break
print(f"MCP stderr: {line.strip()}") debug_print(f"MCP stderr: {line.strip()}")
except Exception: except Exception:
break break
@ -174,7 +178,7 @@ class MCPServer:
async def _handle_message(self, message: dict): async def _handle_message(self, message: dict):
"""Handle an incoming JSON-RPC message.""" """Handle an incoming JSON-RPC message."""
if self.debug: if self.debug:
print(f"Received: {json.dumps(message)}") debug_json("Received", message)
# Check if it's a response to a pending request # Check if it's a response to a pending request
msg_id = message.get("id") msg_id = message.get("id")
@ -192,4 +196,4 @@ class MCPServer:
handler(message) handler(message)
except Exception as e: except Exception as e:
if self.debug: if self.debug:
print(f"Handler error: {e}") debug_print(f"Handler error: {e}")

17
mcp_browser/utils.py Normal file
View File

@ -0,0 +1,17 @@
"""
Utility functions for MCP Browser.
"""
import sys
from typing import Any
def debug_print(message: str):
"""Print debug message to stderr."""
print(message, file=sys.stderr, flush=True)
def debug_json(label: str, data: Any):
"""Print JSON data to stderr for debugging."""
import json
print(f"{label}: {json.dumps(data)}", file=sys.stderr, flush=True)