Make MCP tools/list spec self-updating and lock tool schemas

What does this MR do and why?

The MCP tools/list request specs hard-coded the full serialized shape of every tool in one contain_exactly block (≈640 lines in EE), so every new tool needed a manual edit there — and the spec never actually verified a tool was registered in the Manager. This MR replaces that with a self-updating membership check and moves each tool's contract assertion into that tool's own spec, upgrading those to exhaustive eq so a silently added/removed/retyped property fails the build.

Mostly spec changes, plus a one-line production fix: normalizing the search_labels annotations (a read-only tool that was carrying a stray destructiveHint: false), which the new annotation lock surfaced.

References

Follow-up (separate MR): assert each MCP-enabled API endpoint's route_setting :mcp tool_name/params via the existing 'an endpoint with mcp route setting' shared example, so changes to the API tools' MCP gating are caught near their definitions.

What changed, and how CE/EE is handled
  • tools/list specs (EE + FOSS) — replaced the per-tool golden blocks with self-updating assertions:
    • every concrete Mcp::Tools::BaseService descendant is surfaced by Manager#list_tools (a tool defined but not registered fails);
    • every non-aggregator route_setting :mcp endpoint is surfaced.
  • Annotations are locked by one table. The former expected_tool_names, read-only, and write annotation examples are collapsed into a single expected_annotations map keyed by tool name. Keys assert membership via match_array (add/remove a tool → fails); values assert correctness via eq (a wrong/missing/stray readOnlyHint/destructiveHint → fails). This caught search_labels carrying a stray destructiveHint: false and it was normalized to { readOnlyHint: true }, matching the other read-only tools.
  • Per-tool contracts now live in each tool's own spec. The service specs that previously asserted only a subset of properties (have_key / partial type checks) are upgraded to a single exhaustive eq on input_schema, which catches additions, removals, type changes, and description changes.
  • CE/EE split — schemas that legitimately differ between CE and EE are not locked exhaustively in the FOSS spec:
    • FOSS specs assert version_metadata('0.1.0')[:input_schema], which is CE-stable (the EE overrides for search and link_work_items modify only the runtime input_schema);
    • the EE additions are asserted in the ee/ specs. The EE search spec now covers all four advanced-search / exact-code-search capability states, including the previously untested work item type filter;
    • the EE annotations table includes the EE-only semantic_code_search tool.
  • API tools are unchanged here — their schema is generated from the Grape params block and is covered generically by api_tool_spec.rb; the per-endpoint declaration check is the follow-up above.

Screenshots or screen recordings

No UI changes.

How to set up and validate locally

  1. Run the refactored request specs:
    bundle exec rspec spec/requests/api/mcp/handlers/list_tools_spec.rb ee/spec/requests/api/mcp/handlers/list_tools_spec.rb
  2. Run the upgraded contract specs:
    bundle exec rspec spec/services/mcp/tools ee/spec/services/ee/mcp/tools/search_service_spec.rb
  3. Sanity-check the self-updating guard: comment out a tool's entry in app/services/mcp/tools/manager.rb and confirm the list_tools membership example fails with the offending class name.
  4. Sanity-check the annotation lock: flip a tool's readOnlyHint/destructiveHint and confirm the 'locks each tool to its publicly-contracted annotations' example fails with a clear diff.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Terri Chu

Merge request reports

Loading