Odysseus v1.0
This commit is contained in:
134
src/builtin_mcp.py
Normal file
134
src/builtin_mcp.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
builtin_mcp.py
|
||||
|
||||
Auto-registration of built-in MCP servers on startup.
|
||||
Each server runs as a stdio subprocess managed by McpManager.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import asyncio
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _find_npx() -> str:
|
||||
"""Find npx binary, checking common locations if not on PATH."""
|
||||
npx = shutil.which("npx")
|
||||
if npx:
|
||||
return npx
|
||||
# Common locations when PATH is minimal (e.g. systemd)
|
||||
for candidate in [
|
||||
os.path.expanduser("~/.npm-global/bin/npx"),
|
||||
os.path.expanduser("~/.local/bin/npx"),
|
||||
"/usr/local/bin/npx",
|
||||
"/usr/bin/npx",
|
||||
]:
|
||||
if os.path.isfile(candidate):
|
||||
return candidate
|
||||
# Try to find node and use npx from same dir
|
||||
node = shutil.which("node")
|
||||
if node:
|
||||
npx_candidate = os.path.join(os.path.dirname(node), "npx")
|
||||
if os.path.isfile(npx_candidate):
|
||||
return npx_candidate
|
||||
return "npx" # fallback, will fail with a clear error
|
||||
|
||||
# Server definitions: id -> (script path relative to project root, display name)
|
||||
#
|
||||
# bash / python / filesystem / web_search were folded into native in-process
|
||||
# execution (src/tool_execution.py:_direct_fallback). Those trivial subprocess
|
||||
# wrappers are gone.
|
||||
#
|
||||
# image_gen / memory / rag / email still run as stdio MCP servers — each
|
||||
# carries hundreds of LOC of unique IMAP / HTTP / manager logic not worth
|
||||
# duplicating into the native path right now.
|
||||
_BUILTIN_SERVERS = {
|
||||
"image_gen": ("mcp_servers/image_gen_server.py", "Built-in: Image Generation"),
|
||||
"memory": ("mcp_servers/memory_server.py", "Built-in: Memory"),
|
||||
"rag": ("mcp_servers/rag_server.py", "Built-in: RAG"),
|
||||
"email": ("mcp_servers/email_server.py", "Built-in: Email"),
|
||||
}
|
||||
|
||||
# NPX-based built-in servers (run via npx, not Python)
|
||||
_BUILTIN_NPX_SERVERS = {
|
||||
"builtin_browser": {
|
||||
"name": "Built-in: Browser",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@playwright/mcp@latest", "--headless", "--caps", "vision"],
|
||||
},
|
||||
}
|
||||
|
||||
# Global flag to disable MCP if there are compatibility issues
|
||||
MCP_DISABLED = os.environ.get("ODYSSEUS_DISABLE_MCP", "").lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
async def register_builtin_servers(mcp_manager):
|
||||
"""Connect all built-in MCP servers to the manager."""
|
||||
if MCP_DISABLED:
|
||||
logger.info("Built-in MCP servers disabled via ODYSSEUS_DISABLE_MCP")
|
||||
return
|
||||
|
||||
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
python = sys.executable
|
||||
|
||||
async def _connect_python_server(server_id: str, script_path: str, name: str):
|
||||
try:
|
||||
ok = await mcp_manager.connect_server(
|
||||
server_id=server_id,
|
||||
name=name,
|
||||
transport="stdio",
|
||||
command=python,
|
||||
args=[script_path],
|
||||
env={"PYTHONPATH": base_dir},
|
||||
)
|
||||
if ok:
|
||||
logger.info(f"Built-in MCP server registered: {name}")
|
||||
else:
|
||||
logger.warning(f"Built-in MCP server failed to connect: {name}")
|
||||
except asyncio.CancelledError:
|
||||
logger.warning(f"Built-in MCP server {name} cancelled")
|
||||
raise
|
||||
except BaseException as e:
|
||||
logger.warning(f"Built-in MCP server {name} error: {type(e).__name__}: {e}")
|
||||
|
||||
for server_id, (script, name) in _BUILTIN_SERVERS.items():
|
||||
script_path = os.path.join(base_dir, script)
|
||||
if not os.path.exists(script_path):
|
||||
logger.warning(f"Built-in MCP server script not found: {script_path}")
|
||||
continue
|
||||
asyncio.create_task(_connect_python_server(server_id, script_path, name))
|
||||
|
||||
# Register NPX-based servers in the background (they take longer to start)
|
||||
npx_path = _find_npx()
|
||||
logger.info(f"NPX binary resolved to: {npx_path}")
|
||||
|
||||
async def _start_npx_servers():
|
||||
await asyncio.sleep(3) # let Python servers finish first
|
||||
for server_id, cfg in _BUILTIN_NPX_SERVERS.items():
|
||||
try:
|
||||
logger.info(f"Starting NPX server: {cfg['name']} ({npx_path} {' '.join(cfg['args'])})")
|
||||
ok = await asyncio.wait_for(
|
||||
mcp_manager.connect_server(
|
||||
server_id=server_id,
|
||||
name=cfg["name"],
|
||||
transport="stdio",
|
||||
command=npx_path,
|
||||
args=cfg["args"],
|
||||
),
|
||||
timeout=30,
|
||||
)
|
||||
if ok:
|
||||
logger.info(f"Built-in NPX server registered: {cfg['name']}")
|
||||
else:
|
||||
logger.warning(f"Built-in NPX server failed to connect: {cfg['name']}")
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f"Built-in NPX server timed out: {cfg['name']}")
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except BaseException as e:
|
||||
logger.warning(f"Built-in NPX server {cfg['name']} error: {type(e).__name__}: {e}")
|
||||
|
||||
asyncio.create_task(_start_npx_servers())
|
||||
Reference in New Issue
Block a user