ToolStore Agent
refactor: split super-files into focused modules
7f5f1dd
Raw
History Blame Contribute Delete
4.74 kB
"""
Shared helpers for the management server API handlers.
- Config I/O (load / save with camelCase ↔ snake_case normalisation)
- MCP connection helpers
"""
from __future__ import annotations
import json
import os
import signal
import subprocess
import time
from pathlib import Path
from typing import Any
from ..config_manager import ConfigManager
from ..mcp_client import FullMCPClient, disconnect_all
# ── Constants ──
_SPA_MCP_KEY = "mcp_servers"
_CLI_MCP_KEY = "mcpServers"
_DEFAULT_CFG: dict[str, Any] = {
"mcp_servers": {},
"tools": {},
"skills": {},
"toolsets": {},
}
def _config_manager() -> ConfigManager:
cm = ConfigManager()
cm.load()
return cm
def load_config() -> dict:
cfg = dict(_DEFAULT_CFG)
cm = _config_manager()
cli_servers = cm.config.get(_CLI_MCP_KEY, {})
if cli_servers:
cfg[_SPA_MCP_KEY] = dict(cli_servers)
for k in ("tools", "skills", "toolsets"):
if k in cm.config:
cfg[k] = cm.config[k]
return cfg
def save_config(cfg: dict) -> None:
cm = _config_manager()
spa_servers = cfg.get(_SPA_MCP_KEY, {})
if spa_servers:
cm.config[_CLI_MCP_KEY] = dict(spa_servers)
cm.config["tools"] = cfg.get("tools", {})
cm.config["skills"] = cfg.get("skills", {})
cm.config["toolsets"] = cfg.get("toolsets", {})
cm.save()
# ── MCP connection state ──
_connected_clients: dict[str, FullMCPClient] = {}
_mcp_processes: dict[str, subprocess.Popen] = {}
def mcp_status(server_id: str) -> str:
return "connected" if server_id in _connected_clients else "disconnected"
def _start_mcp_folder(server_id: str, srv: dict) -> bool:
fp = Path(srv.get("folder", "")).expanduser().resolve()
if not fp.is_dir():
raise RuntimeError(f"MCP folder not found: {fp}")
command = srv.get("command", "python")
args = list(srv.get("args", []))
env = {**os.environ, **(srv.get("env") or {})}
for k, v in list(env.items()):
if isinstance(v, str) and v.startswith("${") and v.endswith("}"):
env[k] = os.environ.get(v[2:-1], "")
proc = subprocess.Popen([command] + args, cwd=str(fp), env=env,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
_mcp_processes[server_id] = proc
return True
def connect_and_discover(server_id: str, srv: dict) -> list[dict]:
transport_type = srv.get("transport") or srv.get("type")
if not transport_type:
transport_type = "stdio" if srv.get("command") else "sse"
if transport_type == "folder":
_start_mcp_folder(server_id, srv)
time.sleep(1.5)
sub_transport = srv.get("sub_transport", "sse")
url = srv.get("url", "")
elif transport_type == "docker":
sub_transport, url = "docker", ""
else:
sub_transport, url = transport_type, srv.get("url", "")
client = FullMCPClient(server_id, {
"type": sub_transport, "transport": sub_transport,
"command": srv.get("command", ""),
"args": srv.get("args", []),
"url": url, "env": srv.get("env", {}),
"timeout": srv.get("timeout", 30),
"image": srv.get("image", ""),
"entrypoint": [srv.get("command", "python")] + srv.get("args", []),
})
client.connect()
tools_info = client.list_tools()
_connected_clients[server_id] = client
tool_list = tools_info.get("tools", []) if isinstance(tools_info, dict) else (
tools_info if isinstance(tools_info, list) else [])
tools: list[dict] = []
for t in tool_list:
tools.append({
"name": t.get("name", ""),
"description": t.get("description", ""),
"parameters": t.get("inputSchema", t.get("parameters", {})),
})
return tools
def shutdown_mcp_process(server_id: str) -> None:
proc = _mcp_processes.pop(server_id, None)
if proc is None:
return
try:
proc.send_signal(signal.SIGTERM)
try: proc.wait(timeout=5)
except subprocess.TimeoutExpired: proc.kill()
except Exception: pass
def disconnect_server(server_id: str) -> None:
shutdown_mcp_process(server_id)
client = _connected_clients.pop(server_id, None)
if client:
try: client.disconnect()
except Exception: pass
def disconnect_all_clients() -> None:
for cid in list(_connected_clients.keys()):
disconnect_server(cid)
for pid in list(_mcp_processes.keys()):
shutdown_mcp_process(pid)
try: disconnect_all()
except Exception: pass
def count_mcp_tools(cfg: dict, server_id: str) -> int:
prefix = f"mcp:{server_id}"
return sum(1 for v in cfg.get("tools", {}).values()
if v.get("source") == prefix)