Skip to content

MCP Services: "Start in sandbox" is a no-op that still bills quota; show 404s for detected servers; "View its tools" drops the server key #388

Description

@TonsOfFun

Part of an ActiveAgent + actionagent dashboard functional review (multi-agent, adversarially verified). Severity: 🟠 Major.

The MCP Services view is wired to dead ends: launch provisions a sandbox tagged with the server key but nothing ever starts the MCP server (yet it bills execution quota and reports "running"); GET /api/mcp_servers/:id 404s for detected-but-uncatalogued servers that the index itself returns; and the "View its tools →" button passes a server key the Dashboard handler ignores, so ToolsView can't filter to that server.

Findings

MCP Services 'Start in sandbox' never starts the MCP server: no code path consumes the session's mcp_servers or the catalog command, yet it bills execution quota and reports 'running'

  • Where: actionagent/app/controllers/action_agent/api/mcp_servers_controller.rb:65 · severity: major · kind: missing · repo: activeagent
  • What breaks: POST /api/mcp_servers/:id/launch (McpServersView's 'Start in sandbox' button) creates a SandboxSession tagged mcp_servers=[key] and provisions it, but nothing anywhere launches the server: SandboxProvisionJob in dev/test just sleeps 0.5s and marks the session ready with a hardcoded cloud_run_url of http://localhost:3000/api/sandbox (the platform app's host/port — wrong for an engine mounted anywhere, e.g. the dummy at :3001/activeagents); with no host-registered backend the orchestrator defaults to the in-memory MockSandboxBackend, which stores a hash entry with fake url http://127.0.0.1:8080 and receives neither the session's mcp_servers nor the catalog's command; SandboxRunJob (what actually executes tasks in the sandbox, via api/sandboxes/:id/run) makes plain provider chat calls with a canned system prompt and never references mcp_servers; SandboxSession#mcp_catalog_entries has zero callers; PlaywrightMCPClient connects to a fixed PLAYWRIGHT_MCP_URL env endpoint unrelated to launched sandboxes. Meanwhile the controller runs enforce_execution_quota! and record_execution_usage (line 92), so on a quota-enforcing host each no-op launch consumes an execution unit, and the UI tells the user ' is starting in a sandbox (...)' and shows a green 'sandbox running' chip. The controller comment 'Starts the server inside a sandbox session' and the catalog's 'Provisioning — entries marked sandbox: true can be started inside a sandbox session, using the command recorded here' are both unimplemented.
  • Evidence: Live: curl -X POST http://localhost:3001/activeagents/api/mcp_servers/filesystem/launch -> 201 {"sandbox":{..."status":"ready","cloud_run_url":"http://localhost:3000/api/sandbox","mcp_servers":["filesystem"]},...} — 'ready' with a URL pointing at a different app on a different port, and no @modelcontextprotocol/server-filesystem process anywhere. Code path read end-to-end: mcp_servers_controller.rb:65-97 (launch, record_execution_usage at 92); sandbox_provision_job.rb:39-47 (simulate_provisioning, hardcoded URL at 44) and :22-27 (real path passes only the session to the backend); mock_sandbox_backend.rb:13-33 (ignores mcp_servers, fake url); sandbox_run_job.rb (zero references to mcp — grep -n mcp sandbox_run_job.rb empty; runs are direct Anthropic/OpenAI/Ollama chat calls); grep -rn mcp_catalog_entries actionagent/ shows only the definition (sandbox_session.rb:50-52); playwright_mcp_client.rb:13 (fixed PLAYWRIGHT_MCP_URL). UI claim: McpServersView.jsx:112 ('is starting in a sandbox'), :253-257 ('sandbox running' chip).
  • Suggested fix: In the activeagent repo (engine): (a) short term, in MCPServersController#launch skip record_execution_usage when provisioning is simulated/mock (or move usage recording to when a sandbox run actually executes), and have SandboxSession#summary or the launch response flag simulated provisioning so McpServersView.jsx:112 and the "sandbox running" chip (:253-257) can say "sandbox provisioned (simulated)" instead of claiming the server is starting; (b) derive simulate_provisioning's cloud_run_url (sandbox_provision_job.rb:44) from the engine mount/request host instead of hardcoding http://localhost:3000/api/sandbox; (c) real fix: pass sandbox.mcp_catalog_entries (command/package/transport) into SandboxOrchestrator#create_sandbox and the backend contract, and make SandboxRunJob expose the launched servers' tools to the model, giving mcp_catalog_entries its first caller.

