AI 提效之 MCP - Agent 与执行工具的链接协议

AI Agent MCP 概念

Model Context Protocol (MCP)

  • MCP 是一个标准协议,就像给 AI 大模型装了一个 "万能接口",让 AI 模型能够与不同的数据源和工具进行无缝交互。它就像 USB-C 接口一样,提供了一种标准化的方法,将 AI 模型连接到各种数据源和工具。
  • MCP 旨在替换碎片化的 Agent 代码集成,从而使 AI 系统更可靠,更有效。通过建立通用标准,服务商可以基于协议来推出它们自己服务的 AI 能力,从而支持开发者更快的构建更强大的 AI 应用。开发者也不需要重复造轮子,通过开源项目可以建立强大的 AI Agent 生态。
  • MCP 可以在不同的应用 / 服务之间保持上下文,从而增强整体自主执行任务的能力。

MCP 的工作流程可以简单概括为以下几个步骤:

  1. 连接:MCP 主机连接到一个或多个 MCP 服务器。
  2. 请求:主机发送请求以获取数据或执行工具。
  3. 处理:服务器处理请求,访问相关数据源或外部服务。
  4. 返回:服务器将结果返回给主机。
  5. 生成响应:主机将信息提供给 AI 模型,用于生成用户响应。

MCP 存在的合理性:

为什么 AI 会出现并且需要 MCP 这种东西呢?

  1. 统一工具调用协议: MCP通过采用标准化的通信格式(如JSON-RPC),解决了AI工具调用中的接口碎片化问题。开发者只需实现一次MCP接口,即可让AI模型与所有支持该协议的工具交互。
  2. 提升开发效率: 在传统模式下,开发者需要为每个工具单独编写连接代码,而MCP通过统一的协议,极大地降低了开发成本和复杂性。
  3. 支持动态工具加载: MCP允许AI动态发现和调用工具,扩展了AI的能力边界。例如,AI可以通过MCP调用实时天气API或企业内部数据库,完成复杂任务。
  4. 跨平台兼容性: MCP兼容多种主流大模型(如GPT、Claude等),被称为AI领域的"USB-C接口",实现了"一次开发,全平台通用"的目标。

MCP 的时代局限性

虽然 MCP 的出现可以说是定义了一个应用开放给 AI Agent 链接的接口标准,但是随着科技日新月异的发展,之前的那套也逐渐出现了一定程度不足与局限性。

因为接入 MCP 后会在每次对话开始前就提供了所有 API 方法的结构定义声明给 Agent 读取;这样就会导致每次在对话前,还没开始正式聊天呢,就已经消耗了一部分的 token 和上下文,而这种情况会随着安装和启用的 MCP 工具增多而逐步加重;

因此现在已经出现另外一种的 AI Agent 调用 Api 的新方式:CLI + Skill 的组合。因为内容篇幅的限制,这部分的拓展的知识点,我们会在下一篇 AI 提效系列的分享文章当中详细讲述。


AI Agent MCP 的应用

TAPD 的应用

现代化的迭代项目应该都会使用 TAPD 来进行项目的管理操作,例如需求、缺陷等的管理;因此我们可以接入 TAPD 的 MCP 来使用 agent 接管我们原本手动的操作处理。

通过查看 TAPD 官方 MCP 的使用,能够看到 TAPD MCP 是支持两种不同的授权登录形式的(用户密码登录和 TOKEN 授权),我们这里直接使用 token 授权的形式来接入。

TAPD 的个人 Token 获取路径:www.tapd.cn/personal_se...

  • 记得保存好自己的个人 token,刷新后就不会再明文展示,丢失后只能重新创建了;

TAPD MCP 配置

cloud.tencent.com/developer/m...

  • 这里如果是跟着我前面获取了 TAPD 的个人 token,这里就可以只填写 TAPD_ACCESS_TOKEN 这个配置项
json 复制代码
"mcp-server-tapd": {
  "command": "uvx",
  "args": ["mcp-server-tapd"],
  "env": {
    "TAPD_ACCESS_TOKEN": "TAPD 个人 Token",
    "TAPD_API_USER": "",
    "TAPD_API_PASSWORD": "",
    "TAPD_API_BASE_URL": "https://api.tapd.cn",
    "TAPD_BASE_URL": "https://www.tapd.cn",
    "BOT_URL": ""
  }
}

