feat(workitems): extend MCP coverage, add assignee filter and extend work item fields

Description

Lifts the MCP tool surface from "most commands work, and only inside a repo" to "every read-oriented command works, even when you're standalone". Along the way it expands glab work-items from a thin list skeleton into a planning surface, fixes a GraphQL-compatibility bug in the work-items query, and unblocks strict MCP clients that were rejecting array-shaped JSON responses.

What's in the box

MCP tool coverage and annotation hygiene

  • A new walker test, TestEveryLeafCommandHasMCPAnnotation, fails CI when any leaf command with a RunE lacks an MCP annotation. Forgotten annotations used to drop commands from the tool surface silently, which should be fixed.
  • Closes annotation gaps on mr note list (Safe), mr note resolve / mr note reopen (Destructive), and duo cli (Marked interactive, since the TTY session can't be driven by the MCP subprocess runner via stdio).
  • Standardises the mixed Safe: "false" idiom on Destructive: "true" across milestone create / delete / edit, project members add / remove, and runner assign. Runtime-identical; grep no longer returns the ambiguous form.
  • Per-command annotation tests on the four read commands planning workflows depend on, so any future failure reads as "annotation missing" rather than "some unrelated list test broke".

mr list and issue list work without a repo

Scope now resolves in three tiers: explicit repo (from -R or cwd), then a new default_group config key, then the user-level /issues and /merge_requests endpoints with scope=all. Opt into the middle tier with:

glab config set default_group gitlab-org

Two new helpers, projectListIssueOptionsToAll and projectListMROptionsToAll, translate between the project-scoped and user-level option types. A handful of negation fields flip between scalar and slice and get promoted in the process. listAllMRs mirrors api.ListMRs' per-id fan-out for assignee / reviewer filters on the user-level endpoint, so behaviour matches the repo-scoped path.

Work items list and view

  • --assignee / -A filter wired through to the GraphQL assigneeUsernames input. Repeatable or comma-separated.
  • --mine prepends your username to the assignee filter; with no repo and no --group / -R, falls back to the currentUser.workItems connection so results span every namespace you can see.
  • New STATUS column replaces STATE. Shows the status-widget value when set ("In progress", "Won't do", and friends); falls back to new for open items and completed for closed, so the cell is never blank.
  • NAMESPACE, DUE DATE, and PARENT columns appear when any row has data. [blocked] joins the title in red when the EE LinkedItems widget flags the item.
  • Labels, milestone, and timestamps ride through to --output json alongside the existing fields.
  • glab work-items view <iid> renders a detail block (identity, metadata, description, children) and supports JSON output. Same flag model as list: auto-detects repo scope, accepts --group or -R, carries mcpannotations.Safe.

Work items GraphQL compatibility fix

The original query asked for assignees / labels / milestone at the top level of WorkItem, but GitLab's schema exposes those through the widgets array. UnmarshalJSON now walks widgets and lifts each field onto its flat equivalent, so display, JSON output, and MCP consumers keep seeing the same shape. Verified against gitlab-org/gitlab's work_item_metadata_widgets.fragment.graphql.

WorkItemWidgetStatus, WorkItemWidgetLinkedItems, and WorkItemWidgetHealthStatus are Premium/Ultimate-only - on CE those types don't exist and the fragment fails validation. A new doWithEEFallback includes EE spreads on the first try; on a matching validation error a process-wide flag flips and the retry drops them. CE users pay one extra round-trip per process and that's it.

todo list carries group context

glab todo list --output json now exposes group.full_path, which the SDK's gitlab.Todo doesn't model. MCP clients can tell group-level epic mentions from project issue assignments without a second API call. A local Todo wrapper embeds gitlab.Todo by value and adds the Group field; listTodos hits /todos via NewRequest / Do so the raw JSON flows through the wrapper untouched.

Strict MCP clients accept array responses

glab mr list --output json and glab issue list --output json emit top-level arrays, and strict MCP clients reject structuredContent that isn't a JSON object ("expected record / invalid_type"). New helper ensureStructuredRecord wraps anything that isn't already an object: arrays land under data, scalars or null under value. The accompanying TextContent block still carries the raw payload unchanged.

End-to-end wiring would need an injectable executor on mcpServer; a table-driven unit test covers ensureStructuredRecord for now, and the executor is worth doing when the next MCP test needs it.

Documentation

Regenerated docs/source/work-items/ to match the new surface:

  • list.md picks up the -A / --assignee and --mine flags and gains two example invocations (cross-namespace --mine, and combining --mine with --assignee inside a group).
  • A new view.md documents glab work-items view <iid> with its flag set and example output.
  • _index.md adds the view subcommand to the navigation list.

N/A

How has this been tested?

Local go test ./... passes across the entire project. Specific coverage added or reworked:

  • internal/commands/mcp_coverage_test.go - new walker test that fails when any leaf command lacks an MCP annotation.
  • internal/commands/mcp/serve/server_test.go - table-driven cases for ensureStructuredRecord (object passthrough, array wrap, scalar wrap, nil).
  • internal/commands/issuable/list/issuable_list_test.go and internal/commands/mr/list/mr_list_test.go - cases for the no-repo fallback, the default_group override, and the option-translation helpers.
  • internal/commands/mr/note/mr_note_list_test.go and mr_note_resolve_test.go - annotation assertions for the gaps closed in this MR.
  • internal/commands/todo/list/list_test.go - moved onto httptest fixtures so the new Group field round-trips through the wrapper. Existing assertions kept.
  • internal/commands/workitems/api/api_test.go and ee_test.go - decode paths against GitLab-shaped widget fixtures, plus the EE retry helper exercised through an httptest server.
  • internal/commands/workitems/list/list_test.go, utils/display_test.go, and the new view/view_test.go - column rendering, status fallback, scope selection, and the view command's flag and output paths.
  • internal/commands/duo/cli/cli_test.go - annotation assertion for the new Interactive marking.

Manual MCP smoke testing: ran glab mcp serve against a strict client (Claude Code) outside any git checkout and confirmed mr list, issue list, todo list, and work-items list / view all return without errors. With a default_group set, the middle tier returns group-scoped results; without it, the user-level fallback kicks in. I've been using this with Claude Desktop and Cowork without any issues as part of my daily workflow.

What's not explicitly covered here with tests (pre-existing): end-to-end MCP integration tests for the ensureStructuredRecord wiring (needs an injectable executor on mcpServer), and CE-vs-EE behavioral tests against a real GitLab instance - the EE fallback is exercised via httptest, but a live CE server hasn't been part of this loop.

Screenshots (if appropriate):

N/A - CLI/MCP server changes only, no significant visual changes.

Edited by James Hebden

Merge request reports

Loading
Loading