GET /api/mcp_servers/:id 404s for detected-but-uncatalogued servers that the index itself returns

  • Where: actionagent/app/controllers/action_agent/api/mcp_servers_controller.rb:19 · severity: minor · kind: contract · repo: activeagent
  • What breaks: The MCP services list is documented and implemented as a union — catalog defaults plus servers detected from traffic or declared by agents, with non-catalog servers listed as known:false ('undocumented' badge in McpServersView). But the show action's before_action set_catalog_entry (mcp_servers_controller.rb:19, :113-116) renders 404 'Unknown MCP server' whenever MCPCatalog.find(params[:id]) is nil, so any server the index legitimately lists that isn't in the hardcoded catalog cannot be fetched individually — and show's own fallback expression server: server || @catalog_entry (line 54) is dead code for exactly the rows it was written for. The endpoint's stated purpose ('One server with the tools detected for it, so the view can expand a row without refetching the whole inventory') breaks for the undocumented rows, which are the ones the controller comments call 'worth surfacing'.
  • Evidence: Live: created an agent declaring a non-catalog server (curl -X POST http://localhost:3001/activeagents/api/agents -d '{"agent":{"name":"MCP Probe Agent","provider":"openai","model":"gpt-4o","mcp_servers":["custom-thing"]}}' -> 201). Index then lists it: GET /activeagents/api/mcp_servers -> [('custom-thing','configured',known:false)]. But GET /activeagents/api/mcp_servers/custom-thing -> 404 {"error":"Unknown MCP server: custom-thing"}. Code: set_catalog_entry at mcp_servers_controller.rb:113-116 applied to :show via before_action at line 19; unreachable fallback at line 54. Engine tests only cover the catalog-hit and catalog-miss cases (tool_discovery_test.rb:369, :378), never the listed-but-uncatalogued case.
  • Suggested fix: Change line 19 to before_action :set_catalog_entry, only: [ :launch ]. In show, compute server from discovery as it already does, fall back to MCPCatalog.find(params[:id]), and render 404 only when both are nil. Add a regression test: create an agent with mcp_servers: ["custom-thing"], assert GET /activeagents/api/mcp_servers/custom-thing returns 200 with server.key == "custom-thing" and known == false.

MCP Services 'View its tools' drops the server key: Dashboard's onOpenTools handler ignores it and ToolsView has no server filter

  • Where: actionagent/frontend/pages/Dashboard.jsx:380 · severity: minor · kind: dx · repo: activeagent
  • What breaks: In the expanded MCP server card, the 'View its tools →' button calls onOpenTools(server.key) (McpServersView.jsx:387) intending to show that server's tools. The Dashboard handler is onOpenTools={() => { setFocusServer(null); navigateTo('tools'); }} (Dashboard.jsx:380-383) — the key argument is discarded — and ToolsView accepts only { onOpenServer } with internal filters limited to origin/query/hours (ToolsView.jsx:56-71), so there is no way to express a per-server filter anyway. The user lands on the full, unfiltered tool inventory regardless of which server they clicked. The reverse link works (ToolsView tool row -> onOpenServer(tool.mcp_server) -> setFocusServer(key) -> MCP view opens that server expanded, Dashboard.jsx:370-373), making the asymmetry look like an unfinished port of the same pattern.
  • Evidence: Code path read end-to-end: actionagent/frontend/components/dashboard/McpServersView.jsx:385-391 (button passes server.key); actionagent/frontend/pages/Dashboard.jsx:376-384 (handler takes no parameter, clears focusServer, navigates); actionagent/frontend/components/dashboard/ToolsView.jsx:56 (props: only onOpenServer) and :66-78 (fetch params: hours/origin/q only — no server param; the backend /api/tools does support filtering only by origin/q/hours per tools_controller). No URL query is passed either (navigateTo('tools') -> dashboardPath('/tools')).
  • Suggested fix: In Dashboard.jsx accept the key and pass it down: onOpenTools={(key) => { setFocusServer(null); setToolsServerFilter(key); navigateTo('tools'); }} with new state toolsServerFilter (cleared when the user changes filters or leaves the view). Give ToolsView a serverFilter prop and apply it client-side (filter tools where tool.mcp_server === serverFilter — the inventory is already loaded, no backend change needed), showing a dismissible "filtered to " chip. Rebuild actionagent/app/assets/builds afterward.

Verification

Each finding above was produced by a dedicated per-feature review agent, then confirmed by an independent adversarial verifier (all rated high-confidence; zero rejected in this set). File:line citations are against the current main/HEAD of each repo; many were reproduced live against a booted dashboard.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions