Fix missing namespace on MCP tool-call events

What does this MR do and why?

About 33% of start_mcp_tool_call events have no namespace. The issue lists three causes:

  1. The lookup only read project_id and group_id. Tools that take id (route-backed API tools), url, full_path, or project_full_path got nothing.
  2. track_start_event ran before fetch_tool, so the tool's argument shape was not known yet.
  3. The lookup was memoized with the mutable params hash as its key. Aggregated tools rewrite params[:arguments] in place, so the finish event looked up the rewritten arguments and missed (for example, semantic_search drops project_id).

This MR:

  • Moves track_start_event after fetch_tool in lib/api/mcp/handlers/call_tool.rb. When the tool is not found, the start event still fires next to the finish event, so event counts do not change.
  • Resolves the scope from the tool's own container declaration by calling governed_containers, the same declaration that tool governance uses. That covers project_id, group_id, id (route boundary_type), url (via UrlParser), full_path-style names, and record: resolvers. If the tool is unknown, the default argument names are used.
  • Resolves the scope once per call, before the tool runs. The start event, the finish event, and the log line all reuse that one result, so an aggregated tool's in-place rewrite can no longer change it.
  • Tools marked ungovernable! declare no container arguments, so they are now the only tools that resolve no namespace.
  • Adds gl_root_namespace_id to the mcp.log tool-call line as a regular field, so it is always indexed and can be searched in Kibana. Only the numeric ID is logged, not the path.

Security: no permission check

The lookup uses Gitlab::ResourceLookup and the record resolvers (find_by_id). Neither checks abilities, which matches the old lookup_project / lookup_group behaviour. The result only goes to telemetry and to the expanded-logging gate. It is never returned to the caller. Any error raised during resolution is rescued: ArgumentError (too many identifiers) silently, anything else is reported with Gitlab::ErrorTracking.track_exception. The call then continues with no namespace, so the tool response is never affected.

⚠️ Expanded-logging consequence (needs reviewer sign-off)

The resolved root namespace is also what gates expanded logging in mcp.log. In EE::Gitlab::Mcp::Logger#should_log_expanded?, return false unless namespace means that today, on instances where neither enabled_expanded_logging nor :expanded_ai_logging is on, the roughly 33% of calls with no resolved namespace never log arguments or error_message.

After this MR, those calls resolve a namespace. When that root namespace has ai_usage_data_collection_enabled, their argument values and error messages will appear in mcp.log. No new opt-in is added. This is the customer's existing opt-in, and the gate no longer fails closed by accident. Expect mcp.log volume to grow for opted-in namespaces. This was agreed as an acceptance criterion in the issue discussion. A reviewer needs to sign off on it explicitly.

Deviation from the issue wording: aggregated tools

The acceptance criterion says aggregated tools should resolve "using the rewritten arguments". This MR does not call transform_arguments / select_tool to get them. Those methods can raise, they look up users (SaveMergeRequestService#resolve_usernames), and they could run checks that do not belong in a telemetry path. Instead, it resolves from the aggregated tool's own declaration on the arguments the caller sent. The rule is the same either way: governable_tools_container_spec already requires every governable aggregated tool to declare a container argument from its own input schema. The scope is computed before the rewrite, so the rewrite cannot erase it. The spec covers semantic_search, which removes project_id when it rewrites arguments.

No event definition changes: start_mcp_tool_call and finish_mcp_tool_call already list the namespace identifier. No feature flag: the change only affects telemetry attribution, and the logging consequence is described above.

References

Closes #630105 (closed)

  • Epic: &23391
  • Related: the alias-flag issue. It also needs track_start_event after fetch_tool, which this MR does.

Screenshots or screen recordings

Not applicable. This is a backend telemetry and logging change with no UI.

Before After
get_pipeline with id: "group/project": event namespace nil event namespace is the project's namespace

How to set up and validate locally

  1. In GDK, connect an MCP client to https://gdk.test:3443/api/v4/mcp.
  2. Call get_pipeline with id set to a project path, and get_merge_request with only a url.
  3. In log/mcp.log, check that each tool_call line has gl_root_namespace_id set.
  4. In log/snowplow (or Snowplow Micro), check that the start_mcp_tool_call and finish_mcp_tool_call events carry namespace_id.
  5. Call get_server_version (marked ungovernable!) and check that gl_root_namespace_id is null.

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.

Merge request reports

Loading
Loading