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 ofpublic,internal,private.include_subgroups(boolean, optional): Iftrue, recursively include all descendant subgroups. Defaults tofalse(direct children only).first(integer, optional, 1-100, default: 20): page size.after(string, optional): cursor from the previous response'spage_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:
idfull_pathnamedescription(optional)visibilityparent_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 eitheridorfull_pathdirectly. - Agents to build a mental model of the organizational hierarchy.
Resolved open questions
Should this tool be backed primarily by REST (GraphQL —/groups) or GraphQL?Query.groups(Resolvers::GroupsResolver→Resolvers::Namespaces::BaseGroupsResolver) already covers search, parent-scoping, and cursor pagination with proper authorization; no reason to hand-roll a REST wrapper or aBase::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_pathis optional on the resolver already; omitting it lists top-level/all-accessible groups perall_available/top_level_only.What limits onUse cursor pagination (per_page/total results?first/after) like the rest of the MCP tools, capped at 100 per page — not REST'spage/per_page.ShouldInclude it —include_subgroupsbe supported initially or deferred?GroupsFinderalready supports the underlying behavior (include_parent_descendants), it's just not exposed as a resolver argument yet (see Resources).
Use cases
- Agent navigates the organizational hierarchy
- Agent calls
list_groupsto discover top-level groups. - User selects a group.
- Agent calls
list_groupswithparent_idto 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.
- Agent calls
- Multi-group analysis flows
- Custom agent: "Show me all groups under our organization that have 'platform' in the name."
- Calls
list_groupswithparent_id = org_group,search = "platform". - Iterates over the returned groups to perform follow-up actions (for example, list projects, aggregate issues, etc.).
- 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_groupsto:- Render a group/namespace picker.
- Use the selected group with other GitLab MCP tools (
list_projects,search, etc.).
- Onboarding and discovery
- A new team member asks their AI assistant: "Help me understand how our GitLab is organized."
- Agent calls
list_groupsrecursively to map out the hierarchy, then summarizes the structure.
Resources
- Existing GraphQL query:
Query.groups(app/graphql/types/query_type.rb) →Resolvers::GroupsResolver→Resolvers::Namespaces::BaseGroupsResolver(app/graphql/resolvers/namespaces/base_groups_resolver.rb). Already supportssearch,parent_path,top_level_only,all_available, cursor pagination, and authorization viaGroupType#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 }(forparent_id). - Gap:
visibility/include_subgroupsfilters aren't exposed as resolver arguments yet, even thoughGroupsFinder(app/finders/groups_finder.rb) already supports both underneath (params[:visibility],params[:include_parent_descendants]). Filed#609450to addvisibility_level/include_subgroupsarguments toBaseGroupsResolver, mirroring the existingvisibility_levelargument onResolvers::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 resolvingparent_idto a group'sfull_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-builderskill'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
- Two classes:
Mcp::Tools::Groups::ListGroupsTool < Mcp::Tools::Base::GraphqlToolandMcp::Tools::Groups::ListGroupsService < Base::GraphqlService. - Operation file:
app/graphql/queries/mcp/groups/list_groups.query.graphql, callinggroups(parentPath:, search:, visibilityLevel:, includeSubgroups:, first:, after:), selectingid,fullPath,name,description,visibility,parent { id }. parent_id→parentPath: resolve viafind_parent_by_id_or_path!(:group, identifier)(fromResourceFinder) and pass the resolved group'sfull_path— the resolver'sparent_pathargument only accepts a path string, not a numeric id.visibility/include_subgroups: pass straight through asvisibilityLevel/includeSubgroupsonce#609450lands (blocks this issue).id/parent_id:GroupType#idreturns a GID (BaseObject#id→GitlabSchema.id_from_object) — unwrap the numeric id for bothidand the nestedparent.idinprocess_result, same pattern as the Duo Workflow tools'workflow_id.- Pagination:
first/after, returningpage_info.has_next_page/end_cursorfrom the connection'spageInfo. - Register in
GRAPHQL_TOOLSinapp/services/mcp/tools/manager.rb. - Specs:
spec/graphql/all_queries_spec.rbcoverage comes free from the committed.graphqlfile; add Tool/Service specs mirroringlist_merge_requests_{tool,service}_spec.rb— no filter, parent filter, search filter, visibility filter,include_subgroups, pagination, empty result.