配置了 TAPD MCP 之后,我们就能通过 AI 直接来进行相关 tapd 需求的读写、相关迭代、开发任务的编排等操作;甚至能进行相关缺陷的流转测试甩锅操作等。

文档的读取与编写

我们在开发迭代流程当中会经常的会与文档进行打交道,例如整理知识、需求文档、方案文档等;因此如果能通过 AI agent 直接读取编辑操作相关的文档,那就能使用 agent 一定程度上解放我们的双手,也能加快相关的内容的输出速度或者是加大相关内容的输出量等。

这里为啥选用飞书呢?那必须就是免费并且已经社区已经有现成的解决方案呢(语雀和腾讯文档其实理论上也可行,但是呢,可惜相关的开放 API 是需要充值相关的账户,)。

飞书 api 的前置准备工作(这个比较复杂,但是飞书的开放 api 确实可以操作的比较多,值得研究的)

  1. 首先进入飞书开放平台 open.feishu.cn/ 进行注册登录
  2. 创建企业应用(可以不用进行发布)
  1. 点击进入创建的企业应用,拷贝对应的 AppID 和 AppSecret 两个信息
  1. 获取应用的 token(两种不同的 token 会有权限之间的限制,例如应用的权限创建的文档表格东西,登录的用户是未必有权限能够删除或者管理等)
    1. t 的是使用这个企业应用的权限进行操作
    2. u 的是使用当前这个授权用户的权限进行操作(因此,推荐使用这个类型的 token 接入 MCP)

飞书 lark MCP 配置

open.feishu.cn/document/mc...

  • 将上述描述当中的
perl 复制代码
"lark-mcp-server": {
  "command": "npx",
  "args": [
    "-y",
    "@larksuiteoapi/lark-mcp",
    "mcp",
    "-a",
    "<your_app_id>",
    "-s",
    "<your_app_secret>",
    "-u",
    "<your_user_token>"
  ]
}

通过接入飞书 MCP 后我们能够玩的操作就非常多了,例如通过 AI Agent Plan 模式帮我们构思好相关的需求开发逻辑后直接输出到飞书文档上面形成需求开发方案(当然要不要直接拿这个方案去糊弄上级就你们自己决定 狗头);还有就是能够直接丢一个飞书文档直接让 AI agent 通过飞书 MCP 读取文档内容后根据内容进行各种操作。

自动化测试

作为开发,即使有相关的测试职位人员,开发人员也要在需求任务迭代开发完成后,进行一定程度的自测(冒烟测试)。因此想法是能不能开发尝试打通这条 AI 操作浏览器,然后进行通过自然语言描述去下达给 AI 操纵浏览器的可能呢?甚至是测试人员专门去写这种自然语言描述的测试路径实现 AI 自动化测试呢?答案当然是有啦,chrome-devtools 这个 MCP 就是谷歌浏览器专门提供给 AI 使用的形式。

关于使用浏览器 MCP 实现 AI 自动化测试的这部分我个人使用下来的优缺点吧;

优点: 可以在已经有相关描述比较完善的测试路径情况下,能够解放自己的双手,交给 ai agent 进行操作处理;并且还有一个非常重要的点就是完成上级派发的技术 KPI 啊,能够吹落地实践 AI 自动化测试就是了

缺点: 其实开发自己点击几下都比慢慢敲一大堆文字再交给 AI agent 执行快多了,时间紧迫(上级领导急急急或者需求猛猛的怼怼怼)的时候你就会嫌弃一个点击可能只需要一两秒的时间而 AI 需要慢悠悠的扫一次页面逻辑处理找到相关点击位置再慢悠悠的触发操作处理。

首先需要启动 debug 模式的 Chrome,命令行当中执行下面对应的命令:

ini 复制代码
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
  --remote-debugging-port=9222 \
  --user-data-dir=/tmp/chrome-mcp

Chrome DevTools MCP 配置

github.com/ChromeDevTo...

perl 复制代码
"chrome-devtools-mcp-server": {
  "command": "npx",
  "args": ["-y", "chrome-devtools-mcp@latest"]
}

然后我们就可以在叫 ai agent 下通过自然语言描述让其尝试操作下浏览器进行网页点击操作等处理;从而达到通过文档当中预设的测试用例来操作浏览器进行验证测试开发质量。

代码管理

开发完代码自然是要进行代码的提交啦,这里也有一个 git MCP 能够提供到给 AI agent 对代码进行操作处理。

git-mcp 配置

github.com/github/gith...

perl 复制代码
"git-mcp-server": {
  "command": "npx",
  "args": [
    "@cyanheads/git-mcp-server"
  ],
  "env": {
    "GIT_SIGN_COMMITS": "false",
    "MCP_LOG_LEVEL": "info"
  }
}

配置后接着我们试下在 ai agent 模式下输入 "通过文件的修改来生成符合一个 commitlint 格式的提交信息并且自动进行提交和推送远程仓库" 指令,让 ai agent 自行通过 MCP 自动进行操作处理。

接着推送到远程库之后我们就可以进行代码的 create merge request 操作处理

gitlab-mcp 配置

docs.gitlab.com/user/gitlab...

github.com/zereight/gi...

  • 这里我们就不使用官方 remote 例子,使用的是三方 MCP 库
  • 这里如果是自己搭建的 gitlab 服务,GITLAB_API_URL 参数就是自己部署的 gitlab 服务 url 地址。
perl 复制代码
"gitlab-mcp-server": {
  "command": "npx",
  "args": ["-y", "@zereight/mcp-gitlab"],
  "env": {
    "GITLAB_API_URL": "https://<gitlab.example.com>/api/v4/mcp",
    "GITLAB_PERSONAL_ACCESS_TOKEN": "your_gitlab_token",
    "GITLAB_READ_ONLY_MODE": "false",
    "USE_GITLAB_WIKI": "false", // use wiki api?
    "USE_MILESTONE": "false", // use milestone api?
    "USE_PIPELINE": "false" // use pipeline api?
  }
}

配置之后我们就能够在 ai agent 模式下达指令让其能够自动创建相关的一个 MR 并且甚至是进行 MR 合并操作处理。

至此,这些是笔者作为一个前端网页开发搬砖仔比较常用的相关 MCP 服务,能够更加快捷方便优化一个开发者在开发工作方面的工作流。但是也是要留意下,这些 MCP 的连接和使用会加速消耗 ai 模型的 token,使用要留意自己账号的 token 消耗量。


AI Agent MCP 的自定义编写

MCP 能够使用各种编程语言编写,当然我们作为一个前端崽,那肯定优先选择 node.js 进行开发。

官方 MCP 文档:github.com/modelcontex...

ts 语言版本:ts.sdk.modelcontextprotocol.io/

基础概念

先来了解一些 MCP 的基础概念:

@modelcontextprotocol/sdk

这是官方提供的一个 MCP Server SDK,用于管理和操作模型上下文。它提供API和工具,帮助开发者处理模型的状态、参数和输入输出,支持模型的初始化、配置和状态管理。

通过该 SDK 提供的 ServerStdioServerTransport 启动服务。

  • 模型通过 stdio 通信通道与这个服务交互,是 AI 模型与 MCP 其中一种通讯方式。
javascript 复制代码
const {Server} = require('@modelcontextprotocol/sdk/server/index.js');
const {StdioServerTransport} = require('@modelcontextprotocol/sdk/server/stdio.js');

// 创建 MCP Server
const server = new Server(
    {
        name: 'mcp-server',
        version: '1.0.0',
    },
    {
        capabilities: {
            tools: {},
        },
    }
);

const transport = new StdioServerTransport();
server.connect(transport);

ListToolsRequestSchema

当 AI 模型与 MCP 建立连接时候会请求询问这个 MCP 能够提供什么工具或者方法调用;这时候就是调用这个方法获取到对应的 MCP Server 定义列出可用工具的请求数据结构。

TOOLS 是一个数组,包含所有工具方法的定义,每个工具方法的结构如下:

字段 类型 说明
name string 工具名称
description string 工具描述
inputSchema object JSON Schema 格式的参数定义

