From 4a7d3cdc7fd91555129cfdf831951de232ed38a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claude4=CE=9Elope?= Date: Fri, 27 Jun 2025 15:56:05 +0200 Subject: [PATCH] Add daemon mode and improve debug output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- mcp_browser/__main__.py | 449 +++++++++++++++++++++++++++++++++++++++- mcp_browser/daemon.py | 276 ++++++++++++++++++++++++ mcp_browser/proxy.py | 49 +++++ mcp_browser/server.py | 16 +- mcp_browser/utils.py | 17 ++ 5 files changed, 797 insertions(+), 10 deletions(-) create mode 100644 mcp_browser/daemon.py create mode 100644 mcp_browser/utils.py diff --git a/mcp_browser/__main__.py b/mcp_browser/__main__.py index b4f49e1..a36c7b9 100644 --- a/mcp_browser/__main__.py +++ b/mcp_browser/__main__.py @@ -3,17 +3,158 @@ MCP Browser command-line interface. """ +import os import sys import asyncio import argparse import json +import signal from pathlib import Path -from typing import Optional +from typing import Optional, Dict, Any import yaml from .proxy import MCPBrowser from .config import ConfigLoader 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): @@ -161,6 +302,130 @@ async def test_server_connection(browser: MCPBrowser, server_name: Optional[str] 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): """Run MCP Browser as an MCP server (stdin/stdout).""" import sys @@ -193,16 +458,116 @@ async def run_server_mode(browser: MCPBrowser): 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 - Discover tools using JSONPath") + print(" call - 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(): """Main entry point.""" parser = argparse.ArgumentParser( description="MCP Browser - Universal Model Context Protocol Interface", epilog=""" Examples: + # Interactive mode mcp-browser # Start interactive mode with default server mcp-browser --server brave-search # Use Brave Search server + + # Configuration mcp-browser --list-servers # List configured servers 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) Configuration: @@ -217,7 +582,7 @@ Environment: ) 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("--mode", choices=["interactive", "server"], + parser.add_argument("--mode", choices=["interactive", "server", "daemon"], default="interactive", help="Operating mode (default: interactive)") parser.add_argument("--no-sparse", action="store_true", help="Disable sparse mode (show all tools)") @@ -231,6 +596,49 @@ Environment: help="Test connection to specified server") parser.add_argument("--debug", action="store_true", 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() @@ -243,6 +651,24 @@ Environment: show_configuration(args.config) 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 config_path = Path(args.config) if args.config else None browser = MCPBrowser( @@ -258,9 +684,24 @@ Environment: # Run in appropriate mode if args.mode == "server": - asyncio.run(run_server_mode(browser)) + # 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)) + elif args.mode == "daemon": + # Run as daemon + socket_path = get_socket_path(args.server) + asyncio.run(run_daemon_mode(browser, socket_path)) else: - asyncio.run(async_main(browser)) + # 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: + asyncio.run(async_main(browser)) async def async_main(browser: MCPBrowser): diff --git a/mcp_browser/daemon.py b/mcp_browser/daemon.py new file mode 100644 index 0000000..d2ce6e3 --- /dev/null +++ b/mcp_browser/daemon.py @@ -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 \ No newline at end of file diff --git a/mcp_browser/proxy.py b/mcp_browser/proxy.py index 3b76786..3e1a8e9 100644 --- a/mcp_browser/proxy.py +++ b/mcp_browser/proxy.py @@ -16,6 +16,7 @@ from .multi_server import MultiServerManager from .registry import ToolRegistry from .filter import MessageFilter, VirtualToolHandler from .buffer import JsonRpcBuffer +from .utils import debug_print, debug_json class MCPBrowser: @@ -166,6 +167,52 @@ class MCPBrowser: "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 future = asyncio.Future() self._response_buffer[request_id] = future @@ -176,6 +223,8 @@ class MCPBrowser: # Wait for response try: response = await asyncio.wait_for(future, timeout=self.config.timeout) + if self.config.debug: + debug_json("MCP Browser received", response) return response except asyncio.TimeoutError: del self._response_buffer[request_id] diff --git a/mcp_browser/server.py b/mcp_browser/server.py index f0f6216..25f34c3 100644 --- a/mcp_browser/server.py +++ b/mcp_browser/server.py @@ -14,6 +14,7 @@ from pathlib import Path from .buffer import JsonRpcBuffer from .config import MCPServerConfig +from .utils import debug_print, debug_json class MCPServer: @@ -46,7 +47,7 @@ class MCPServer: cmd = self.config.command + self.config.args if self.debug: - print(f"Starting MCP server: {' '.join(cmd)}") + debug_print(f"Starting MCP server: {' '.join(cmd)}") # Start process self.process = subprocess.Popen( @@ -116,7 +117,7 @@ class MCPServer: self.process.stdin.flush() if self.debug: - print(f"Sent: {request_str.strip()}") + debug_print(f"Sent: {request_str.strip()}") # Wait for response try: @@ -134,6 +135,9 @@ class MCPServer: if not message.endswith('\n'): message += '\n' + if self.debug: + debug_print(f"MCP Server sending: {message.strip()}") + self.process.stdin.write(message) self.process.stdin.flush() @@ -155,7 +159,7 @@ class MCPServer: except Exception as e: if self.debug: - print(f"Error reading stdout: {e}") + debug_print(f"Error reading stdout: {e}") break async def _read_stderr(self): @@ -166,7 +170,7 @@ class MCPServer: if not line: break - print(f"MCP stderr: {line.strip()}") + debug_print(f"MCP stderr: {line.strip()}") except Exception: break @@ -174,7 +178,7 @@ class MCPServer: async def _handle_message(self, message: dict): """Handle an incoming JSON-RPC message.""" if self.debug: - print(f"Received: {json.dumps(message)}") + debug_json("Received", message) # Check if it's a response to a pending request msg_id = message.get("id") @@ -192,4 +196,4 @@ class MCPServer: handler(message) except Exception as e: if self.debug: - print(f"Handler error: {e}") \ No newline at end of file + debug_print(f"Handler error: {e}") \ No newline at end of file diff --git a/mcp_browser/utils.py b/mcp_browser/utils.py new file mode 100644 index 0000000..3d2e98c --- /dev/null +++ b/mcp_browser/utils.py @@ -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) \ No newline at end of file