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 the tools/list request

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_project and read_group on the resolved container immediately afterwards and answers not found otherwise. That check is added in !253147.
  • A :record kind on a list argument would mean one record load per identifier, all before authorization. Restrict :record to 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

Edited by 🤖 GitLab Bot 🤖