Part of an ActiveAgent + actionagent dashboard functional review (multi-agent, adversarially verified). Severity: 🟡 Minor.
GET <mount>/mcp falls through the SPA catch-all and returns 200 text/html (the dashboard page) to MCP clients instead of the spec-required text/event-stream or a 405. And the catalog's first-party activeagents entry hardcodes the literal placeholder <mount>/mcp as its endpoint and advertises tools (call_agent, list_agents) the facade doesn't actually expose.
Findings
GET /mcp returns 200 text/html (the dashboard SPA) to MCP clients instead of the spec-required text/event-stream or 405
- Where:
actionagent/config/routes.rb:143 · severity: minor · kind: contract · repo: activeagent
- What breaks: The engine routes POST /mcp to the JSON-RPC facade (routes.rb:137) but the SPA catch-all (routes.rb:143, constraint only excludes '/api/' paths) serves GET /mcp as the dashboard HTML page — the same path deliberately doubles as the MCP Services view deep-link (Dashboard.jsx:58-62). Under the MCP Streamable HTTP transport (protocol 2025-03-26, which both facades advertise), a client MAY issue GET on the endpoint to open the server->client SSE stream, and a server that does not offer one MUST respond 405 Method Not Allowed; clients also DELETE the endpoint to end a session. The engine instead answers GET with 200 text/html (a full HTML document a client will attempt to parse as an event stream — worse than the app copy's plain 404) and DELETE with 404. Real SDK clients treat this as a stream error rather than the clean 'no stream offered' signal, and any operator curl-testing the documented endpoint gets the dashboard page, which reads as the facade being broken.
- Evidence: Live:
curl -H 'Accept: text/event-stream' http://localhost:3001/activeagents/mcp -> 200 text/html; charset=utf-8 (dashboard SPA); curl -X DELETE http://localhost:3001/activeagents/mcp -> 404. App comparison: GET http://localhost:3000/mcp -> 404 text/html (no SPA collision, but also not the spec's 405). Code: routes.rb:137 (post only), :143 (catch-all constraint !request.path.include?("/api/") lets /mcp through to dashboard#index); Dashboard.jsx:58-62 uses the same path as the MCP Services client-side route.
- Suggested fix: In actionagent/config/routes.rb, before the catch-all: route
get "mcp" and delete "mcp" to a small handler in api/mcp (or a lambda constraint on the catch-all) that renders dashboard#index only when the request's Accept prefers text/html, and otherwise responds head :method_not_allowed with Allow: POST (per Streamable HTTP 2025-03-26: GET without SSE support and unsupported DELETE both return 405). Optionally mirror match "mcp", via: [:get, :delete] => 405 in the platform app (app has no /mcp SPA view, so no negotiation needed there).
Catalog's first-party 'activeagents' entry serves the literal placeholder '/mcp' as its endpoint and advertises tools (call_agent, list_agents) the facade does not expose
- Where:
actionagent/app/services/action_agent/mcp_catalog.rb:153 · severity: minor · kind: dx · repo: activeagent
- What breaks: MCPCatalog's activeagents entry hardcodes url: "/mcp" (mcp_catalog.rb:153) with a comment saying it is 'relative to wherever the engine is mounted' — but nothing ever substitutes the real mount path: present() passes it through, ToolDiscovery#server_row passes it through, and McpServersView's detail row renders
server.command || server.url verbatim in the Endpoint code block (McpServersView.jsx:334, 341-345). A user expanding the first-party card to connect an MCP client is shown the un-substituted template string '/mcp' instead of e.g. '/activeagents/mcp'. The same entry's tool_hints ["call_agent", "list_agents"] (mcp_catalog.rb:159) surface in the card's 'Tools offered' row and in TOOL_HINTS attribution, but the facade's actual tools are run_ (mcp_controller.rb:112-131) — neither call_agent nor list_agents exists, so the card documents tools that can never be called and the hint table can misattribute a genuine agent-defined tool named call_agent/list_agents to this server.
- Evidence: Live:
curl http://localhost:3001/activeagents/api/mcp_servers/activeagents -> server.url == "/mcp", server.tools == ["call_agent","list_agents"], while the engine's real MCP tool surface (mcp_controller.rb tools_list) is run_/run___, confirmed on the identical app facade live: tools/list at :3000 returned only run_* names. Rendering path: McpServersView.jsx ServerDetail const connection = server.command || server.url (line 334) displayed in a block labeled Endpoint (lines 341-345).
- Suggested fix: Substitute the mount path at serialization time: have Api::McpServersController (or MCPCatalog.all/find via a mount_path: argument) replace "" with the engine's actual mount point (e.g. ActionAgent::Engine.routes.find_script_name({}) or request.script_name + engine mount) before rendering, so the card shows e.g. "/activeagents/mcp". For tool_hints, drop them for the activeagents entry or replace with the real run_* naming note: keep "call_agent" out (it is already a builtin and ToolDiscovery gives builtins priority, making the hint dead weight) and remove "list_agents" (not an MCP tool and risks misattributing a genuine agent-defined tool of that name). The presented tools list for this first-party entry could instead be derived from the account's agents (run_) or omitted.
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.
GET <mount>/mcpfalls through the SPA catch-all and returns 200 text/html (the dashboard page) to MCP clients instead of the spec-requiredtext/event-streamor a 405. And the catalog's first-partyactiveagentsentry hardcodes the literal placeholder<mount>/mcpas its endpoint and advertises tools (call_agent,list_agents) the facade doesn't actually expose.Findings
GET /mcp returns 200 text/html (the dashboard SPA) to MCP clients instead of the spec-required text/event-stream or 405
actionagent/config/routes.rb:143· severity: minor · kind: contract · repo:activeagentcurl -H 'Accept: text/event-stream' http://localhost:3001/activeagents/mcp-> 200 text/html; charset=utf-8 (dashboard SPA);curl -X DELETE http://localhost:3001/activeagents/mcp-> 404. App comparison: GET http://localhost:3000/mcp -> 404 text/html (no SPA collision, but also not the spec's 405). Code: routes.rb:137 (post only), :143 (catch-all constraint!request.path.include?("/api/")lets /mcp through to dashboard#index); Dashboard.jsx:58-62 uses the same path as the MCP Services client-side route.get "mcp"anddelete "mcp"to a small handler in api/mcp (or a lambda constraint on the catch-all) that renders dashboard#index only when the request's Accept prefers text/html, and otherwise respondshead :method_not_allowedwithAllow: POST(per Streamable HTTP 2025-03-26: GET without SSE support and unsupported DELETE both return 405). Optionally mirrormatch "mcp", via: [:get, :delete] => 405in the platform app (app has no /mcp SPA view, so no negotiation needed there).Catalog's first-party 'activeagents' entry serves the literal placeholder '/mcp' as its endpoint and advertises tools (call_agent, list_agents) the facade does not expose
actionagent/app/services/action_agent/mcp_catalog.rb:153· severity: minor · kind: dx · repo:activeagentserver.command || server.urlverbatim in the Endpoint code block (McpServersView.jsx:334, 341-345). A user expanding the first-party card to connect an MCP client is shown the un-substituted template string '/mcp' instead of e.g. '/activeagents/mcp'. The same entry's tool_hints ["call_agent", "list_agents"] (mcp_catalog.rb:159) surface in the card's 'Tools offered' row and in TOOL_HINTS attribution, but the facade's actual tools are run_ (mcp_controller.rb:112-131) — neither call_agent nor list_agents exists, so the card documents tools that can never be called and the hint table can misattribute a genuine agent-defined tool named call_agent/list_agents to this server.curl http://localhost:3001/activeagents/api/mcp_servers/activeagents-> server.url == "/mcp", server.tools == ["call_agent","list_agents"], while the engine's real MCP tool surface (mcp_controller.rb tools_list) is run_/run___, confirmed on the identical app facade live: tools/list at :3000 returned only run_* names. Rendering path: McpServersView.jsx ServerDetailconst connection = server.command || server.url(line 334) displayed in ablock labeled Endpoint (lines 341-345).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.