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/listspecs (EE + FOSS) — replaced the per-tool golden blocks with self-updating assertions:- every concrete
Mcp::Tools::BaseServicedescendant is surfaced byManager#list_tools(a tool defined but not registered fails); - every non-aggregator
route_setting :mcpendpoint is surfaced.
- every concrete
- Annotations are locked by one table. The former
expected_tool_names, read-only, and write annotation examples are collapsed into a singleexpected_annotationsmap keyed by tool name. Keys assert membership viamatch_array(add/remove a tool → fails); values assert correctness viaeq(a wrong/missing/strayreadOnlyHint/destructiveHint→ fails). This caughtsearch_labelscarrying a straydestructiveHint: falseand 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 exhaustiveeqoninput_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 forsearchandlink_work_itemsmodify only the runtimeinput_schema); - the EE additions are asserted in the
ee/specs. The EEsearchspec now covers all four advanced-search / exact-code-search capability states, including the previously untested work itemtypefilter; - the EE annotations table includes the EE-only
semantic_code_searchtool.
- FOSS specs assert
- API tools are unchanged here — their schema is generated from the Grape
paramsblock and is covered generically byapi_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
- 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 - Run the upgraded contract specs:
bundle exec rspec spec/services/mcp/tools ee/spec/services/ee/mcp/tools/search_service_spec.rb - Sanity-check the self-updating guard: comment out a tool's entry in
app/services/mcp/tools/manager.rband confirm thelist_toolsmembership example fails with the offending class name. - Sanity-check the annotation lock: flip a tool's
readOnlyHint/destructiveHintand 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.