InputSchema是对方法参数的描述,具体的结构如下:

字段 类型 必填 说明
type string 固定值为字符串 'object'
properties Record<string, object> 调用方法时候传入的参数数据结构描述
required Array<string> 调用方法时候必传的 properties 当中的参数

properties内的对象是调用方法时候传入的参数数据结构描述,具体的结构如下:

字段 类型 必填 说明
type string 参数类型字符串描述
description string 参数描述
go 复制代码
const {
  ListToolsRequestSchema,
} = require('@modelcontextprotocol/sdk/types.js');

const TOOLS = [
    {
        name: 'get_current_user',
        description: '获取当前 GitLab 用户信息',
        inputSchema: {
            type: 'object',
            properties: {}
        }
    },
    {
        name: 'get_project_ids',
        description: '搜索项目并获取项目 ID 映射',
        inputSchema: {
            type: 'object',
            properties: {
                projectName: {
                    type: 'string',
                    description: '项目名称'
                }
            },
            required: ['projectName']
        }
    },
    // ...
]

server.setRequestHandler(ListToolsRequestSchema, async () => {
    return {tools: TOOLS};
});

CallToolRequestSchema

当 AI 模型触发 MCP 工具的调用的时候,这个类型的调用就会触发,会传递一个结构化的调用参数(如下),MCP Server 则需要根据结构化的调用参数进行相关对应的处理。

csharp 复制代码
const {
  CallToolRequestSchema,
} = require('@modelcontextprotocol/sdk/types.js');

server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const {name, arguments: args} = request.params;
})

实战例子:gitlab-mcp-server

有了上面的 MCP Server 基础知识,我们现在来尝试下写一个操作 gitlab 的 MCP。

应该会有人会疑问,前面不是已经有相关的一个 gitlab 的 MCP 服务了吗(官方也好,三方的也罢)为啥还要继续另外自己写一个呢?那当然就是公司使用的 gitlab 版本过于落后,api 已经与现网的 MCP 不支持;因此这里我们就顺手进行一次 MCP 的编写,进行一次 MCP 的真正的有实际用处的实战开发。

  • 这里我们就不进行实现最完整完善的 gitlab 所有操作,主要是实现创建 MR、合并 MR 以及一些相关配套必要的操作处理;
  • 因为文章偏重点主要是 MCP 服务如何进行编写,这里并不会进行详细解释描述 gitlab MR 操作的具体实现逻辑。

初始化配置项目

MCP Server package.json 文件

留意这里有个 bin 的配置,这是 npx 比较关键的一句代码声明,只有配置了这个才能在使用 npx 命令时候知道应该执行什么命令和文件处理。

这里就再不多进行赘述了,如果有开发过相关 npm 包库应该也熟悉,不太熟悉或者不懂的童鞋可以右转找 AI 问问或者进行搜索引擎搜索相关的资料查阅。

perl 复制代码
{
  "name": "@jesbrian/gitlab-mcp-server",
  "version": "0.2.1",
  "description": "GitLab MCP Server with Modular Configuration",
  "license": "ISC",
  "author": "JesBrian",
  "type": "commonjs",
  "main": "index.js",
  "bin": {
    "gitlab-mcp-server": "index.js"
  },
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.27.1",
    "axios": "^1.6.0"
  }
}

定义相关 API 方法

封装的配置文件 Config.js

这个配置文件主要是配置参数的功能:

  • 包括服务器地址路由、授权 token、超时等基本配置处理;
  • 还有就是最重要的一个配置信息,就是提供给 AI Agent 接入这个 MCP 后提供的 Tools 的信息的定义;

不过,这里就不另外进行进行相关配置的解释了,感兴趣的童鞋可以自行解读。

css 复制代码
/**
 * GitLab MCP Server 配置
 * 集中管理所有配置项
 */

const env = process.env;

// 默认配置
const DEFAULT_CONFIG = {
    // GitLab API 配置
    gitlab: {
        url: env.GITLAB_URL || env.gitlab_url,
        privateToken: env.GITLAB_PRIVATE_TOKEN || env.gitlab_private_token,
    },
    
    // 客户端选项
    client: {
        timeout: parseInt(env.TIMEOUT) || 10000,
        waitInterval: parseInt(env.WAIT_INTERVAL) || 2000,
    },
    
    // MCP Server 配置
    server: {
        name: 'gitlab-mcp-server',
        version: '1.0.0',
    },
};

