Files
gh-daispacy-py-claude-marke…/skills/mcp-tool-generator/examples.md
2025-11-29 18:17:22 +08:00

26 KiB
Raw Blame History

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

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

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

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

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

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:

if (format === "concise") {
    return { content: [{ type: "text", text:
        `🔍 Resource #${id}: ${title}\n` +
        `📊 Status: ${state}\n` +
        `🔗 URL: ${web_url}`
    }] };
}

Multi-Action - Structured:

return { content: [{ type: "text", text: JSON.stringify({
    status: 'success',
    action: 'close',
    message: 'Issue closed successfully',
    issue: { /* key fields */ }
}, null, 2) }] };

Complex - Custom:

return { content: [{ type: "text", text: JSON.stringify({
    action: "updated",
    discussion_id: "...",
    updated_note: {...}
}) }] };

Error Handling Pattern

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!