[Refactor] Govern MCP work item and note tools through their own container lookup
What does this MR do and why?
MCP governance resolves the containers of a tools/call with the GovernanceContainerResolver. The resolver parses the declared container arguments itself. Each tool finds its container with its own code. !258209 (merged) made the two agree for every tool, but only by keeping two pieces of code in sync.
This is the first of three stacked MRs that remove that duplication. Each governable tool exposes governed_containers, built on the same resolve methods its execute path uses. This MR adds the plumbing and converts the work item and note tools:
GraphqlServicebuilds itsGraphqlTooland asks it forgoverned_containers. A tool that doesn't have the method yet keeps the declared lookup, so tools that haven't converted keepmasterbehaviour.- The work item tools return their parent through
resolve_parentorresolve_work_item_parent.save_work_itemandget_saved_view_work_itemsbuild their tool throughbuild_graphql_tool, because governance now builds a tool for every GraphQL service.save_work_itemcheckswork_item_iidwithpresent?, so a malformed value reaches schema validation instead of raising while governance builds the tool. save_notereturns the parent of the noteable. Aresolve_discussioncall names no noteable, so the tool returns no container and a newrecord:lookup ondiscussion_idgoverns the call by the project or group that holds the discussion.- Record arguments (
parent_id, saved views, work item GIDs,discussion_id) use the declaredrecord:lookups. - When a tool's own lookup raises (not found, forbidden, access denied, or bad arguments), governance treats the call as having no container. The tool then reports its own error at execute time. The tool path runs the
mcp_container_lookup_capcheck first and letsTooManyContainersErrorthrough. BaseService#tool_governed_containersdrops omitted arguments (null,"", zero-filled IDs) before the tool's lookup, the same wayexecutedoes. Without this, a call withurl: ""orproject_id: 0made the lookup raise, so no rule was checked, and the tool then ran ongroup_id.- Telemetry keeps the declaration-based path as
declared_containers. The start event runs beforeset_cred, so it must not do an access-checked lookup. This has to land before any tool's own lookup does one. UrlParser#resolve_work_item_from_urlnow returns the work item instead of its Global ID, andsave_notecalls it to find the work item a URL names.
The stack:
- This MR: plumbing, work items and notes.
- !268283: merge requests, commits and repositories.
- !268284: the rest of CE and EE, then
GraphqlTool#governed_containersraisesNotImplementedErrorby default and the fallback goes.
!261051 stacks on the third.
How to reproduce the issue
There is no user-facing defect. On master, governance for a work item tool goes through the resolver's parsing of the declared arguments and never reaches the tool's resolve methods.
- On
master, buildMcp::Tools::WorkItems::GetWorkItemServiceand callgoverned_containerswith a work item URL. - You can see that the result comes from
containers_from_identifiers, andGetWorkItemTool#resolve_work_item_parentis never called.
How to test the fix
- On this branch, repeat the call. The result is the parent that
GetWorkItemTool#resolve_work_item_parentreturns, which is the parentbuild_variablesuses. - Call
governed_containersfor a tool this MR doesn't convert, such asget_merge_request. It still returns the declared lookup's result.
Run the specs:
bin/rspec spec/services/mcp ee/spec/services/mcp ee/spec/services/ee/mcp spec/lib/api/mcp/handlers ee/spec/lib/ee/api/mcp/handlers ee/spec/lib/ai/tool_rules spec/requests/api/mcp ee/spec/requests/api/mcpReferences
- Issue: https://gitlab.com/gitlab-org/gitlab/-/work_items/633015
- Built on (merged): !258209 (merged)
- Split proposal: !261045 (comment 4162846677)
- Next in the stack: !268283
- Follow-up MR: !261051