Log accurate tool_status for Orbit tools on the main MCP server

What does this MR do and why?

Orbit tools on the main MCP server logged tool_status: ok for a failed query_graph, and used coarse reasons for Orbit errors. A failed query now logs its real status, NOT_FOUND logs not_found, a rejected Rails JWT (UNAUTHENTICATED) logs error, and "no enabled namespaces" logs unauthorized. What the agent sees is unchanged.

Closes #634643 (closed)

How to verify

Call query_graph through /api/v4/mcp with a query Orbit rejects and check log/mcp.log: tool_status is no longer ok. The response body is identical to before.

UNAUTHENTICATED maps to error, not unauthorized: for this call it only means Orbit rejected, or never got, the JWT Rails signed, which is a Rails-to-Orbit trust problem rather than a user lacking access.

Agent context
  • Mcp::Tools::Orbit::CommandTool now runs the QueryRunner itself and returns OrbitQuery with the runner response attached, so the query finishes before CallTool logs. render_mcp_result renders that stored response through the new render_orbit_response!; the standalone Orbit endpoints still use render_orbit_query!.
  • QueryRunner::Response gets an error_reason, set by McpRenderer from the query error code or failure status (via Response.reason_for_http_status). The EE failed_result override turns a failed one into a Response.error, and a new discard_result hook closes the streamed body if CallTool raises after the query ran.
  • For a failed OrbitQuery, CallTool logs the failure but returns the tool's own result untouched. Hash error results behave as before.
  • gRPC mapping, checked against orbit-server origin/main (24575c48d): InvokeAgentCommand returns NOT_FOUND for an unknown command or get_query_dsl under GQL, INVALID_ARGUMENT for bad parameters, FAILED_PRECONDITION for intercepted commands (kept bad_request), and UNAUTHENTICATED only from JWT validation (kept error, see above). PERMISSION_DENIED is only raised by authorize_traversal_path, which the traversal-path RPCs call, so there is no mapping for it and it falls to error. Error text is unchanged.
  • Limits: a streamed query logs ok once streaming starts, so a mid-stream failure (already logged by QueryRunner) is not reflected in tool_status. A client disconnect (499 response, no error_reason) also logs ok. Fixing both needs deferring the log until the body closes.
  • The Workhorse send_orbit_query path was removed by !263077 (merged), so only the QueryRunner path exists.
  • Exceptional path: an exception from params_for or QueryRunner.new now happens inside the tool, so it becomes an isError tool result ("Tool execution failed: ...") instead of a Grape 500.
  • The DB connection release now also runs at the start of the stream body, since CallTool's tail work runs between execute and the stream.
  • CallTool#failed_result is overridden in EE for OrbitQuery; CE only treats isError hashes as failures.
  • Specs run in a Caproni instance: command_tool_spec, both call_tool_specs, orbit/mcp_handlers/call_tool_spec, query_runner_params_for_spec, orbit_tools_spec, orbit/mcp_spec, orbit/data_spec and both MCP base_specs pass. query_runner_spec needs mirrord limited to the DB and Redis ports, since mirrord otherwise breaks its in-process gRPC server; with that, all 53 examples pass. RuboCop is clean.
  • Labels copied from the issue, minus automation:quick-win-judged. Milestone 19.6 matches the issue.
Edited by Dmitry Gruzd

Merge request reports

Loading
Loading