mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-28 10:49:46 +02:00
[eric] agents: the three ungated meta MCP servers ride one process, cutting three idle interpreters per parked chat
This commit is contained in:
@@ -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()
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -288,9 +288,11 @@ const MCP_SERVER_BRAND: Record<string, string> = {
|
||||
'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',
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user