Add list_groups MCP tool to GitLab MCP server

Note

Before picking up this work: this issue adds an MCP tool. Please follow the Adding a new tool guidance first. That includes creating an MCP Tool Proposal, using verb_object naming (get_ / list_ / save_ / delete_), and the shared resource-identification input base classes.

Summary

Add an MCP server tool (list_groups) that returns a list of GitLab groups and subgroups accessible to the authenticated user, so MCP clients and Duo Agent Platform agents can dynamically discover and navigate the GitLab group hierarchy without hard-coding group IDs or paths.

Parent epic: &20529 (closed)

Related: #591304 (closed) (list_projects)


Problem to solve

Today, the GitLab MCP server exposes several tools (for example, create_issue, get_issue, create_merge_request, get_merge_request_*, get_pipeline_jobs, search, semantic_code_search, etc.), but there is no dedicated tool to list groups or subgroups via MCP.

The search tool can use scope="projects" to search projects, but there is no equivalent for groups. For MCP-based agents and external MCP clients that orchestrate work across a GitLab organization, it is hard to:

  • Discover which groups and subgroups are available to the current user.
  • Navigate the organizational hierarchy (top-level group → subgroups → projects).
  • Build flows like "show me the group structure, then drill into a subgroup's projects and issues" without hard-coding group IDs or full paths.
  • Understand the organizational structure of a GitLab instance or namespace.

