forked from zereight/gitlab-mcp
-
Notifications
You must be signed in to change notification settings - Fork 1
refactor(milestones): CQRS consolidation - 9 tools → 2 tools #13
Copy link
Copy link
Closed
Labels
refactorCode restructuring without behavior changeCode restructuring without behavior change
Description
Summary
Consolidate 9 milestone tools into 2 CQRS-aligned tools for better MCP client compatibility and cleaner read/write separation.
Current State (9 tools)
| Tool | Type | Description |
|---|---|---|
list_milestones |
READ | List milestones with filtering |
get_milestone |
READ | Get milestone details |
get_milestone_issue |
READ | List issues in milestone |
get_milestone_merge_requests |
READ | List MRs in milestone |
get_milestone_burndown_events |
READ | Get burndown chart data (Premium) |
create_milestone |
WRITE | Create new milestone |
edit_milestone |
WRITE | Modify milestone |
delete_milestone |
WRITE | Remove milestone |
promote_milestone |
WRITE | Promote to group level |
Target State (2 tools)
browse_milestones (Query)
Consolidates all READ operations:
{
action: "list" | "get" | "issues" | "merge_requests" | "burndown",
// Common - scope selection
scope: "project" | "group",
projectId?: string,
groupId?: string,
// For "list" action
state?: "active" | "closed" | "all",
title?: string,
search?: string,
include_parent_milestones?: boolean,
include_ancestors?: boolean,
updated_before?: string,
updated_after?: string,
// For "get", "issues", "merge_requests", "burndown"
milestoneId?: number,
// Pagination for list/issues/merge_requests
per_page?: number,
page?: number
}Read-only mode: Always allowed
Tier requirements:
burndownaction requires Premium tier
manage_milestone (Command)
Consolidates all WRITE operations:
{
action: "create" | "update" | "delete" | "promote",
// Common - scope selection
scope: "project" | "group",
projectId?: string,
groupId?: string,
// For "update", "delete", "promote"
milestoneId?: number,
// For "create" and "update"
title?: string,
description?: string,
start_date?: string, // YYYY-MM-DD
due_date?: string, // YYYY-MM-DD
state_event?: "close" | "activate"
}Read-only mode: Blocked entirely
Implementation Tasks
- Create new
browse_milestoneshandler with action dispatch - Create new
manage_milestonehandler with action dispatch - Update Zod schemas with flat object pattern (see Schema Pattern below)
- Handle both project and group milestone APIs
- Gate burndown action on Premium tier
- Update registry to export new tool names
- Update read-only tools list (only
browse_milestones) - Remove old handlers after migration
- Update unit tests
- Update integration tests
Schema Pattern (AI-Compatible)
CRITICAL: Per #29, do NOT use
z.discriminatedUnion()- it generatesoneOfat JSON Schema root level which is incompatible with Claude API.
Use flat z.object() with .refine() for conditional validation:
// browse_milestones - READ operations
const BrowseMilestonesSchema = z.object({
action: z.enum(["list", "get", "issues", "merge_requests", "burndown"]),
scope: z.enum(["project", "group"]),
projectId: z.string().optional(),
groupId: z.string().optional(),
// list-specific
state: z.enum(["active", "closed", "all"]).optional(),
title: z.string().optional(),
search: z.string().optional(),
include_parent_milestones: z.boolean().optional(),
include_ancestors: z.boolean().optional(),
updated_before: z.string().optional(),
updated_after: z.string().optional(),
// get/issues/merge_requests/burndown specific
milestoneId: z.number().optional(),
// pagination
per_page: z.number().optional(),
page: z.number().optional(),
}).refine(
(data) => data.scope !== "project" || data.projectId !== undefined,
{ message: "projectId is required when scope is 'project'", path: ["projectId"] }
).refine(
(data) => data.scope !== "group" || data.groupId !== undefined,
{ message: "groupId is required when scope is 'group'", path: ["groupId"] }
).refine(
(data) => data.action === "list" || data.milestoneId !== undefined,
{ message: "milestoneId is required for get/issues/merge_requests/burndown actions", path: ["milestoneId"] }
);
// manage_milestone - WRITE operations
const ManageMilestoneSchema = z.object({
action: z.enum(["create", "update", "delete", "promote"]),
scope: z.enum(["project", "group"]),
projectId: z.string().optional(),
groupId: z.string().optional(),
milestoneId: z.number().optional(),
title: z.string().optional(),
description: z.string().optional(),
start_date: z.string().optional(),
due_date: z.string().optional(),
state_event: z.enum(["close", "activate"]).optional(),
}).refine(
(data) => data.scope !== "project" || data.projectId !== undefined,
{ message: "projectId is required when scope is 'project'", path: ["projectId"] }
).refine(
(data) => data.scope !== "group" || data.groupId !== undefined,
{ message: "groupId is required when scope is 'group'", path: ["groupId"] }
).refine(
(data) => data.action === "create" || data.milestoneId !== undefined,
{ message: "milestoneId is required for update/delete/promote actions", path: ["milestoneId"] }
).refine(
(data) => data.action !== "create" || data.title !== undefined,
{ message: "title is required for 'create' action", path: ["title"] }
);Breaking Changes
| Old Tool | Migration Path |
|---|---|
list_milestones |
browse_milestones with action: "list" |
get_milestone |
browse_milestones with action: "get" |
get_milestone_issue |
browse_milestones with action: "issues" |
get_milestone_merge_requests |
browse_milestones with action: "merge_requests" |
get_milestone_burndown_events |
browse_milestones with action: "burndown" |
create_milestone |
manage_milestone with action: "create" |
edit_milestone |
manage_milestone with action: "update" |
delete_milestone |
manage_milestone with action: "delete" |
promote_milestone |
manage_milestone with action: "promote" |
Feature Flag
Existing USE_MILESTONE flag continues to control entire milestones entity visibility.
Testing Requirements
- Unit tests for flat schema validation with refine
- Test project-level milestones
- Test group-level milestones
- Test issues/MRs listing in milestone
- Test burndown (Premium tier only)
- Test promote operation
- Test read-only mode blocks
manage_milestoneentirely - Integration tests for all operations
- Verify JSON Schema output has NO
oneOf/allOf/anyOfat top level
Acceptance Criteria
- Total tools reduced from 9 to 2
- All existing functionality preserved
- Both project and group milestones supported
- Burndown requires Premium tier
- Promote operation works correctly
- Read-only mode correctly gates write operations
- Clear error messages for invalid action/parameter combinations
- JSON Schema compatible with Claude API (no oneOf/allOf/anyOf at root)
Related
- refactor(schemas): Replace discriminated unions with flat schemas for Claude API compatibility #29 - AI-compatible schema requirement
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
refactorCode restructuring without behavior changeCode restructuring without behavior change