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.
/plugin marketplace add daispacy/py-claude-marketplace/plugin install py-plugin@py-claude-marketplaceThis skill is limited to using the following tools:
examples.mdGenerate 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.
Basic get/list/create/update/delete operations with standard patterns:
Pattern: gitlab-[action]-[resource] (e.g., gitlab-get-issue, gitlab-list-pipelines)
Comprehensive tools that handle multiple related operations in one tool:
action enum parameterPattern: gitlab-[resource]-[operation] (e.g., gitlab-manage-issue)
Tools with advanced logic:
Pattern: Based on specific operation (e.g., gitlab-review-merge-request-code)
Extract key information:
Tool Name (kebab-case):
gitlab-[action]-[resource]
gitlab-get-merge-request, gitlab-list-pipelines, gitlab-create-branchgitlab-[manage|handle]-[resource]
gitlab-manage-issue, gitlab-handle-milestonegitlab-[specific-operation]
gitlab-review-merge-request-code, gitlab-find-related-issuesFunction Name (PascalCase):
register[Action][Resource]gitlab-get-merge-request → registerGetMergeRequestgitlab-manage-issue → registerManageIssuegitlab-review-merge-request-code → registerReviewMergeRequestCodeFile Name (kebab-case):
gitlab-[tool-name].tssrc/tools/gitlab/Use when: Single operation (get, list, create, update, delete)
Standard features:
projectname parameter (optional, with prompt fallback)format parameter (detailed/concise) for get/list operationscleanGitLabHtmlContent()Template structure:
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) }) }] };
}
}
);
}
Use when: Multiple related operations on same resource type
Standard features:
action parameter with enum of actions{ status: "success"/"failure", action: "...", message: "...", [resource]: {...} }Template structure:
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) }] };
}
}
);
}
Use when: Advanced logic like discussion management, position-based operations, multi-step workflows
Standard features:
Template structure: Highly variable based on specific needs
Common Parameter Patterns:
// 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:
.describe() with clear examples for all parametersz.array(z.string()) for arrays in multi-action tools[] are allowed in descriptions for paths/labels/markdownConcise format (with emojis):
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):
return { content: [{ type: "text", text: JSON.stringify({resource}, null, 2) }] };
Emoji Guide:
Structured JSON format:
// 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
}
}
Custom format based on operation needs. Examples:
// Discussion update/create
{
action: "updated" | "created",
discussion_id: "...",
note_id: "...",
updated_note: {...}
}
Standard Error Patterns:
// 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) }] };
}
After creating the tool file, add registration:
// 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);
}
Tool Type:
Tool Purpose:
Required Parameters:
Optional Parameters:
API Method (if not obvious):
src/services/gitlab-client.ts for available methodssrc/tools/gitlab/gitlab-{tool-name}.tssrc/tools/gitlab-tool.tsAfter generating the tool:
✅ 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);
Rebuild the project:
npm run build
Test the tool:
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({...})
});
projectname and format parametersaction enum and structured responsescleanGitLabHtmlContent() where applicable[] support in descriptions where relevant (file paths, labels, markdown)Before presenting the generated tool:
{ content: [{ type: "text", text: ... }] } formatReady to generate MCP tools! Tell me what GitLab operation you want to create a tool for.