mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-08-08 22:48:46 -04:00
Merge verified Odysseus fixes
This commit is contained in:
+61
-15
@@ -87,6 +87,7 @@ _BUILTIN_NPX_SERVERS = {
|
||||
|
||||
# Global flag to disable MCP if there are compatibility issues
|
||||
MCP_DISABLED = os.environ.get("ODYSSEUS_DISABLE_MCP", "").lower() in ("1", "true", "yes")
|
||||
BROWSER_MCP_REQUIRE_CACHE = os.environ.get("ODYSSEUS_BROWSER_MCP_REQUIRE_CACHE", "").lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
# Strong references to the fire-and-forget startup tasks scheduled below.
|
||||
@@ -103,6 +104,46 @@ def _spawn_bg(coro) -> asyncio.Task:
|
||||
task.add_done_callback(_BG_TASKS.discard)
|
||||
return task
|
||||
|
||||
def _find_browser_executable() -> str:
|
||||
"""Find a browser binary for the built-in Playwright MCP server.
|
||||
|
||||
Docker images ship Debian's `chromium`; desktop installs may already have
|
||||
Chrome/Chromium in a conventional location. If nothing is found, return an
|
||||
empty string and let Playwright MCP use its own default browser/channel.
|
||||
"""
|
||||
configured = os.environ.get("ODYSSEUS_BROWSER_EXECUTABLE", "").strip()
|
||||
if configured:
|
||||
return configured
|
||||
for name in ("google-chrome", "chromium", "chromium-browser"):
|
||||
path = shutil.which(name)
|
||||
if path:
|
||||
return path
|
||||
for candidate in (
|
||||
"/opt/google/chrome/chrome",
|
||||
"/usr/bin/google-chrome",
|
||||
"/usr/bin/chromium",
|
||||
"/usr/bin/chromium-browser",
|
||||
):
|
||||
if os.path.isfile(candidate):
|
||||
return candidate
|
||||
return ""
|
||||
|
||||
|
||||
def _browser_mcp_args(args: list[str]) -> list[str]:
|
||||
"""Return Playwright MCP args with a concrete browser executable when found."""
|
||||
out = list(args or [])
|
||||
if "--executable-path" not in out:
|
||||
browser = _find_browser_executable()
|
||||
if browser:
|
||||
out.extend(["--executable-path", browser])
|
||||
if os.environ.get("ODYSSEUS_BROWSER_ISOLATED", "1").lower() not in ("0", "false", "no"):
|
||||
if "--isolated" not in out and "--user-data-dir" not in out:
|
||||
out.append("--isolated")
|
||||
if os.environ.get("ODYSSEUS_BROWSER_NO_SANDBOX", "1").lower() not in ("0", "false", "no"):
|
||||
if "--no-sandbox" not in out and "--sandbox" not in out:
|
||||
out.append("--no-sandbox")
|
||||
return out
|
||||
|
||||
|
||||
def builtin_python_env(base_dir: str) -> dict[str, str]:
|
||||
"""Environment for built-in Python MCP subprocesses.
|
||||
@@ -162,39 +203,44 @@ async def register_builtin_servers(mcp_manager):
|
||||
async def _start_npx_servers():
|
||||
await asyncio.sleep(3) # let Python servers finish first
|
||||
for server_id, cfg in _BUILTIN_NPX_SERVERS.items():
|
||||
# Skip the server if its npx package isn't cached. Without this
|
||||
# check, npx would try to download/install the package on first
|
||||
# use, which can take minutes (or hang) on fresh installs without
|
||||
# Playwright system deps. Wrapping that in asyncio.wait_for to
|
||||
# bound the wait sounds reasonable, but mcp.client.stdio uses an
|
||||
# internal anyio task group that can't survive the resulting
|
||||
# cross-task cancellation: it raises "Attempted to exit cancel
|
||||
# scope in a different task than it was entered in" in a sibling
|
||||
# task, which cascades cancellations into the rest of the event
|
||||
# loop and downs the app. Detecting installed-state up-front lets
|
||||
# us bail with a useful warning before we ever touch stdio_client.
|
||||
args = cfg["args"]
|
||||
# Browser automation is a shipped built-in, so the default path
|
||||
# lets `npx -y` install @playwright/mcp on first start. Locked-down
|
||||
# installs can opt back into the old no-network startup behavior
|
||||
# with ODYSSEUS_BROWSER_MCP_REQUIRE_CACHE=1.
|
||||
args = _browser_mcp_args(cfg["args"]) if server_id == "builtin_browser" else list(cfg["args"])
|
||||
pkg_spec = _npx_package_from_args(args)
|
||||
if pkg_spec and not await _is_npx_package_cached(npx_path, pkg_spec):
|
||||
if BROWSER_MCP_REQUIRE_CACHE and pkg_spec and not await _is_npx_package_cached(npx_path, pkg_spec):
|
||||
logger.warning(
|
||||
f"{cfg['name']} is not available.\n"
|
||||
f" Reason: npm package {pkg_spec!r} is not installed in the npx cache.\n"
|
||||
f" Impact: tools provided by this MCP server will be unavailable.\n"
|
||||
f" Fix: {os.path.basename(npx_path)} -y {pkg_spec} --version\n"
|
||||
f" (run once, then restart Odysseus)\n"
|
||||
f" Notes: this server is optional; see README.md "
|
||||
f"'Built-in MCP servers' for details."
|
||||
f" Notes: ODYSSEUS_BROWSER_MCP_REQUIRE_CACHE=1 is set, "
|
||||
f"so Odysseus will not install browser automation on startup."
|
||||
)
|
||||
continue
|
||||
|
||||
logger.info(f"Starting NPX server: {cfg['name']} ({npx_path} {' '.join(args)})")
|
||||
try:
|
||||
env = None
|
||||
if server_id == "builtin_browser":
|
||||
cache_home = os.environ.get(
|
||||
"ODYSSEUS_BROWSER_MCP_CACHE",
|
||||
os.path.join(base_dir, "data", "local", "playwright-mcp-cache"),
|
||||
)
|
||||
os.makedirs(cache_home, exist_ok=True)
|
||||
env = {
|
||||
"XDG_CACHE_HOME": cache_home,
|
||||
"PLAYWRIGHT_BROWSERS_PATH": os.path.join(cache_home, "browsers"),
|
||||
}
|
||||
ok = await mcp_manager.connect_server(
|
||||
server_id=server_id,
|
||||
name=cfg["name"],
|
||||
transport="stdio",
|
||||
command=npx_path,
|
||||
args=args,
|
||||
env=env,
|
||||
)
|
||||
if ok:
|
||||
logger.info(f"Built-in NPX server registered: {cfg['name']}")
|
||||
|
||||
Reference in New Issue
Block a user