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
  1. Filter, node-selection, and pagination logic live in the shared Mcp::Tools::WorkItems::WorkItemsQueryBuilder (CE plus an EE overlay), so this tool and get_saved_view_work_items share one filter set instead of ~150 duplicated lines.
  2. 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.
  3. The state enum covers only opened, closed, and all; the resolver's prepare lambda raises on locked, so it is excluded from the schema rather than surfaced at runtime.
  4. The sort enum is the stable, edition-independent subset from WorkItems::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.
  5. EE-only filters ride an input_schema deep-merge plus an agent_filters hook. health_status_filter accepts only the three real statuses (onTrack, needsAttention, atRisk); NONE/ANY are cut because the shared camelize(:lower) transform mangles all-caps values, a latent issue shared with saved views and out of scope here. status accepts only {name}; both are additive later.
  6. author_username is singular, matching the CE resolver argument; plural exists only inside the or: composite input.
  7. The output shape is {"work_items": [...], "pageInfo": {"endCursor", "hasNextPage"}}, matching the example ratified in the issue.
  8. Parent (project/group) resolution reuses the shared Mcp::Tools::Concerns::ResourceFinder concern 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

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: pass
MCP Inspector screenshots

The tool as an MCP client renders it, with the read-only annotation and the advertised schema:

inspector_tool_form

Cursor round-trip, first page (first: 2): pageInfo returns endCursor and hasNextPage: true, first row is iid 28:

inspector_cursor_page1

Second page (after set to that cursor): continues at iid 26 with no overlap:

inspector_cursor_page2

How to set up and validate locally

  1. Restart Rails web so the tool registry picks up the new entry (memoized at boot): gdk restart rails-web.
  2. 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!
  3. Call tools/list over the MCP endpoint with curl; confirm an entry with .name == "list_work_items".
  4. Call with {"project_id": "<project>"}, then {"group_id": "<group>"}; confirm both return compact rows plus pageInfo.
  5. Narrow filters (state, types, label_name), set first: 2, pass endCursor back as after, and confirm the two pages do not overlap.
  6. 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.

Edited by Tian Gao

Merge request reports

Loading
Loading