Skip to content

refactor(labels): CQRS consolidation - 5 tools → 2 tools #8

@polaz

Description

@polaz

Summary

Consolidate 5 label tools into 2 CQRS-aligned tools for better MCP client compatibility and cleaner read/write separation.

Current State (5 tools)

Tool Type Description
list_labels READ List labels with filtering
get_label READ Get single label details
create_label WRITE Create new label
update_label WRITE Modify existing label
delete_label WRITE Remove label

Target State (2 tools)

browse_labels (Query)

Consolidates all READ operations:

{
  action: "list" | "get",
  
  // Common
  projectId?: string,
  groupId?: string,
  
  // For "list" action
  search?: string,
  include_ancestor_groups?: boolean,
  per_page?: number,
  page?: number,
  
  // For "get" action
  labelId?: number,
  name?: string
}

Read-only mode: Always allowed

manage_label (Command)

Consolidates all WRITE operations:

{
  action: "create" | "update" | "delete",
  
  // Common
  projectId?: string,
  groupId?: string,
  
  // For "update" and "delete"
  labelId?: number,
  name?: string,           // Can identify by name
  
  // For "create" and "update"
  new_name?: string,       // For rename in update
  color?: string,          // Hex format #RRGGBB
  description?: string,
  priority?: number
}

Read-only mode: Blocked entirely

Implementation Tasks

  • Create new browse_labels handler with action dispatch
  • Create new manage_label handler with action dispatch
  • Update Zod schemas with flat object pattern (see Schema Pattern below)
  • Update registry to export new tool names
  • Update read-only tools list (only browse_labels)
  • Add deprecation warnings for old tool names (optional migration period)
  • Remove old handlers after migration
  • Update unit tests
  • Update integration tests

Schema Pattern (AI-Compatible)

CRITICAL: Per #29, do NOT use z.discriminatedUnion() - it generates oneOf at JSON Schema root level which is incompatible with Claude API.

Use flat z.object() with .refine() for conditional validation:

// browse_labels - READ operations
const BrowseLabelsSchema = z.object({
  action: z.enum(["list", "get"]),
  projectId: z.string().optional(),
  groupId: z.string().optional(),
  // list-specific (optional)
  search: z.string().optional(),
  include_ancestor_groups: z.boolean().optional(),
  per_page: z.number().optional(),
  page: z.number().optional(),
  // get-specific (optional but required for "get")
  labelId: z.number().optional(),
  name: z.string().optional(),
}).refine(
  (data) => data.action !== "get" || data.labelId !== undefined || data.name !== undefined,
  { message: "labelId or name is required for 'get' action", path: ["labelId"] }
).refine(
  (data) => data.projectId !== undefined || data.groupId !== undefined,
  { message: "Either projectId or groupId is required", path: ["projectId"] }
);

// manage_label - WRITE operations  
const ManageLabelSchema = z.object({
  action: z.enum(["create", "update", "delete"]),
  projectId: z.string().optional(),
  groupId: z.string().optional(),
  // identification (for update/delete)
  labelId: z.number().optional(),
  name: z.string().optional(),
  // create/update fields
  new_name: z.string().optional(),
  color: z.string().optional(),
  description: z.string().optional(),
  priority: z.number().optional(),
}).refine(
  (data) => data.action === "create" || data.labelId !== undefined || data.name !== undefined,
  { message: "labelId or name is required for update/delete actions", path: ["labelId"] }
).refine(
  (data) => data.action !== "create" || data.name !== undefined,
  { message: "name is required for 'create' action", path: ["name"] }
).refine(
  (data) => data.action !== "create" || data.color !== undefined,
  { message: "color is required for 'create' action", path: ["color"] }
);

Breaking Changes

Old Tool Migration Path
list_labels browse_labels with action: "list"
get_label browse_labels with action: "get"
create_label manage_label with action: "create"
update_label manage_label with action: "update"
delete_label manage_label with action: "delete"

Testing Requirements

  • Unit tests for flat schema validation with refine
  • Test each action variant
  • Test read-only mode blocks manage_label entirely
  • Integration tests for all operations
  • Verify JSON Schema output has NO oneOf/allOf/anyOf at top level

Acceptance Criteria

  • Total tools reduced from 5 to 2
  • All existing functionality preserved
  • Read-only mode correctly gates write operations
  • Clear error messages for invalid action/parameter combinations
  • Tool descriptions are agent-friendly
  • JSON Schema compatible with Claude API (no oneOf/allOf/anyOf at root)

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    refactorCode restructuring without behavior change

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions