mcp-browser/mcp_browser/__main__.py

917 lines
33 KiB
Python

#!/usr/bin/env python3
"""
MCP Browser command-line interface.
"""
import os
import sys
import asyncio
import argparse
import json
from pathlib import Path
from typing import Optional, Dict, Any
from .proxy import MCPBrowser
from .config import ConfigLoader
from .daemon import MCPBrowserDaemon, MCPBrowserClient, get_socket_path, is_daemon_running, kill_daemon_with_children
from .logging_config import setup_logging, get_logger
from .http_gateway import run_streamable_http_gateway
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."""
logger = get_logger(__name__)
if args.debug:
logger.debug(f"Request: {json.dumps(request)}")
logger.debug(f"Response: {json.dumps(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:
logger.debug(f"Error details: {json.dumps(response['error'])}")
else:
print(json.dumps(response, indent=2))
async def interactive_mode(browser: MCPBrowser):
"""Run MCP Browser in interactive mode."""
print("MCP Browser Interactive Mode")
print("=" * 50)
print(f"Server: {browser._server_name or browser.config.default_server}")
print(f"Sparse mode: {'enabled' if browser.config.sparse_mode else 'disabled'}")
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(" onboard [<id>] - Show/set onboarding for identity")
print(" reload - Reload configuration")
print(" status - Show connection status")
print(" exit - Exit interactive mode")
print("\nExamples:")
print(' discover $.tools[*].name # Get all tool names')
print(' discover $.tools[0].inputSchema # Get first tool schema')
print(' call {"method": "tools/list"} # Raw JSON-RPC call')
print(' onboard MyProject # Get project onboarding')
print('\nJSONPath syntax:')
print(' $ - Root object')
print(' .tools[*] - All tools')
print(' .tools[0] - First tool')
print(' .tools[*].name - All tool names')
continue
elif command.startswith("discover "):
jsonpath = command[9:]
result = browser.discover(jsonpath)
print(json.dumps(result, indent=2))
elif command.startswith("call "):
json_str = command[5:]
request = json.loads(json_str)
if "jsonrpc" not in request:
request["jsonrpc"] = "2.0"
response = await browser.call(request)
print(json.dumps(response, indent=2))
elif command.startswith("onboard "):
identity = command[8:]
response = await browser.call({
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "onboarding",
"arguments": {"identity": identity}
}
})
if "result" in response:
print(response["result"]["content"][0]["text"])
elif command == "list":
response = await browser.call({
"jsonrpc": "2.0",
"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 show_available_servers(config_path: Optional[str] = None):
"""Show list of available MCP servers from configuration."""
loader = ConfigLoader(Path(config_path) if config_path else None)
config = loader.load()
print("Available MCP Servers:")
print("=" * 50)
for name, server in config.servers.items():
print(f"\n{name}:")
print(f" Description: {server.description or 'No description'}")
print(f" Command: {' '.join(server.command) if server.command else 'Built-in only'}")
if server.env:
print(f" Environment: {', '.join(server.env.keys())}")
print(f"\nDefault server: {config.default_server}")
print(f"Config location: {loader.config_path}")
def show_configuration(config_path: Optional[str] = None):
"""Show current configuration file path and content."""
loader = ConfigLoader(Path(config_path) if config_path else None)
print(f"Configuration file: {loader.config_path}")
print("=" * 50)
if loader.config_path.exists():
with open(loader.config_path) as f:
print(f.read())
else:
print("Configuration file not found. Will be created on first run.")
async def test_server_connection(browser: MCPBrowser, server_name: Optional[str] = None):
"""Test connection to specified MCP server."""
print(f"Testing connection to server: {server_name or 'default'}")
print("=" * 50)
try:
await browser.initialize()
print("✓ Successfully connected to server")
# Try to list tools
response = await browser.call({
"jsonrpc": "2.0",
"method": "tools/list"
})
if "result" in response:
tools = response["result"]["tools"]
print(f"✓ Server provides {len(tools)} tools")
if browser.config.sparse_mode:
print(" (Showing sparse tools only)")
else:
print("✗ Failed to list tools")
except Exception as e:
print(f"✗ Connection failed: {e}")
return
finally:
await browser.close()
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,
transport_override=getattr(args, "transport_override", None)
)
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"Killing existing daemon for server: {args.server or 'default'}")
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
pid = os.fork()
if pid > 0:
# Parent process
print(f"Starting daemon in background (PID: {pid})")
return
# Child process
# Detach from terminal
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
log_dir = socket_path.parent / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / f"mcp-browser-{args.server or 'default'}.log"
# Open new file descriptors
sys.stdin = open(os.devnull, 'r')
sys.stdout = open(log_file, 'a', buffering=1)
sys.stderr = sys.stdout
# 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,
transport_override=getattr(args, "transport_override", None)
)
# Run daemon
try:
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):
"""Stop running daemon and all child processes."""
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())
print(f"Stopping daemon (PID: {pid}) and all child processes...")
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:
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
# 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 line by line
while True:
try:
line = sys.stdin.readline()
if not line: # EOF
break
line = line.strip()
if not line: # Empty line
continue
try:
request = json.loads(line)
response = await browser.call(request)
print(json.dumps(response))
sys.stdout.flush()
except json.JSONDecodeError as e:
# 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:
break
except EOFError:
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 line by line, forward to daemon
while True:
try:
line = sys.stdin.readline()
if not line: # EOF
break
line = line.strip()
if not line: # Empty line
continue
try:
request = json.loads(line)
response = await client.call(request)
print(json.dumps(response))
sys.stdout.flush()
except json.JSONDecodeError as e:
# 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:
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():
"""Main entry point."""
# Parse args first to check for log configuration
args_parser = argparse.ArgumentParser(add_help=False)
args_parser.add_argument("--debug", action="store_true")
args_parser.add_argument("--log-level")
args_parser.add_argument("--log-file")
early_args, _ = args_parser.parse_known_args()
# 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
# In server mode, use syslog unless a log file is specified
use_syslog = is_server_mode and not log_file
setup_logging(
debug=early_args.debug,
log_file=log_file,
log_level=early_args.log_level,
use_syslog=use_syslog
)
# Import version
from . import __version__
# Now create the full parser
parser = argparse.ArgumentParser(
description=f"MCP Browser v{__version__} - 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:
Default config: ~/.claude/mcp-browser/config.yaml
First run creates default configuration with examples
Environment:
Set API keys as needed: BRAVE_API_KEY, GITHUB_TOKEN, etc.
Or source from: source ~/.secrets/api-keys.sh
""",
formatter_class=argparse.RawDescriptionHelpFormatter
)
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", "daemon", "streamable-http"],
default="interactive", help="Operating mode (default: interactive)")
parser.add_argument("--no-sparse", action="store_true",
help="Disable sparse mode (show all tools)")
parser.add_argument("--no-builtin", action="store_true",
help="Disable built-in servers (screen, memory, patterns)")
parser.add_argument("--list-servers", action="store_true",
help="List available MCP servers from config")
parser.add_argument("--show-config", action="store_true",
help="Show current configuration path and content")
parser.add_argument("--test", action="store_true",
help="Test connection to specified server")
parser.add_argument("--debug", action="store_true",
help="Enable debug output")
parser.add_argument("--log-level", choices=["TRACE", "DEBUG", "INFO", "WARNING", "ERROR"],
help="Set logging level")
parser.add_argument("--log-file", help="Log to file instead of stderr")
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")
parser.add_argument("--version", "-v", action="version",
version=f"%(prog)s {__version__}",
help="Show program version and exit")
parser.add_argument("--transport", choices=["stdio", "streamable-http"],
help="Override transport type for the target server")
parser.add_argument("--transport-url",
help="Endpoint URL for streamable-http transport override")
parser.add_argument("--transport-header", action="append", metavar="KEY=VALUE",
help="Additional HTTP headers for streamable-http transport (repeatable)")
parser.add_argument("--transport-timeout", type=float,
help="Override HTTP request timeout in seconds")
parser.add_argument("--transport-sse-timeout", type=float,
help="Override SSE read timeout in seconds")
parser.add_argument("--oauth-token-url",
help="OAuth token endpoint for client credentials flow")
parser.add_argument("--oauth-client-id", help="OAuth client identifier")
parser.add_argument("--oauth-client-secret", help="OAuth client secret")
parser.add_argument("--oauth-scope",
help="OAuth scopes (space-separated)")
parser.add_argument("--oauth-audience",
help="OAuth audience/resource parameter")
parser.add_argument("--oauth-extra-param", action="append", metavar="KEY=VALUE",
help="Additional form parameters for OAuth token requests (repeatable)")
parser.add_argument("--oauth-token",
help="Static OAuth bearer token (applies Authorization header)")
parser.add_argument("--http-host", default="127.0.0.1",
help="Host/interface for streamable-http gateway (when --mode streamable-http)")
parser.add_argument("--http-port", type=int, default=0,
help="TCP port for streamable-http gateway (default 0 = random)")
parser.add_argument("--http-path", default="/mcp",
help="HTTP path prefix for streamable-http gateway (default /mcp)")
parser.add_argument("--http-allow-origin",
help="Optional Access-Control-Allow-Origin value for streamable-http gateway")
# MCP method commands
subparsers = parser.add_subparsers(dest="command", help="MCP methods")
# tools/list command
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
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
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()
def parse_key_value_pairs(pairs):
result = {}
if not pairs:
return result
for item in pairs:
if "=" not in item:
print(f"Invalid KEY=VALUE format: {item}", file=sys.stderr)
sys.exit(2)
key, value = item.split("=", 1)
key = key.strip()
value = value.strip()
if not key:
print(f"Header key is empty for entry: {item}", file=sys.stderr)
sys.exit(2)
result[key] = value
return result
transport_headers = parse_key_value_pairs(args.transport_header)
oauth_extra = parse_key_value_pairs(args.oauth_extra_param)
if args.oauth_token:
transport_headers.setdefault("Authorization", f"Bearer {args.oauth_token}")
oauth_override = {}
if args.oauth_token_url:
oauth_override["token_url"] = args.oauth_token_url
if args.oauth_client_id:
oauth_override["client_id"] = args.oauth_client_id
if args.oauth_client_secret is not None:
oauth_override["client_secret"] = args.oauth_client_secret
if args.oauth_scope is not None:
oauth_override["scope"] = args.oauth_scope
if args.oauth_audience is not None:
oauth_override["audience"] = args.oauth_audience
if oauth_extra:
oauth_override["extra_params"] = oauth_extra
if args.oauth_token:
oauth_override["access_token"] = args.oauth_token
transport_override = None
if any([
args.transport,
args.transport_url,
transport_headers,
args.transport_timeout is not None,
args.transport_sse_timeout is not None,
oauth_override,
]):
transport_override = {}
if args.transport:
transport_override["type"] = args.transport
if args.transport_url:
transport_override["url"] = args.transport_url
if transport_headers:
transport_override["headers"] = transport_headers
if args.transport_timeout is not None:
transport_override["timeout"] = args.transport_timeout
if args.transport_sse_timeout is not None:
transport_override["sse_timeout"] = args.transport_sse_timeout
if oauth_override:
transport_override["oauth"] = oauth_override
setattr(args, "transport_override", transport_override)
# Handle special commands first
if args.list_servers:
show_available_servers(args.config)
return
if args.show_config:
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
# Apply log level to config if set
if args.log_level == "TRACE" and config_path is None:
from .config import ConfigLoader
loader = ConfigLoader()
loader.load()
# TRACE level shows raw I/O
browser = MCPBrowser(
server_name=args.server,
config_path=config_path,
enable_builtin_servers=not args.no_builtin,
transport_override=transport_override
)
# Handle test mode
if args.test:
asyncio.run(test_server_connection(browser, args.server))
return
# Run in appropriate mode
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))
elif args.mode == "daemon":
# Run as daemon
socket_path = get_socket_path(args.server)
asyncio.run(run_daemon_mode(browser, socket_path))
elif args.mode == "streamable-http":
asyncio.run(
run_streamable_http_gateway(
browser,
host=args.http_host,
port=args.http_port,
path=args.http_path,
allow_origin=args.http_allow_origin,
)
)
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:
asyncio.run(async_main(browser))
async def async_main(browser: MCPBrowser):
"""Async main for interactive mode."""
try:
await browser.initialize()
await interactive_mode(browser)
finally:
await browser.close()
if __name__ == "__main__":
main()