Skip to content

refactor(wiki): CQRS consolidation - 5 tools → 2 tools #10

@polaz

Description

@polaz

Summary

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

Current State (5 tools)

Tool Type Description
list_wiki_pages READ List all wiki pages
get_wiki_page READ Get single wiki page content
create_wiki_page WRITE Create new wiki page
update_wiki_page WRITE Modify existing wiki page
delete_wiki_page WRITE Remove wiki page

Target State (2 tools)

browse_wiki (Query)

Consolidates all READ operations:

{
  action: "list" | "get",
  
  // Common - scope selection
  scope: "project" | "group",
  projectId?: string,        // Required if scope="project"
  groupId?: string,          // Required if scope="group"
  
  // For "list" action
  with_content?: boolean,    // Include page content in list
  
  // For "get" action
  slug: string,              // Page slug/path
  version?: string,          // Specific version SHA
  render_html?: boolean      // Return rendered HTML
}

Read-only mode: Always allowed

manage_wiki (Command)

Consolidates all WRITE operations:

{
  action: "create" | "update" | "delete",
  
  // Common - scope selection
  scope: "project" | "group",
  projectId?: string,
  groupId?: string,
  
  // Page identification (for update/delete)
  slug?: string,
  
  // For "create" and "update"
  title?: string,
  content?: string,
  format?: "markdown" | "rdoc" | "asciidoc" | "org"
}

Read-only mode: Blocked entirely

Implementation Tasks

  • Create new browse_wiki handler with action dispatch
  • Create new manage_wiki handler with action dispatch
  • Update Zod schemas with flat object pattern (see Schema Pattern below)
  • Handle both project and group wiki APIs
  • Update registry to export new tool names
  • Update read-only tools list (only browse_wiki)
  • 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_wiki - READ operations
const BrowseWikiSchema = z.object({
  action: z.enum(["list", "get"]),
  scope: z.enum(["project", "group"]),
  projectId: z.string().optional(),
  groupId: z.string().optional(),
  // list-specific
  with_content: z.boolean().optional(),
  // get-specific
  slug: z.string().optional(),
  version: z.string().optional(),
  render_html: z.boolean().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 !== "get" || data.slug !== undefined,
  { message: "slug is required for 'get' action", path: ["slug"] }
);

// manage_wiki - WRITE operations
const ManageWikiSchema = z.object({
  action: z.enum(["create", "update", "delete"]),
  scope: z.enum(["project", "group"]),
  projectId: z.string().optional(),
  groupId: z.string().optional(),
  slug: z.string().optional(),
  title: z.string().optional(),
  content: z.string().optional(),
  format: z.enum(["markdown", "rdoc", "asciidoc", "org"]).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.slug !== undefined,
  { message: "slug is required for update/delete actions", path: ["slug"] }
).refine(
  (data) => data.action !== "create" || data.title !== undefined,
  { message: "title is required for 'create' action", path: ["title"] }
).refine(
  (data) => data.action !== "create" || data.content !== undefined,
  { message: "content is required for 'create' action", path: ["content"] }
);

Breaking Changes

Old Tool Migration Path
list_wiki_pages browse_wiki with action: "list"
get_wiki_page browse_wiki with action: "get"
create_wiki_page manage_wiki with action: "create"
update_wiki_page manage_wiki with action: "update"
delete_wiki_page manage_wiki with action: "delete"

Feature Flag

Existing USE_GITLAB_WIKI flag continues to control entire wiki entity visibility.

Testing Requirements

  • Unit tests for flat schema validation with refine
  • Test project-level wiki
  • Test group-level wiki (if available)
  • Test different content formats (markdown, asciidoc, etc.)
  • Test version retrieval
  • Test read-only mode blocks manage_wiki 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
  • Both project and group wikis supported
  • Content format handling 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

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