mcp-browser/mcp_browser/daemon_main.py

141 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""
MCP Browser Daemon - Socket server for MCP Browser.
This daemon provides a persistent MCP Browser instance that clients can connect to.
"""
import os
import sys
import asyncio
import argparse
import signal
from pathlib import Path
from typing import Optional
from .proxy import MCPBrowser
from .daemon import MCPBrowserDaemon, get_socket_path
from .logging_config import setup_logging, get_logger
async def run_daemon(args):
"""Run the MCP Browser daemon."""
logger = get_logger(__name__)
# Create browser instance
browser = MCPBrowser(
server_name=args.server,
config_path=Path(args.config) if args.config else None,
enable_builtin_servers=not args.no_builtin
)
# Get socket path
socket_path = get_socket_path(args.server)
# Create and run daemon
daemon = MCPBrowserDaemon(browser, socket_path)
logger.info(f"Starting MCP Browser daemon on {socket_path}")
try:
await daemon.start()
except KeyboardInterrupt:
logger.info("Daemon shutting down...")
except Exception as e:
logger.error(f"Daemon error: {e}")
raise
finally:
await daemon.stop()
def handle_systemd_socket():
"""Check for systemd socket activation."""
# Check if we're running under systemd with socket activation
listen_pid = os.environ.get('LISTEN_PID')
listen_fds = os.environ.get('LISTEN_FDS')
if listen_pid and listen_fds and int(listen_pid) == os.getpid():
# We have systemd socket activation
num_fds = int(listen_fds)
if num_fds > 0:
# Use the first socket FD (SD_LISTEN_FDS_START = 3)
return 3
return None
def main():
"""Main entry point for daemon."""
parser = argparse.ArgumentParser(
description="MCP Browser Daemon - Persistent socket server",
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument("--server", "-s", help="Target MCP server name")
parser.add_argument("--config", "-c", help="Custom configuration file path")
parser.add_argument("--no-builtin", action="store_true",
help="Disable built-in servers")
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("--socket", help="Custom socket path (overrides default)")
parser.add_argument("--foreground", "-f", action="store_true",
help="Run in foreground (don't daemonize)")
parser.add_argument("--pid-file", help="Write PID to file")
args = parser.parse_args()
# Setup logging
log_file = Path(args.log_file) if args.log_file else None
setup_logging(
debug=args.log_level == "DEBUG",
log_file=log_file,
log_level=args.log_level
)
logger = get_logger(__name__)
# Check for systemd socket activation
systemd_fd = handle_systemd_socket()
if systemd_fd:
logger.info("Running with systemd socket activation")
# TODO: Implement systemd socket handling
# Handle PID file
if args.pid_file:
with open(args.pid_file, 'w') as f:
f.write(str(os.getpid()))
# Daemonize if not in foreground mode
if not args.foreground:
# Fork to background
pid = os.fork()
if pid > 0:
# Parent process
print(f"Started daemon with PID {pid}")
sys.exit(0)
# Child process continues
os.setsid()
# Redirect stdin/stdout/stderr
with open(os.devnull, 'r') as devnull:
os.dup2(devnull.fileno(), sys.stdin.fileno())
with open(os.devnull, 'w') as devnull:
os.dup2(devnull.fileno(), sys.stdout.fileno())
if not args.log_file:
os.dup2(devnull.fileno(), sys.stderr.fileno())
# Set up signal handlers
def signal_handler(signum, frame):
logger.info(f"Received signal {signum}")
asyncio.create_task(daemon.stop())
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
# Run the daemon
asyncio.run(run_daemon(args))
if __name__ == "__main__":
main()