Files

628 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 <type> 传输方式: 'stdio' 或 'http' (默认: stdio)
-p, --port <number> 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);
});