diff --git a/README.md b/README.md
index 9c4297b..e2ed190 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,11 @@ bun dist/index.js --http-stream
## Environment Variables
```env
+# Required for all modes (optional for httpStream mode - can be provided via HTTP headers)
GITLAB_API_URL=https://your-gitlab-instance.com
+
+# Required for stdio mode, optional for httpStream mode
+# (can be provided via HTTP headers in httpStream mode)
GITLAB_TOKEN=your_access_token
# Optional: Provide a mapping from usernames to user IDs (JSON string)
@@ -89,7 +93,77 @@ MCP_ENDPOINT=/mcp
## Usage Examples
-See [USAGE.md](./USAGE.md) for detailed examples of each tool's parameters.
+### Direct HTTP API Usage
+
+You can also interact with the MCP server directly via HTTP requests:
+
+```bash
+# Example: Get user tasks using Bearer token
+curl -X POST http://localhost:3000/mcp \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer your-gitlab-token" \
+ -d '{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "tools/call",
+ "params": {
+ "name": "Gitlab Get User Tasks Tool",
+ "arguments": {
+ "taskFilterType": "ASSIGNED_MRS",
+ "fields": ["id", "title", "source_branch", "target_branch"]
+ }
+ }
+ }'
+```
+
+```bash
+# Example: Search projects using PRIVATE-TOKEN header
+curl -X POST http://localhost:3000/mcp \
+ -H "Content-Type: application/json" \
+ -H "PRIVATE-TOKEN: your-gitlab-token" \
+ -d '{
+ "jsonrpc": "2.0",
+ "id": 2,
+ "method": "tools/call",
+ "params": {
+ "name": "Gitlab Search Project Details Tool",
+ "arguments": {
+ "projectName": "my-project",
+ "fields": ["id", "name", "description", "web_url"]
+ }
+ }
+ }'
+```
+
+```bash
+# Example: Use dynamic GitLab instance URL with Bearer token
+curl -X POST http://localhost:3000/mcp \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer your-gitlab-token" \
+ -H "x-gitlab-url: https://gitlab.company.com" \
+ -d '{
+ "jsonrpc": "2.0",
+ "id": 3,
+ "method": "tools/call",
+ "params": {
+ "name": "Gitlab Get User Tasks Tool",
+ "arguments": {
+ "taskFilterType": "ASSIGNED_MRS",
+ "fields": ["id", "title", "source_branch", "target_branch"]
+ }
+ }
+ }'
+```
+
+### Tool Examples
+
+For detailed examples of each tool's parameters, see [USAGE.md](./USAGE.md).
+
+Key benefits of HTTP Stream mode with dynamic authentication:
+- **Multi-tenant support**: Single server instance can serve multiple users
+- **Security**: Each request uses its own authentication token and GitLab instance URL
+- **Flexibility**: Tokens and GitLab URLs can be configured per client without server restart
+- **Multi-instance support**: Connect to different GitLab instances from the same server
## Transport Modes
@@ -105,6 +179,7 @@ This server supports two transport modes:
- Uses HTTP POST requests with streaming responses
- Allows multiple clients to connect to the same server instance
- Ideal for production deployments
+- **Supports dynamic token authentication via HTTP headers**
When using HTTP Stream mode, clients can connect to:
```
@@ -112,6 +187,73 @@ POST http://localhost:3000/mcp
Content-Type: application/json
```
+#### Authentication Methods
+
+HTTP Stream mode supports multiple ways to provide GitLab tokens and instance URLs:
+
+**Token Authentication:**
+
+**1. Bearer Token (Recommended):**
+```http
+POST http://localhost:3000/mcp
+Content-Type: application/json
+Authorization: Bearer your-gitlab-access-token
+```
+
+**2. Private Token Header:**
+```http
+POST http://localhost:3000/mcp
+Content-Type: application/json
+PRIVATE-TOKEN: your-gitlab-access-token
+```
+
+**3. Alternative Private Token Header:**
+```http
+POST http://localhost:3000/mcp
+Content-Type: application/json
+private-token: your-gitlab-access-token
+```
+
+**4. Custom GitLab Token Header:**
+```http
+POST http://localhost:3000/mcp
+Content-Type: application/json
+x-gitlab-token: your-gitlab-access-token
+```
+
+**GitLab Instance URL Configuration:**
+
+**1. GitLab URL Header (Recommended):**
+```http
+POST http://localhost:3000/mcp
+Content-Type: application/json
+x-gitlab-url: https://gitlab.company.com
+```
+
+**2. Alternative GitLab URL Headers:**
+```http
+POST http://localhost:3000/mcp
+Content-Type: application/json
+gitlab-url: https://gitlab.company.com
+```
+
+```http
+POST http://localhost:3000/mcp
+Content-Type: application/json
+gitlab-api-url: https://gitlab.company.com
+```
+
+**5. Fallback to Environment Variables:**
+If no token or URL is provided in headers, the server will fall back to the `GITLAB_TOKEN` and `GITLAB_API_URL` environment variables.
+
+**Complete Example:**
+```http
+POST http://localhost:3000/mcp
+Content-Type: application/json
+Authorization: Bearer your-gitlab-access-token
+x-gitlab-url: https://gitlab.company.com
+```
+
## Project Structure
```
@@ -157,26 +299,77 @@ Add to your config:
```
#### HTTP Stream Mode (Server Deployment)
-For remote server deployment, first start the server:
+
+**Server Setup:**
+First start the server (note that both `GITLAB_TOKEN` and `GITLAB_API_URL` are optional when using HTTP headers):
```bash
-# On your server
-MCP_TRANSPORT_TYPE=httpStream MCP_PORT=3000 npx @zephyr-mcp/gitlab
+# On your server - no token or URL required in env vars
+MCP_TRANSPORT_TYPE=httpStream MCP_PORT=3000 MCP_HOST=0.0.0.0 npx @zephyr-mcp/gitlab
+
+# Or with Docker
+docker run -d \
+ -p 3000:3000 \
+ -e MCP_TRANSPORT_TYPE=httpStream \
+ -e MCP_HOST=0.0.0.0 \
+ -e MCP_PORT=3000 \
+ gitlab-mcp-server
```
-Then configure Claude Desktop with HTTP transport:
+**Client Configuration:**
+Option 1: **With Bearer Token** (Recommended)
```json
{
"mcpServers": {
"@zephyr-mcp/gitlab": {
"command": "npx",
- "args": ["@modelcontextprotocol/client-cli", "http://your-server:3000/mcp"]
+ "args": [
+ "@modelcontextprotocol/client-cli",
+ "http://your-server:3000/mcp",
+ "--header", "Authorization: Bearer your-gitlab-access-token"
+ ]
}
}
}
```
+Option 2: **With Private Token Header**
+```json
+{
+ "mcpServers": {
+ "@zephyr-mcp/gitlab": {
+ "command": "npx",
+ "args": [
+ "@modelcontextprotocol/client-cli",
+ "http://your-server:3000/mcp",
+ "--header", "PRIVATE-TOKEN: your-gitlab-access-token"
+ ]
+ }
+ }
+}
+```
+
+Option 3: **With Dynamic GitLab URL and Token**
+```json
+{
+ "mcpServers": {
+ "@zephyr-mcp/gitlab": {
+ "command": "npx",
+ "args": [
+ "@modelcontextprotocol/client-cli",
+ "http://your-server:3000/mcp",
+ "--header", "Authorization: Bearer your-gitlab-access-token",
+ "--header", "x-gitlab-url: https://gitlab.company.com"
+ ]
+ }
+ }
+}
+```
+
+**Multi-tenant Usage:**
+Each user can configure their own token and GitLab instance URL in their client configuration, allowing the same server instance to serve multiple users with different GitLab permissions and instances.
+
### Smithery
Use directly on Smithery platform:
@@ -189,8 +382,8 @@ Or search "@zephyr-mcp/gitlab" in Smithery UI and add to your workspace.
Environment variables:
-- `GITLAB_API_URL`: Base URL of your GitLab API
-- `GITLAB_TOKEN`: Access token for GitLab API authentication
+- `GITLAB_API_URL`: Base URL of your GitLab API (required for stdio mode, optional for httpStream mode - can be provided via HTTP headers)
+- `GITLAB_TOKEN`: Access token for GitLab API authentication (required for stdio mode, optional for httpStream mode - can be provided via HTTP headers)
- `MCP_TRANSPORT_TYPE`: Transport type (stdio/httpStream)
- `MCP_HOST`: Server binding address for HTTP stream mode
- `MCP_PORT`: HTTP port for HTTP stream mode
@@ -206,11 +399,9 @@ The repository includes a Dockerfile for easy deployment:
# Build the Docker image
docker build -t gitlab-mcp-server .
-# Run with environment variables
+# Run with environment variables (both token and URL can be provided via HTTP headers)
docker run -d \
-p 3000:3000 \
- -e GITLAB_API_URL=https://your-gitlab-instance.com \
- -e GITLAB_TOKEN=your_access_token \
-e MCP_TRANSPORT_TYPE=httpStream \
-e MCP_HOST=0.0.0.0 \
-e MCP_PORT=3000 \
@@ -227,11 +418,12 @@ services:
ports:
- "3000:3000"
environment:
- - GITLAB_TOKEN=your_gitlab_token
- - GITLAB_API_URL=your-gitlab-instance.com
- MCP_TRANSPORT_TYPE=httpStream
- MCP_HOST=0.0.0.0
- MCP_PORT=3000
+ # Both GITLAB_API_URL and GITLAB_TOKEN are optional when using HTTP headers
+ # - GITLAB_API_URL=https://your-gitlab-instance.com
+ # - GITLAB_TOKEN=your_gitlab_token
command: npx -y @zephyr-mcp/gitlab@latest
```
diff --git a/README_TOOLS.md b/README_TOOLS.md
new file mode 100644
index 0000000..78a5924
--- /dev/null
+++ b/README_TOOLS.md
@@ -0,0 +1,337 @@
+# GitLab MCP Tools
+
+这个包提供了 GitLab API 的 MCP 工具集合,可以作为 npm 包在其他项目中使用,支持 `@modelcontextprotocol/sdk` 和 `fastmcp` 两种框架。
+
+## 安装
+
+```bash
+npm install @zephyr-mcp/gitlab
+# 或
+bun add @zephyr-mcp/gitlab
+```
+
+## 使用方式
+
+### 1. 与 @modelcontextprotocol/sdk 配合使用
+
+```typescript
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { registerGitLabToolsForMcpSDK } from '@zephyr-mcp/gitlab/tools';
+
+const server = new McpServer({
+ name: 'my-app',
+ version: '1.0.0',
+ capabilities: { tools: {} }
+});
+
+// 注册所有 GitLab 工具
+registerGitLabToolsForMcpSDK(server);
+
+// 或者只注册特定工具
+registerGitLabToolsForMcpSDK(server, {
+ toolFilter: {
+ allowList: ['Gitlab_Create_MR_Tool', 'Gitlab_Search_User_Projects_Tool']
+ }
+});
+```
+
+### 2. 与 fastmcp 配合使用
+
+```typescript
+import { FastMCP } from 'fastmcp';
+import { registerGitLabToolsForFastMCP } from '@zephyr-mcp/gitlab/tools';
+
+const server = new FastMCP({
+ name: 'my-app',
+ version: '1.0.0'
+});
+
+// 注册所有 GitLab 工具
+registerGitLabToolsForFastMCP(server);
+
+// 或者只注册特定工具
+registerGitLabToolsForFastMCP(server, {
+ toolFilter: {
+ allowList: ['Gitlab_Create_MR_Tool', 'Gitlab_Search_User_Projects_Tool']
+ }
+});
+```
+
+## 环境变量配置
+
+在使用工具前,需要设置以下环境变量:
+
+```bash
+# 必需配置
+GITLAB_API_URL=https://your-gitlab-instance.com
+GITLAB_TOKEN=your_access_token
+
+# 可选映射配置
+GITLAB_USER_MAPPING={"username1": 123}
+GITLAB_PROJECT_MAPPING={"project-name": 1001}
+
+# 日志配置
+ENABLE_LOGGER=true
+```
+
+## 可用工具
+
+### 1. Gitlab_Search_User_Projects_Tool
+搜索用户及其活跃项目。
+
+```typescript
+// 调用示例
+{
+ "tool": "Gitlab_Search_User_Projects_Tool",
+ "arguments": {
+ "username": "john_doe",
+ "fields": ["id", "name", "web_url"]
+ }
+}
+```
+
+### 2. Gitlab_Get_User_Tasks_Tool
+获取当前用户的待办任务。
+
+```typescript
+// 调用示例
+{
+ "tool": "Gitlab_Get_User_Tasks_Tool",
+ "arguments": {
+ "taskFilterType": "ASSIGNED_MRS",
+ "fields": ["id", "title", "web_url"]
+ }
+}
+```
+
+### 3. Gitlab_Search_Project_Details_Tool
+搜索项目详情。
+
+```typescript
+// 调用示例
+{
+ "tool": "Gitlab_Search_Project_Details_Tool",
+ "arguments": {
+ "projectName": "my-project",
+ "fields": ["id", "name", "description", "web_url"]
+ }
+}
+```
+
+### 4. Gitlab_Create_MR_Tool
+创建新的 Merge Request。
+
+```typescript
+// 调用示例
+{
+ "tool": "Gitlab_Create_MR_Tool",
+ "arguments": {
+ "projectId": "my-project",
+ "sourceBranch": "feature-branch",
+ "targetBranch": "main",
+ "title": "Add new feature",
+ "description": "Description of the feature",
+ "assigneeId": "john_doe",
+ "reviewerIds": ["jane_doe"],
+ "labels": ["feature", "enhancement"]
+ }
+}
+```
+
+### 5. Gitlab_Update_MR_Tool
+更新 Merge Request。
+
+```typescript
+// 调用示例
+{
+ "tool": "Gitlab_Update_MR_Tool",
+ "arguments": {
+ "projectId": "my-project",
+ "mergeRequestId": 123,
+ "title": "Updated title",
+ "description": "Updated description"
+ }
+}
+```
+
+### 6. Gitlab_Accept_MR_Tool
+接受并合并 Merge Request。
+
+```typescript
+// 调用示例
+{
+ "tool": "Gitlab_Accept_MR_Tool",
+ "arguments": {
+ "projectId": "my-project",
+ "mergeRequestId": 123,
+ "mergeOptions": {
+ "squash": true,
+ "shouldRemoveSourceBranch": true
+ }
+ }
+}
+```
+
+### 7. Gitlab_Create_MR_Comment_Tool
+为 Merge Request 添加评论。
+
+```typescript
+// 调用示例
+{
+ "tool": "Gitlab_Create_MR_Comment_Tool",
+ "arguments": {
+ "projectId": "my-project",
+ "mergeRequestId": 123,
+ "comment": "Looks good to me!"
+ }
+}
+```
+
+### 8. Gitlab_Raw_API_Tool
+通用 GitLab API 调用工具。
+
+```typescript
+// 调用示例
+{
+ "tool": "Gitlab_Raw_API_Tool",
+ "arguments": {
+ "endpoint": "/projects",
+ "method": "GET",
+ "params": {
+ "owned": true
+ }
+ }
+}
+```
+
+## 配置选项
+
+### GitLabToolsRegistryOptions
+
+```typescript
+interface GitLabToolsRegistryOptions {
+ /** 过滤选项,控制注册哪些工具 */
+ toolFilter?: {
+ /** 允许注册的工具列表 */
+ allowList?: GitLabToolName[];
+ /** 禁止注册的工具列表 */
+ blockList?: GitLabToolName[];
+ };
+ /** 是否启用日志 */
+ enableLogger?: boolean;
+}
+```
+
+### 类型定义
+
+```typescript
+/** 可用的 GitLab 工具名称 */
+type GitLabToolName =
+ | "Gitlab_Search_User_Projects_Tool"
+ | "Gitlab_Get_User_Tasks_Tool"
+ | "Gitlab_Search_Project_Details_Tool"
+ | "Gitlab_Create_MR_Tool"
+ | "Gitlab_Update_MR_Tool"
+ | "Gitlab_Accept_MR_Tool"
+ | "Gitlab_Create_MR_Comment_Tool"
+ | "Gitlab_Raw_API_Tool";
+```
+
+## 示例项目
+
+### 完整的 MCP SDK 项目示例
+
+```typescript
+#!/usr/bin/env node
+
+import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { registerGitLabTools } from '@zephyr-mcp/gitlab/tools';
+
+const server = new McpServer({
+ name: 'my-gitlab-app',
+ version: '1.0.0',
+ capabilities: {
+ tools: {}
+ }
+});
+
+// 注册 GitLab 工具
+registerGitLabTools(server, {
+ enableLogger: true,
+ toolFilter: {
+ allowList: [
+ 'Gitlab_Create_MR_Tool',
+ 'Gitlab_Search_User_Projects_Tool',
+ 'Gitlab_Get_User_Tasks_Tool'
+ ]
+ }
+});
+
+async function main() {
+ const transport = new StdioServerTransport();
+ await server.connect(transport);
+}
+
+main().catch(console.error);
+```
+
+### 完整的 FastMCP 项目示例
+
+```typescript
+#!/usr/bin/env node
+
+import { FastMCP } from 'fastmcp';
+import { registerGitLabToolsForFastMCP } from '@zephyr-mcp/gitlab/tools';
+
+const server = new FastMCP({
+ name: 'my-gitlab-app',
+ version: '1.0.0'
+});
+
+// 注册 GitLab 工具
+registerGitLabToolsForFastMCP(server, {
+ enableLogger: true,
+ toolFilter: {
+ allowList: ["Gitlab_Create_MR_Tool", "Gitlab_Search_User_Projects_Tool"]
+ }
+});
+
+// 启动服务器
+server.start({
+ transportType: 'stdio'
+});
+```
+
+## 工具特性
+
+1. **自动映射解析**: 自动将用户名和项目名映射为对应的 ID
+2. **字段过滤**: 支持过滤 API 响应字段,优化性能
+3. **错误处理**: 统一的错误处理和用户友好的错误消息
+4. **类型安全**: 完整的 TypeScript 类型定义
+5. **灵活配置**: 支持工具过滤和日志配置
+6. **双框架支持**: 同时支持 MCP SDK 和 fastmcp
+7. **简单明确**: 开发者明确选择使用哪个框架的注册函数
+
+## 故障排除
+
+### 常见问题
+
+1. **认证失败**: 确保 `GITLAB_TOKEN` 环境变量设置正确
+2. **API URL 错误**: 确保 `GITLAB_API_URL` 指向正确的 GitLab 实例
+3. **权限不足**: 确保访问令牌具有足够的权限
+4. **用户/项目映射失败**: 检查映射配置或使用 ID 而非名称
+
+### 调试
+
+启用日志以查看详细的 API 调用信息:
+
+```typescript
+registerGitLabTools(server, {
+ enableLogger: true
+});
+```
+
+## 许可证
+
+MIT License
\ No newline at end of file
diff --git a/README_zh-CN.md b/README_zh-CN.md
index 06b900c..4783bd4 100644
--- a/README_zh-CN.md
+++ b/README_zh-CN.md
@@ -1,4 +1,4 @@
-[English Version](./README.en.md)
+[English Version](./README.md)
 [](https://github.com/ZephyrDeng/mcp-server-gitlab/actions) [](https://nodejs.org) [](./LICENSE)
@@ -9,7 +9,7 @@
-基于 Model Context Protocol (MCP) 框架构建的 GitLab 集成服务器,提供多种 GitLab RESTful API 工具,支持 Claude、Smithery 等平台集成。
+基于 fastmcp 框架构建的 GitLab 集成服务器,提供多种 GitLab RESTful API 工具,支持 Claude、Smithery 等平台集成。
## 功能概览
@@ -24,6 +24,7 @@
## 快速开始
+### Stdio 模式(默认)
```bash
# 安装依赖
bun install
@@ -31,14 +32,33 @@ bun install
# 构建项目
bun run build
-# 启动服务
+# 启动 stdio 传输模式的服务器(默认)
bun run start
```
+### HTTP Stream 模式(服务器部署)
+```bash
+# 安装依赖
+bun install
+
+# 构建项目
+bun run build
+
+# 启动 HTTP stream 传输模式的服务器
+MCP_TRANSPORT_TYPE=httpStream MCP_PORT=3000 bun run start
+
+# 或使用命令行标志
+bun dist/index.js --http-stream
+```
+
## 环境变量配置
```env
+# 所有模式必需(httpStream 模式下可通过 HTTP headers 提供)
GITLAB_API_URL=https://your-gitlab-instance.com
+
+# stdio 模式必需,httpStream 模式可选
+# (可在 httpStream 模式下通过 HTTP headers 提供)
GITLAB_TOKEN=your_access_token
# 可选:提供用户名到用户 ID 的映射(JSON 字符串)
@@ -51,11 +71,185 @@ GITLAB_USER_MAPPING={"username1": 123, "username2": 456}
# 这可以减少 API 调用次数,并确保使用正确的项目
# 示例:'{"project-name-a": 1001, "group/project-b": "group/project-b"}'
GITLAB_PROJECT_MAPPING={"project-name-a": 1001, "group/project-b": "group/project-b"}
+
+# MCP 传输配置(可选)
+# 传输类型:stdio(默认)或 httpStream
+MCP_TRANSPORT_TYPE=stdio
+
+# HTTP Stream 配置(仅在 MCP_TRANSPORT_TYPE=httpStream 时使用)
+# 服务器绑定地址(httpStream 模式默认为 0.0.0.0,stdio 模式默认为 localhost)
+# 对于 Docker 部署,请使用 0.0.0.0 以允许外部访问
+MCP_HOST=0.0.0.0
+
+# 服务器端口(默认:3000)
+MCP_PORT=3000
+
+# API 端点路径(默认:/mcp)
+MCP_ENDPOINT=/mcp
```
-## 工具示例
+## 使用示例
+
+### 直接 HTTP API 使用
+
+您也可以直接通过 HTTP 请求与 MCP 服务器交互:
+
+```bash
+# 示例:使用 Bearer token 获取用户任务
+curl -X POST http://localhost:3000/mcp \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer your-gitlab-token" \
+ -d '{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "tools/call",
+ "params": {
+ "name": "Gitlab Get User Tasks Tool",
+ "arguments": {
+ "taskFilterType": "ASSIGNED_MRS",
+ "fields": ["id", "title", "source_branch", "target_branch"]
+ }
+ }
+ }'
+```
+
+```bash
+# 示例:使用 PRIVATE-TOKEN header 搜索项目
+curl -X POST http://localhost:3000/mcp \
+ -H "Content-Type: application/json" \
+ -H "PRIVATE-TOKEN: your-gitlab-token" \
+ -d '{
+ "jsonrpc": "2.0",
+ "id": 2,
+ "method": "tools/call",
+ "params": {
+ "name": "Gitlab Search Project Details Tool",
+ "arguments": {
+ "projectName": "my-project",
+ "fields": ["id", "name", "description", "web_url"]
+ }
+ }
+ }'
+```
+
+```bash
+# 示例:使用 Bearer token 和动态 GitLab 实例 URL
+curl -X POST http://localhost:3000/mcp \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer your-gitlab-token" \
+ -H "x-gitlab-url: https://gitlab.company.com" \
+ -d '{
+ "jsonrpc": "2.0",
+ "id": 3,
+ "method": "tools/call",
+ "params": {
+ "name": "Gitlab Get User Tasks Tool",
+ "arguments": {
+ "taskFilterType": "ASSIGNED_MRS",
+ "fields": ["id", "title", "source_branch", "target_branch"]
+ }
+ }
+ }'
+```
+
+### 工具示例
+
+详细工具参数示例请参见 [USAGE.md](./USAGE.md)。
+
+HTTP Stream 模式下动态认证的主要优势:
+- **多租户支持**:单个服务器实例可以服务多个用户
+- **安全性**:每个请求使用自己的认证令牌和 GitLab 实例 URL
+- **灵活性**:可以在不重启服务器的情况下轮换客户端令牌
+- **多实例支持**:从同一服务器连接到不同的 GitLab 实例
+
+## 传输模式
-详见 [USAGE.md](./USAGE.md),包括每个工具的参数示例。
+本服务器支持两种传输模式:
+
+### 1. Stdio 传输(默认)
+- 最适合本地开发与 MCP 客户端的直接集成
+- 使用 stdin/stdout 进行通信
+- 无需网络配置
+
+### 2. HTTP Stream 传输
+- 支持服务器部署以实现远程访问
+- 使用 HTTP POST 请求和流式响应
+- 允许多个客户端连接到同一服务器实例
+- 适用于生产部署
+- **支持通过 HTTP headers 进行动态 token 认证**
+
+使用 HTTP Stream 模式时,客户端可以连接到:
+```
+POST http://localhost:3000/mcp
+Content-Type: application/json
+```
+
+#### 认证方式
+
+HTTP Stream 模式支持多种提供 GitLab token 和实例 URL 的方式:
+
+**Token 认证:**
+
+**1. Bearer Token(推荐):**
+```http
+POST http://localhost:3000/mcp
+Content-Type: application/json
+Authorization: Bearer your-gitlab-access-token
+```
+
+**2. Private Token Header:**
+```http
+POST http://localhost:3000/mcp
+Content-Type: application/json
+PRIVATE-TOKEN: your-gitlab-access-token
+```
+
+**3. 替代 Private Token Header:**
+```http
+POST http://localhost:3000/mcp
+Content-Type: application/json
+private-token: your-gitlab-access-token
+```
+
+**4. 自定义 GitLab Token Header:**
+```http
+POST http://localhost:3000/mcp
+Content-Type: application/json
+x-gitlab-token: your-gitlab-access-token
+```
+
+**GitLab 实例 URL 配置:**
+
+**1. GitLab URL Header(推荐):**
+```http
+POST http://localhost:3000/mcp
+Content-Type: application/json
+x-gitlab-url: https://gitlab.company.com
+```
+
+**2. 替代 GitLab URL Headers:**
+```http
+POST http://localhost:3000/mcp
+Content-Type: application/json
+gitlab-url: https://gitlab.company.com
+```
+
+```http
+POST http://localhost:3000/mcp
+Content-Type: application/json
+gitlab-api-url: https://gitlab.company.com
+```
+
+**5. 回退到环境变量:**
+如果 headers 中没有提供 token 或 URL,服务器将回退到 `GITLAB_TOKEN` 和 `GITLAB_API_URL` 环境变量。
+
+**完整示例:**
+```http
+POST http://localhost:3000/mcp
+Content-Type: application/json
+Authorization: Bearer your-gitlab-access-token
+x-gitlab-url: https://gitlab.company.com
+```
## 项目结构
@@ -87,6 +281,8 @@ tsconfig.json
### Claude 桌面客户端
+#### Stdio 模式(默认)
+
在配置文件中添加:
```json
@@ -94,15 +290,188 @@ tsconfig.json
"mcpServers": {
"@zephyr-mcp/gitlab": {
"command": "npx",
- "args": ["-y", "@zephyr-mcp/gitlab@0.3.0"]
+ "args": ["-y", "@zephyr-mcp/gitlab"]
+ }
+ }
+}
+```
+
+#### HTTP Stream 模式(服务器部署)
+
+**服务器设置:**
+首先启动服务器(注意使用 HTTP headers 时 `GITLAB_TOKEN` 是可选的):
+
+```bash
+# 在您的服务器上 - 环境变量中不需要 token
+MCP_TRANSPORT_TYPE=httpStream MCP_PORT=3000 MCP_HOST=0.0.0.0 npx @zephyr-mcp/gitlab
+
+# 或使用 Docker
+docker run -d \
+ -p 3000:3000 \
+ -e GITLAB_API_URL=https://your-gitlab-instance.com \
+ -e MCP_TRANSPORT_TYPE=httpStream \
+ -e MCP_HOST=0.0.0.0 \
+ -e MCP_PORT=3000 \
+ gitlab-mcp-server
+```
+
+**客户端配置:**
+
+选项 1:**使用 Bearer Token**(推荐)
+```json
+{
+ "mcpServers": {
+ "@zephyr-mcp/gitlab": {
+ "command": "npx",
+ "args": [
+ "@modelcontextprotocol/client-cli",
+ "http://your-server:3000/mcp",
+ "--header", "Authorization: Bearer your-gitlab-access-token"
+ ]
}
}
}
```
-配置参数:
-- `GITLAB_API_URL`: GitLab API 的基础 URL
-- `GITLAB_TOKEN`: 用于验证 GitLab API 请求的访问令牌
+选项 2:**使用 Private Token Header**
+```json
+{
+ "mcpServers": {
+ "@zephyr-mcp/gitlab": {
+ "command": "npx",
+ "args": [
+ "@modelcontextprotocol/client-cli",
+ "http://your-server:3000/mcp",
+ "--header", "PRIVATE-TOKEN: your-gitlab-access-token"
+ ]
+ }
+ }
+}
+```
+
+选项 3:**使用动态 GitLab URL 和 Token**
+```json
+{
+ "mcpServers": {
+ "@zephyr-mcp/gitlab": {
+ "command": "npx",
+ "args": [
+ "@modelcontextprotocol/client-cli",
+ "http://your-server:3000/mcp",
+ "--header", "Authorization: Bearer your-gitlab-access-token",
+ "--header", "x-gitlab-url: https://gitlab.company.com"
+ ]
+ }
+ }
+}
+```
+
+**多租户使用:**
+每个用户可以在自己的客户端配置中配置自己的 token 和 GitLab 实例 URL,允许同一个服务器实例服务具有不同 GitLab 权限和实例的多个用户。
+
+### Smithery
+
+直接在 Smithery 平台上使用:
+
+```bash
+smithery add @zephyr-mcp/gitlab
+```
+
+或在 Smithery UI 中搜索 "@zephyr-mcp/gitlab" 并添加到您的工作区。
+
+环境变量:
+
+- `GITLAB_API_URL`: GitLab API 的基础 URL(stdio 模式必需,httpStream 模式可选 - 可通过 HTTP headers 提供)
+- `GITLAB_TOKEN`: GitLab API 认证的访问令牌(stdio 模式必需,httpStream 模式可选 - 可通过 HTTP headers 提供)
+- `MCP_TRANSPORT_TYPE`: 传输类型(stdio/httpStream)
+- `MCP_HOST`: HTTP stream 模式的服务器绑定地址
+- `MCP_PORT`: HTTP stream 模式的 HTTP 端口
+- `MCP_ENDPOINT`: HTTP stream 模式的 HTTP 端点路径
+
+## 部署
+
+### Docker 部署
+
+仓库包含用于轻松部署的 Dockerfile:
+
+```bash
+# 构建 Docker 镜像
+docker build -t gitlab-mcp-server .
+
+# 使用环境变量运行(token 和 URL 都可通过 HTTP headers 提供)
+docker run -d \
+ -p 3000:3000 \
+ -e MCP_TRANSPORT_TYPE=httpStream \
+ -e MCP_HOST=0.0.0.0 \
+ -e MCP_PORT=3000 \
+ gitlab-mcp-server
+```
+
+#### Docker Compose 示例
+
+```yaml
+services:
+ gitlab-mcp:
+ image: node:22.14.0
+ container_name: gitlab-mcp
+ ports:
+ - "3000:3000"
+ environment:
+ - MCP_TRANSPORT_TYPE=httpStream
+ - MCP_HOST=0.0.0.0
+ - MCP_PORT=3000
+ # 使用 HTTP headers 时 GITLAB_API_URL 和 GITLAB_TOKEN 都是可选的
+ # - GITLAB_API_URL=https://your-gitlab-instance.com
+ # - GITLAB_TOKEN=your_gitlab_token
+ command: npx -y @zephyr-mcp/gitlab@latest
+```
+
+**Docker 重要提示**:在 Docker 容器中运行时,确保设置 `MCP_HOST=0.0.0.0` 以允许外部访问。httpStream 传输的默认值已经是 `0.0.0.0`,但明确设置它可确保兼容性。
+
+### 手动部署
+
+```bash
+# 安装依赖并构建
+npm install
+npm run build
+
+# 启动 HTTP stream 模式的服务器(使用 HTTP headers 时 GITLAB_API_URL 是可选的)
+export MCP_TRANSPORT_TYPE=httpStream
+export MCP_PORT=3000
+# 可选:设置默认的 GitLab 实例 URL(可在 HTTP headers 中覆盖)
+# export GITLAB_API_URL=https://your-gitlab-instance.com
+
+# 运行服务器
+node dist/index.js
+```
+
+### 进程管理器 (PM2)
+
+```bash
+# 安装 PM2
+npm install -g pm2
+
+# 创建生态系统文件
+cat > ecosystem.config.js << EOF
+module.exports = {
+ apps: [{
+ name: 'gitlab-mcp-server',
+ script: 'dist/index.js',
+ env: {
+ MCP_TRANSPORT_TYPE: 'httpStream',
+ MCP_PORT: 3000
+ // 使用 HTTP headers 时 GITLAB_API_URL 是可选的
+ // GITLAB_API_URL: 'https://your-gitlab-instance.com',
+ }
+ }]
+}
+EOF
+
+# 使用 PM2 启动
+pm2 start ecosystem.config.js
+pm2 save
+pm2 startup
+```
## 相关链接
diff --git a/package.json b/package.json
index e721703..0f2ec45 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,25 @@
"description": "基于 Model Context Protocol (MCP) 框架构建的 GitLab 集成服务器,提供与 GitLab 实例的强大集成能力",
"type": "module",
"main": "dist/index.js",
+ "module": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js",
+ "require": "./dist/index.js"
+ },
+ "./tools": {
+ "types": "./dist/gitlab-tools-sdk.d.ts",
+ "import": "./dist/gitlab-tools-sdk.js",
+ "require": "./dist/gitlab-tools-sdk.js"
+ },
+ "./tools.js": {
+ "types": "./dist/gitlab-tools-sdk.d.ts",
+ "import": "./dist/gitlab-tools-sdk.js",
+ "require": "./dist/gitlab-tools-sdk.js"
+ }
+ },
"bin": {
"gitlab-mcp": "dist/index.js"
},
diff --git a/src/config/GitlabConfig.ts b/src/config/GitlabConfig.ts
index d227e30..41106c2 100644
--- a/src/config/GitlabConfig.ts
+++ b/src/config/GitlabConfig.ts
@@ -4,6 +4,7 @@ export interface GitlabConfigOptions {
timeout?: number;
userMapping?: { [username: string]: number };
projectMapping?: { [projectName: string]: string | number };
+ allowEmptyBaseUrl?: boolean; // Allow empty baseUrl for HTTP stream mode
}
export class GitlabConfig {
@@ -21,7 +22,7 @@ export class GitlabConfig {
this.projectMapping = this.loadMapping(options.projectMapping, process.env.GITLAB_PROJECT_MAPPING);
- this.validate();
+ this.validate(options.allowEmptyBaseUrl);
}
private loadMapping(optionValue?: T, envValue?: string): T {
@@ -32,18 +33,16 @@ export class GitlabConfig {
try {
return JSON.parse(envValue);
} catch (error) {
- console.warn(`无法解析环境变量中的映射配置: ${envValue}`, error);
+ console.warn(`Unable to parse mapping configuration from environment variable: ${envValue}`, error);
}
}
return {} as T; // Return empty object if neither option nor env var is provided/valid
}
- private validate() {
- if (!this.privateToken) {
- throw new Error("GitLab configuration error: missing access token (privateToken), please set environment variable GITLAB_TOKEN or pass it as parameter");
- }
- if (!this.baseUrl) {
+ private validate(allowEmptyBaseUrl = false) {
+ if (!this.baseUrl && !allowEmptyBaseUrl) {
throw new Error("GitLab configuration error: missing API URL (baseUrl), please set environment variable GITLAB_API_URL or pass it as parameter");
}
+ // Remove privateToken validation as it can be provided dynamically via headers
}
}
diff --git a/src/gitlab-tools-sdk.ts b/src/gitlab-tools-sdk.ts
new file mode 100644
index 0000000..946e220
--- /dev/null
+++ b/src/gitlab-tools-sdk.ts
@@ -0,0 +1,268 @@
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type { FastMCP, Tool } from "fastmcp";
+
+
+import { gitlabApiClient } from "./utils/gitlabApiClientInstance";
+import { GitlabAcceptMRTool } from "./tools/GitlabAcceptMRTool";
+import { GitlabCreateMRCommentTool } from "./tools/GitlabCreateMRCommentTool";
+import { GitlabCreateMRTool } from "./tools/GitlabCreateMRTool";
+import { GitlabGetUserTasksTool } from "./tools/GitlabGetUserTasksTool";
+import { GitlabRawApiTool } from "./tools/GitlabRawApiTool";
+import { GitlabSearchProjectDetailsTool } from "./tools/GitlabSearchProjectDetailsTool";
+import { GitlabSearchUserProjectsTool } from "./tools/GitlabSearchUserProjectsTool";
+import { GitlabUpdateMRTool } from "./tools/GitlabUpdateMRTool";
+
+/**
+ * Union type representing all available GitLab tool names.
+ *
+ * @public
+ */
+export type GitLabToolName =
+ | "Gitlab_Search_User_Projects_Tool"
+ | "Gitlab_Get_User_Tasks_Tool"
+ | "Gitlab_Search_Project_Details_Tool"
+ | "Gitlab_Create_MR_Tool"
+ | "Gitlab_Update_MR_Tool"
+ | "Gitlab_Accept_MR_Tool"
+ | "Gitlab_Create_MR_Comment_Tool"
+ | "Gitlab_Raw_API_Tool";
+
+/**
+ * Configuration options for filtering which tools should be registered.
+ *
+ * @public
+ */
+export interface GitLabToolFilterOptions {
+ /** List of tools that are allowed to be registered. If provided, only these tools will be registered. */
+ allowList?: GitLabToolName[];
+ /** List of tools that should not be registered. These tools will be excluded from registration. */
+ blockList?: GitLabToolName[];
+}
+
+/**
+ * Configuration options for the GitLab tools registry.
+ *
+ * @public
+ */
+export interface GitLabToolsRegistryOptions {
+ /** Filter options to control which tools are registered. */
+ toolFilter?: GitLabToolFilterOptions;
+ /** Enable/disable logger. Defaults to process.env.ENABLE_LOGGER */
+ enableLogger?: boolean;
+}
+
+/**
+ * Mapping of fastmcp tools to their standardized names.
+ */
+const toolNameMapping = {
+ [GitlabSearchUserProjectsTool.name]: "Gitlab_Search_User_Projects_Tool",
+ [GitlabGetUserTasksTool.name]: "Gitlab_Get_User_Tasks_Tool",
+ [GitlabSearchProjectDetailsTool.name]: "Gitlab_Search_Project_Details_Tool",
+ [GitlabCreateMRTool.name]: "Gitlab_Create_MR_Tool",
+ [GitlabUpdateMRTool.name]: "Gitlab_Update_MR_Tool",
+ [GitlabAcceptMRTool.name]: "Gitlab_Accept_MR_Tool",
+ [GitlabCreateMRCommentTool.name]: "Gitlab_Create_MR_Comment_Tool",
+ [GitlabRawApiTool.name]: "Gitlab_Raw_API_Tool",
+} as const;
+
+/**
+ * Available fastmcp tools.
+ */
+const fastmcpTools = [
+ GitlabAcceptMRTool,
+ GitlabCreateMRCommentTool,
+ GitlabCreateMRTool,
+ GitlabGetUserTasksTool,
+ GitlabRawApiTool,
+ GitlabSearchProjectDetailsTool,
+ GitlabSearchUserProjectsTool,
+ GitlabUpdateMRTool,
+];
+
+/**
+ * Determines whether a tool should be registered based on the provided filter options.
+ *
+ * @param toolName - The name of the tool to check
+ * @param filter - Filter options containing allowList and/or blockList
+ * @returns `true` if the tool should be registered, `false` otherwise
+ *
+ * @internal
+ */
+function shouldRegisterTool(toolName: GitLabToolName, filter?: GitLabToolFilterOptions): boolean {
+ if (!filter) {
+ return true;
+ }
+
+ if (filter.allowList && filter.allowList.length > 0) {
+ return filter.allowList.includes(toolName);
+ }
+
+ if (filter.blockList && filter.blockList.length > 0) {
+ return !filter.blockList.includes(toolName);
+ }
+
+ return true;
+}
+
+/**
+ * Registers GitLab tools with a fastmcp server instance.
+ *
+ * @param server - The FastMCP server instance
+ * @param options - Configuration options for tool registration
+ *
+ * @example
+ * ```typescript
+ * import { FastMCP } from 'fastmcp';
+ * import { registerGitLabToolsForFastMCP } from './gitlab-tools-sdk';
+ *
+ * const server = new FastMCP({ name: 'my-app', version: '1.0.0' });
+ *
+ * // Register all tools
+ * registerGitLabToolsForFastMCP(server);
+ *
+ * // Register only specific tools
+ * registerGitLabToolsForFastMCP(server, {
+ * toolFilter: {
+ * allowList: ['Gitlab_Create_MR_Tool', 'Gitlab_Search_User_Projects_Tool']
+ * }
+ * });
+ * ```
+ *
+ * @public
+ */
+export function registerGitLabToolsForFastMCP(server: FastMCP, options: GitLabToolsRegistryOptions = {}) {
+ const { enableLogger = process.env.ENABLE_LOGGER === 'true' } = options;
+
+ // Configure logger if enabled
+ if (enableLogger) {
+ gitlabApiClient.setLogger(console);
+ }
+
+ // Register tools based on filter options
+ fastmcpTools.forEach(tool => {
+ const standardizedName = toolNameMapping[tool.name as keyof typeof toolNameMapping];
+ if (shouldRegisterTool(standardizedName as GitLabToolName, options.toolFilter)) {
+ // GitLabTool is now fully compatible with FastMCP's base type, can be registered directly
+ server.addTool(tool);
+ }
+ });
+}
+
+/**
+ * Registers GitLab tools with an MCP SDK server instance.
+ *
+ * @param server - The MCP SDK server instance
+ * @param options - Configuration options for tool registration
+ *
+ * @example
+ * ```typescript
+ * import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
+ * import { registerGitLabToolsForMcpSDK } from './gitlab-tools-sdk';
+ *
+ * const server = new McpServer({
+ * name: 'my-app',
+ * version: '1.0.0',
+ * capabilities: { tools: {} }
+ * });
+ *
+ * // Register all tools
+ * registerGitLabToolsForMcpSDK(server);
+ *
+ * // Register only specific tools
+ * registerGitLabToolsForMcpSDK(server, {
+ * toolFilter: {
+ * allowList: ['Gitlab_Create_MR_Tool', 'Gitlab_Search_User_Projects_Tool']
+ * }
+ * });
+ * ```
+ *
+ * @public
+ */
+export function registerGitLabToolsForMcpSDK(server: McpServer, options: GitLabToolsRegistryOptions = {}) {
+ const { enableLogger = process.env.ENABLE_LOGGER === 'true' } = options;
+
+ // Configure logger if enabled
+ if (enableLogger) {
+ gitlabApiClient.setLogger(console);
+ }
+
+ // Convert fastmcp tools to MCP SDK format
+ fastmcpTools.forEach(tool => {
+ const standardizedName = toolNameMapping[tool.name as keyof typeof toolNameMapping];
+ if (shouldRegisterTool(standardizedName as GitLabToolName, options.toolFilter)) {
+ server.tool(
+ tool.name,
+ tool.description || '',
+ tool.parameters || {},
+ async (params) => {
+ // fastmcp tools return ContentResult, convert to MCP SDK format
+ // Create an empty context as MCP SDK doesn't support session authentication
+ const context = {
+ log: {
+ debug: () => {},
+ error: () => {},
+ info: () => {},
+ warn: () => {},
+ },
+ reportProgress: async () => {},
+ session: undefined,
+ streamContent: async () => {},
+ };
+ const result = await tool.execute(params, context);
+
+ // Convert fastmcp ContentResult to MCP SDK format
+ if (typeof result === 'object' && result !== null && 'isError' in result && result.isError) {
+ const errorResult = result as { isError: boolean; content: Array<{ type: string; text: string }> };
+ throw new Error(
+ errorResult.content?.[0]?.type === 'text'
+ ? errorResult.content[0].text
+ : 'Unknown error'
+ );
+ }
+
+ // Return the result in MCP SDK format
+ if (typeof result === 'object' && result !== null && 'content' in result) {
+ const contentResult = result as { content: Array<{ type: string; text: string }> };
+ if (contentResult.content?.[0]?.type === 'text') {
+ try {
+ return JSON.parse(contentResult.content[0].text);
+ } catch {
+ return contentResult.content[0].text;
+ }
+ }
+ }
+
+ return result;
+ }
+ );
+ }
+ });
+}
+
+
+/**
+ * Get all available GitLab tool names.
+ *
+ * @returns Array of all available GitLab tool names
+ *
+ * @public
+ */
+export function getAvailableGitLabTools(): GitLabToolName[] {
+ return Object.values(toolNameMapping);
+}
+
+/**
+ * Legacy function for backward compatibility.
+ * @deprecated Use registerGitLabToolsForFastMCP instead.
+ */
+export const registerTools = registerGitLabToolsForFastMCP;
+
+/**
+ * Legacy function for backward compatibility.
+ * @deprecated Use registerGitLabTools with enableLogger option instead.
+ */
+export const registerLogger = () => {
+ if (process.env.ENABLE_LOGGER) {
+ gitlabApiClient.setLogger(console);
+ }
+};
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index 5ee6ca2..03f50ce 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -9,6 +9,7 @@ import { FastMCP } from "fastmcp";
// GitLab related tools
import { registerLogger, registerTools } from "./tools";
+import { GitLabSessionWithExtras } from "./types/GitLabSession";
// Parse command line arguments and environment variables
const args = process.argv.slice(2);
@@ -19,9 +20,50 @@ const endpoint = process.env.MCP_ENDPOINT || '/mcp';
const host = process.env.MCP_HOST || (transportType === 'httpStream' ? '0.0.0.0' : 'localhost');
// Create FastMCP server instance
-const server = new FastMCP({
+const server = new FastMCP({
name: "GitLab MCP Server",
version: "1.0.0",
+ authenticate: async (request) => {
+ // Extract GitLab token from HTTP headers
+ const tokenHeader = request.headers["authorization"]?.replace('Bearer ', '') ||
+ request.headers["private-token"] ||
+ request.headers["PRIVATE-TOKEN"] ||
+ request.headers["x-gitlab-token"];
+
+ // Extract GitLab base URL from HTTP headers
+ const baseUrlHeader = request.headers["x-gitlab-url"] ||
+ request.headers["gitlab-url"] ||
+ request.headers["gitlab-api-url"];
+
+ // Ensure token and base URL are string types
+ const token = Array.isArray(tokenHeader) ? tokenHeader[0] : tokenHeader;
+ const baseUrl = Array.isArray(baseUrlHeader) ? baseUrlHeader[0] : baseUrlHeader;
+
+ // Validate token format if provided
+ if (token && typeof token === 'string') {
+ // GitLab tokens are typically at least 20 characters
+ if (token.length < 20) {
+ throw new Error('Invalid GitLab token format: token must be at least 20 characters');
+ }
+ }
+
+ // Validate URL format if provided
+ if (baseUrl && typeof baseUrl === 'string') {
+ try {
+ new URL(baseUrl);
+ } catch {
+ throw new Error('Invalid GitLab API URL format: must be a valid URL');
+ }
+ }
+
+ // Return session data that will be available in context.session
+ return {
+ gitlabToken: token,
+ gitlabBaseUrl: baseUrl,
+ headers: request.headers,
+ authenticatedAt: new Date().toISOString(),
+ };
+ },
});
// Register demo resources: all GitLab projects
diff --git a/src/tools.ts b/src/tools.ts
index f13c12f..b8e13a5 100644
--- a/src/tools.ts
+++ b/src/tools.ts
@@ -1,29 +1,5 @@
-import type { FastMCP } from 'fastmcp';
-
-import { gitlabApiClient } from './utils/gitlabApiClientInstance';
-
-import { GitlabAcceptMRTool } from './tools/GitlabAcceptMRTool';
-import { GitlabCreateMRCommentTool } from './tools/GitlabCreateMRCommentTool';
-import { GitlabCreateMRTool } from './tools/GitlabCreateMRTool';
-import { GitlabGetUserTasksTool } from './tools/GitlabGetUserTasksTool';
-import { GitlabRawApiTool } from './tools/GitlabRawApiTool';
-import { GitlabSearchProjectDetailsTool } from './tools/GitlabSearchProjectDetailsTool';
-import { GitlabSearchUserProjectsTool } from './tools/GitlabSearchUserProjectsTool';
-import { GitlabUpdateMRTool } from './tools/GitlabUpdateMRTool';
-
-export const registerTools = (server: FastMCP) => {
- server.addTool(GitlabAcceptMRTool);
- server.addTool(GitlabCreateMRCommentTool);
- server.addTool(GitlabCreateMRTool);
- server.addTool(GitlabGetUserTasksTool);
- server.addTool(GitlabRawApiTool);
- server.addTool(GitlabSearchProjectDetailsTool);
- server.addTool(GitlabSearchUserProjectsTool);
- server.addTool(GitlabUpdateMRTool);
-};
-
-export const registerLogger = () => {
- if (process.env.ENABLE_LOGGER) {
- gitlabApiClient.setLogger(console);
- }
-};
\ No newline at end of file
+/**
+ * @deprecated Import from './gitlab-tools-sdk' instead
+ * This file is kept for backward compatibility only.
+ */
+export { registerTools, registerLogger } from './gitlab-tools-sdk';
\ No newline at end of file
diff --git a/src/tools/GitlabAcceptMRTool.ts b/src/tools/GitlabAcceptMRTool.ts
index ab31b09..1c191c3 100644
--- a/src/tools/GitlabAcceptMRTool.ts
+++ b/src/tools/GitlabAcceptMRTool.ts
@@ -1,5 +1,5 @@
import { z } from "zod";
-import { gitlabApiClient } from "../utils/gitlabApiClientInstance";
+import { createGitlabClientFromContext } from "../utils/gitlabClientFactory";
import { filterResponseFields } from "./gitlab/FieldFilterUtils";
import type { Tool, ContentResult, TextContent, Context } from 'fastmcp';
@@ -31,15 +31,16 @@ export const GitlabAcceptMRTool: Tool | undefined> = {
const { projectId: projectIdOrName, mergeRequestId, mergeOptions, fields } = typedArgs;
try {
- const resolvedProjectId = await gitlabApiClient.resolveProjectId(projectIdOrName);
+ const client = createGitlabClientFromContext(context);
+ const resolvedProjectId = await client.resolveProjectId(projectIdOrName);
if (!resolvedProjectId) {
throw new Error(`无法解析项目 ID 或名称:${projectIdOrName}`);
}
const endpoint = `/projects/${encodeURIComponent(String(resolvedProjectId))}/merge_requests/${mergeRequestId}/merge`;
- const response = await gitlabApiClient.apiRequest(endpoint, "PUT", undefined, mergeOptions);
+ const response = await client.apiRequest(endpoint, "PUT", undefined, mergeOptions);
- if (!gitlabApiClient.isValidResponse(response)) {
+ if (!client.isValidResponse(response)) {
throw new Error(`GitLab API error: ${response?.message || 'Unknown error'}`);
}
diff --git a/src/tools/GitlabCreateMRCommentTool.ts b/src/tools/GitlabCreateMRCommentTool.ts
index 3e841f9..2a55bea 100644
--- a/src/tools/GitlabCreateMRCommentTool.ts
+++ b/src/tools/GitlabCreateMRCommentTool.ts
@@ -1,5 +1,5 @@
import { z } from "zod";
-import { gitlabApiClient } from "../utils/gitlabApiClientInstance";
+import { createGitlabClientFromContext } from "../utils/gitlabClientFactory";
import { filterResponseFields } from "./gitlab/FieldFilterUtils";
import type { Tool, ContentResult, Context } from 'fastmcp';
@@ -23,15 +23,16 @@ export const GitlabCreateMRCommentTool: Tool | undefined
const { projectId: projectIdOrName, mergeRequestId, comment, fields } = typedArgs;
try {
- const resolvedProjectId = await gitlabApiClient.resolveProjectId(projectIdOrName);
+ const client = createGitlabClientFromContext(context);
+ const resolvedProjectId = await client.resolveProjectId(projectIdOrName);
if (!resolvedProjectId) {
throw new Error(`无法解析项目 ID 或名称:${projectIdOrName}`);
}
const endpoint = `/projects/${encodeURIComponent(String(resolvedProjectId))}/merge_requests/${mergeRequestId}/notes`;
- const response = await gitlabApiClient.apiRequest(endpoint, "POST", undefined, { body: comment });
+ const response = await client.apiRequest(endpoint, "POST", undefined, { body: comment });
- if (!gitlabApiClient.isValidResponse(response)) {
+ if (!client.isValidResponse(response)) {
throw new Error(`GitLab API error: ${response?.message || 'Unknown error'}`);
}
diff --git a/src/tools/GitlabCreateMRTool.ts b/src/tools/GitlabCreateMRTool.ts
index 419ced2..411442c 100644
--- a/src/tools/GitlabCreateMRTool.ts
+++ b/src/tools/GitlabCreateMRTool.ts
@@ -1,5 +1,5 @@
import { z } from "zod";
-import { gitlabApiClient } from "../utils/gitlabApiClientInstance";
+import { createGitlabClientFromContext } from "../utils/gitlabClientFactory";
import { filterResponseFields } from "./gitlab/FieldFilterUtils";
import type { Tool, ContentResult, Context } from "fastmcp";
@@ -33,8 +33,9 @@ export const GitlabCreateMRTool: Tool | undefined> = {
const { projectId: projectIdOrName, sourceBranch, targetBranch, title, description, assigneeId: assigneeIdOrName, reviewerIds: reviewerIdsOrNames, labels, fields } = typedArgs;
try {
+ const client = createGitlabClientFromContext(context);
// 解析项目 ID
- const resolvedProjectId = await gitlabApiClient.resolveProjectId(projectIdOrName);
+ const resolvedProjectId = await client.resolveProjectId(projectIdOrName);
if (!resolvedProjectId) {
throw new Error(`无法解析项目 ID 或名称:${projectIdOrName}`);
}
@@ -42,7 +43,7 @@ export const GitlabCreateMRTool: Tool | undefined> = {
let resolvedAssigneeId: number | undefined = undefined;
if (assigneeIdOrName !== undefined) {
- const resolved = await gitlabApiClient.resolveUserId(assigneeIdOrName);
+ const resolved = await client.resolveUserId(assigneeIdOrName);
if (resolved) {
resolvedAssigneeId = resolved;
} else {
@@ -54,7 +55,7 @@ export const GitlabCreateMRTool: Tool | undefined> = {
let resolvedReviewerIds: number[] = [];
if (reviewerIdsOrNames && reviewerIdsOrNames.length > 0) {
- const reviewerPromises = reviewerIdsOrNames.map(idOrName => gitlabApiClient.resolveUserId(idOrName));
+ const reviewerPromises = reviewerIdsOrNames.map(idOrName => client.resolveUserId(idOrName));
const results = await Promise.all(reviewerPromises);
resolvedReviewerIds = results.filter((id): id is number => {
if (id === null) {
@@ -78,9 +79,9 @@ export const GitlabCreateMRTool: Tool | undefined> = {
if (labels !== undefined) createData.labels = labels.join(",");
- const response = await gitlabApiClient.apiRequest(endpoint, "POST", undefined, createData);
+ const response = await client.apiRequest(endpoint, "POST", undefined, createData);
- if (!gitlabApiClient.isValidResponse(response)) {
+ if (!client.isValidResponse(response)) {
throw new Error(`GitLab API error: ${response?.message || 'Unknown error'}`);
}
diff --git a/src/tools/GitlabGetUserTasksTool.ts b/src/tools/GitlabGetUserTasksTool.ts
index 8facbfc..73078b8 100644
--- a/src/tools/GitlabGetUserTasksTool.ts
+++ b/src/tools/GitlabGetUserTasksTool.ts
@@ -1,6 +1,6 @@
import { z } from "zod";
import { createFieldsSchema } from "../utils/zodSchemas";
-import { gitlabApiClient } from "../utils/gitlabApiClientInstance";
+import { createGitlabClientFromContext } from "../utils/gitlabClientFactory";
import { filterResponseFields } from "./gitlab/FieldFilterUtils";
import type { Tool, ContentResult, Context } from 'fastmcp';
@@ -60,7 +60,8 @@ export const GitlabGetUserTasksTool: Tool | undefined> =
break;
}
- const response = await gitlabApiClient.apiRequest("/merge_requests", "GET", params);
+ const client = createGitlabClientFromContext(context);
+ const response = await client.apiRequest("/merge_requests", "GET", params);
if (typedArgs.fields) {
const filteredResponse = filterResponseFields(response, typedArgs.fields);
return {
diff --git a/src/tools/GitlabRawApiTool.ts b/src/tools/GitlabRawApiTool.ts
index 777640a..c54c013 100644
--- a/src/tools/GitlabRawApiTool.ts
+++ b/src/tools/GitlabRawApiTool.ts
@@ -1,6 +1,6 @@
import { z } from "zod";
import { createFieldsSchema } from "../utils/zodSchemas";
-import { gitlabApiClient } from "../utils/gitlabApiClientInstance";
+import { createGitlabClientFromContext } from "../utils/gitlabClientFactory";
import { filterResponseFields } from "./gitlab/FieldFilterUtils";
import type { Tool, ContentResult, Context } from 'fastmcp';
@@ -22,22 +22,23 @@ export const GitlabRawApiTool: Tool | undefined> = {
data?: Record;
fields?: string[];
};
-
+
try {
- const response = await gitlabApiClient.apiRequest(
- typedArgs.endpoint,
- typedArgs.method,
- typedArgs.params,
+ const client = createGitlabClientFromContext(context);
+ const response = await client.apiRequest(
+ typedArgs.endpoint,
+ typedArgs.method,
+ typedArgs.params,
typedArgs.data
);
-
+
if (typedArgs.fields) {
const filteredResponse = filterResponseFields(response, typedArgs.fields);
return {
content: [{ type: "text", text: JSON.stringify(filteredResponse) }]
} as ContentResult;
}
-
+
return {
content: [{ type: "text", text: JSON.stringify(response) }]
} as ContentResult;
diff --git a/src/tools/GitlabSearchProjectDetailsTool.ts b/src/tools/GitlabSearchProjectDetailsTool.ts
index e55407b..786bf4a 100644
--- a/src/tools/GitlabSearchProjectDetailsTool.ts
+++ b/src/tools/GitlabSearchProjectDetailsTool.ts
@@ -1,6 +1,6 @@
import { z } from "zod";
import { createFieldsSchema } from "../utils/zodSchemas";
-import { gitlabApiClient } from "../utils/gitlabApiClientInstance";
+import { createGitlabClientFromContext } from "../utils/gitlabClientFactory";
import type { Tool, ContentResult, Context } from 'fastmcp';
import { filterResponseFields } from './gitlab/FieldFilterUtils';
@@ -18,7 +18,8 @@ export const GitlabSearchProjectDetailsTool: Tool | unde
};
try {
- const response = await gitlabApiClient.apiRequest('/projects', 'GET', { search: typedArgs.projectName });
+ const client = createGitlabClientFromContext(context);
+ const response = await client.apiRequest('/projects', 'GET', { search: typedArgs.projectName });
let result = response;
if (typedArgs.fields) {
diff --git a/src/tools/GitlabSearchUserProjectsTool.ts b/src/tools/GitlabSearchUserProjectsTool.ts
index 9876584..e081453 100644
--- a/src/tools/GitlabSearchUserProjectsTool.ts
+++ b/src/tools/GitlabSearchUserProjectsTool.ts
@@ -1,5 +1,5 @@
import { z } from "zod";
-import { gitlabApiClient } from "../utils/gitlabApiClientInstance";
+import { createGitlabClientFromContext } from "../utils/gitlabClientFactory";
import { createFieldsSchema } from '../utils/zodSchemas'
import type { Tool, ContentResult, Context } from 'fastmcp';
import { filterResponseFields } from '../tools/gitlab/FieldFilterUtils'
@@ -18,7 +18,8 @@ export const GitlabSearchUserProjectsTool: Tool | undefi
};
try {
- const users = await gitlabApiClient.apiRequest("/users", "GET", { search: typedArgs.username });
+ const client = createGitlabClientFromContext(context);
+ const users = await client.apiRequest("/users", "GET", { search: typedArgs.username });
if (!Array.isArray(users) || users.length === 0) {
return {
content: [
@@ -32,7 +33,7 @@ export const GitlabSearchUserProjectsTool: Tool | undefi
}
const user = users[0];
- const projects = await gitlabApiClient.apiRequest(`/users/${user.id}/projects`, "GET", {});
+ const projects = await client.apiRequest(`/users/${user.id}/projects`, "GET", {});
const result = { user, projects };
if (typedArgs.fields) {
diff --git a/src/tools/GitlabUpdateMRTool.ts b/src/tools/GitlabUpdateMRTool.ts
index 774d362..296da40 100644
--- a/src/tools/GitlabUpdateMRTool.ts
+++ b/src/tools/GitlabUpdateMRTool.ts
@@ -1,5 +1,5 @@
import { z } from "zod";
-import { gitlabApiClient } from "../utils/gitlabApiClientInstance";
+import { createGitlabClientFromContext } from "../utils/gitlabClientFactory";
import { filterResponseFields } from "./gitlab/FieldFilterUtils";
import type { Tool, ContentResult, Context } from "fastmcp";
@@ -31,7 +31,8 @@ export const GitlabUpdateMRTool: Tool | undefined> = {
const { projectId: projectIdOrName, mergeRequestId, assigneeId: assigneeIdOrName, reviewerIds: reviewerIdsOrNames, title, description, labels, fields } = typedArgs;
try {
- const resolvedProjectId = await gitlabApiClient.resolveProjectId(projectIdOrName);
+ const client = createGitlabClientFromContext(context);
+ const resolvedProjectId = await client.resolveProjectId(projectIdOrName);
if (!resolvedProjectId) {
throw new Error(`无法解析项目 ID 或名称:${projectIdOrName}`);
}
@@ -42,7 +43,7 @@ export const GitlabUpdateMRTool: Tool | undefined> = {
if (assigneeIdOrName === 0 || assigneeIdOrName === "") {
resolvedAssigneeId = null;
} else {
- const resolved = await gitlabApiClient.resolveUserId(assigneeIdOrName);
+ const resolved = await client.resolveUserId(assigneeIdOrName);
if (resolved) {
resolvedAssigneeId = resolved;
} else {
@@ -58,7 +59,7 @@ export const GitlabUpdateMRTool: Tool | undefined> = {
if (reviewerIdsOrNames.length === 0) {
resolvedReviewerIds = [];
} else {
- const reviewerPromises = reviewerIdsOrNames.map(idOrName => gitlabApiClient.resolveUserId(idOrName));
+ const reviewerPromises = reviewerIdsOrNames.map(idOrName => client.resolveUserId(idOrName));
const results = await Promise.all(reviewerPromises);
resolvedReviewerIds = results.filter((id): id is number => {
if (id === null) {
@@ -89,9 +90,9 @@ export const GitlabUpdateMRTool: Tool | undefined> = {
}
- const response = await gitlabApiClient.apiRequest(endpoint, "PUT", undefined, updateData);
+ const response = await client.apiRequest(endpoint, "PUT", undefined, updateData);
- if (!gitlabApiClient.isValidResponse(response)) {
+ if (!client.isValidResponse(response)) {
throw new Error(`GitLab API error: ${response?.message || 'Unknown error'}`);
}
diff --git a/src/types/GitLabSession.ts b/src/types/GitLabSession.ts
new file mode 100644
index 0000000..f231cd4
--- /dev/null
+++ b/src/types/GitLabSession.ts
@@ -0,0 +1,27 @@
+/**
+ * GitLab Session type definition
+ * Used for FastMCP session authentication data
+ */
+
+export interface GitLabSession {
+ /** GitLab access token */
+ gitlabToken?: string;
+ /** GitLab API base URL */
+ gitlabBaseUrl?: string;
+ /** Original request headers */
+ headers?: Record;
+ /** Authentication timestamp */
+ authenticatedAt?: string;
+ /** User role */
+ role?: string;
+ /** User ID */
+ userId?: string;
+ /** Username */
+ username?: string;
+}
+
+/**
+ * GitLab Session type with extra dynamic properties
+ * Uses index signature to satisfy FastMCPSessionAuth constraint while maintaining type safety
+ */
+export type GitLabSessionWithExtras = GitLabSession & Record;
\ No newline at end of file
diff --git a/src/types/GitLabTool.ts b/src/types/GitLabTool.ts
new file mode 100644
index 0000000..070507d
--- /dev/null
+++ b/src/types/GitLabTool.ts
@@ -0,0 +1,10 @@
+import { Tool } from "fastmcp";
+import { GitLabSession } from "./GitLabSession";
+
+/**
+ * Base type definition for GitLab tools
+ *
+ * Uses Record | undefined to maintain full compatibility with FastMCPSessionAuth
+ * In tool implementations, we ensure type safety through type guards
+ */
+export type GitLabTool = Tool | undefined>;
\ No newline at end of file
diff --git a/src/utils/gitlabApiClientInstance.ts b/src/utils/gitlabApiClientInstance.ts
index b167464..41b86ce 100644
--- a/src/utils/gitlabApiClientInstance.ts
+++ b/src/utils/gitlabApiClientInstance.ts
@@ -1,9 +1,2 @@
-import { GitlabApiClient } from "../tools/gitlab/GitlabApiClient";
-import { GitlabConfig } from "../config/GitlabConfig";
-
-const config = new GitlabConfig({
- baseUrl: process.env.GITLAB_API_URL,
- privateToken: process.env.GITLAB_TOKEN,
-});
-
-export const gitlabApiClient = new GitlabApiClient(config);
\ No newline at end of file
+// Re-export new factory functions for backward compatibility
+export { createGitlabApiClient, gitlabApiClient } from "./gitlabClientFactory";
\ No newline at end of file
diff --git a/src/utils/gitlabClientFactory.ts b/src/utils/gitlabClientFactory.ts
new file mode 100644
index 0000000..aa33d3c
--- /dev/null
+++ b/src/utils/gitlabClientFactory.ts
@@ -0,0 +1,67 @@
+import { GitlabApiClient } from "../tools/gitlab/GitlabApiClient";
+import { GitlabConfig } from "../config/GitlabConfig";
+import { Context } from "fastmcp";
+import { extractGitlabToken, extractGitlabBaseUrl } from "./typeGuards";
+
+const config = new GitlabConfig({
+ baseUrl: process.env.GITLAB_API_URL,
+ privateToken: process.env.GITLAB_TOKEN,
+ allowEmptyBaseUrl: process.env.MCP_TRANSPORT_TYPE === 'httpStream',
+});
+
+/**
+ * Extract GitLab token and base URL from context and create client
+ * @param context MCP request context
+ * @returns GitLab API client instance
+ */
+export function createGitlabClientFromContext(context?: Context | undefined>): GitlabApiClient {
+ // Use type-safe functions to extract token and base URL
+ const token = extractGitlabToken(context?.session);
+ const baseUrl = extractGitlabBaseUrl(context?.session);
+
+ // Only validate when neither context nor config has values
+ // This allows environment variables to be used as fallback
+ const finalToken = token || config.privateToken;
+ const finalBaseUrl = baseUrl || config.baseUrl;
+
+ if (!finalToken) {
+ throw new Error(
+ 'GitLab token is required. Please provide it via HTTP headers (Authorization/PRIVATE-TOKEN/x-gitlab-token) or set GITLAB_TOKEN environment variable'
+ );
+ }
+
+ if (!finalBaseUrl && process.env.MCP_TRANSPORT_TYPE !== 'httpStream') {
+ throw new Error(
+ 'GitLab API URL is required. Please provide it via HTTP headers (x-gitlab-url/gitlab-url/gitlab-api-url) or set GITLAB_API_URL environment variable'
+ );
+ }
+
+ // Create config with dynamic values if provided
+ const configWithDynamicValues = new GitlabConfig({
+ baseUrl: finalBaseUrl,
+ privateToken: finalToken,
+ allowEmptyBaseUrl: process.env.MCP_TRANSPORT_TYPE === 'httpStream',
+ });
+
+ return new GitlabApiClient(configWithDynamicValues);
+}
+
+/**
+ * Create GitLab API client instance (maintains backward compatibility)
+ * @param token Optional dynamic token, uses configured token if not provided
+ * @param baseUrl Optional dynamic base URL, uses configured URL if not provided
+ * @returns GitLab API client instance
+ */
+export function createGitlabApiClient(token?: string, baseUrl?: string): GitlabApiClient {
+ // Create config with dynamic values if provided
+ const configWithDynamicValues = new GitlabConfig({
+ baseUrl: baseUrl || config.baseUrl,
+ privateToken: token || config.privateToken,
+ allowEmptyBaseUrl: process.env.MCP_TRANSPORT_TYPE === 'httpStream',
+ });
+
+ return new GitlabApiClient(configWithDynamicValues);
+}
+
+// Default instance for backward compatibility
+export const gitlabApiClient = createGitlabApiClient();
\ No newline at end of file
diff --git a/src/utils/typeGuards.ts b/src/utils/typeGuards.ts
new file mode 100644
index 0000000..9e6d5dc
--- /dev/null
+++ b/src/utils/typeGuards.ts
@@ -0,0 +1,40 @@
+import { GitLabSession } from "../types/GitLabSession";
+
+/**
+ * Type guard: Check if session is GitLabSession
+ */
+export function isGitLabSession(session: unknown): session is GitLabSession {
+ return session !== null &&
+ typeof session === 'object' &&
+ 'gitlabToken' in session;
+}
+
+/**
+ * Safely extract GitLab token from context
+ */
+export function extractGitlabToken(session: unknown): string | undefined {
+ if (isGitLabSession(session)) {
+ return session.gitlabToken;
+ }
+ return undefined;
+}
+
+/**
+ * Safely extract GitLab base URL from context
+ */
+export function extractGitlabBaseUrl(session: unknown): string | undefined {
+ if (isGitLabSession(session)) {
+ return session.gitlabBaseUrl;
+ }
+ return undefined;
+}
+
+/**
+ * Safely extract headers from context
+ */
+export function extractHeaders(session: unknown): Record | undefined {
+ if (isGitLabSession(session)) {
+ return session.headers;
+ }
+ return undefined;
+}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index 1476df0..7f940de 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -8,12 +8,18 @@
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
- "types": ["node", "jest"]
+ "types": ["node", "jest"],
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true
},
"include": [
"src/**/*"
],
"exclude": [
- "node_modules"
+ "node_modules",
+ "dist",
+ "**/*.test.ts",
+ "**/*.spec.ts"
]
}
\ No newline at end of file