490 lines
13 KiB
JavaScript
490 lines
13 KiB
JavaScript
// 引入必要的模块
|
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
import { z } from "zod";
|
|
|
|
// 创建 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://gitlab.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,
|
|
};
|
|
}
|
|
}
|
|
)
|
|
|
|
|
|
// 启动服务器,使用标准输入/输出(stdio)与客户端通信
|
|
async function main() {
|
|
const transport = new StdioServerTransport();
|
|
await server.connect(transport);
|
|
console.error("GitLab MCP 服务器已通过 stdio 启动"); // 日志输出到 stderr
|
|
}
|
|
|
|
// 运行主函数,并捕获可能的错误
|
|
main().catch((error) => {
|
|
console.error("服务器运行出现致命错误:", error);
|
|
process.exit(1);
|
|
});
|
|
|
|
export { server }; |