MCP tool rules cannot enforce on record-id or argument-less calls
Problem
The external MCP server resolves which project or group a tool call acts on by having each tool declare which argument names the container, for example { project: :project_id }. Some tools cannot declare a container at all and are marked ungovernable!, so admin-configured Allow, Ask and Deny rules on these tools cannot be enforced.
Two categories exist. Category A tools take only a record id, and the owning project can only be found by loading that record, which the namespace resolver deliberately avoids. Category B tools take no arguments at all, so there is no container to resolve, and they fall back to the caller's default GitLab Duo namespace, which is often unset.
Reproduced on a local GDK on 2026-08-30: tools/call get_mcp_server_version with a group_id argument was correctly denied under an Always Deny rule, but the same call with no arguments executed ungoverned.
Affected tools
- Category A, record id only:
get_duo_session(workflow_id),get_vulnerability(vulnerability_id) - Category B, no arguments:
get_mcp_server_version, and thetools/listrequest
Proposed direction
For Category A, add a :record container kind so a tool can declare, for example, { record: :vulnerability_id }, plus a loader method returning the record's owning project. This is a proposal, not a decided design, and carries three constraints:
- Resolution runs before the tool authorizes anything, so this loads a record from a caller-supplied id before knowing the caller may see it. That is acceptable only because the handler checks
read_projectandread_groupon the resolved container immediately afterwards and answers not found otherwise. That check is added in !253147. - A
:recordkind on a list argument would mean one record load per identifier, all before authorization. Restrict:recordto scalar arguments. - Tool instances are created fresh per call, so the loader can memoize and the tool can reuse the loaded record when it executes, avoiding a second query.
Open questions
- Category B has no id to load, so the only lever is the default-namespace fallback. No fix is proposed here.
- Should a call that resolves no namespace deny by default, instead of falling back to an unset default namespace?
References
- Declaration contract added in !253145 (merged)
- Enforcement added in !253147