This is particularly limiting for Duo Agent Platform + MCP workflows where agents should be able to reason over "my groups" or "subgroups under this parent group" in a generic way. Combined with the proposed list_projects tool (#591304 (closed)), this would enable full hierarchy navigation: groups → subgroups → projects.


Proposal

Add a new MCP server tool, list_groups, backed by the existing Query.groups GraphQL query — no new API surface needed for most of this, see Resources.

Tool name

  • list_groups

User-facing description

List GitLab groups and subgroups accessible to the current user, optionally filtered by parent group, search term, or visibility. Useful for navigating the organizational hierarchy, discovering groups to operate on with other MCP tools, or understanding the structure of a GitLab namespace.

Input parameters

  • parent_id (string, optional): ID or full path of a parent group to list subgroups of. If omitted, lists top-level groups accessible to the user.
  • search (string, optional): Filter by group name or path.
  • visibility (string, optional): One of public, internal, private.
  • include_subgroups (boolean, optional): If true, recursively include all descendant subgroups. Defaults to false (direct children only).
  • first (integer, optional, 1-100, default: 20): page size.
  • after (string, optional): cursor from the previous response's page_info.end_cursor.

Output

A structured list of groups, designed to be easy for agents to parse and use in follow-up calls. For each group, include at least:

  • id
  • full_path
  • name
  • description (optional)
  • visibility
  • parent_id (if a subgroup)

This allows:

  • Duo agents and external MCP clients to show a group/namespace picker.
  • Follow-up tool calls (for example, list_projects, search, or future tools) to use either id or full_path directly.
  • Agents to build a mental model of the organizational hierarchy.

Resolved open questions

  • Should this tool be backed primarily by REST (/groups) or GraphQL? GraphQL — Query.groups (Resolvers::GroupsResolverResolvers::Namespaces::BaseGroupsResolver) already covers search, parent-scoping, and cursor pagination with proper authorization; no reason to hand-roll a REST wrapper or a Base::CustomService. See Resources below for the two filters (visibility, include_subgroups) that need a small resolver addition first.
  • Should we support both "global" and "parent-scoped" listing, or start with parent-scoped only? Both, for free — parent_path is optional on the resolver already; omitting it lists top-level/all-accessible groups per all_available/top_level_only.
  • What limits on per_page/total results? Use cursor pagination (first/after) like the rest of the MCP tools, capped at 100 per page — not REST's page/per_page.
  • Should include_subgroups be supported initially or deferred? Include it — GroupsFinder already supports the underlying behavior (include_parent_descendants), it's just not exposed as a resolver argument yet (see Resources).

Use cases

  1. Agent navigates the organizational hierarchy
    • Agent calls list_groups to discover top-level groups.
    • User selects a group.
    • Agent calls list_groups with parent_id to discover subgroups.
    • Agent calls list_projects (from #591304 (closed)) scoped to the selected group.
    • Agent then operates on the selected project with existing MCP tools.
  2. Multi-group analysis flows
    • Custom agent: "Show me all groups under our organization that have 'platform' in the name."
    • Calls list_groups with parent_id = org_group, search = "platform".
    • Iterates over the returned groups to perform follow-up actions (for example, list projects, aggregate issues, etc.).
  3. External MCP clients (Cursor, Claude Code, etc.)
    • User connects an MCP-compatible client to the GitLab MCP server.
    • User asks: "What groups do I have access to?" or "Show me the subgroups under my-org."
    • Client uses list_groups to:
      • Render a group/namespace picker.
      • Use the selected group with other GitLab MCP tools (list_projects, search, etc.).
  4. Onboarding and discovery
    • A new team member asks their AI assistant: "Help me understand how our GitLab is organized."
    • Agent calls list_groups recursively to map out the hierarchy, then summarizes the structure.

Resources

  • Existing GraphQL query: Query.groups (app/graphql/types/query_type.rb) → Resolvers::GroupsResolverResolvers::Namespaces::BaseGroupsResolver (app/graphql/resolvers/namespaces/base_groups_resolver.rb). Already supports search, parent_path, top_level_only, all_available, cursor pagination, and authorization via GroupType#authorize :read_group.
  • Output fields all exist on Types::GroupType (app/graphql/types/group_type.rb): id (GID, needs unwrapping — see plan step 3), full_path, name, description, visibility, parent { id } (for parent_id).
  • Gap: visibility/include_subgroups filters aren't exposed as resolver arguments yet, even though GroupsFinder (app/finders/groups_finder.rb) already supports both underneath (params[:visibility], params[:include_parent_descendants]). Filed #609450 to add visibility_level/include_subgroups arguments to BaseGroupsResolver, mirroring the existing visibility_level argument on Resolvers::ProjectsResolver — this issue is blocked by it.
  • Resource resolution precedent: Mcp::Tools::Concerns::ResourceFinder#find_parent_by_id_or_path! (app/services/mcp/tools/concerns/resource_finder.rb) for resolving parent_id to a group's full_path.
  • Reference shape: list_merge_requests (app/services/mcp/tools/merge_requests/list_merge_requests_{tool,service}.rb) — closest existing GraphQL-backed list tool with cursor pagination.
  • MCP dev guidelines: doc/development/duo_agent_platform/mcp/_index.md; gitlab-mcp-tool-builder skill's build recipe — verify a GraphQL field exists before writing a custom one.
  • Add list_projects MCP tool (#591304) — companion issue for project listing.

Implementation Plan

  1. Two classes: Mcp::Tools::Groups::ListGroupsTool < Mcp::Tools::Base::GraphqlTool and Mcp::Tools::Groups::ListGroupsService < Base::GraphqlService.
  2. Operation file: app/graphql/queries/mcp/groups/list_groups.query.graphql, calling groups(parentPath:, search:, visibilityLevel:, includeSubgroups:, first:, after:), selecting id, fullPath, name, description, visibility, parent { id }.
  3. parent_idparentPath: resolve via find_parent_by_id_or_path!(:group, identifier) (from ResourceFinder) and pass the resolved group's full_path — the resolver's parent_path argument only accepts a path string, not a numeric id.
  4. visibility/include_subgroups: pass straight through as visibilityLevel/includeSubgroups once #609450 lands (blocks this issue).
  5. id/parent_id: GroupType#id returns a GID (BaseObject#idGitlabSchema.id_from_object) — unwrap the numeric id for both id and the nested parent.id in process_result, same pattern as the Duo Workflow tools' workflow_id.
  6. Pagination: first/after, returning page_info.has_next_page/end_cursor from the connection's pageInfo.
  7. Register in GRAPHQL_TOOLS in app/services/mcp/tools/manager.rb.
  8. Specs: spec/graphql/all_queries_spec.rb coverage comes free from the committed .graphql file; add Tool/Service specs mirroring list_merge_requests_{tool,service}_spec.rb — no filter, parent filter, search filter, visibility filter, include_subgroups, pagination, empty result.
Edited by 🤖 GitLab Bot 🤖