// MCP Tools 定义
const TOOLS = [    {        name: 'get_current_user',        description: '获取当前 GitLab 用户信息',        inputSchema: {            type: 'object',            properties: {}        }    },    {        name: 'get_project_ids',        description: '搜索项目并获取项目 ID 映射',        inputSchema: {            type: 'object',            properties: {                projectName: {                    type: 'string',                    description: '项目名称'                }            },            required: ['projectName']
        }
    },
    {
        name: 'create_merge_request',
        description: '创建合并请求 (MR)',
        inputSchema: {
            type: 'object',
            properties: {
                sourceProjectId: {type: 'number', description: '源项目 ID'},
                targetProjectId: {type: 'number', description: '目标项目 ID'},
                sourceBranch: {type: 'string', description: '源分支名'},
                targetBranch: {type: 'string', description: '目标分支名'},
                title: {type: 'string', description: 'MR 标题'},
                description: {type: 'string', description: 'MR 描述'}
            },
            required: ['sourceProjectId', 'targetProjectId', 'sourceBranch', 'targetBranch']
        }
    },
    {
        name: 'create_merge_request_to_public',
        description: '创建从个人仓库到公共仓库的合并请求',
        inputSchema: {
            type: 'object',
            properties: {
                projectName: {type: 'string', description: '项目名称'},
                sourceBranch: {type: 'string', description: '源分支名'},
                targetBranch: {type: 'string', description: '目标分支名'},
                title: {type: 'string', description: 'MR 标题'},
                description: {type: 'string', description: 'MR 描述'}
            },
            required: ['projectName', 'sourceBranch', 'targetBranch']
        }
    },
    {
        name: 'merge_branch',
        description: '执行分支合并',
        inputSchema: {
            type: 'object',
            properties: {
                targetProjectId: {type: 'number', description: '目标项目 ID'},
                mergeRequestIid: {type: 'string', description: 'MR 内部 ID'}
            },
            required: ['targetProjectId', 'mergeRequestIid']
        }
    },
    {
        name: 'get_merge_request_status',
        description: '获取合并请求状态',
        inputSchema: {
            type: 'object',
            properties: {
                targetProjectId: {type: 'number', description: '目标项目 ID'},
                mergeRequestIid: {type: 'string', description: 'MR 内部 ID'}
            },
            required: ['targetProjectId', 'mergeRequestIid']
        }
    },
    {
        name: 'close_merge_request',
        description: '关闭合并请求',
        inputSchema: {
            type: 'object',
            properties: {
                targetProjectId: {type: 'number', description: '目标项目 ID'},
                mergeRequestIid: {type: 'string', description: 'MR 内部 ID'}
            },
            required: ['targetProjectId', 'mergeRequestIid']
        }
    },
    {
        name: 'wait_for_mergeable_status',
        description: '等待 MR 状态变为可合并',
        inputSchema: {
            type: 'object',
            properties: {
                targetProjectId: {type: 'number', description: '目标项目 ID'},
                mergeRequestIid: {type: 'string', description: 'MR 内部 ID'},
                timeout: {type: 'number', description: '超时时间 (毫秒)', default: 300000}
            },
            required: ['targetProjectId', 'mergeRequestIid']
        }
    },
    {
        name: 'get_project',
        description: '获取项目详情',
        inputSchema: {
            type: 'object',
            properties: {
                projectId: {type: 'number', description: '项目 ID'}
            },
            required: ['projectId']
        }
    },
    {
        name: 'get_repository_branches',
        description: '获取仓库所有分支',
        inputSchema: {
            type: 'object',
            properties: {
                projectId: {type: 'number', description: '项目 ID'}
            },
            required: ['projectId']
        }
    },
    {
        name: 'create_branch',
        description: '创建新分支',
        inputSchema: {
            type: 'object',
            properties: {
                projectId: {type: 'number', description: '项目 ID'},
                branchName: {type: 'string', description: '新分支名'},
                ref: {type: 'string', description: '源分支/标签', default: 'master'}
            },
            required: ['projectId', 'branchName']
        }
    },
    {
        name: 'delete_branch',
        description: '删除分支',
        inputSchema: {
            type: 'object',
            properties: {
                projectId: {type: 'number', description: '项目 ID'},
                branchName: {type: 'string', description: '分支名'}
            },
            required: ['projectId', 'branchName']
        }
    }
];

