Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:17:22 +08:00
commit 2c8fd6d9a0
22 changed files with 8353 additions and 0 deletions

View File

@@ -0,0 +1,605 @@
---
name: mcp-tool-generator
description: Generate new MCP tools for GitLab operations following the project's standardized pattern. Creates complete TypeScript files with imports, registration functions, Zod schemas, error handling, and format options. Supports simple CRUD operations, complex multi-action tools, and advanced patterns like discussion management. Use when "create mcp tool", "generate gitlab tool", "new tool for", "add tool to gitlab", or building new GitLab integration features.
tools: [Read, Write, Glob, Grep]
---
# MCP Tool Generator
Generate new MCP tools following the standardized patterns from the project. Creates complete tool files with proper imports, Zod schemas, error handling, and GitLab API integration.
## Activation Triggers
- "create an mcp tool for..."
- "generate a gitlab tool to..."
- "I need a new tool that..."
- "add a tool for [operation]"
- "create tool to [action] [resource]"
## Tool Types Supported
### 1. Simple CRUD Tools
Basic get/list/create/update/delete operations with standard patterns:
- Get single resource (issue, MR, milestone, etc.)
- List multiple resources with filtering and pagination
- Create new resources
- Update existing resources
- Delete resources
**Pattern**: `gitlab-[action]-[resource]` (e.g., `gitlab-get-issue`, `gitlab-list-pipelines`)
### 2. Multi-Action Tools
Comprehensive tools that handle multiple related operations in one tool:
- Multiple actions via `action` enum parameter
- Conditional logic based on action type
- Structured responses with status/action/message format
- More efficient than multiple separate tools
**Pattern**: `gitlab-[resource]-[operation]` (e.g., `gitlab-manage-issue`)
### 3. Complex Operation Tools
Tools with advanced logic:
- Discussion/comment management with update detection
- Multi-step workflows
- Direct API calls using fetch for specific needs
- Position-based operations (code reviews, inline comments)
**Pattern**: Based on specific operation (e.g., `gitlab-review-merge-request-code`)
## Autonomous Generation Process
### Step 1: Analyze User Request
Extract key information:
1. **Tool Type**: Simple CRUD, multi-action, or complex?
2. **Tool Purpose**: What GitLab operation? (e.g., "get merge request details", "manage issues", "review code")
3. **Resource Type**: What GitLab entity? (issue, MR, branch, milestone, pipeline, label, etc.)
4. **Action Type**: What operation? (get, list, create, update, delete, search, manage, review, etc.)
5. **Required Parameters**: What inputs needed? (projectname, IID, branch name, action, etc.)
6. **Optional Parameters**: What's optional? (format, labels, assignee, filters, etc.)
7. **Special Features**: Multi-action? Position-based? Discussion management?
### Step 2: Auto-Generate Names
**Tool Name** (kebab-case):
- Simple CRUD: `gitlab-[action]-[resource]`
- Examples: `gitlab-get-merge-request`, `gitlab-list-pipelines`, `gitlab-create-branch`
- Multi-action: `gitlab-[manage|handle]-[resource]`
- Examples: `gitlab-manage-issue`, `gitlab-handle-milestone`
- Complex: `gitlab-[specific-operation]`
- Examples: `gitlab-review-merge-request-code`, `gitlab-find-related-issues`
**Function Name** (PascalCase):
- Pattern: `register[Action][Resource]`
- Examples:
- `gitlab-get-merge-request``registerGetMergeRequest`
- `gitlab-manage-issue``registerManageIssue`
- `gitlab-review-merge-request-code``registerReviewMergeRequestCode`
**File Name** (kebab-case):
- Pattern: `gitlab-[tool-name].ts`
- Location: `src/tools/gitlab/`
### Step 3: Select Tool Pattern
#### Pattern A: Simple CRUD Tool
**Use when**: Single operation (get, list, create, update, delete)
**Standard features**:
- `projectname` parameter (optional, with prompt fallback)
- `format` parameter (detailed/concise) for get/list operations
- HTML content cleaning with `cleanGitLabHtmlContent()`
- Project validation before API calls
- Descriptive error messages
- Emojis in concise format
**Template structure**:
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
import { z } from 'zod';
import { cleanGitLabHtmlContent } from '../../core/utils';
import { getGitLabService, getProjectNameFromUser } from './gitlab-shared';
export function register{FunctionName}(server: McpServer) {
server.registerTool(
"{tool-name}",
{
title: "{Human Readable Title}",
description: "{Detailed description}",
inputSchema: {
{param1}: z.{type}().describe("{description}"),
projectname: z.string().optional().describe("GitLab project name (if not provided, you'll be prompted to select)"),
format: z.enum(["detailed", "concise"]).optional().describe("Response format - 'detailed' includes all metadata, 'concise' includes only key information")
}
},
async ({ {params}, projectname, format = "detailed" }) => {
try {
// Standard workflow
} catch (e) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: String(e) }) }] };
}
}
);
}
```
#### Pattern B: Multi-Action Tool
**Use when**: Multiple related operations on same resource type
**Standard features**:
- `action` parameter with enum of actions
- Switch/case logic for each action
- Structured responses: `{ status: "success"/"failure", action: "...", message: "...", [resource]: {...} }`
- Direct API calls using fetch when needed
- Conditional parameters based on action
- No format parameter (uses structured JSON)
**Template structure**:
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
import fetch from 'node-fetch';
import { z } from 'zod';
import { cleanGitLabHtmlContent } from '../../core/utils';
import { getGitLabService, getProjectNameFromUser } from './gitlab-shared';
export function register{FunctionName}(server: McpServer) {
server.registerTool(
"{tool-name}",
{
title: "{Human Readable Title}",
description: "{Comprehensive description covering all actions}",
inputSchema: {
{resourceId}: z.number().describe("The ID/IID of the resource"),
projectname: z.string().optional().describe("GitLab project name (if not provided, you'll be prompted to select)"),
action: z.enum(["action1", "action2", "action3"]).describe("Action to perform"),
// Conditional parameters for different actions
param1: z.{type}().optional().describe("For action1: description"),
param2: z.{type}().optional().describe("For action2: description")
}
},
async ({ {resourceId}, projectname, action, {params} }) => {
try {
// Get project and resource
const projectName = projectname || await getProjectNameFromUser(server, false, "prompt");
if (!projectName) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: "Project not found" }) }] };
}
const service = await getGitLabService(server);
const projectId = await service.getProjectId(projectName);
if (!projectId) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Project "${projectName}" not found` }) }] };
}
// Get resource first
const rawResource = await service.get{Resource}(projectId, {resourceId});
if (!rawResource) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Resource not found` }) }] };
}
const resource = cleanGitLabHtmlContent(rawResource, ['description', 'title']);
// Handle actions
switch (action) {
case "action1":
// Implementation
return { content: [{ type: "text", text: JSON.stringify({
status: 'success',
action: 'action1',
message: 'Action completed',
{resource}: { /* key fields */ }
}, null, 2) }] };
case "action2":
// Implementation
break;
default:
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
error: `Unknown action "${action}"`
}, null, 2) }] };
}
} catch (e) {
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
error: String(e)
}, null, 2) }] };
}
}
);
}
```
#### Pattern C: Complex Operation Tool
**Use when**: Advanced logic like discussion management, position-based operations, multi-step workflows
**Standard features**:
- Specialized parameters (may not include projectname if using projectId directly)
- Custom logic for specific use cases
- May use direct API calls
- May fetch and update existing data
- Structured responses appropriate to operation
**Template structure**: Highly variable based on specific needs
### Step 4: Generate Zod Schema
**Common Parameter Patterns**:
```typescript
// IDs (internal issue/MR number)
{name}Iid: z.number().describe("The internal ID (IID) of the {resource} to {action}")
// Project ID (for tools that need direct ID)
projectId: z.number().describe("The project ID")
// Names/identifiers
{name}: z.string().describe("{Resource} name (e.g., 'feature/user-auth')")
// Action enums (for multi-action tools)
action: z.enum(["action1", "action2", "action3"]).describe("Action to perform on the {resource}")
// Optional filters
state: z.enum(["opened", "closed", "all"]).optional().describe("Filter by state (default: 'opened')")
labels: z.string().optional().describe("Comma-separated list of label names to filter by")
// OR for multi-action tools:
labels: z.array(z.string()).optional().describe("For add-labels action: labels to add")
// Pagination
page: z.number().optional().describe("Page number for pagination (default: 1)")
perPage: z.number().optional().describe("Number of items per page (default: 20, max: 100)")
// Dates
dueDate: z.string().optional().describe("Due date in ISO 8601 format (YYYY-MM-DD)")
// Position-based parameters (for code review tools)
baseSha: z.string().describe("Base SHA for the diff")
startSha: z.string().describe("Start SHA for the diff")
headSha: z.string().describe("Head SHA for the diff")
newPath: z.string().describe("Path to the file being reviewed")
newLine: z.number().optional().describe("Line number in the new file")
// Project (standard for simple CRUD, optional for complex tools)
projectname: z.string().optional().describe("GitLab project name (if not provided, you'll be prompted to select)")
// Format (only for simple get/list operations)
format: z.enum(["detailed", "concise"]).optional().describe("Response format - 'detailed' includes all metadata, 'concise' includes only key information")
```
**Important notes**:
- Add `.describe()` with clear examples for all parameters
- Use `z.array(z.string())` for arrays in multi-action tools
- Note when square brackets `[]` are allowed in descriptions for paths/labels/markdown
- Make parameters optional when sensible defaults exist
### Step 5: Generate Response Formats
#### Simple CRUD Tools (Pattern A)
**Concise format** (with emojis):
```typescript
if (format === "concise") {
return { content: [{ type: "text", text:
`{emoji} {Resource} #{id}: {title}\n` +
`📊 Status: {state}\n` +
`👤 {role}: {user}\n` +
`🏷️ Labels: {labels}\n` +
`🎯 Milestone: {milestone}\n` +
`📅 Due: {due_date}\n` +
`🔗 URL: {web_url}`
}] };
}
```
**Detailed format** (full JSON):
```typescript
return { content: [{ type: "text", text: JSON.stringify({resource}, null, 2) }] };
```
**Emoji Guide**:
- 🔍 - Get/View operations
- 📋 - List operations
- ✨ - Create operations
- 🔄 - Update operations
- 🗑️ - Delete operations
- 📊 - Status/State
- 👤 - User/Assignee
- 🏷️ - Labels
- 🎯 - Milestone
- 📅 - Dates
- 🔗 - URLs
- ✅ - Success/Completed
- ❌ - Error/Failed
#### Multi-Action Tools (Pattern B)
**Structured JSON format**:
```typescript
// Success response
{
status: 'success',
action: 'action-name',
message: 'Human-readable success message',
{resource}: {
id: resource.id,
iid: resource.iid,
title: resource.title,
webUrl: resource.web_url,
// Other key fields relevant to the action
}
}
// Failure response
{
status: 'failure',
action: 'action-name',
error: 'Detailed error message with context',
{resource}: {
id: resource.id,
iid: resource.iid,
title: resource.title,
webUrl: resource.web_url
}
}
```
#### Complex Tools (Pattern C)
Custom format based on operation needs. Examples:
```typescript
// Discussion update/create
{
action: "updated" | "created",
discussion_id: "...",
note_id: "...",
updated_note: {...}
}
```
### Step 6: Add Error Handling
**Standard Error Patterns**:
```typescript
// Project not selected (for tools with projectname parameter)
if (!projectName) {
return { content: [{ type: "text", text: JSON.stringify({
type: "error",
error: "Project not found or not selected. Please provide a valid project name."
}) }] };
}
// Project not found
if (!projectId) {
return { content: [{ type: "text", text: JSON.stringify({
type: "error",
error: `Could not find project "${projectName}". Please verify the project name is correct and you have access to it.`
}) }] };
}
// Resource not found
if (!resource) {
return { content: [{ type: "text", text: JSON.stringify({
type: "error",
error: `{Resource} not found. Please verify the {parameters} are correct.`
}) }] };
}
// Missing required parameters (for multi-action tools)
if (!requiredParam) {
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
action: action,
error: "Required parameter missing. Please specify...",
{resource}: { /* minimal info */ }
}, null, 2) }] };
}
// API call failure (for multi-action tools using fetch)
if (!response.ok) {
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
action: action,
error: `Failed to {action}. Status: ${response.status}`,
{resource}: { /* minimal info */ }
}, null, 2) }] };
}
// General error (catch block)
catch (e) {
// For simple CRUD tools:
return { content: [{ type: "text", text: JSON.stringify({
type: "error",
error: `Error {operation}: ${String(e)}. Please check your GitLab connection and permissions.`
}) }] };
// For multi-action tools:
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
error: `Error {operation}: ${String(e)}`
}, null, 2) }] };
}
```
### Step 7: Register in gitlab-tool.ts
After creating the tool file, add registration:
```typescript
// In src/tools/gitlab-tool.ts
// Add import at top
import { register{FunctionName} } from './gitlab/gitlab-{tool-name}';
// Add registration in registerGitlabTools function
export function registerGitlabTools(server: McpServer) {
// ... other registrations
register{FunctionName}(server);
}
```
## Interactive Generation Workflow
### Ask User (Only if unclear):
1. **Tool Type**:
- "Is this a simple CRUD operation, multi-action tool, or complex operation?"
- Clarify if multiple actions should be combined in one tool
2. **Tool Purpose**:
- "What GitLab operation should this tool perform?"
- Examples: "Get merge request details", "Manage issues (get, update, close)", "Review code inline"
3. **Required Parameters**:
- "What parameters are required?"
- Examples: "merge request IID", "issue IID and action type", "project ID and position data"
4. **Optional Parameters**:
- "Any optional filters or options?"
- Examples: "state filter", "label filter", "format option"
5. **API Method** (if not obvious):
- "Which GitLab service method to use?"
- Check `src/services/gitlab-client.ts` for available methods
- Note if direct fetch API calls are needed
### Generate Files:
1. **Create tool file**: `src/tools/gitlab/gitlab-{tool-name}.ts`
2. **Show registration code** for `src/tools/gitlab-tool.ts`
3. **Provide usage examples** based on tool type
## Output Format
After generating the tool:
```markdown
✅ MCP Tool Created: {tool-name}
📁 Files Created:
- `src/tools/gitlab/gitlab-{tool-name}.ts`
🔧 Type: {Simple CRUD | Multi-Action | Complex Operation}
🔧 Function: register{FunctionName}
📝 Next Steps:
1. Add registration to `src/tools/gitlab-tool.ts`:
```typescript
import { register{FunctionName} } from './gitlab/gitlab-{tool-name}';
// In registerGitlabTools:
register{FunctionName}(server);
```
2. Rebuild the project:
```bash
npm run build
```
3. Test the tool:
```bash
npm run dev
```
🎯 Usage Examples:
{Type-specific examples}
📖 Tool registered as: "{tool-name}"
```
## GitLab Service Methods Reference
Common methods available in `gitlab-client.ts`. Latest update 31/10/2025:
**Issues**:
- `getIssue(projectId, iid)`
- `getIssues(projectId, options)`
- `createIssue(projectId, data)`
- `updateIssue(projectId, iid, data)`
**Merge Requests**:
- `getMergeRequest(projectId, iid)`
- `getMergeRequests(projectId, options)`
- `createMergeRequest(projectId, data)`
- `updateMergeRequest(projectId, iid, data)`
- `approveMergeRequest(projectId, iid)`
- `getMrDiscussions(projectId, iid)`
- `addMrComments(projectId, iid, data)`
- `updateMrDiscussionNote(projectId, iid, discussionId, noteId, body)`
**Branches**:
- `getBranches(projectId, options)`
- `createBranch(projectId, branchName, ref)`
- `deleteBranch(projectId, branchName)`
**Pipelines**:
- `getPipelines(projectId, options)`
- `getPipeline(projectId, pipelineId)`
- `createPipeline(projectId, ref)`
**Milestones**:
- `getMilestone(projectId, milestoneId)`
- `getMilestones(projectId, options)`
- `createMilestone(projectId, data)`
- `updateMilestone(projectId, milestoneId, data)`
**Projects**:
- `getProjectId(projectName)`
- `getProject(projectId)`
- `searchProjects(search)`
**Users**:
- `getUserIdByUsername(username)`
**Direct Fetch API**:
For operations not covered by service methods, use direct fetch:
```typescript
const response = await fetch(`${service.gitlabUrl}/api/v4/projects/${projectId}/{endpoint}`, {
method: 'PUT' | 'POST' | 'GET' | 'DELETE',
headers: service['getHeaders'](),
body: JSON.stringify({...})
});
```
## Key Patterns to Follow
1. **Always use appropriate tool pattern** based on operation type
2. **Simple CRUD tools**: Include `projectname` and `format` parameters
3. **Multi-action tools**: Use `action` enum and structured responses
4. **Always clean HTML content** with `cleanGitLabHtmlContent()` where applicable
5. **Always validate project exists** before API calls (if using projectname)
6. **Always use descriptive error messages** with context
7. **Always use emojis in concise format** for simple CRUD tools
8. **Always follow kebab-case** for file and tool names
9. **Always follow PascalCase** for function names
10. **Always provide detailed Zod descriptions** with examples
11. **Always handle null/undefined responses** gracefully
12. **Multi-action tools**: Return structured JSON with status/action/message
13. **Direct API calls**: Use fetch and check response.ok
14. **Note square bracket support**: Add notes about `[]` support in descriptions where relevant (file paths, labels, markdown)
## Quality Checklist
Before presenting the generated tool:
- ✅ File name is kebab-case
- ✅ Function name is PascalCase with "register" prefix
- ✅ All imports are correct
- ✅ Zod schema has detailed descriptions
- ✅ Appropriate tool pattern selected (Simple CRUD / Multi-Action / Complex)
- ✅ For simple CRUD: projectname optional, format parameter included
- ✅ For multi-action: action enum, structured responses, conditional params
- ✅ HTML content cleaned where applicable
- ✅ Error messages are descriptive and actionable
- ✅ Response format matches tool type
- ✅ Try-catch wraps the entire handler
- ✅ All responses follow `{ content: [{ type: "text", text: ... }] }` format
- ✅ Tool follows MCP SDK patterns
- ✅ Code matches project conventions from CLAUDE.md
---
**Ready to generate MCP tools!** Tell me what GitLab operation you want to create a tool for.

View File

@@ -0,0 +1,556 @@
# MCP Tool Generator Examples
Complete examples of generated MCP tools following the standardized patterns. Includes simple CRUD tools, multi-action tools, and complex operation tools.
## Example 1: Simple CRUD - Get Merge Request Details
**Tool Type**: Simple CRUD (Pattern A)
### User Request
"Create a tool to get merge request details by IID"
### Generated File: `src/tools/gitlab/gitlab-get-merge-request.ts`
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
import { z } from 'zod';
import { cleanGitLabHtmlContent } from '../../core/utils';
import { getGitLabService, getProjectNameFromUser } from './gitlab-shared';
export function registerGetMergeRequest(server: McpServer) {
server.registerTool(
"gitlab-get-merge-request",
{
title: "Get Merge Request Details",
description: "Retrieve detailed information for a specific merge request by IID in a GitLab project. Returns merge request metadata including title, description, state, author, assignee, reviewers, labels, milestone, source/target branches, and approval status. Use this when you need comprehensive information about a specific merge request.",
inputSchema: {
mergeRequestIid: z.number().describe("The internal ID (IID) of the merge request to retrieve"),
projectname: z.string().optional().describe("GitLab project name (if not provided, you'll be prompted to select)"),
format: z.enum(["detailed", "concise"]).optional().describe("Response format - 'detailed' includes all metadata, 'concise' includes only key information")
}
},
async ({ mergeRequestIid, projectname, format = "detailed" }) => {
const iid = mergeRequestIid as number;
try {
const projectName = projectname || await getProjectNameFromUser(server, false, "Please select the project for getting merge request");
if (!projectName) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: "Project not found or not selected. Please provide a valid project name." }) }] };
}
const service = await getGitLabService(server);
const projectId = await service.getProjectId(projectName);
if (!projectId) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Could not find project "${projectName}". Please verify the project name is correct and you have access to it.` }) }] };
}
const rawMr = await service.getMergeRequest(projectId, iid);
if (!rawMr) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Merge request !${iid} not found in project "${projectName}". Please verify the merge request IID is correct.` }) }] };
}
// Clean HTML content from merge request fields
const mr = cleanGitLabHtmlContent(rawMr, ['description', 'title']);
// Format response based on requested format
if (format === "concise") {
const conciseInfo = {
title: mr.title,
state: mr.state,
author: mr.author?.name || "Unknown",
assignee: mr.assignee?.name || "Unassigned",
labels: mr.labels || [],
milestone: mr.milestone?.title || "No milestone",
source_branch: mr.source_branch,
target_branch: mr.target_branch,
web_url: mr.web_url
};
return { content: [{ type: "text", text: `🔍 MR !${iid}: ${mr.title}\n📊 Status: ${mr.state}\n👤 Author: ${conciseInfo.author}\n👤 Assignee: ${conciseInfo.assignee}\n🏷 Labels: ${conciseInfo.labels.join(', ') || 'None'}\n🎯 Milestone: ${conciseInfo.milestone}\n🔀 ${conciseInfo.source_branch}${conciseInfo.target_branch}\n🔗 URL: ${mr.web_url}` }] };
}
return { content: [{ type: "text", text: JSON.stringify(mr, null, 2) }] };
} catch (e) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Error retrieving merge request !${iid}: ${String(e)}. Please check your GitLab connection and permissions.` }) }] };
}
}
);
}
```
### Registration in `gitlab-tool.ts`
```typescript
import { registerGetMergeRequest } from './gitlab/gitlab-get-merge-request';
export function registerGitlabTools(server: McpServer) {
// ... other registrations
registerGetMergeRequest(server);
}
```
---
## Example 2: Multi-Action Tool - Manage Issues
**Tool Type**: Multi-Action (Pattern B)
### User Request
"Create a tool that can manage issues - get details, close, reopen, add labels, set assignees, and set due dates"
### Generated File: `src/tools/gitlab/gitlab-manage-issue.ts`
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
import fetch from 'node-fetch';
import { z } from 'zod';
import { cleanGitLabHtmlContent } from '../../core/utils';
import { getGitLabService, getProjectNameFromUser } from './gitlab-shared';
export function registerManageIssue(server: McpServer) {
server.registerTool(
"gitlab-manage-issue",
{
title: "Manage GitLab Issue",
description: "Comprehensive issue management tool that can get, update, or modify issues in a single operation. More efficient than using multiple separate tools. Supports getting issue details, updating status, adding labels, setting assignees, and modifying due dates.",
inputSchema: {
issueIid: z.number().describe("The internal ID (IID) of the issue to manage"),
projectname: z.string().optional().describe("GitLab project name (if not provided, you'll be prompted to select)"),
action: z.enum(["get", "close", "reopen", "add-labels", "set-assignee", "set-due-date"]).describe("Action to perform on the issue"),
// Parameters for different actions
labels: z.array(z.string()).optional().describe("For add-labels action: labels to add to the issue. Square brackets [] are allowed in label names."),
assignee_username: z.string().optional().describe("For set-assignee action: username to assign the issue to"),
due_date: z.string().optional().describe("For set-due-date action: due date in YYYY-MM-DD format")
}
},
async ({ issueIid, projectname, action, labels, assignee_username, due_date }) => {
const iid = issueIid as number;
try {
const projectName = projectname || await getProjectNameFromUser(server, false, "Please select the project for issue management");
if (!projectName) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: "Project not found or not selected. Please provide a valid project name." }) }] };
}
const service = await getGitLabService(server);
const projectId = await service.getProjectId(projectName);
if (!projectId) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Could not find project "${projectName}". Please verify the project name is correct and you have access to it.` }) }] };
}
// Get issue first for all actions
const rawIssue = await service.getIssue(projectId, iid);
if (!rawIssue) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Issue #${iid} not found in project "${projectName}". Please verify the issue IID is correct.` }) }] };
}
// Clean HTML content from issue fields
const issue = cleanGitLabHtmlContent(rawIssue, ['description', 'title']);
switch (action) {
case "get":
return { content: [{ type: "text", text: JSON.stringify({
status: 'success',
action: 'get',
issue: {
id: issue.id,
iid: issue.iid,
title: issue.title,
webUrl: issue.web_url,
state: issue.state,
assignee: issue.assignee?.name || null,
labels: issue.labels || [],
milestone: issue.milestone?.title || null,
dueDate: issue.due_date || null,
description: issue.description
}
}, null, 2) }] };
case "close":
const closeResponse = await fetch(`${service.gitlabUrl}/api/v4/projects/${projectId}/issues/${iid}`, {
method: 'PUT',
headers: service['getHeaders'](),
body: JSON.stringify({ state_event: "close" })
});
if (!closeResponse.ok) {
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
action: 'close',
error: `Failed to close issue #${iid}. Status: ${closeResponse.status}`,
issue: { id: issue.id, iid: issue.iid, title: issue.title, webUrl: issue.web_url }
}, null, 2) }] };
}
const closedIssue = await closeResponse.json();
return { content: [{ type: "text", text: JSON.stringify({
status: 'success',
action: 'close',
message: `Issue #${iid} has been closed successfully`,
issue: {
id: closedIssue.id,
iid: closedIssue.iid,
title: closedIssue.title,
webUrl: closedIssue.web_url,
state: closedIssue.state
}
}, null, 2) }] };
case "add-labels":
if (!labels || labels.length === 0) {
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
action: 'add-labels',
error: "No labels provided. Please specify labels to add using the 'labels' parameter.",
issue: { id: issue.id, iid: issue.iid, title: issue.title, webUrl: issue.web_url }
}, null, 2) }] };
}
const currentLabels = issue.labels || [];
const newLabels = [...new Set([...currentLabels, ...labels])];
const labelsResponse = await fetch(`${service.gitlabUrl}/api/v4/projects/${projectId}/issues/${iid}`, {
method: 'PUT',
headers: service['getHeaders'](),
body: JSON.stringify({ labels: newLabels.join(',') })
});
if (!labelsResponse.ok) {
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
action: 'add-labels',
error: `Failed to add labels. Status: ${labelsResponse.status}`,
issue: { id: issue.id, iid: issue.iid, title: issue.title, webUrl: issue.web_url }
}, null, 2) }] };
}
const labeledIssue = await labelsResponse.json();
return { content: [{ type: "text", text: JSON.stringify({
status: 'success',
action: 'add-labels',
message: `Added labels to issue #${iid}`,
addedLabels: labels,
issue: {
id: labeledIssue.id,
iid: labeledIssue.iid,
title: labeledIssue.title,
webUrl: labeledIssue.web_url,
labels: labeledIssue.labels
}
}, null, 2) }] };
default:
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
action: action,
error: `Unknown action "${action}"`
}, null, 2) }] };
}
} catch (e) {
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
error: `Error managing issue #${iid}: ${String(e)}`
}, null, 2) }] };
}
}
);
}
```
---
## Example 3: Complex Operation - Review Merge Request Code
**Tool Type**: Complex Operation (Pattern C)
### User Request
"Create a tool to add inline code review comments on merge requests with position tracking and duplicate detection"
### Generated File: `src/tools/gitlab/gitlab-review-merge-request-code.ts`
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
import { z } from 'zod';
import { getGitLabService } from './gitlab-shared';
export function registerReviewMergeRequestCode(server: McpServer) {
server.registerTool(
"gitlab-review-merge-request-code",
{
title: "Review Merge Request Code",
description: "Add or update a code review comment on a merge request at a specific file and line position. This tool is designed for inline code reviews - it intelligently updates existing comments at the same position instead of creating duplicates. Requires diff SHA references (base, start, head) and file path with optional line numbers.",
inputSchema: {
projectId: z.number().describe("The project ID"),
mrIid: z.number().describe("The merge request IID"),
body: z.string().describe("The review comment body. Square brackets [] are allowed and commonly used in code references, markdown links, and examples."),
positionType: z.string().default("text").describe("Position type (text, image, etc.)"),
baseSha: z.string().describe("Base SHA for the diff"),
startSha: z.string().describe("Start SHA for the diff"),
headSha: z.string().describe("Head SHA for the diff"),
newPath: z.string().describe("Path to the file being reviewed. Square brackets [] are allowed in file paths."),
newLine: z.number().optional().describe("Line number in the new file (for line comments)"),
oldPath: z.string().optional().describe("Path to the old file (defaults to newPath). Square brackets [] are allowed in file paths."),
oldLine: z.number().optional().describe("Line number in the old file (for line comments)")
}
},
async ({ projectId, mrIid, body, positionType, baseSha, startSha, headSha, newPath, newLine, oldPath, oldLine }) => {
const pid = projectId as number;
const iid = mrIid as number;
const commentBody = body as string;
const posType = positionType as string;
const base = baseSha as string;
const start = startSha as string;
const head = headSha as string;
const path = newPath as string;
const line = newLine as number | undefined;
const oldFilePath = (oldPath as string | undefined) || path;
const oldFileLine = oldLine as number | undefined;
try {
const service = await getGitLabService(server);
// Get existing discussions to check for existing review comments
const discussions = await service.getMrDiscussions(String(pid), iid);
// Find existing review comment at the same position
let existingDiscussion = null;
let existingNote = null;
for (const discussion of discussions) {
if (discussion.notes && discussion.notes.length > 0) {
const firstNote = discussion.notes[0];
// Check if the position matches our target position
if (firstNote.position &&
firstNote.position.new_path === path &&
firstNote.position.base_sha === base &&
firstNote.position.head_sha === head &&
firstNote.position.start_sha === start) {
// Check if line position matches (if specified)
const positionMatches = line !== undefined ?
firstNote.position.new_line === line :
!firstNote.position.new_line;
if (positionMatches) {
existingDiscussion = discussion;
existingNote = firstNote;
break;
}
}
}
}
let result;
if (existingNote && existingDiscussion) {
// Update existing comment
result = await service.updateMrDiscussionNote(
String(pid),
iid,
existingDiscussion.id,
existingNote.id,
commentBody
);
return {
content: [{
type: "text",
text: JSON.stringify({
action: "updated",
discussion_id: existingDiscussion.id,
note_id: existingNote.id,
updated_note: result
})
}]
};
} else {
// Create new comment
const position: any = {
position_type: posType,
base_sha: base,
start_sha: start,
head_sha: head,
new_path: path,
old_path: oldFilePath
};
if (line !== undefined) {
position.new_line = line;
}
if (oldFileLine !== undefined) {
position.old_line = oldFileLine;
}
const data = { body: commentBody, position };
result = await service.addMrComments(String(pid), iid, data);
return {
content: [{
type: "text",
text: JSON.stringify({
action: "created",
discussion: result
})
}]
};
}
} catch (e) {
return {
content: [{
type: "text",
text: JSON.stringify({ type: "error", error: String(e) })
}]
};
}
}
);
}
```
---
## Example 4: Simple CRUD - List Pipelines
**Tool Type**: Simple CRUD (Pattern A)
### User Request
"I need a tool to list all pipelines with status filtering and pagination"
### Generated File: `src/tools/gitlab/gitlab-list-pipelines.ts`
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
import { z } from 'zod';
import { cleanGitLabHtmlContent } from '../../core/utils';
import { getGitLabService, getProjectNameFromUser } from './gitlab-shared';
export function registerListPipelines(server: McpServer) {
server.registerTool(
"gitlab-list-pipelines",
{
title: "List Pipelines",
description: "Retrieve a list of pipelines for a GitLab project. Supports filtering by ref (branch/tag), status, and pagination. Returns pipeline information including ID, status, ref, commit details, and timestamps. Use this to monitor CI/CD pipeline execution, check build status, or find specific pipeline runs.",
inputSchema: {
projectname: z.string().optional().describe("GitLab project name (if not provided, you'll be prompted to select)"),
ref: z.string().optional().describe("Filter pipelines by git reference (branch or tag name, e.g., 'main', 'develop')"),
status: z.enum(["running", "pending", "success", "failed", "canceled", "skipped", "manual"]).optional().describe("Filter pipelines by status"),
page: z.number().optional().describe("Page number for pagination (default: 1)"),
perPage: z.number().optional().describe("Number of pipelines per page (default: 20, max: 100)"),
format: z.enum(["detailed", "concise"]).optional().describe("Response format - 'detailed' includes all metadata, 'concise' includes only key information")
}
},
async ({ projectname, ref, status, page = 1, perPage = 20, format = "detailed" }) => {
try {
const projectName = projectname || await getProjectNameFromUser(server, false, "Please select the project for listing pipelines");
if (!projectName) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: "Project not found or not selected. Please provide a valid project name." }) }] };
}
const service = await getGitLabService(server);
const projectId = await service.getProjectId(projectName);
if (!projectId) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Could not find project "${projectName}". Please verify the project name is correct and you have access to it.` }) }] };
}
const options: any = { page, per_page: perPage };
if (ref) options.ref = ref;
if (status) options.status = status;
const rawPipelines = await service.getPipelines(projectId, options);
if (!rawPipelines || rawPipelines.length === 0) {
return { content: [{ type: "text", text: JSON.stringify({ type: "info", message: `No pipelines found in project "${projectName}" with the specified filters.` }) }] };
}
const pipelines = rawPipelines.map(p => cleanGitLabHtmlContent(p, []));
if (format === "concise") {
const summary = pipelines.map(p =>
`📋 Pipeline #${p.id} | ${p.status} | ${p.ref} | ${new Date(p.created_at).toLocaleDateString()}`
).join('\n');
return { content: [{ type: "text", text: `📋 Found ${pipelines.length} pipeline(s) in "${projectName}":\n\n${summary}` }] };
}
return { content: [{ type: "text", text: JSON.stringify(pipelines, null, 2) }] };
} catch (e) {
return { content: [{ type: "text", text: JSON.stringify({ type: "error", error: `Error listing pipelines: ${String(e)}. Please check your GitLab connection and permissions.` }) }] };
}
}
);
}
```
---
## Common Patterns Summary
### Tool Pattern Selection Guide
| Tool Type | When to Use | Key Features | Example |
|-----------|-------------|--------------|---------|
| **Simple CRUD** | Single operation on resource | projectname, format, emojis | `gitlab-get-issue` |
| **Multi-Action** | Multiple operations on same resource | action enum, structured responses | `gitlab-manage-issue` |
| **Complex** | Advanced logic, discussions, position-based | Custom parameters, specialized logic | `gitlab-review-merge-request-code` |
### Response Format Patterns
**Simple CRUD - Concise**:
```typescript
if (format === "concise") {
return { content: [{ type: "text", text:
`🔍 Resource #${id}: ${title}\n` +
`📊 Status: ${state}\n` +
`🔗 URL: ${web_url}`
}] };
}
```
**Multi-Action - Structured**:
```typescript
return { content: [{ type: "text", text: JSON.stringify({
status: 'success',
action: 'close',
message: 'Issue closed successfully',
issue: { /* key fields */ }
}, null, 2) }] };
```
**Complex - Custom**:
```typescript
return { content: [{ type: "text", text: JSON.stringify({
action: "updated",
discussion_id: "...",
updated_note: {...}
}) }] };
```
### Error Handling Pattern
```typescript
try {
// Operation logic
} catch (e) {
// Simple CRUD
return { content: [{ type: "text", text: JSON.stringify({
type: "error",
error: `Error: ${String(e)}`
}) }] };
// Multi-Action
return { content: [{ type: "text", text: JSON.stringify({
status: 'failure',
error: `Error: ${String(e)}`
}, null, 2) }] };
}
```
---
## Tool Comparison Table
| Feature | Simple CRUD | Multi-Action | Complex |
|---------|-------------|--------------|---------|
| projectname param | ✅ Optional | ✅ Optional | ❌ May use projectId |
| format param | ✅ Required | ❌ Not used | ❌ Not used |
| action enum | ❌ Not used | ✅ Required | ❌ Custom |
| Emoji output | ✅ Concise format | ❌ Not used | ❌ Not used |
| HTML cleaning | ✅ Always | ✅ Always | ⚠️ If applicable |
| Response type | JSON or text | Structured JSON | Custom |
| Direct fetch API | ❌ Use service | ✅ Often used | ✅ If needed |
| Complexity | Low | Medium | High |
---
**All examples follow the project's standardized patterns and conventions from CLAUDE.md!**