From 4d86b3377d4d654bacdec010ce0d437d7ce4bc54 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Sat, 8 Aug 2026 09:41:45 -0700 Subject: [PATCH] [eric] agents: the three ungated meta MCP servers ride one process, cutting three idle interpreters per parked chat --- .../apps/agents/combined_meta_mcp_server.py | 90 +++++++++++++++++++ .../manager/register_builtin_mcp_servers.py | 41 ++------- backend/tests/test_headless_mode.py | 2 +- .../test_register_builtin_mcp_servers.py | 15 ++-- .../app/pages/AgentChat/parsing/toolLabels.ts | 2 + 5 files changed, 107 insertions(+), 43 deletions(-) create mode 100644 backend/apps/agents/combined_meta_mcp_server.py diff --git a/backend/apps/agents/combined_meta_mcp_server.py b/backend/apps/agents/combined_meta_mcp_server.py new file mode 100644 index 00000000..da315ae1 --- /dev/null +++ b/backend/apps/agents/combined_meta_mcp_server.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""One stdio MCP process hosting the always-on, ungated meta tools that used to be three separate +python interpreters per agent CLI (MCPList/Search/Activate, SettingsRead/Write, CreateApp). + +Why merge only these three: their tool NAMES are globally unique, none of them is referenced by +the non-bypassable permission gate (grep for `mcp__openswarm-mcp-meta__` etc. finds nothing in +build_effective_tool_lists / path_gate), and all three are registered unconditionally with the +same env. So collapsing them into one process is pure fan-out reduction with zero behavior change: +5 parked CLIs drop 15 idle interpreters to 5 (ENG-208). The gate-coupled servers (schedule, web, +browser-agent) and the conditional ones (skill, show-ui, spawn, invoke) stay separate on purpose. + +We reuse each sub-server's own TOOLS + handle_tool_call; we own the stdio loop so their main() and +send_response never run. Sibling import (not backend.*) matches how these scripts are launched by +path in both dev and the packaged bundle.""" + +import json +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import apps_mcp_server as p_apps # noqa: E402 +import mcp_meta_server as p_meta # noqa: E402 +import settings_meta_server as p_settings # noqa: E402 + +P_SUBSERVERS = [p_meta, p_settings, p_apps] + +TOOLS = [] +P_ROUTE = {} +for p_mod in P_SUBSERVERS: + for p_tool in p_mod.TOOLS: + TOOLS.append(p_tool) + P_ROUTE[p_tool["name"]] = p_mod + + +def send_response(id_, result=None, error=None): + msg = {"jsonrpc": "2.0", "id": id_} + if error is not None: + msg["error"] = error + else: + msg["result"] = result + sys.stdout.write(json.dumps(msg) + "\n") + sys.stdout.flush() + + +def main(): + for line in sys.stdin: + line = line.strip() + if not line: + continue + try: + msg = json.loads(line) + except json.JSONDecodeError: + continue + method = msg.get("method") + id_ = msg.get("id") + params = msg.get("params", {}) + if method == "initialize": + send_response(id_, { + "protocolVersion": "2024-11-05", + "capabilities": {"tools": {}}, + "serverInfo": {"name": "openswarm-core", "version": "1.0.0"}, + }) + elif method == "notifications/initialized": + pass + elif method == "tools/list": + send_response(id_, {"tools": TOOLS}) + elif method == "tools/call": + tool_name = params.get("name", "") + arguments = params.get("arguments", {}) + mod = P_ROUTE.get(tool_name) + if mod is None: + send_response(id_, {"content": [{"type": "text", "text": f"Unknown tool: {tool_name}"}], "isError": True}) + continue + try: + send_response(id_, mod.handle_tool_call(tool_name, arguments)) + except Exception as e: + send_response(id_, error={"code": -32000, "message": str(e)}) + elif method in ("resources/list",): + send_response(id_, {"resources": []}) + elif method in ("prompts/list",): + send_response(id_, {"prompts": []}) + elif method == "ping": + send_response(id_, {}) + elif id_ is not None: + send_response(id_, error={"code": -32601, "message": f"Method not found: {method}"}) + + +if __name__ == "__main__": + main() diff --git a/backend/apps/agents/manager/register_builtin_mcp_servers.py b/backend/apps/agents/manager/register_builtin_mcp_servers.py index f0a69348..f72ff938 100644 --- a/backend/apps/agents/manager/register_builtin_mcp_servers.py +++ b/backend/apps/agents/manager/register_builtin_mcp_servers.py @@ -98,13 +98,14 @@ def register_builtin_mcp_servers( "type": "stdio", } - # Always-on meta-MCP server. Exposes MCPList / MCPSearch / MCPActivate so the model can discover and activate user MCPs at runtime. The activation gate (active_mcps filter in build_mcp_servers above) ensures the model cannot reach any other MCP server's tools without going through this layer first. - mcp_meta_server_path = os.path.join( - agents_dir, "mcp_meta_server.py" - ) - mcp_servers["openswarm-mcp-meta"] = { + # Always-on core meta-MCP server: ONE process hosting the three ungated always-on servers that + # used to be three interpreters (MCP discovery MCPList/Search/Activate, SettingsRead/Write, + # CreateApp). None is referenced by the permission gate, so this is pure fan-out reduction + # (ENG-208). The activation gate still governs external MCPs exactly as before, via MCPActivate. + combined_meta_path = os.path.join(agents_dir, "combined_meta_mcp_server.py") + mcp_servers["openswarm-core"] = { "command": sys.executable, - "args": [mcp_meta_server_path], + "args": [combined_meta_path], "env": { "OPENSWARM_PORT": os.environ.get("OPENSWARM_PORT", "8324"), "OPENSWARM_AUTH_TOKEN": get_auth_token(), @@ -134,34 +135,6 @@ def register_builtin_mcp_servers( "type": "stdio", } - # Always-on settings-meta server: SettingsRead / SettingsWrite let the agent read and edit its own OpenSwarm Settings autonomously. The backend (/api/settings-meta) enforces the only two guardrails: it can't disconnect the credential powering this run, and reads come back with secrets redacted. No activation gate, Settings is the agent's own house, not a third-party MCP. - settings_meta_server_path = os.path.join( - agents_dir, "settings_meta_server.py" - ) - mcp_servers["openswarm-settings-meta"] = { - "command": sys.executable, - "args": [settings_meta_server_path], - "env": { - "OPENSWARM_PORT": os.environ.get("OPENSWARM_PORT", "8324"), - "OPENSWARM_AUTH_TOKEN": get_auth_token(), - "OPENSWARM_PARENT_SESSION_ID": session.id, - }, - "type": "stdio", - } - - # Always-on apps server: CreateApp lets ANY agent spin up a live App card on the canvas. This replaced the standalone App Builder page; the tool result carries the App Builder reference so no mode switch is needed. - apps_server_path = os.path.join(agents_dir, "apps_mcp_server.py") - mcp_servers["openswarm-apps"] = { - "command": sys.executable, - "args": [apps_server_path], - "env": { - "OPENSWARM_PORT": os.environ.get("OPENSWARM_PORT", "8324"), - "OPENSWARM_AUTH_TOKEN": get_auth_token(), - "OPENSWARM_PARENT_SESSION_ID": session.id, - }, - "type": "stdio", - } - # ShowUI renders rich inline components from the tool_call input (display only, server just # validates); AskUI renders an interactive component and BLOCKS on /api/ui-requests/wait until # the user answers in the transcript. Gated on the ShowUI builtin perm. diff --git a/backend/tests/test_headless_mode.py b/backend/tests/test_headless_mode.py index 2f884d1e..edc65f27 100644 --- a/backend/tests/test_headless_mode.py +++ b/backend/tests/test_headless_mode.py @@ -100,7 +100,7 @@ def test_headless_drops_the_renderer_bound_servers_and_tools(monkeypatch, no_ren # The rest of the surface is untouched; headless prunes the renderer, it doesn't lobotomise the agent. assert "Read" in allowed and "Bash" in allowed assert "openswarm-invoke-agent" in mcp_servers - assert "openswarm-apps" in mcp_servers + assert "openswarm-core" in mcp_servers def test_without_headless_every_one_of_them_is_offered(monkeypatch, no_renderer): diff --git a/backend/tests/test_register_builtin_mcp_servers.py b/backend/tests/test_register_builtin_mcp_servers.py index 1147708b..217e3808 100644 --- a/backend/tests/test_register_builtin_mcp_servers.py +++ b/backend/tests/test_register_builtin_mcp_servers.py @@ -16,18 +16,18 @@ def test_registers_always_on_and_delegation_servers(): mcp_servers = {} browser_tools, invoke_tools = register_builtin_mcp_servers( mcp_servers, p_session(), {}, None, None) - # always-on - assert "openswarm-mcp-meta" in mcp_servers - assert "openswarm-settings-meta" in mcp_servers - assert "openswarm-apps" in mcp_servers + # always-on: the three ungated meta servers ride ONE combined process now (ENG-208) + assert "openswarm-core" in mcp_servers + assert "openswarm-mcp-meta" not in mcp_servers + assert "openswarm-settings-meta" not in mcp_servers + assert "openswarm-apps" not in mcp_servers # delegation (not denied) assert "openswarm-browser-agent" in mcp_servers assert "openswarm-invoke-agent" in mcp_servers assert browser_tools == ["CreateBrowserAgent", "BrowserAgent", "BrowserAgents", "AppAgent"] assert invoke_tools == ["InvokeAgent"] # Every registered server's script path must resolve to a file that ACTUALLY EXISTS. This is the assertion that catches a moved-caller resolving the wrong agents dir. - for name in ("openswarm-mcp-meta", "openswarm-settings-meta", "openswarm-apps", - "openswarm-browser-agent", "openswarm-invoke-agent"): + for name in ("openswarm-core", "openswarm-browser-agent", "openswarm-invoke-agent"): script = mcp_servers[name]["args"][0] assert os.path.isfile(script), f"{name} script does not exist on disk: {script}" @@ -38,5 +38,4 @@ def test_fully_denied_delegation_servers_are_not_registered(): register_builtin_mcp_servers(mcp_servers, p_session(), perms, None, None) assert "openswarm-browser-agent" not in mcp_servers # all browser tools denied -> skip assert "openswarm-invoke-agent" not in mcp_servers - assert "openswarm-mcp-meta" in mcp_servers # always-on regardless - assert "openswarm-apps" in mcp_servers # always-on regardless + assert "openswarm-core" in mcp_servers # always-on regardless diff --git a/frontend/src/app/pages/AgentChat/parsing/toolLabels.ts b/frontend/src/app/pages/AgentChat/parsing/toolLabels.ts index a28b7ca2..fb376e7b 100644 --- a/frontend/src/app/pages/AgentChat/parsing/toolLabels.ts +++ b/frontend/src/app/pages/AgentChat/parsing/toolLabels.ts @@ -288,9 +288,11 @@ const MCP_SERVER_BRAND: Record = { 'stripe': 'Stripe', 'openswarm-browser-agent': 'browser', 'openswarm-invoke-agent': 'helper', + 'openswarm-core': 'tools', 'openswarm-mcp-meta': 'tools', 'openswarm-outputs-meta': 'views', 'openswarm-settings-meta': 'settings', + 'openswarm-apps': 'apps', 'openswarm-web': 'the web', };