module.exports = {
    DEFAULT_CONFIG,
    TOOLS,
};

MCP Server 主执行文件 index.js

留意这里必须需要先在文件首行设置声明 #!/usr/bin/env node

  • 这是一句 Shebang 语言,这里也同样不多展开描述了,简单来说就是能够支持在 Linux/Unix 系统下能够识别你这个脚本文件使用什么解释器来进行脚本的执行。
  • 注:windows 是根据文件的后缀名来选定对应的解释器执行。

这个文件就是 MCP 服务启动的逻辑:

  1. 其实就是调用文章前面讲述的 sdk 的 Server 创建 node.js 服务;
    1. 设置 setRequestHandler 和 setRequestHandler 的调用处理;
  1. 并且选择一种 Agent 能识别的传输格式(例如. StdioServerTransport)来与这个 server 进行通信调用。
javascript 复制代码
#!/usr/bin/env node

/**
 * GitLab MCP Server
 * 基于 Model Context Protocol 的 GitLab API 服务
 */

const {Server} = require('@modelcontextprotocol/sdk/server/index.js');
const {StdioServerTransport} = require('@modelcontextprotocol/sdk/server/stdio.js');
const {
    CallToolRequestSchema,
    ListToolsRequestSchema,
} = require('@modelcontextprotocol/sdk/types.js');
const GitLabClient = require('./lib/GitLabClient');
const { DEFAULT_CONFIG, TOOLS } = require('./config/Config');

// 初始化 GitLab Client
const gitlabClient = new GitLabClient(
    DEFAULT_CONFIG.gitlab.url,
    DEFAULT_CONFIG.gitlab.privateToken,
    DEFAULT_CONFIG.client
);

// 创建 MCP Server
const server = new Server(
    DEFAULT_CONFIG.server,
    {
        capabilities: {
            tools: {},
        },
    }
);

// 处理 ListTools 请求
server.setRequestHandler(ListToolsRequestSchema, async () => {
    return {tools: TOOLS};
});

// 处理 CallTool 请求
server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const {name, arguments: args} = request.params;

    try {
        let result;

        switch (name) {
            case 'get_current_user':
                result = await gitlabClient.getCurrentUser();
                break;

            case 'get_project_ids':
                result = await gitlabClient.getProjectIds(args.projectName);
                break;

            case 'create_merge_request':
                result = await gitlabClient.createMergeRequest(
                    args.sourceProjectId,
                    args.targetProjectId,
                    args.sourceBranch,
                    args.targetBranch,
                    {title: args.title, description: args.description}
                );
                break;
            
            case 'create_merge_request_to_public':
                result = await gitlabClient.createMergeRequestToPublic(
                    args.projectName,
                    args.sourceBranch,
                    args.targetBranch,
                    {title: args.title, description: args.description}
                );
                break;

            case 'merge_branch':
                result = await gitlabClient.mergeBranch(args.targetProjectId, args.mergeRequestIid);
                break;

            case 'get_merge_request_status':
                result = await gitlabClient.getMergeRequestStatus(args.targetProjectId, args.mergeRequestIid);
                break;

            case 'close_merge_request':
                result = await gitlabClient.closeMergeRequest(args.targetProjectId, args.mergeRequestIid);
                break;

            case 'wait_for_mergeable_status':
                result = await gitlabClient.waitForMergeableStatus(
                    args.targetProjectId,
                    args.mergeRequestIid,
                    args.timeout
                );
                break;

            case 'get_project':
                result = await gitlabClient.getProject(args.projectId);
                break;

            case 'get_repository_branches':
                result = await gitlabClient.getRepositoryBranches(args.projectId);
                break;

            case 'create_branch':
                result = await gitlabClient.createBranch(args.projectId, args.branchName, args.ref);
                break;

            case 'delete_branch':
                result = await gitlabClient.deleteBranch(args.projectId, args.branchName);
                break;

            default:
                throw new Error(`未知的工具:${name}`);
        }

        return {
            content: [
                {
                    type: 'text',
                    text: JSON.stringify(result, null, 2)
                }
            ]
        };
    } catch (error) {
        return {
            content: [
                {
                    type: 'text',
                    text: `错误:${error.message}`
                }
            ],
            isError: true
        };
    }
});

