Add list_work_items MCP server tool
What does this MR do and why?
This MR adds list_work_items, a GraphQL-backed MCP server tool, implementing
#605850 (closed) from the work-items tools bucket
gitlab-org#22782 (closed).
The tool is a collection reader for the work-item domain, scoped to a group or a project: it filters by state,
author, assignees, labels, milestone, dates, type, and free text, sorts, and paginates via a cursor (first,
default 20, max 100, after). Each call returns compact rows plus pageInfo. The tool is read-only,
annotated readOnlyHint: true. EE adds a health_status_filter and a status filter.
The tool replaces the DAP Python list_work_items tool; deprecating it is tracked in
#609451, out of scope here.
The change follows the merged !246413 (merged)
(list_merge_requests) and the gitlab-mcp-tool-builder skill.
The first commit extracts the shared WorkItemsQueryBuilder out of get_saved_view_work_items with no
behavior change; the saved-view specs pass unmodified as the parity proof. The second commit adds the tool on
top of the extracted builder.
Design decisions
- Filter, node-selection, and pagination logic live in the shared
Mcp::Tools::WorkItems::WorkItemsQueryBuilder(CE plus an EE overlay), so this tool andget_saved_view_work_itemsshare one filter set instead of ~150 duplicated lines. - The query root is
Namespace.workItems, so one document covers project and group scope; descendant-scoping arguments exist only on the namespace/group resolver. Group scope includes descendant projects and subgroups by fixed policy, matching the saved-view tool's default. - The
stateenum covers onlyopened,closed, andall; the resolver's prepare lambda raises onlocked, so it is excluded from the schema rather than surfaced at runtime. - The
sortenum is the stable, edition-independent subset fromWorkItems::SortingKeys::DEFAULT_SORTING_KEYS, upcased; widget-derived sort keys vary by edition/license and are excluded to keep the versioned schema identical on CE and EE. A spec locks the enum literally, so any drift in the constant is a conscious decision. - EE-only filters ride an
input_schemadeep-merge plus anagent_filtershook.health_status_filteraccepts only the three real statuses (onTrack,needsAttention,atRisk);NONE/ANYare cut because the sharedcamelize(:lower)transform mangles all-caps values, a latent issue shared with saved views and out of scope here.statusaccepts only{name}; both are additive later. author_usernameis singular, matching the CE resolver argument; plural exists only inside theor:composite input.- The output shape is
{"work_items": [...], "pageInfo": {"endCursor", "hasNextPage"}}, matching the example ratified in the issue. - Parent (project/group) resolution reuses the shared
Mcp::Tools::Concerns::ResourceFinderconcern used by every parent-scoped MCP tool. A nonexistent namespace raises"... not found or inaccessible"while an existing namespace the caller cannot read raises"Access denied to project: '...'", so the two cases stay distinguishable. This MR adds specs covering both paths but leaves the concern's behavior unchanged, since unifying the messages would affect every tool that uses it and belongs in a shared follow-up rather than this tool MR.
References
- Issue: #605850 (closed)
- Bucket epic: gitlab-org#22782 (closed)
- DAP migration plan: #609451
- Precedent MR: !246413 (merged)
- The tool description instructs agents to fetch a single work item's details with
get_work_item, which is added in !250081 (merged); that MR is expected to be reviewed alongside this one.
The bucket issue carries the ratified per-tool spec for this tool.
Screenshots or screen recordings
No UI changes.
All identification paths and filters were exercised end to end on a running GDK (EE, SaaS-simulated), via raw JSON-RPC and the MCP Inspector (CLI and web UI, over the OAuth flow), using the official SDK handshake.
== tools/list: advertised=true annotations={"readOnlyHint"=>true} schema_size=2959 chars
== EE schema overlay active: ["health_status_filter", "status"]
== project scope: isError=false count=20 pageInfo=["endCursor", "hasNextPage"]
== group scope (descendants): isError=false includes_project_items=true
== state filter: opened=10 closed=13 disjoint=true
== types=[TASK]: isError=false count=1 sample=28/Task
== sort UPDATED_DESC first=3: sorted=true
== cursor round-trip: page1=2 hasNext=true page2=2 overlap=[]
== EE health_status_filter=onTrack: isError=false count=1
== state=locked rejected: Validation error: Invalid state: 'locked'. Must be one of: opened, closed, all
== first=101 rejected: Validation error: first is invalid
== unknown param rejected: Validation error: bogus is invalid
== no identifier rejected: Validation error: Must provide either project_id or group_id
== saved-view + get_workitem_notes regression: passMCP Inspector screenshots
The tool as an MCP client renders it, with the read-only annotation and the advertised schema:
Cursor round-trip, first page (first: 2): pageInfo returns endCursor and hasNextPage: true, first row is iid 28:
Second page (after set to that cursor): continues at iid 26 with no overlap:
How to set up and validate locally
- Restart Rails web so the tool registry picks up the new entry (memoized at boot):
gdk restart rails-web. - Create a personal access token scoped to
[:api, :mcp]in the Rails console:token = User.find_by_username('root').personal_access_tokens.create!( name: 'mcp-verify', scopes: [:api, :mcp], expires_at: 7.days.from_now) token.set_token('someknownvalue') token.save! - Call
tools/listover the MCP endpoint with curl; confirm an entry with.name == "list_work_items". - Call with
{"project_id": "<project>"}, then{"group_id": "<group>"}; confirm both return compact rows pluspageInfo. - Narrow filters (
state,types,label_name), setfirst: 2, passendCursorback asafter, and confirm the two pages do not overlap. - Send
state: "locked"; confirm the validation error names the allowed values (opened,closed,all).
Automated coverage: builder, tool, and service unit specs, incl. a full input_schema version lock and
uniform not-found coverage; EE specs for licensed filters; entries in CE and EE list_tools annotation
contracts; saved-view specs unchanged.
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.


