#!/usr/bin/env node // 引入必要的模块 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { z } from "zod"; import http from "http"; // 创建 MCP 服务器实例 // 为其设置一个名字和版本 const server = new McpServer({ name: "gitlab-mcp-server", version: "1.0.0", }); /** * GitLab API 客户端配置 * 用于连接到GitLab实例并执行操作 */ const gitlabApiClient = { baseUrl: process.env.GITLAB_API_BASE_URL || "https://git.ifengqun.com/api/v4", token: process.env.GITLAB_ACCESS_TOKEN, async makeRequest(endpoint, options = {}) { if (!this.token) { throw new Error("GITLAB_ACCESS_TOKEN 环境变量未设置"); } const url = `${this.baseUrl}${endpoint}`; const defaultOptions = { method: "GET", headers: { "PRIVATE-TOKEN": this.token, "Content-Type": "application/json", }, }; const requestOptions = { ...defaultOptions, ...options }; const response = await fetch(url, requestOptions); if (!response.ok) { throw new Error(`GitLab API请求失败: ${response.status} ${response.statusText}`); } return await response.json(); }, /** * 创建Merge Request */ async createMergeRequest(projectId, sourceBranch, targetBranch, title, description = "", assigneeId, reviewerIds) { const body = { id: projectId, source_branch: sourceBranch, target_branch: targetBranch, title, description, ...(assigneeId !== undefined && { assignee_id: assigneeId }), ...(reviewerIds !== undefined && { reviewer_ids: reviewerIds }), }; return await this.makeRequest( `/projects/${projectId}/merge_requests`, { method: "POST", body: JSON.stringify(body), headers: { "PRIVATE-TOKEN": this.token, "Content-Type": "application/json", }, } ); }, /** * 合并Merge Request */ async mergeMergeRequest(projectId, mrIid, mergeCommitMessage = "", shouldRemoveSourceBranch = false, mergeWhenPipelineSucceeds = false) { const body = { id: projectId, merge_request_iid: mrIid, ...(mergeCommitMessage && { merge_commit_message: mergeCommitMessage }), remove_source_branch: shouldRemoveSourceBranch, merge_when_pipeline_succeeds: mergeWhenPipelineSucceeds, }; return await this.makeRequest( `/projects/${projectId}/merge_requests/${mrIid}/merge`, { method: "PUT", body: JSON.stringify(body), headers: { "PRIVATE-TOKEN": this.token, "Content-Type": "application/json", }, } ); }, /** * 获取Merge Request详情 */ async getMergeRequest(projectId, mrIid) { return await this.makeRequest(`/projects/${projectId}/merge_requests/${mrIid}`); }, /** * 关闭Merge Request */ async closeMergeRequest(projectId, mrIid) { return await this.makeRequest( `/projects/${projectId}/merge_requests/${mrIid}`, { method: "PUT", body: JSON.stringify({ state_event: "close" }), headers: { "PRIVATE-TOKEN": this.token, "Content-Type": "application/json", }, } ); } }; /** * 创建Merge Request工具 */ server.registerTool( "create_merge_request", { description: "在指定的GitLab项目中创建一个新的Merge Request", inputSchema: z.object({ projectId: z.string().describe("GitLab项目的ID或URL编码的路径"), sourceBranch: z.string().describe("源分支名称"), targetBranch: z.string().describe("目标分支名称"), title: z.string().describe("Merge Request的标题"), description: z.string().optional().describe("Merge Request的描述"), assigneeId: z.number().optional().describe("负责人ID"), reviewerIds: z.array(z.number()).optional().describe("审阅者ID数组"), }), }, async ({ projectId, sourceBranch, targetBranch, title, description = "", assigneeId, reviewerIds }) => { try { const result = await gitlabApiClient.createMergeRequest( projectId, sourceBranch, targetBranch, title, description, assigneeId, reviewerIds ); return { content: [ { type: "text", text: JSON.stringify({ success: true, message: "成功创建Merge Request", data: { url: result.web_url, title: result.title, state: result.state, iid: result.iid, id: result.id } }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ success: false, message: "创建Merge Request失败", error: error.message }, null, 2), }, ], isError: true, }; } } ); /** * 合并Merge Request工具 */ server.registerTool( "merge_merge_request", { description: "合并指定的GitLab Merge Request", inputSchema: z.object({ projectId: z.string().describe("GitLab项目的ID或URL编码的路径"), mrIid: z.number().describe("Merge Request的内部ID"), mergeCommitMessage: z.string().optional().describe("合并提交的消息"), shouldRemoveSourceBranch: z.boolean().optional().describe("是否在合并后删除源分支"), mergeWhenPipelineSucceeds: z.boolean().optional().describe("是否在CI管道成功后合并"), }), }, async ({ projectId, mrIid, mergeCommitMessage = "", shouldRemoveSourceBranch = false, mergeWhenPipelineSucceeds = false }) => { try { const result = await gitlabApiClient.mergeMergeRequest( projectId, mrIid, mergeCommitMessage, shouldRemoveSourceBranch, mergeWhenPipelineSucceeds ); return { content: [ { type: "text", text: JSON.stringify({ success: true, message: `成功合并Merge Request #${mrIid}`, data: { iid: result.iid, state: result.state, mergedAt: result.merged_at, mergedBy: result.merged_by?.name, id: result.id } }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ success: false, message: "合并Merge Request失败", error: error.message }, null, 2), }, ], isError: true, }; } } ); /** * 获取Merge Request详情工具 */ server.registerTool( "get_merge_request", { description: "获取指定Merge Request的详细信息", inputSchema: z.object({ projectId: z.string().describe("GitLab项目的ID或URL编码的路径"), mrIid: z.number().describe("Merge Request的内部ID"), }), }, async ({ projectId, mrIid }) => { try { const result = await gitlabApiClient.getMergeRequest(projectId, mrIid); return { content: [ { type: "text", text: JSON.stringify({ success: true, message: `Merge Request #${mrIid} 详情`, data: { iid: result.iid, title: result.title, author: result.author.name, state: result.state, sourceBranch: result.source_branch, targetBranch: result.target_branch, createdAt: result.created_at, updatedAt: result.updated_at, description: result.description, url: result.web_url } }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ success: false, message: "获取Merge Request详情失败", error: error.message }, null, 2), }, ], isError: true, }; } } ); /** * 关闭Merge Request工具 */ server.registerTool( "close_merge_request", { description: "关闭指定的GitLab Merge Request而不合并", inputSchema: z.object({ projectId: z.string().describe("GitLab项目的ID或URL编码的路径"), mrIid: z.number().describe("Merge Request的内部ID"), }), }, async ({ projectId, mrIid }) => { try { const result = await gitlabApiClient.closeMergeRequest(projectId, mrIid); return { content: [ { type: "text", text: JSON.stringify({ success: true, message: `成功关闭Merge Request #${mrIid}`, data: { iid: result.iid, title: result.title, state: result.state, updatedAt: result.updated_at, id: result.id } }, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ success: false, message: "关闭Merge Request失败", error: error.message }, null, 2), }, ], isError: true, }; } } ); server.registerTool( 'merge_to_test', { description: "把源分支名合并到测试分支,并部署到测试环境", inputSchema: z.object({ projectId: z.string().describe("GitLab项目projectId"), sourceBranch: z.string().describe("源分支名称,默认请使用git branch --show-current获取当前分支传递"), }), }, async ({ projectId, sourceBranch, }) => { if (!sourceBranch) { // 获取当前分支,用 git branch --show-current 命令 const { execSync } = await import('child_process'); try { sourceBranch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim(); } catch (error) { console.error('执行 git branch --show-current 命令失败:', error); return { content: [ { type: "text", text: JSON.stringify({ success: false, message: "获取当前分支失败", error: error.message }, null, 2), }, ], isError: true, }; } } try { const result = await gitlabApiClient.createMergeRequest( projectId, sourceBranch, 'test', `Merge ${sourceBranch} to test`, // 标题 `Automated merge request from ${sourceBranch} to test branch` // 描述 ); const mrIid = result.iid; // 等待3秒钟,让GitLab有时间处理新创建的MR await new Promise(resolve => setTimeout(resolve, 3000)); // 尝试合并MR try { const mergeResult = await gitlabApiClient.mergeMergeRequest( projectId, mrIid ); return { content: [ { type: "text", text: JSON.stringify({ success: true, message: "成功创建并合并Merge Request", data: { url: mergeResult.web_url, title: mergeResult.title, state: mergeResult.state, mergedAt: mergeResult.merged_at, iid: mergeResult.iid, id: mergeResult.id } }, null, 2), }, ], }; } catch (mergeError) { // 如果合并失败,返回MR创建成功的信息,但提示合并需要审核 return { content: [ { type: "text", text: JSON.stringify({ success: false, message: "成功创建Merge Request,但自动合并失败,请手动审核", data: { url: result.web_url, title: result.title, state: result.state, iid: result.iid, id: result.id }, error: mergeError.message }, null, 2), }, ], }; } } catch (error) { return { content: [ { type: "text", text: JSON.stringify({ success: false, message: "合并到test分支失败", error: error.message }, null, 2), }, ], isError: true, }; } } ) /** * 解析命令行参数 */ function parseArgs() { const args = process.argv.slice(2); const config = { transport: 'stdio', // 默认使用 stdio port: 8899, // 默认 HTTP 端口 }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--transport' || arg === '-t') { const value = args[i + 1]; if (value === 'stdio' || value === 'http') { config.transport = value; i++; // 跳过下一个参数 } else { console.error(`错误: --transport 参数必须是 'stdio' 或 'http'`); process.exit(1); } } else if (arg === '--port' || arg === '-p') { const value = parseInt(args[i + 1], 10); if (!isNaN(value) && value > 0 && value < 65536) { config.port = value; i++; // 跳过下一个参数 } else { console.error(`错误: --port 参数必须是 1-65535 之间的有效端口号`); process.exit(1); } } else if (arg === '--help' || arg === '-h') { console.log(` GitLab MCP 服务器 用法: node index.js [选项] npx gitlab-mcp-server [选项] 选项: -t, --transport 传输方式: 'stdio' 或 'http' (默认: stdio) -p, --port HTTP 服务器端口 (默认: 3000, 仅在 http 模式下有效) -h, --help 显示帮助信息 说明: - stdio 模式: 使用标准输入/输出(MCP 标准),适合与 MCP 客户端集成 - http 模式: 使用 Streamable HTTP 传输,适合远程访问和调试 示例: node index.js # stdio 模式 node index.js --transport http --port 3000 # HTTP 模式 npx gitlab-mcp-server # stdio 模式 npx gitlab-mcp-server -t http -p 8080 # HTTP 模式 环境变量: GITLAB_ACCESS_TOKEN GitLab 访问令牌(必需) GITLAB_API_BASE_URL GitLab API 基础 URL(可选,默认: https://gitlab.com/api/v4) `); process.exit(0); } } return config; } /** * 启动 HTTP 服务器(使用 Streamable HTTP 传输) */ async function startHttpServer(port) { const httpServer = http.createServer(async (req, res) => { // 处理 CORS res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } if (req.method === 'GET' && req.url === '/health') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'healthy', transport: 'http' })); return; } // 所有的 MCP 请求都通过 transport 处理 // StreamableHTTPServerTransport 会处理 GET (SSE) 和 POST (Message) try { await transport.handleRequest(req, res); } catch (error) { console.error('Error handling request:', error); if (!res.headersSent) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Internal Server Error' })); } } }); const transport = new StreamableHTTPServerTransport({ endpoint: '/message', }); await server.connect(transport); return new Promise((resolve, reject) => { httpServer.listen(port, () => { console.error(`GitLab MCP 服务器已启动 (Streamable HTTP 模式)`); console.error(`监听端口: ${port}`); console.error(`MCP 端点: http://localhost:${port}/message`); resolve(httpServer); }); httpServer.on('error', (error) => { console.error('HTTP 服务器启动失败:', error); reject(error); }); }); } /** * 启动 Stdio 服务器 */ async function startStdioServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("GitLab MCP 服务器已启动 (Stdio 模式)"); } // 主函数:根据参数选择传输方式启动服务器 async function main() { const config = parseArgs(); if (config.transport === 'http') { await startHttpServer(config.port); } else { await startStdioServer(); } } // 运行主函数,并捕获可能的错误 main().catch((error) => { console.error("服务器运行出现致命错误:", error); process.exit(1); });