// 启动服务器
async function main() {
    const transport = new StdioServerTransport();
    await server.connect(transport);
    console.info('GitLab MCP Server 已启动');
}

main().catch(error => {
    console.error('服务器启动失败:', error);
    process.exit(1);
});

具体操作逻辑

封装的 gitlab 操作文件 GitLabClient.js

这个就是具体对 gitlab 进行操作方法的封装逻辑,提供给前面各类 Tools 的实际运行逻辑使用。

  • 这里就不展开一个个方法来讲述了,感兴趣的童鞋自行研究吧。
ini 复制代码
/**
 * GitLab API Client
 * 封装 GitLab API 调用的客户端类
 */

const axios = require('axios');

class GitLabClient {
    constructor(gitlabUrl, privateToken, options = {}) {
        this.gitlabUrl = gitlabUrl;
        this.privateToken = privateToken;
        this.timeout = options.timeout || 10000;
        this.waitInterval = options.waitInterval || 2000;

        this.client = axios.create({
            baseURL: this.gitlabUrl,
            headers: {
                'PRIVATE-TOKEN': this.privateToken,
                'Content-Type': 'application/json'
            },
            timeout: this.timeout
        });

        this.client.interceptors.response.use(
            response => response,
            error => {
                const message = error.response?.data?.message || error.message;
                console.error(`[GitLab API 错误] ${error.config?.method?.toUpperCase()} ${error.config?.url}: ${message}`);
                throw error;
            }
        );
    }

    async getCurrentUser() {
        const response = await this.client.get('/api/v4/user');
        return response.data;
    }

    async getProjectIds(projectName) {
        const url = `/api/v4/projects?search=${encodeURIComponent(projectName)}`;
        const response = await this.client.get(url);
        const projects = response.data;

        const projectMap = {};
        for (const project of projects) {
            const namespace = project.namespace?.path;
            if (namespace) {
                projectMap[namespace] = project.id;
            }
        }
        return projectMap;
    }

    async createMergeRequest(sourceProjectId, targetProjectId, sourceBranch, targetBranch, options = {}) {
        const url = `/api/v4/projects/${sourceProjectId}/merge_requests`;
        const data = {
            source_branch: sourceBranch,
            target_branch: targetBranch,
            target_project_id: targetProjectId,
            title: options.title || `Merge ${sourceBranch} to ${targetBranch}`,
            description: options.description || 'Merged via MCP Server'
        };

        const response = await this.client.post(url, data);
        return response.data;
    }
    
    async createMergeRequestToPublic(projectName, sourceBranch, targetBranch, options = {}) {
        const user = await this.getCurrentUser();
        const projectIds = await this.getProjectIds(projectName);

        if (Object.keys(projectIds).length < 2) {
            throw new Error(`项目 '${projectName}' 的 ID 映射不完整,无法确定个人仓库和公共仓库。`);
        }

        const sourceProjectId = projectIds[user.username];
        if (!sourceProjectId) {
            throw new Error(`在项目 '${projectName}' 中未找到用户 '${user.username}' 的个人仓库。`);
        }

        const targetProjectId = Object.values(projectIds).find(id => id !== sourceProjectId);
        if (!targetProjectId) {
            throw new Error(`在项目 '${projectName}' 中未找到公共仓库。`);
        }

        return this.createMergeRequest(sourceProjectId, targetProjectId, sourceBranch, targetBranch, options);
    }

    async mergeBranch(targetProjectId, mergeRequestIid) {
        const url = `/api/v4/projects/${targetProjectId}/merge_requests/${mergeRequestIid}/merge`;
        const response = await this.client.put(url);
        return response.data;
    }

