Refactor GitLab MCP server implementation
- Removed gitlab-index.js and integrated its functionality into index.js. - Updated server name to "gitlab-mcp-server" and version to "1.1.0". - Enhanced GitLab API client with methods for creating, merging, retrieving, and closing merge requests. - Added tools for handling merge requests with detailed input validation using Zod. - Implemented command-line argument parsing for transport mode (stdio or http) and port configuration. - Introduced Streamable HTTP transport for remote access and debugging. - Updated package dependencies to the latest versions.
This commit is contained in:
1 parent
09d624a464
commit
347096b2e1
5 files changed
+1057
-660
No files matched your search
@@ -1,103 +1,490 @@
|
|||||||
# GitLab MCP 服务器
|
# GitLab MCP Server
|
||||||
|
|
||||||
这是一个 Model Context Protocol (MCP) 服务器,专门用于与 GitLab 交互,主要功能包括创建和合并 Merge Request。
|
一个支持 GitLab 操作的 Model Context Protocol (MCP) 服务器,主要用于创建和管理 Merge Requests。
|
||||||
|
|
||||||
## 功能
|
## 功能特性
|
||||||
|
|
||||||
- `create_merge_request`: 在指定的 GitLab 项目中创建一个新的 Merge Request
|
- ✅ 创建 Merge Request
|
||||||
- `merge_merge_request`: 合并指定的 GitLab Merge Request
|
- ✅ 合并 Merge Request
|
||||||
- `get_merge_request`: 获取指定 Merge Request 的详细信息
|
- ✅ 获取 Merge Request 详情
|
||||||
- `close_merge_request`: 关闭指定的 GitLab Merge Request 而不合并
|
- ✅ 关闭 Merge Request
|
||||||
|
- ✅ 快捷合并到测试分支
|
||||||
|
- ✅ 支持 Stdio 和 Streamable HTTP 两种传输方式
|
||||||
|
- ✅ 支持通过 npx 直接运行,无需安装
|
||||||
|
|
||||||
## 环境变量
|
## 快速开始
|
||||||
|
|
||||||
- `GITLAB_ACCESS_TOKEN`: 用于访问 GitLab API 的访问令牌
|
### 方式一:使用 npx(推荐,无需安装)
|
||||||
- `GITLAB_API_BASE_URL`: GitLab API 的基础 URL(默认为 `https://gitlab.com/api/v4`)
|
|
||||||
|
|
||||||
## 安装和运行
|
```bash
|
||||||
|
# 1. 设置环境变量
|
||||||
|
export GITLAB_ACCESS_TOKEN="your_gitlab_token_here"
|
||||||
|
|
||||||
1. 首先,确保你已经安装了 Node.js 和 npm。
|
# 2. 直接运行(默认 stdio 模式)
|
||||||
2. 安装依赖:
|
npx gitlab-mcp-server
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
3. 设置环境变量:
|
|
||||||
```bash
|
|
||||||
export GITLAB_ACCESS_TOKEN="your_gitlab_access_token"
|
|
||||||
export GITLAB_API_BASE_URL="https://your-gitlab-instance.com/api/v4" # 可选,仅当使用自托管的 GitLab 时
|
|
||||||
```
|
|
||||||
4. 运行 GitLab MCP 服务器:
|
|
||||||
```bash
|
|
||||||
npm run gitlab
|
|
||||||
```
|
|
||||||
|
|
||||||
## 架构
|
# 或使用 HTTP 模式
|
||||||
|
npx gitlab-mcp-server --transport http --port 3000
|
||||||
|
|
||||||
该项目使用一个封装的 `gitlabApiClient` 对象来处理所有与 GitLab API 的交互,这使得代码更模块化和可维护。API 客户端包含以下方法:
|
# 查看帮助
|
||||||
|
npx gitlab-mcp-server --help
|
||||||
|
```
|
||||||
|
|
||||||
- `createMergeRequest`: 创建 Merge Request
|
### 方式二:本地安装运行
|
||||||
- `mergeMergeRequest`: 合并 Merge Request
|
|
||||||
- `getMergeRequest`: 获取 Merge Request 详情
|
|
||||||
- `closeMergeRequest`: 关闭 Merge Request
|
|
||||||
|
|
||||||
## 工具详情
|
```bash
|
||||||
|
# 1. 克隆或下载项目
|
||||||
|
git clone <repository-url>
|
||||||
|
cd gitlab-mcp-server
|
||||||
|
|
||||||
### create_merge_request
|
# 2. 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
创建一个新的 Merge Request。
|
# 3. 设置环境变量
|
||||||
|
export GITLAB_ACCESS_TOKEN="your_gitlab_token_here"
|
||||||
|
export GITLAB_API_BASE_URL="https://gitlab.com/api/v4" # 可选
|
||||||
|
|
||||||
参数:
|
# 4. 运行
|
||||||
- `projectId`: GitLab 项目的 ID 或 URL 编码的路径
|
npm start # 默认 stdio 模式
|
||||||
- `sourceBranch`: 源分支名称
|
```
|
||||||
- `targetBranch`: 目标分支名称
|
|
||||||
- `title`: Merge Request 的标题
|
|
||||||
- `description`: (可选) Merge Request 的描述
|
|
||||||
- `assigneeId`: (可选) 负责人 ID
|
|
||||||
- `reviewerIds`: (可选) 审阅者 ID 数组
|
|
||||||
|
|
||||||
### merge_merge_request
|
## 环境变量配置
|
||||||
|
|
||||||
合并一个 Merge Request。
|
### Windows (PowerShell)
|
||||||
|
```powershell
|
||||||
|
# 临时设置(当前会话)
|
||||||
|
$env:GITLAB_ACCESS_TOKEN="your_token_here"
|
||||||
|
$env:GITLAB_API_BASE_URL="https://gitlab.com/api/v4"
|
||||||
|
|
||||||
参数:
|
# 永久设置(用户级别)
|
||||||
- `projectId`: GitLab 项目的 ID 或 URL 编码的路径
|
[System.Environment]::SetEnvironmentVariable('GITLAB_ACCESS_TOKEN', 'your_token_here', 'User')
|
||||||
- `mrIid`: Merge Request 的内部 ID
|
```
|
||||||
- `mergeCommitMessage`: (可选) 合并提交的消息
|
|
||||||
- `shouldRemoveSourceBranch`: (可选) 是否在合并后删除源分支
|
|
||||||
- `mergeWhenPipelineSucceeds`: (可选) 是否在 CI 管道成功后合并
|
|
||||||
|
|
||||||
### get_merge_request
|
### Linux/Mac
|
||||||
|
```bash
|
||||||
|
# 临时设置
|
||||||
|
export GITLAB_ACCESS_TOKEN="your_token_here"
|
||||||
|
export GITLAB_API_BASE_URL="https://gitlab.com/api/v4"
|
||||||
|
|
||||||
获取 Merge Request 的详细信息。
|
# 永久设置(添加到 ~/.bashrc 或 ~/.zshrc)
|
||||||
|
echo 'export GITLAB_ACCESS_TOKEN="your_token_here"' >> ~/.bashrc
|
||||||
|
source ~/.bashrc
|
||||||
|
```
|
||||||
|
|
||||||
参数:
|
### 如何获取 GitLab Access Token
|
||||||
- `projectId`: GitLab 项目的 ID 或 URL 编码的路径
|
|
||||||
- `mrIid`: Merge Request 的内部 ID
|
|
||||||
|
|
||||||
### close_merge_request
|
1. 登录 GitLab
|
||||||
|
2. 进入 **Settings** > **Access Tokens**
|
||||||
|
3. 创建新的 Personal Access Token
|
||||||
|
4. 选择 `api` 权限
|
||||||
|
5. 复制生成的 token
|
||||||
|
|
||||||
关闭一个 Merge Request 而不合并。
|
## 使用方式
|
||||||
|
|
||||||
参数:
|
### 通过 npx 运行(推荐)
|
||||||
- `projectId`: GitLab 项目的 ID 或 URL 编码的路径
|
|
||||||
- `mrIid`: Merge Request 的内部 ID
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stdio 模式(默认)
|
||||||
|
npx gitlab-mcp-server
|
||||||
|
|
||||||
## 配置 MCP 服务器
|
# HTTP 模式
|
||||||
|
npx gitlab-mcp-server --transport http --port 3000
|
||||||
|
|
||||||
``` json
|
# 使用简写参数
|
||||||
|
npx gitlab-mcp-server -t http -p 8080
|
||||||
|
|
||||||
|
# 查看帮助
|
||||||
|
npx gitlab-mcp-server --help
|
||||||
|
```
|
||||||
|
|
||||||
|
### 本地运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 默认 stdio 模式
|
||||||
|
node index.js
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# 明确指定 stdio 模式
|
||||||
|
node index.js --transport stdio
|
||||||
|
npm run start:stdio
|
||||||
|
|
||||||
|
# HTTP 模式
|
||||||
|
node index.js --transport http --port 3000
|
||||||
|
npm run start:http
|
||||||
|
|
||||||
|
# 开发模式(HTTP)
|
||||||
|
npm run dev:http
|
||||||
|
```
|
||||||
|
|
||||||
|
### 命令行参数
|
||||||
|
|
||||||
|
```
|
||||||
|
用法:
|
||||||
|
node index.js [选项]
|
||||||
|
|
||||||
|
选项:
|
||||||
|
-t, --transport <type> 传输方式: 'stdio' 或 'http' (默认: stdio)
|
||||||
|
-p, --port <number> HTTP 服务器端口 (默认: 3000, 仅在 http 模式下有效)
|
||||||
|
-h, --help 显示帮助信息
|
||||||
|
|
||||||
|
示例:
|
||||||
|
node index.js # stdio 模式
|
||||||
|
node index.js --transport http --port 3000 # HTTP 模式
|
||||||
|
npx gitlab-mcp-server -t http -p 8080 # HTTP 模式(使用 npx)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 传输模式说明
|
||||||
|
|
||||||
|
### Stdio 模式(默认)
|
||||||
|
|
||||||
|
通过标准输入/输出与客户端通信,这是 MCP 的标准传输方式。
|
||||||
|
|
||||||
|
**适用场景:**
|
||||||
|
- 与 Claude Desktop 或其他 MCP 客户端集成
|
||||||
|
- 作为子进程运行
|
||||||
|
- 本地开发和使用
|
||||||
|
|
||||||
|
**特点:**
|
||||||
|
- MCP 标准传输方式
|
||||||
|
- 低延迟
|
||||||
|
- 适合本地集成
|
||||||
|
|
||||||
|
### Streamable HTTP 模式
|
||||||
|
|
||||||
|
使用最新的 Streamable HTTP 传输协议,支持远程访问和多客户端连接。
|
||||||
|
|
||||||
|
**适用场景:**
|
||||||
|
- 远程服务器部署
|
||||||
|
- 多客户端访问
|
||||||
|
- 调试和测试
|
||||||
|
- 需要 HTTP 接口的场景
|
||||||
|
|
||||||
|
**特点:**
|
||||||
|
- 基于 HTTP POST 的消息传输
|
||||||
|
- 支持 Server-Sent Events (SSE) 流式响应
|
||||||
|
- 支持断线重连和消息重放
|
||||||
|
- 更好的网络适应性
|
||||||
|
|
||||||
|
**HTTP 端点:**
|
||||||
|
- `POST /message` - MCP 消息端点(接收 JSON-RPC 请求,返回 JSON 或 SSE 流)
|
||||||
|
|
||||||
|
**测试 HTTP 模式:**
|
||||||
|
```bash
|
||||||
|
# 启动服务器
|
||||||
|
npm run start:http
|
||||||
|
|
||||||
|
# 在另一个终端发送测试请求
|
||||||
|
curl -X POST http://localhost:3000/message \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Accept: application/json, text/event-stream" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 在 MCP 客户端中使用
|
||||||
|
|
||||||
|
### Claude Desktop 配置(使用 npx)
|
||||||
|
|
||||||
|
编辑 Claude Desktop 的配置文件:
|
||||||
|
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||||
|
- **Mac**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||||
|
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
||||||
|
|
||||||
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"gitlab-mcp-server": {
|
"gitlab": {
|
||||||
"command": "node",
|
"command": "npx",
|
||||||
"args": [
|
"args": ["gitlab-mcp-server"],
|
||||||
"/your/code/path/index-gitlab.js"
|
|
||||||
],
|
|
||||||
"env": {
|
"env": {
|
||||||
"GITLAB_API_BASE_URL": "https://git.ifengqun.com/api/v4",
|
"GITLAB_ACCESS_TOKEN": "your_token_here",
|
||||||
"GITLAB_ACCESS_TOKEN": "your_gitlab_token"
|
"GITLAB_API_BASE_URL": "https://gitlab.com/api/v4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 使用本地路径
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"gitlab": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["d:/path/to/gitlab-mcp-server/index.js"],
|
||||||
|
"env": {
|
||||||
|
"GITLAB_ACCESS_TOKEN": "your_token_here"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP 模式配置
|
||||||
|
|
||||||
|
如果你部署了 HTTP 模式的服务器,客户端可以通过 HTTP 连接:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"gitlab-http": {
|
||||||
|
"url": "http://localhost:3000/message",
|
||||||
|
"transport": "streamable-http",
|
||||||
|
"env": {
|
||||||
|
"GITLAB_ACCESS_TOKEN": "your_token_here"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 可用工具
|
||||||
|
|
||||||
|
### 1. create_merge_request
|
||||||
|
|
||||||
|
在指定的 GitLab 项目中创建一个新的 Merge Request。
|
||||||
|
|
||||||
|
**参数:**
|
||||||
|
- `projectId` (string): GitLab 项目的 ID 或 URL 编码的路径
|
||||||
|
- `sourceBranch` (string): 源分支名称
|
||||||
|
- `targetBranch` (string): 目标分支名称
|
||||||
|
- `title` (string): Merge Request 的标题
|
||||||
|
- `description` (string, 可选): Merge Request 的描述
|
||||||
|
- `assigneeId` (number, 可选): 负责人 ID
|
||||||
|
- `reviewerIds` (array, 可选): 审阅者 ID 数组
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"tool": "create_merge_request",
|
||||||
|
"arguments": {
|
||||||
|
"projectId": "123",
|
||||||
|
"sourceBranch": "feature/new-feature",
|
||||||
|
"targetBranch": "main",
|
||||||
|
"title": "Add new feature",
|
||||||
|
"description": "This PR adds a new feature",
|
||||||
|
"assigneeId": 456,
|
||||||
|
"reviewerIds": [789, 101]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. merge_merge_request
|
||||||
|
|
||||||
|
合并指定的 GitLab Merge Request。
|
||||||
|
|
||||||
|
**参数:**
|
||||||
|
- `projectId` (string): GitLab 项目的 ID 或 URL 编码的路径
|
||||||
|
- `mrIid` (number): Merge Request 的内部 ID
|
||||||
|
- `mergeCommitMessage` (string, 可选): 合并提交的消息
|
||||||
|
- `shouldRemoveSourceBranch` (boolean, 可选): 是否在合并后删除源分支
|
||||||
|
- `mergeWhenPipelineSucceeds` (boolean, 可选): 是否在 CI 管道成功后合并
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"tool": "merge_merge_request",
|
||||||
|
"arguments": {
|
||||||
|
"projectId": "123",
|
||||||
|
"mrIid": 42,
|
||||||
|
"mergeCommitMessage": "Merge feature branch",
|
||||||
|
"shouldRemoveSourceBranch": true,
|
||||||
|
"mergeWhenPipelineSucceeds": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. get_merge_request
|
||||||
|
|
||||||
|
获取指定 Merge Request 的详细信息。
|
||||||
|
|
||||||
|
**参数:**
|
||||||
|
- `projectId` (string): GitLab 项目的 ID 或 URL 编码的路径
|
||||||
|
- `mrIid` (number): Merge Request 的内部 ID
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"tool": "get_merge_request",
|
||||||
|
"arguments": {
|
||||||
|
"projectId": "123",
|
||||||
|
"mrIid": 42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. close_merge_request
|
||||||
|
|
||||||
|
关闭指定的 GitLab Merge Request 而不合并。
|
||||||
|
|
||||||
|
**参数:**
|
||||||
|
- `projectId` (string): GitLab 项目的 ID 或 URL 编码的路径
|
||||||
|
- `mrIid` (number): Merge Request 的内部 ID
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"tool": "close_merge_request",
|
||||||
|
"arguments": {
|
||||||
|
"projectId": "123",
|
||||||
|
"mrIid": 42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. merge_to_test
|
||||||
|
|
||||||
|
快捷工具:将源分支合并到测试分支(test)并自动部署。
|
||||||
|
|
||||||
|
**参数:**
|
||||||
|
- `projectId` (string): GitLab 项目的 ID
|
||||||
|
- `sourceBranch` (string): 源分支名称(如果未提供,将自动获取当前分支)
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"tool": "merge_to_test",
|
||||||
|
"arguments": {
|
||||||
|
"projectId": "123",
|
||||||
|
"sourceBranch": "feature/my-feature"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 如何找到项目 ID?
|
||||||
|
**A:**
|
||||||
|
1. 进入 GitLab 项目页面
|
||||||
|
2. 项目 ID 显示在项目名称下方
|
||||||
|
3. 或者使用项目的 URL 编码路径,如 `username%2Fproject-name`
|
||||||
|
|
||||||
|
### Q: Stdio 和 HTTP 模式有什么区别?
|
||||||
|
**A:**
|
||||||
|
- **Stdio 模式**: 通过标准输入/输出通信,MCP 标准方式,适合本地集成
|
||||||
|
- **HTTP 模式**: 使用 Streamable HTTP 协议,适合远程访问、多客户端和调试
|
||||||
|
|
||||||
|
### Q: 什么是 Streamable HTTP?
|
||||||
|
**A:** Streamable HTTP 是 MCP 的最新 HTTP 传输协议,相比旧的 SSE 传输有以下优势:
|
||||||
|
- 统一的连接模型(单个 POST 请求即可发送和接收)
|
||||||
|
- 支持断线重连和消息重放
|
||||||
|
- 更好的网络适应性
|
||||||
|
- 更高的可靠性
|
||||||
|
|
||||||
|
### Q: 端口被占用怎么办?
|
||||||
|
**A:** 使用不同的端口:
|
||||||
|
```bash
|
||||||
|
node index.js --transport http --port 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: 如何查看详细日志?
|
||||||
|
**A:** 日志输出到 stderr,可以重定向到文件:
|
||||||
|
```bash
|
||||||
|
node index.js --transport http 2> server.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: npx 和本地安装有什么区别?
|
||||||
|
**A:**
|
||||||
|
- **npx**: 无需安装,直接运行最新版本,适合快速测试和使用
|
||||||
|
- **本地安装**: 需要先安装依赖,但可以离线使用,适合开发和定制
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
### 使用 MCP Inspector 调试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stdio 模式
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# HTTP 模式
|
||||||
|
npm run dev:http
|
||||||
|
```
|
||||||
|
|
||||||
|
MCP Inspector 会启动一个交互式界面,让你可以测试服务器的各项功能。
|
||||||
|
|
||||||
|
### 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
gitlab-mcp-server/
|
||||||
|
├── index.js # 主入口文件
|
||||||
|
├── package.json # 项目配置
|
||||||
|
├── README.md # 文档
|
||||||
|
├── .npmignore # npm 发布配置
|
||||||
|
└── .gitignore # Git 忽略配置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 发布到 npm
|
||||||
|
|
||||||
|
如果你想发布自己的版本到 npm:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 登录 npm
|
||||||
|
npm login
|
||||||
|
|
||||||
|
# 2. 更新版本号
|
||||||
|
npm version patch # 或 minor, major
|
||||||
|
|
||||||
|
# 3. 发布
|
||||||
|
npm publish
|
||||||
|
|
||||||
|
# 4. 发布后,其他人可以通过 npx 使用
|
||||||
|
npx your-package-name
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **@modelcontextprotocol/sdk**: MCP SDK (最新版本,支持 Streamable HTTP)
|
||||||
|
- **zod**: 参数验证
|
||||||
|
- **Node.js**: 运行时环境
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### [1.1.0] - 2025-12-15
|
||||||
|
|
||||||
|
**新增功能:**
|
||||||
|
- ✨ 支持通过命令行参数选择传输方式(stdio 或 http)
|
||||||
|
- ✨ 使用最新的 Streamable HTTP 传输协议(替代已弃用的 SSE)
|
||||||
|
- ✨ 支持通过 npx 直接运行,无需安装
|
||||||
|
- ✨ 添加 `bin` 字段,支持 `gitlab-mcp-server` 和 `gitlab-mcp` 命令
|
||||||
|
- ✨ 添加命令行帮助信息 `--help`
|
||||||
|
|
||||||
|
**改进:**
|
||||||
|
- 📝 完善文档,整合所有内容到 README
|
||||||
|
- 🔧 默认使用 stdio 模式(MCP 标准)
|
||||||
|
- 🔧 升级 SDK 到最新版本,移除已弃用的 API
|
||||||
|
- 🔧 优化服务器启动逻辑,支持参数化配置
|
||||||
|
|
||||||
|
**技术细节:**
|
||||||
|
- 使用 `StreamableHTTPServerTransport` 替代已弃用的 `SSEServerTransport`
|
||||||
|
- 升级 `@modelcontextprotocol/sdk` 到最新版本(1.24.0+)
|
||||||
|
- 添加 DNS rebinding 保护(SDK 1.24.0 的安全特性)
|
||||||
|
|
||||||
|
### [1.0.0] - 初始版本
|
||||||
|
|
||||||
|
**功能:**
|
||||||
|
- ✅ 创建 Merge Request
|
||||||
|
- ✅ 合并 Merge Request
|
||||||
|
- ✅ 获取 Merge Request 详情
|
||||||
|
- ✅ 关闭 Merge Request
|
||||||
|
- ✅ 快捷合并到测试分支
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
ISC
|
||||||
|
|
||||||
|
## 作者
|
||||||
|
|
||||||
|
Jasery
|
||||||
|
|
||||||
|
## 贡献
|
||||||
|
|
||||||
|
欢迎提交 Issue 和 Pull Request!
|
||||||
|
|
||||||
|
## 支持
|
||||||
|
|
||||||
|
如果遇到问题,请检查:
|
||||||
|
1. ✅ 环境变量 `GITLAB_ACCESS_TOKEN` 是否正确设置
|
||||||
|
2. ✅ GitLab API 是否可访问
|
||||||
|
3. ✅ Token 是否有足够的权限(需要 `api` 权限)
|
||||||
|
4. ✅ 项目 ID 是否正确
|
||||||
|
5. ✅ 使用的 SDK 版本是否为最新版本(1.24.0+)
|
||||||
-490
@@ -1,490 +0,0 @@
|
|||||||
// 引入必要的模块
|
|
||||||
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 };
|
|
||||||
@@ -1,111 +1,585 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
// 引入必要的模块
|
// 引入必要的模块
|
||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
// 创建 MCP 服务器实例
|
// 创建 MCP 服务器实例
|
||||||
// 为其设置一个名字和版本
|
// 为其设置一个名字和版本
|
||||||
const server = new McpServer({
|
const server = new McpServer({
|
||||||
name: "my-first-nodejs-mcp-server",
|
name: "gitlab-mcp-server",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 完善版的打招呼工具
|
* GitLab API 客户端配置
|
||||||
* 1. 使用 registerTool 方法进行更完整的描述
|
* 用于连接到GitLab实例并执行操作
|
||||||
* 2. 利用 Zod 库定义严格的参数模式和验证规则
|
|
||||||
*/
|
*/
|
||||||
server.registerTool('say_hello',
|
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",
|
||||||
{
|
{
|
||||||
// 工具描述至关重要,它直接帮助AI理解工具的用途、适用场景和参数要求
|
description: "在指定的GitLab项目中创建一个新的Merge Request",
|
||||||
description: "向指定的用户生成一条个性化的问候语。该工具适用于在对话开始时或需要欢迎用户时使用。",
|
|
||||||
// 使用 Zod 定义参数模式,这是确保AI正确传参的关键[8](@ref)
|
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
// 第一个参数:用户名
|
projectId: z.string().describe("GitLab项目的ID或URL编码的路径"),
|
||||||
name: z.string()
|
sourceBranch: z.string().describe("源分支名称"),
|
||||||
.min(1, { message: "姓名不能为空字符串。" }) // 基础验证:确保非空字符串
|
targetBranch: z.string().describe("目标分支名称"),
|
||||||
.max(50, { message: "姓名长度不能超过50个字符。" }) // 防止过长输入
|
title: z.string().describe("Merge Request的标题"),
|
||||||
.describe("被问候者的姓名。请尽量提供完整的姓名或常用称呼。"), // 清晰的参数描述
|
description: z.string().optional().describe("Merge Request的描述"),
|
||||||
// 第二个参数(可选):语言/风格
|
assigneeId: z.number().optional().describe("负责人ID"),
|
||||||
language: z.enum(["casual", "formal", "warm"]) // 枚举类型,将AI的输入严格限制在预设的选项内
|
reviewerIds: z.array(z.number()).optional().describe("审阅者ID数组"),
|
||||||
.default("casual") // 设置默认值,当AI未提供此参数时使用
|
|
||||||
.describe("问候的语言风格。'casual'为随意,'formal'为正式,'warm'为热情。默认为'casual'。"),
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
async ({ name, language }) => {
|
async ({ projectId, sourceBranch, targetBranch, title, description = "", assigneeId, reviewerIds }) => {
|
||||||
// 根据不同的语言风格生成不同的问候语
|
try {
|
||||||
const greetings = {
|
const result = await gitlabApiClient.createMergeRequest(
|
||||||
casual: `嘿,${name}!最近怎么样?`,
|
projectId,
|
||||||
formal: `您好,${name}。很高兴见到您。`,
|
sourceBranch,
|
||||||
warm: `欢迎你,${name}!真高兴你今天能来!`,
|
targetBranch,
|
||||||
};
|
title,
|
||||||
|
description,
|
||||||
|
assigneeId,
|
||||||
|
reviewerIds
|
||||||
|
);
|
||||||
|
|
||||||
const greetingMessage = greetings[language] || greetings.casual;
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// MCP 工具要求返回特定的格式
|
/**
|
||||||
return {
|
* 合并Merge Request工具
|
||||||
content: [
|
*/
|
||||||
{
|
server.registerTool(
|
||||||
type: "text", // 内容类型为文本
|
"merge_merge_request",
|
||||||
text: greetingMessage,
|
{
|
||||||
},
|
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: 3000, // 默认 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 transport = new StreamableHTTPServerTransport({
|
||||||
|
endpoint: '/message',
|
||||||
|
port: port,
|
||||||
});
|
});
|
||||||
|
|
||||||
// // 在 server.tool() 方法内,实现一个获取天气的工具
|
await server.connect(transport);
|
||||||
// server.registerTool(
|
|
||||||
// "get_weather",
|
|
||||||
// {
|
|
||||||
// city: z.string().describe("要查询天气的城市名称"),
|
|
||||||
// },
|
|
||||||
// async ({ city }) => {
|
|
||||||
// // 重要:API密钥等敏感信息应从环境变量读取
|
|
||||||
// const apiKey = process.env.WEATHER_API_KEY;
|
|
||||||
// if (!apiKey) {
|
|
||||||
// throw new Error("WEATHER_API_KEY 环境变量未设置");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// try {
|
console.error(`GitLab MCP 服务器已启动 (Streamable HTTP 模式)`);
|
||||||
// // 调用外部天气 API
|
console.error(`监听端口: ${port}`);
|
||||||
// const response = await fetch(
|
console.error(`MCP 端点: http://localhost:${port}/message`);
|
||||||
// `https://api.weatherapi.com/v1/current.json?key=${apiKey}&q=${city}&aqi=no`
|
console.error(`\n使用 POST 请求发送消息到 /message 端点`);
|
||||||
// );
|
}
|
||||||
// const weatherData = await response.json();
|
|
||||||
|
|
||||||
// // 处理并返回需要的信息
|
/**
|
||||||
// return {
|
* 启动 Stdio 服务器
|
||||||
// content: [
|
*/
|
||||||
// {
|
async function startStdioServer() {
|
||||||
// type: "text",
|
|
||||||
// text: `城市 ${city} 的天气情况:
|
|
||||||
// 温度:${weatherData.current.temp_c}°C
|
|
||||||
// 天气状况:${weatherData.current.condition.text}
|
|
||||||
// `,
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// };
|
|
||||||
// } catch (error) {
|
|
||||||
// // 良好的错误处理至关重要
|
|
||||||
// return {
|
|
||||||
// content: [
|
|
||||||
// {
|
|
||||||
// type: "text",
|
|
||||||
// text: `获取天气信息失败: ${error.message}`,
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// isError: true, // 标记这是一个错误结果
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
// 启动服务器,使用标准输入/输出(stdio)与客户端通信
|
|
||||||
async function main() {
|
|
||||||
const transport = new StdioServerTransport();
|
const transport = new StdioServerTransport();
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
console.error("MCP 服务器已通过 stdio 启动"); // 日志输出到 stderr
|
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) => {
|
main().catch((error) => {
|
||||||
console.error("服务器运行出现致命错误:", error);
|
console.error("服务器运行出现致命错误:", error);
|
||||||
|
|||||||
Generated
+35
-15
@@ -1,17 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "my-nodejs-mcp-server",
|
"name": "gitlab-mcp-server",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "my-nodejs-mcp-server",
|
"name": "gitlab-mcp-server",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.22.0",
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
|
"bin": {
|
||||||
|
"gitlab-mcp": "index.js",
|
||||||
|
"gitlab-mcp-server": "index.js"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@modelcontextprotocol/inspector": "^0.17.2"
|
"@modelcontextprotocol/inspector": "^0.17.2"
|
||||||
}
|
}
|
||||||
@@ -221,9 +225,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk": {
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
"version": "1.22.0",
|
"version": "1.24.3",
|
||||||
"resolved": "https://registry.npmmirror.com/@modelcontextprotocol/sdk/-/sdk-1.22.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@modelcontextprotocol/sdk/-/sdk-1.24.3.tgz",
|
||||||
"integrity": "sha512-VUpl106XVTCpDmTBil2ehgJZjhyLY2QZikzF8NvTXtLRF1CvO5iEE2UNZdVIUer35vFOwMKYeUGbjJtvPWan3g==",
|
"integrity": "sha512-YgSHW29fuzKKAHTGe9zjNoo+yF8KaQPzDC2W9Pv41E7/57IfY+AMGJ/aDFlgTLcVVELoggKE4syABCE75u3NCw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
"ajv-formats": "^3.0.1",
|
"ajv-formats": "^3.0.1",
|
||||||
@@ -234,20 +239,25 @@
|
|||||||
"eventsource-parser": "^3.0.0",
|
"eventsource-parser": "^3.0.0",
|
||||||
"express": "^5.0.1",
|
"express": "^5.0.1",
|
||||||
"express-rate-limit": "^7.5.0",
|
"express-rate-limit": "^7.5.0",
|
||||||
|
"jose": "^6.1.1",
|
||||||
"pkce-challenge": "^5.0.0",
|
"pkce-challenge": "^5.0.0",
|
||||||
"raw-body": "^3.0.0",
|
"raw-body": "^3.0.0",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.25 || ^4.0",
|
||||||
"zod-to-json-schema": "^3.24.1"
|
"zod-to-json-schema": "^3.25.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@cfworker/json-schema": "^4.1.1"
|
"@cfworker/json-schema": "^4.1.1",
|
||||||
|
"zod": "^3.25 || ^4.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@cfworker/json-schema": {
|
"@cfworker/json-schema": {
|
||||||
"optional": true
|
"optional": true
|
||||||
|
},
|
||||||
|
"zod": {
|
||||||
|
"optional": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2178,6 +2188,15 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
|
||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/jose": {
|
||||||
|
"version": "6.1.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/jose/-/jose-6.1.3.tgz",
|
||||||
|
"integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -3277,11 +3296,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod-to-json-schema": {
|
"node_modules/zod-to-json-schema": {
|
||||||
"version": "3.24.6",
|
"version": "3.25.0",
|
||||||
"resolved": "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
|
"resolved": "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz",
|
||||||
"integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
|
"integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==",
|
||||||
|
"license": "ISC",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.25 || ^4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-4
@@ -1,12 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "gitlab-mcp-server",
|
"name": "gitlab-mcp-server",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"gitlab-mcp-server": "./index.js",
|
||||||
|
"gitlab-mcp": "./index.js"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
"gitlab": "npx @modelcontextprotocol/inspector node gitlab-index.js",
|
"start:stdio": "node index.js --transport stdio",
|
||||||
"dev": "npx @modelcontextprotocol/inspector node index.js"
|
"start:http": "node index.js --transport http --port 3000",
|
||||||
|
"dev": "npx @modelcontextprotocol/inspector node index.js",
|
||||||
|
"dev:http": "node index.js --transport http --port 3000"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"mcp",
|
"mcp",
|
||||||
@@ -17,7 +23,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "A Model Context Protocol server for GitLab operations, primarily for creating and merging merge requests",
|
"description": "A Model Context Protocol server for GitLab operations, primarily for creating and merging merge requests",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.22.0",
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Reference in new issue
Block a user