    async getMergeRequestStatus(targetProjectId, mergeRequestIid) {
        const url = `/api/v4/projects/${targetProjectId}/merge_requests/${mergeRequestIid}`;
        const response = await this.client.get(url);
        return {
            merge_status: response.data.merge_status,
            has_conflicts: response.data.has_conflicts,
            ...response.data
        };
    }

    async closeMergeRequest(targetProjectId, mergeRequestIid) {
        const url = `/api/v4/projects/${targetProjectId}/merge_requests/${mergeRequestIid}`;
        const response = await this.client.put(url, {state_event: 'close'});
        return response.data;
    }

    async getProject(projectId) {
        const url = `/api/v4/projects/${projectId}`;
        const response = await this.client.get(url);
        return response.data;
    }

    async getRepositoryBranches(projectId) {
        const url = `/api/v4/projects/${projectId}/repository/branches`;
        const response = await this.client.get(url);
        return response.data;
    }

    async createBranch(projectId, branchName, ref) {
        const url = `/api/v4/projects/${projectId}/repository/branches`;
        const response = await this.client.post(url, {
            branch: branchName,
            ref: ref || 'master'
        });
        return response.data;
    }

    async deleteBranch(projectId, branchName) {
        const url = `/api/v4/projects/${projectId}/repository/branches/${encodeURIComponent(branchName)}`;
        const response = await this.client.delete(url);
        return response.data;
    }

    async waitForMergeableStatus(targetProjectId, mergeRequestIid, timeout = 300000) {
        const startTime = Date.now();
        const interval = this.waitInterval;

        while (Date.now() - startTime < timeout) {
            const stat = await this.getMergeRequestStatus(targetProjectId, mergeRequestIid);

            if (stat.merge_status === 'cannot_be_merged') {
                return {canMerge: false, hasConflicts: true, status: stat.merge_status};
            }

            if (stat.merge_status === 'can_be_merged') {
                return {canMerge: true, hasConflicts: false, status: stat.merge_status};
            }

            await new Promise(resolve => setTimeout(resolve, interval));
        }

        return {canMerge: false, hasConflicts: false, status: 'timeout'};
    }
}

module.exports = GitLabClient;

接入使用

好了,剩下的就看你们要不要发布到 npm 仓库当中,如果只是想要自己使用则可以配置本地启动的形式:

perl 复制代码
{
  "mcpServers": {
    "gitlab-mpc-server": {
      "command": "npx",
      "args": [
        "@jesbrian/gitlab-mcp-server"
      ],
      "env": {
        "GITLAB_URL": "https://your-gitlab-url.com",
        "GITLAB_PRIVATE_TOKEN": "your-private-token"
      }
    }
  }
}

而如果推送到了 npm 仓库当中,则可以通过 npx 的配置形式,直接通过远程 npm 包启动!

这里就回收前面的 package.json 当中配置的 bin 命令还有 #!/usr/bin/env node 发挥作用的时候。

perl 复制代码
{
  "mcpServers": {
    "gitlab-mcp-server": {
      "command": "npx",
      "args": ["@jesbrian/gitlab-mcp-server"],
      "env": {
        "GITLAB_URL": "https://your-gitlab-url.com",
        "GITLAB_PRIVATE_TOKEN": "your-private-token"
      }
    }
  }
}

参考资料:

相关推荐
ZC跨境爬虫2 小时前
跟着 MDN 学 HTML day_37:(深入掌握 CustomEvent 自定义事件接口)
前端·javascript·ui·html·音视频
whinc9 小时前
JavaScript技术周刊 2026年第18周
javascript
码海扬帆:前端探索之旅9 小时前
深度定制 uni-combox:新增功能详解与实战指南
前端·vue.js·uni-app
谷雨不太卷9 小时前
进程的状态码
java·前端·算法
打小就很皮...9 小时前
基于 Python + LangChain + RAG 的知识检索系统实战
前端·langchain·embedding·rag
whinc9 小时前
JavaScript技术周刊 2026年第17周
javascript
BJ-Giser9 小时前
Cesium 烟雾粒子特效
前端·可视化·cesium