cann-gitcode-mcp 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cann_gitcode_mcp-0.1.0 → cann_gitcode_mcp-0.2.0}/AGENTS.md +7 -3
- {cann_gitcode_mcp-0.1.0 → cann_gitcode_mcp-0.2.0}/CLAUDE.md +2 -1
- {cann_gitcode_mcp-0.1.0 → cann_gitcode_mcp-0.2.0}/PKG-INFO +23 -57
- cann_gitcode_mcp-0.2.0/README.md +86 -0
- cann_gitcode_mcp-0.2.0/docs/pipeline-tools-research.md +210 -0
- {cann_gitcode_mcp-0.1.0 → cann_gitcode_mcp-0.2.0}/pyproject.toml +1 -1
- {cann_gitcode_mcp-0.1.0 → cann_gitcode_mcp-0.2.0}/src/cann_gitcode_mcp/__init__.py +1 -1
- {cann_gitcode_mcp-0.1.0 → cann_gitcode_mcp-0.2.0}/src/cann_gitcode_mcp/server.py +2 -0
- cann_gitcode_mcp-0.2.0/src/cann_gitcode_mcp/tools/pipeline.py +273 -0
- {cann_gitcode_mcp-0.1.0 → cann_gitcode_mcp-0.2.0}/src/cann_gitcode_mcp/tools/pull_request.py +38 -1
- cann_gitcode_mcp-0.2.0/tests/test_pipeline_tools.py +296 -0
- {cann_gitcode_mcp-0.1.0 → cann_gitcode_mcp-0.2.0}/tests/test_pull_request_tools.py +29 -0
- {cann_gitcode_mcp-0.1.0 → cann_gitcode_mcp-0.2.0}/tests/test_server.py +7 -2
- cann_gitcode_mcp-0.1.0/README.md +0 -120
- {cann_gitcode_mcp-0.1.0 → cann_gitcode_mcp-0.2.0}/.gitignore +0 -0
- {cann_gitcode_mcp-0.1.0 → cann_gitcode_mcp-0.2.0}/LICENSE +0 -0
- {cann_gitcode_mcp-0.1.0 → cann_gitcode_mcp-0.2.0}/docs/gitcode_mcp_design.md +0 -0
- {cann_gitcode_mcp-0.1.0 → cann_gitcode_mcp-0.2.0}/src/cann_gitcode_mcp/__main__.py +0 -0
- {cann_gitcode_mcp-0.1.0 → cann_gitcode_mcp-0.2.0}/src/cann_gitcode_mcp/client.py +0 -0
- {cann_gitcode_mcp-0.1.0 → cann_gitcode_mcp-0.2.0}/src/cann_gitcode_mcp/tools/__init__.py +0 -0
- {cann_gitcode_mcp-0.1.0 → cann_gitcode_mcp-0.2.0}/tests/__init__.py +0 -0
- {cann_gitcode_mcp-0.1.0 → cann_gitcode_mcp-0.2.0}/tests/test_client.py +0 -0
|
@@ -4,7 +4,7 @@ Guidelines for AI agents (and humans) contributing to this repository.
|
|
|
4
4
|
|
|
5
5
|
## Project Purpose
|
|
6
6
|
|
|
7
|
-
`cann-gitcode-mcp` wraps the GitCode REST API (v5) as an MCP server so that AI assistants (Claude Code, etc.) can perform GitCode operations on behalf of CANN developers. Current scope: Pull Request management. Planned:
|
|
7
|
+
`cann-gitcode-mcp` wraps the GitCode REST API (v5) as an MCP server so that AI assistants (Claude Code, etc.) can perform GitCode operations on behalf of CANN developers. Current scope: Pull Request management and CI/CD pipeline summary (Level 1 — parsing cann-robot PR comments). Planned: pipeline details via openLiBing API (Level 2), repository tools, Issue tools.
|
|
8
8
|
|
|
9
9
|
## Repository Structure
|
|
10
10
|
|
|
@@ -17,9 +17,12 @@ cann-gitcode-mcp/
|
|
|
17
17
|
│ ├── client.py # GitCodeClient (async httpx wrapper)
|
|
18
18
|
│ └── tools/
|
|
19
19
|
│ ├── __init__.py
|
|
20
|
-
│
|
|
20
|
+
│ ├── pull_request.py # 7 PR tools (incl. list comments)
|
|
21
|
+
│ └── pipeline.py # Pipeline summary from PR bot comments
|
|
21
22
|
├── tests/
|
|
22
|
-
│ ├──
|
|
23
|
+
│ ├── test_pull_request_tools.py
|
|
24
|
+
│ ├── test_pipeline_tools.py
|
|
25
|
+
│ ├── test_client.py
|
|
23
26
|
│ └── test_server.py
|
|
24
27
|
├── pyproject.toml
|
|
25
28
|
├── README.md
|
|
@@ -130,4 +133,5 @@ All existing and new tests must pass before committing.
|
|
|
130
133
|
- Base URL: `https://gitcode.com/api/v5`
|
|
131
134
|
- Authentication: `Authorization: Bearer <token>` header
|
|
132
135
|
- PR endpoints: `/repos/{owner}/{repo}/pulls[/{number}[/merge|comments|files]]`
|
|
136
|
+
- Pipeline data: extracted from cann-robot PR comments (no dedicated API); see `docs/pipeline-tools-research.md` for details
|
|
133
137
|
- Full API docs: consult GitCode platform documentation
|
|
@@ -14,7 +14,8 @@ src/cann_gitcode_mcp/
|
|
|
14
14
|
├── client.py # GitCodeClient: async HTTP client wrapping httpx
|
|
15
15
|
└── tools/
|
|
16
16
|
├── __init__.py
|
|
17
|
-
|
|
17
|
+
├── pull_request.py # register_pull_request_tools(mcp) — 7 PR tools
|
|
18
|
+
└── pipeline.py # register_pipeline_tools(mcp) — 2 pipeline tools (trigger + summary)
|
|
18
19
|
```
|
|
19
20
|
|
|
20
21
|
- **`server.py`** — creates the `FastMCP("gitcode")` instance and calls every `register_*_tools(mcp)` function. This is the single wiring point.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cann-gitcode-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: CANN GitCode MCP Server - GitCode platform tools for CANN developers
|
|
5
5
|
Project-URL: Homepage, https://gitcode.com/shengnan666/cann-gitcode-mcp
|
|
6
6
|
Project-URL: Repository, https://gitcode.com/shengnan666/cann-gitcode-mcp
|
|
@@ -33,35 +33,19 @@ Description-Content-Type: text/markdown
|
|
|
33
33
|
[](https://pypi.org/project/cann-gitcode-mcp/)
|
|
34
34
|
[](https://github.com/shengnan666/cann-gitcode-mcp/blob/main/LICENSE)
|
|
35
35
|
|
|
36
|
-
CANN GitCode
|
|
36
|
+
CANN 社区的代码托管在 [GitCode](https://gitcode.com/) 平台。`cann-gitcode-mcp` 将 GitCode API 封装为 [MCP](https://modelcontextprotocol.io/) 工具,让 CANN 开发者在 Claude Code 等 AI 助手中直接操作仓库的 Pull Request、Issue、流水线等,无需离开对话界面。
|
|
37
37
|
|
|
38
|
-
##
|
|
38
|
+
## 快速开始
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
CANN 社区的代码托管在 GitCode 平台,本项目为 CANN 开发者提供在 AI 助手中操作 GitCode 仓库(Pull Request、流水线、Issue 等)的能力,无需离开对话界面即可完成常见研发工作流。
|
|
43
|
-
|
|
44
|
-
## 安装
|
|
40
|
+
### 1. 安装
|
|
45
41
|
|
|
46
42
|
```bash
|
|
47
43
|
pip install cann-gitcode-mcp
|
|
48
44
|
```
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
### 1. 获取 GitCode API Token
|
|
53
|
-
|
|
54
|
-
在 [GitCode 用户设置](https://gitcode.com/profile/tokens) 中生成个人访问令牌。
|
|
55
|
-
|
|
56
|
-
### 2. 设置环境变量
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
export GITCODE_API_TOKEN=your_token_here
|
|
60
|
-
```
|
|
46
|
+
### 2. 配置 Claude Code
|
|
61
47
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
在 Claude Code 的 MCP 配置文件中添加:
|
|
48
|
+
先在 [GitCode 用户设置](https://gitcode.com/setting/token-classic) 中生成个人访问令牌,然后在 Claude Code 的 MCP 配置文件(`~/.claude/settings.json` 或项目级 `.mcp.json`)中添加:
|
|
65
49
|
|
|
66
50
|
```json
|
|
67
51
|
{
|
|
@@ -76,23 +60,9 @@ export GITCODE_API_TOKEN=your_token_here
|
|
|
76
60
|
}
|
|
77
61
|
```
|
|
78
62
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
```json
|
|
82
|
-
{
|
|
83
|
-
"mcpServers": {
|
|
84
|
-
"gitcode": {
|
|
85
|
-
"command": "python",
|
|
86
|
-
"args": ["-m", "cann_gitcode_mcp"],
|
|
87
|
-
"env": {
|
|
88
|
-
"GITCODE_API_TOKEN": "your_token_here"
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
```
|
|
63
|
+
重启 Claude Code 即可使用。
|
|
94
64
|
|
|
95
|
-
|
|
65
|
+
> **环境变量说明**:Token 通过 MCP 配置的 `env` 字段传入即可,无需手动 `export`。如需覆盖 API 地址,可额外设置 `GITCODE_API_BASE_URL`(默认 `https://gitcode.com/api/v5`)。
|
|
96
66
|
|
|
97
67
|
## 可用工具
|
|
98
68
|
|
|
@@ -106,19 +76,25 @@ export GITCODE_API_TOKEN=your_token_here
|
|
|
106
76
|
| `merge_pull_request` | 合并 Pull Request |
|
|
107
77
|
| `comment_pull_request` | 在 PR 上发表评论 |
|
|
108
78
|
| `get_pull_request_files` | 获取 PR 的变更文件列表 |
|
|
79
|
+
| `list_pull_request_comments` | 获取 PR 的所有评论 |
|
|
109
80
|
|
|
110
|
-
|
|
81
|
+
### Pipeline(流水线)
|
|
111
82
|
|
|
112
|
-
|
|
|
113
|
-
|
|
114
|
-
| `
|
|
115
|
-
|
|
83
|
+
| 工具名 | 说明 |
|
|
84
|
+
|--------|------|
|
|
85
|
+
| `get_pr_pipeline_summary` | 获取 PR 的 CI/CD 流水线摘要(解析 cann-robot 评论) |
|
|
86
|
+
|
|
87
|
+
`get_pr_pipeline_summary` 从 PR 评论中解析 cann-robot 发布的流水线结果,返回每个任务的名称、状态(SUCCESS/FAILED/ABORTED)、日志链接、构建产物下载链接,以及整体 CI 结论。无需 openLiBing token,仅使用 GitCode API。
|
|
116
88
|
|
|
117
89
|
## 路线图
|
|
118
90
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
91
|
+
当前处于早期开发阶段(`0.x`),已实现 PR 工具集和流水线摘要。后续将按 CANN 社区实际研发流程的优先级逐步扩展:
|
|
92
|
+
|
|
93
|
+
1. **Issue 工具** — Issue 增删改查、评论管理(CANN 社区日常协作最频繁的场景)
|
|
94
|
+
2. **仓库工具** — 分支管理、文件读取、提交历史
|
|
95
|
+
3. **流水线详情(Level 2)** — 通过 openLiBing API 获取 stage/job/step 级别的详细信息和错误消息
|
|
96
|
+
|
|
97
|
+
欢迎在 [Issue](https://gitcode.com/shengnan666/cann-gitcode-mcp/issues) 中提出需求或反馈。
|
|
122
98
|
|
|
123
99
|
## 开发
|
|
124
100
|
|
|
@@ -129,21 +105,11 @@ pip install -e ".[dev]"
|
|
|
129
105
|
# 运行测试
|
|
130
106
|
pytest
|
|
131
107
|
|
|
132
|
-
#
|
|
108
|
+
# 构建与发布
|
|
133
109
|
python -m build
|
|
134
|
-
|
|
135
|
-
# 上传到 PyPI
|
|
136
110
|
twine upload dist/*
|
|
137
111
|
```
|
|
138
112
|
|
|
139
|
-
## 版本规则
|
|
140
|
-
|
|
141
|
-
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/):
|
|
142
|
-
|
|
143
|
-
- `0.x.y` — 早期开发阶段,接口可能不兼容变更
|
|
144
|
-
- `1.0.0` — 工具集稳定后的首个正式版本
|
|
145
|
-
- MAJOR.MINOR.PATCH — 不兼容变更.新功能.修复
|
|
146
|
-
|
|
147
113
|
## 许可证
|
|
148
114
|
|
|
149
115
|
[Apache License 2.0](LICENSE)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# cann-gitcode-mcp
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/cann-gitcode-mcp/)
|
|
4
|
+
[](https://pypi.org/project/cann-gitcode-mcp/)
|
|
5
|
+
[](https://github.com/shengnan666/cann-gitcode-mcp/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
CANN 社区的代码托管在 [GitCode](https://gitcode.com/) 平台。`cann-gitcode-mcp` 将 GitCode API 封装为 [MCP](https://modelcontextprotocol.io/) 工具,让 CANN 开发者在 Claude Code 等 AI 助手中直接操作仓库的 Pull Request、Issue、流水线等,无需离开对话界面。
|
|
8
|
+
|
|
9
|
+
## 快速开始
|
|
10
|
+
|
|
11
|
+
### 1. 安装
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install cann-gitcode-mcp
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### 2. 配置 Claude Code
|
|
18
|
+
|
|
19
|
+
先在 [GitCode 用户设置](https://gitcode.com/setting/token-classic) 中生成个人访问令牌,然后在 Claude Code 的 MCP 配置文件(`~/.claude/settings.json` 或项目级 `.mcp.json`)中添加:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"gitcode": {
|
|
25
|
+
"command": "cann-gitcode-mcp",
|
|
26
|
+
"env": {
|
|
27
|
+
"GITCODE_API_TOKEN": "your_token_here"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
重启 Claude Code 即可使用。
|
|
35
|
+
|
|
36
|
+
> **环境变量说明**:Token 通过 MCP 配置的 `env` 字段传入即可,无需手动 `export`。如需覆盖 API 地址,可额外设置 `GITCODE_API_BASE_URL`(默认 `https://gitcode.com/api/v5`)。
|
|
37
|
+
|
|
38
|
+
## 可用工具
|
|
39
|
+
|
|
40
|
+
### Pull Request
|
|
41
|
+
|
|
42
|
+
| 工具名 | 说明 |
|
|
43
|
+
|--------|------|
|
|
44
|
+
| `create_pull_request` | 创建 Pull Request |
|
|
45
|
+
| `list_pull_requests` | 列出仓库的 Pull Request(支持状态、排序等过滤) |
|
|
46
|
+
| `get_pull_request` | 获取指定 PR 的详细信息 |
|
|
47
|
+
| `merge_pull_request` | 合并 Pull Request |
|
|
48
|
+
| `comment_pull_request` | 在 PR 上发表评论 |
|
|
49
|
+
| `get_pull_request_files` | 获取 PR 的变更文件列表 |
|
|
50
|
+
| `list_pull_request_comments` | 获取 PR 的所有评论 |
|
|
51
|
+
|
|
52
|
+
### Pipeline(流水线)
|
|
53
|
+
|
|
54
|
+
| 工具名 | 说明 |
|
|
55
|
+
|--------|------|
|
|
56
|
+
| `get_pr_pipeline_summary` | 获取 PR 的 CI/CD 流水线摘要(解析 cann-robot 评论) |
|
|
57
|
+
|
|
58
|
+
`get_pr_pipeline_summary` 从 PR 评论中解析 cann-robot 发布的流水线结果,返回每个任务的名称、状态(SUCCESS/FAILED/ABORTED)、日志链接、构建产物下载链接,以及整体 CI 结论。无需 openLiBing token,仅使用 GitCode API。
|
|
59
|
+
|
|
60
|
+
## 路线图
|
|
61
|
+
|
|
62
|
+
当前处于早期开发阶段(`0.x`),已实现 PR 工具集和流水线摘要。后续将按 CANN 社区实际研发流程的优先级逐步扩展:
|
|
63
|
+
|
|
64
|
+
1. **Issue 工具** — Issue 增删改查、评论管理(CANN 社区日常协作最频繁的场景)
|
|
65
|
+
2. **仓库工具** — 分支管理、文件读取、提交历史
|
|
66
|
+
3. **流水线详情(Level 2)** — 通过 openLiBing API 获取 stage/job/step 级别的详细信息和错误消息
|
|
67
|
+
|
|
68
|
+
欢迎在 [Issue](https://gitcode.com/shengnan666/cann-gitcode-mcp/issues) 中提出需求或反馈。
|
|
69
|
+
|
|
70
|
+
## 开发
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# 安装开发依赖
|
|
74
|
+
pip install -e ".[dev]"
|
|
75
|
+
|
|
76
|
+
# 运行测试
|
|
77
|
+
pytest
|
|
78
|
+
|
|
79
|
+
# 构建与发布
|
|
80
|
+
python -m build
|
|
81
|
+
twine upload dist/*
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 许可证
|
|
85
|
+
|
|
86
|
+
[Apache License 2.0](LICENSE)
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Pipeline Tools 前期调研与架构设计
|
|
2
|
+
|
|
3
|
+
> 调研日期: 2026-03-29
|
|
4
|
+
|
|
5
|
+
## 1. 背景
|
|
6
|
+
|
|
7
|
+
CANN 的 CI/CD 流水线托管在 openLiBing (www.openlibing.com)。需要通过 MCP 工具查看流水线结果(编译结果、用例执行结果等)。
|
|
8
|
+
|
|
9
|
+
## 2. openLiBing 平台调研
|
|
10
|
+
|
|
11
|
+
### 2.1 平台概况
|
|
12
|
+
|
|
13
|
+
- **URL**: https://www.openlibing.com
|
|
14
|
+
- **性质**: MaJun 平台上的 CI/CD 服务,用于华为开源生态
|
|
15
|
+
- **登录方式**: 通过 GitCode 账号 OAuth 授权登录
|
|
16
|
+
- **公开 API**: 无。无公开文档、无 SDK、无 CLI
|
|
17
|
+
- **内部接口**: 有,Web UI 背后通过 JSON 接口获取数据
|
|
18
|
+
|
|
19
|
+
### 2.2 已确认的内部接口
|
|
20
|
+
|
|
21
|
+
**Pipeline Run Detail(流水线运行详情)**
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
GET /gateway/openlibing-cicd/project/pipeline/pipeline-run/detail
|
|
25
|
+
?projectId={projectId}
|
|
26
|
+
&pipelineId={pipelineId}
|
|
27
|
+
&pipelineRunId={pipelineRunId}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- Host: `www.openlibing.com`
|
|
31
|
+
- 认证方式: JWT Token,通过 Cookie 和 Header 双重传递
|
|
32
|
+
- Cookie: `token={jwt}; csrf-token-open-li-bing={jwt}`
|
|
33
|
+
- Header: `Csrf-Token-Open-Li-Bing: {jwt}`
|
|
34
|
+
- Token 有效期: 30 分钟(`Max-Age=1800`),响应 Set-Cookie 自动续期
|
|
35
|
+
- JWT Payload 含: accountId, accountLogin, accountPlatform("gitcode"), exp 等
|
|
36
|
+
|
|
37
|
+
### 2.3 流水线数据没有列表接口
|
|
38
|
+
|
|
39
|
+
用户确认:openLiBing 没有通过 pipelineId 查询运行列表的接口。每次流水线的入口是从 GitCode PR 上 bot 评论中的链接点进去的。
|
|
40
|
+
|
|
41
|
+
## 3. GitCode PR Bot 评论分析
|
|
42
|
+
|
|
43
|
+
### 3.1 Bot 信息
|
|
44
|
+
|
|
45
|
+
- **Bot 用户**: `cann-robot` (login: `cann-robot`)
|
|
46
|
+
- **评论 API**: `GET /api/v5/repos/{owner}/{repo}/pulls/{number}/comments`
|
|
47
|
+
- **认证**: `Authorization: Bearer {GITCODE_API_TOKEN}`(注意是 Bearer 不是 token 前缀)
|
|
48
|
+
|
|
49
|
+
### 3.2 Bot 评论结构
|
|
50
|
+
|
|
51
|
+
Bot 发两条评论:
|
|
52
|
+
|
|
53
|
+
**评论 1**: PR 审批状态(CLA签名、lgtm/approve 进度)
|
|
54
|
+
|
|
55
|
+
**评论 2**: 流水线结果(关键),包含:
|
|
56
|
+
|
|
57
|
+
1. **触发信息**: "流水线任务触发成功"
|
|
58
|
+
2. **openLiBing 链接**:
|
|
59
|
+
```
|
|
60
|
+
https://www.openlibing.com/apps/pipelineDetail
|
|
61
|
+
?pipelineId={pipelineId}
|
|
62
|
+
&pipelineRunId={pipelineRunId}
|
|
63
|
+
&projectName=CANN
|
|
64
|
+
```
|
|
65
|
+
3. **HTML 表格**,每行一个任务:
|
|
66
|
+
- 任务名称: `Check_Pr`, `Compile_X86_compiler`, `UT_Test_dflow` 等
|
|
67
|
+
- 状态: `SUCCESS` / `FAILED` / `ABORTED`
|
|
68
|
+
- 日志链接: openLiBing 详情页链接
|
|
69
|
+
- 下载链接: `ascend-ci.obs.cn-north-4.myhuaweicloud.com` 上的构建产物
|
|
70
|
+
4. **结论**: `CI执行失败` 或 `CI执行成功`
|
|
71
|
+
|
|
72
|
+
### 3.3 Bot 评论示例(关键部分)
|
|
73
|
+
|
|
74
|
+
```html
|
|
75
|
+
流水线任务触发成功
|
|
76
|
+
任务链接 [<a href='https://www.openlibing.com/apps/pipelineDetail?pipelineId=xxx&pipelineRunId=yyy&projectName=CANN'>yyy</a>]
|
|
77
|
+
<table>
|
|
78
|
+
<tr><th>任务名称</th><th>状态</th><th>日志</th><th>下载链接</th></tr>
|
|
79
|
+
<tr>
|
|
80
|
+
<td><strong>Compile_X86_compiler</strong></td>
|
|
81
|
+
<td>❌ FAILED</td>
|
|
82
|
+
<td><a href=https://www.openlibing.com/apps/pipelineDetail?...>>>>>></a></td>
|
|
83
|
+
<td><a href=></a></td>
|
|
84
|
+
</tr>
|
|
85
|
+
<tr>
|
|
86
|
+
<td><strong>Compile_X86_executor</strong></td>
|
|
87
|
+
<td>✅ SUCCESS</td>
|
|
88
|
+
<td><a href=...>>>>>></a></td>
|
|
89
|
+
<td><a href=https://ascend-ci.obs.cn-north-4.myhuaweicloud.com/ge/package/1601/cann-ge-executor_linux-x86_64.run>>>>>></a></td>
|
|
90
|
+
</tr>
|
|
91
|
+
...
|
|
92
|
+
</table>
|
|
93
|
+
[2026-03-28 16:45:30] CI执行失败
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 4. openLiBing Pipeline Run Detail 数据结构
|
|
97
|
+
|
|
98
|
+
从实际抓包获得的 JSON 结构(以 cann_ge pipeline #7071 为例):
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
Pipeline Run
|
|
102
|
+
├── id, pipeline_id, name("cann_ge"), status("FAILED")
|
|
103
|
+
├── executor_name, trigger_type("Note"), run_number(7071)
|
|
104
|
+
├── start_time, end_time (epoch ms)
|
|
105
|
+
├── sources[] — 代码来源信息
|
|
106
|
+
│ └── git_type("gitcode"), repo_name("ge"), build_params(MR信息)
|
|
107
|
+
├── artifacts[] — 构建产物列表
|
|
108
|
+
│ └── name, packageType, version
|
|
109
|
+
└── stages[] — 流水线阶段列表
|
|
110
|
+
├── 阶段_1 (sequence:0) — 解析CI分支, COMPLETED
|
|
111
|
+
├── Image (sequence:1) — 解析镜像版本, COMPLETED
|
|
112
|
+
├── 编译构建 (sequence:2) — 14个并行Job, FAILED
|
|
113
|
+
│ └── jobs[]
|
|
114
|
+
│ ├── name, identifier, status, condition
|
|
115
|
+
│ ├── message (失败时有错误信息)
|
|
116
|
+
│ └── steps[]
|
|
117
|
+
│ ├── name, task, business_type, status, message
|
|
118
|
+
│ └── inputs[] (key/value配置)
|
|
119
|
+
├── LLT (sequence:3) — UT/ST测试 + codecheck等, 23个Job, INIT(未执行)
|
|
120
|
+
├── lcov (sequence:4) — 覆盖率报告, INIT
|
|
121
|
+
└── 后处理阶段 (sequence:5) — last_comment + Resource Clean, FAILED
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 4.1 Stage 依赖关系
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
阶段_1 → Image → 编译构建 → LLT → lcov
|
|
128
|
+
└→ 后处理阶段 (run_always:true)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 4.2 Job 状态枚举
|
|
132
|
+
|
|
133
|
+
- `COMPLETED` — 成功
|
|
134
|
+
- `FAILED` — 失败(message 字段含错误信息)
|
|
135
|
+
- `INIT` — 未执行(因前置阶段失败)
|
|
136
|
+
- `ABORTED` — 被中止
|
|
137
|
+
|
|
138
|
+
### 4.3 典型错误信息
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{"errorMessage":" 构建任务执行失败!","errorCode":"DEV-CODECI-35002"}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## 5. 架构设计方案
|
|
145
|
+
|
|
146
|
+
### 5.1 分层设计(已与用户达成共识)
|
|
147
|
+
|
|
148
|
+
**Level 1: PR 评论解析 + 触发(纯 GitCode API)** ✅ 已实现
|
|
149
|
+
- 不依赖 openLiBing,只用 GITCODE_API_TOKEN
|
|
150
|
+
- 工具 1: `get_pr_pipeline_summary(owner, repo, pr_number)` — 解析 cann-robot 评论提取任务状态表,通过 PR labels 判断流水线状态(running/passed/failed),支持分页获取所有评论确保取到最后一次 pipeline 结果
|
|
151
|
+
- 工具 2: `trigger_pr_pipeline(owner, repo, pr_number)` — 在 PR 中发 `compile` 评论触发流水线
|
|
152
|
+
|
|
153
|
+
**Level 2: openLiBing 详情(需 openLiBing JWT)**
|
|
154
|
+
- 获取 stage/job/step 级别的详细信息和错误消息
|
|
155
|
+
- 工具: `get_pipeline_run_detail(pipeline_id, pipeline_run_id)`
|
|
156
|
+
- 需要解决 JWT 获取和续期问题
|
|
157
|
+
- **后续实现**,用于获取失败的详细信息
|
|
158
|
+
|
|
159
|
+
### 5.2 流水线状态判断
|
|
160
|
+
|
|
161
|
+
通过 PR labels 判断流水线状态(比解析评论更准确):
|
|
162
|
+
|
|
163
|
+
| PR Label | pipeline_status |
|
|
164
|
+
|----------|----------------|
|
|
165
|
+
| `ci-pipeline-running` | `running` |
|
|
166
|
+
| `ci-pipeline-passed` | `passed` |
|
|
167
|
+
| `ci-pipeline-failed` | `failed` |
|
|
168
|
+
| 都没有 | `unknown` |
|
|
169
|
+
|
|
170
|
+
### 5.3 职责划分
|
|
171
|
+
|
|
172
|
+
| 职责 | 归属 | 原因 |
|
|
173
|
+
|------|------|------|
|
|
174
|
+
| 发 `compile` 评论触发流水线 | MCP 工具 | 原子操作 |
|
|
175
|
+
| 查询最新 pipeline 结果 | MCP 工具 | 原子操作 |
|
|
176
|
+
| 等待流水线完成(轮询) | 调用方(Claude Code / 用户) | 等待时间分钟~几十分钟级 |
|
|
177
|
+
| 判断是否需要重新触发 | 调用方 | 业务决策 |
|
|
178
|
+
|
|
179
|
+
### 5.4 代码结构
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
src/cann_gitcode_mcp/
|
|
183
|
+
├── client.py # 现有 GitCodeClient (Bearer token auth)
|
|
184
|
+
├── openlibing_client.py # 待实现: OpenLiBingClient (JWT + CSRF auth, auto-refresh)
|
|
185
|
+
└── tools/
|
|
186
|
+
├── pull_request.py # 7 个 PR 工具(含 list_pull_request_comments)
|
|
187
|
+
└── pipeline.py # 2 个 pipeline 工具(trigger + summary)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 5.5 环境变量
|
|
191
|
+
|
|
192
|
+
| 变量 | 用途 | Level |
|
|
193
|
+
|------|------|-------|
|
|
194
|
+
| `GITCODE_API_TOKEN` | GitCode API 认证 | Level 1 |
|
|
195
|
+
| `OPENLIBING_TOKEN` | openLiBing JWT token | Level 2 |
|
|
196
|
+
|
|
197
|
+
## 6. 待确认/待探索
|
|
198
|
+
|
|
199
|
+
- [ ] openLiBing 是否有获取单个 Job 日志的接口(需要用户在 Web UI 上抓包)
|
|
200
|
+
- [ ] openLiBing JWT 的自动获取方案(OAuth flow 还是手动提供)
|
|
201
|
+
- [ ] 是否有其他 openLiBing 接口(如项目列表、流水线列表等)
|
|
202
|
+
|
|
203
|
+
## 7. 参考
|
|
204
|
+
|
|
205
|
+
- GitCode PR 评论 API: `GET /api/v5/repos/{owner}/{repo}/pulls/{number}/comments`
|
|
206
|
+
- openLiBing Pipeline Detail: `GET /gateway/openlibing-cicd/project/pipeline/pipeline-run/detail`
|
|
207
|
+
- 示例 PR: https://gitcode.com/cann/ge/pull/1601
|
|
208
|
+
- 示例 Pipeline Run ID: `aaa5c9122dbb44b0bbdd2890218a92f2`
|
|
209
|
+
- 示例 Pipeline ID: `8033cdebd5e5420e9165181589392a80`
|
|
210
|
+
- Project ID: `300033`, Project Name: `CANN`
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from mcp.server.fastmcp import FastMCP
|
|
2
2
|
|
|
3
|
+
from cann_gitcode_mcp.tools.pipeline import register_pipeline_tools
|
|
3
4
|
from cann_gitcode_mcp.tools.pull_request import register_pull_request_tools
|
|
4
5
|
|
|
5
6
|
mcp = FastMCP("gitcode")
|
|
6
7
|
|
|
7
8
|
register_pull_request_tools(mcp)
|
|
9
|
+
register_pipeline_tools(mcp)
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
def main():
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from html.parser import HTMLParser
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from mcp.server.fastmcp import FastMCP
|
|
9
|
+
|
|
10
|
+
from cann_gitcode_mcp.client import GitCodeClient
|
|
11
|
+
|
|
12
|
+
_client: GitCodeClient | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_client() -> GitCodeClient:
|
|
16
|
+
global _client
|
|
17
|
+
if _client is None:
|
|
18
|
+
_client = GitCodeClient()
|
|
19
|
+
return _client
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _text_result(data: Any) -> str:
|
|
23
|
+
return json.dumps(data, indent=2, ensure_ascii=False)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _PipelineTableParser(HTMLParser):
|
|
27
|
+
"""Parse the HTML table from cann-robot's pipeline comment."""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
super().__init__()
|
|
31
|
+
self.tasks: list[dict[str, str]] = []
|
|
32
|
+
self._in_table = False
|
|
33
|
+
self._in_row = False
|
|
34
|
+
self._in_cell = False
|
|
35
|
+
self._in_header = False
|
|
36
|
+
self._current_row: list[str] = []
|
|
37
|
+
self._current_cell = ""
|
|
38
|
+
self._current_href = ""
|
|
39
|
+
|
|
40
|
+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
41
|
+
if tag == "table":
|
|
42
|
+
self._in_table = True
|
|
43
|
+
elif tag == "tr" and self._in_table:
|
|
44
|
+
self._in_row = True
|
|
45
|
+
self._current_row = []
|
|
46
|
+
elif tag == "th" and self._in_row:
|
|
47
|
+
self._in_header = True
|
|
48
|
+
elif tag == "td" and self._in_row:
|
|
49
|
+
self._in_cell = True
|
|
50
|
+
self._current_cell = ""
|
|
51
|
+
self._current_href = ""
|
|
52
|
+
elif tag == "a" and self._in_cell:
|
|
53
|
+
href = dict(attrs).get("href", "")
|
|
54
|
+
if href:
|
|
55
|
+
self._current_href = href
|
|
56
|
+
|
|
57
|
+
def handle_endtag(self, tag: str) -> None:
|
|
58
|
+
if tag == "table":
|
|
59
|
+
self._in_table = False
|
|
60
|
+
elif tag == "tr" and self._in_row:
|
|
61
|
+
self._in_row = False
|
|
62
|
+
if self._current_row and not self._in_header:
|
|
63
|
+
self._emit_task()
|
|
64
|
+
elif tag == "th":
|
|
65
|
+
self._in_header = False
|
|
66
|
+
elif tag == "td" and self._in_cell:
|
|
67
|
+
self._in_cell = False
|
|
68
|
+
self._current_row.append((self._current_cell.strip(), self._current_href))
|
|
69
|
+
|
|
70
|
+
def handle_data(self, data: str) -> None:
|
|
71
|
+
if self._in_cell:
|
|
72
|
+
self._current_cell += data
|
|
73
|
+
|
|
74
|
+
def _emit_task(self) -> None:
|
|
75
|
+
if len(self._current_row) < 2:
|
|
76
|
+
return
|
|
77
|
+
# Columns: 任务名称, 状态, 日志, 下载链接
|
|
78
|
+
name = self._current_row[0][0] if self._current_row[0] else ""
|
|
79
|
+
status_text = self._current_row[1][0] if len(self._current_row) > 1 else ""
|
|
80
|
+
log_link = self._current_row[2][1] if len(self._current_row) > 2 else ""
|
|
81
|
+
download_link = self._current_row[3][1] if len(self._current_row) > 3 else ""
|
|
82
|
+
|
|
83
|
+
# Clean status text: remove emoji prefixes like ✅ ❌
|
|
84
|
+
status = re.sub(r"^[^\w]*", "", status_text).strip()
|
|
85
|
+
|
|
86
|
+
task: dict[str, str] = {"name": name, "status": status}
|
|
87
|
+
if log_link:
|
|
88
|
+
task["log_url"] = log_link
|
|
89
|
+
if download_link:
|
|
90
|
+
task["download_url"] = download_link
|
|
91
|
+
self.tasks.append(task)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _parse_pipeline_comment(body: str) -> dict[str, Any] | None:
|
|
95
|
+
"""Extract pipeline info from a cann-robot comment body.
|
|
96
|
+
|
|
97
|
+
Returns None if the comment is not a pipeline comment.
|
|
98
|
+
"""
|
|
99
|
+
if "流水线任务触发成功" not in body:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
result: dict[str, Any] = {}
|
|
103
|
+
|
|
104
|
+
# Extract openLiBing link
|
|
105
|
+
link_match = re.search(
|
|
106
|
+
r"https://www\.openlibing\.com/apps/pipelineDetail\?[^'\"<>\s]+", body,
|
|
107
|
+
)
|
|
108
|
+
if link_match:
|
|
109
|
+
url = link_match.group(0)
|
|
110
|
+
result["openlibing_url"] = url
|
|
111
|
+
# Extract pipelineId and pipelineRunId from URL
|
|
112
|
+
pid = re.search(r"pipelineId=([^&]+)", url)
|
|
113
|
+
prid = re.search(r"pipelineRunId=([^&]+)", url)
|
|
114
|
+
if pid:
|
|
115
|
+
result["pipeline_id"] = pid.group(1)
|
|
116
|
+
if prid:
|
|
117
|
+
result["pipeline_run_id"] = prid.group(1)
|
|
118
|
+
|
|
119
|
+
# Parse HTML table for task details
|
|
120
|
+
parser = _PipelineTableParser()
|
|
121
|
+
parser.feed(body)
|
|
122
|
+
result["tasks"] = parser.tasks
|
|
123
|
+
|
|
124
|
+
# Extract overall conclusion — handle and <p> tags in real data
|
|
125
|
+
conclusion_match = re.search(
|
|
126
|
+
r"\]\s*(?: |\s)*(CI执行[^\n<]+)", body,
|
|
127
|
+
)
|
|
128
|
+
if conclusion_match:
|
|
129
|
+
result["conclusion"] = conclusion_match.group(1).strip()
|
|
130
|
+
|
|
131
|
+
# Extract timestamp
|
|
132
|
+
ts_match = re.search(r"\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]", body)
|
|
133
|
+
if ts_match:
|
|
134
|
+
result["timestamp"] = ts_match.group(1)
|
|
135
|
+
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
_PIPELINE_LABELS = {
|
|
140
|
+
"ci-pipeline-running": "running",
|
|
141
|
+
"ci-pipeline-passed": "passed",
|
|
142
|
+
"ci-pipeline-failed": "failed",
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _get_pipeline_status(labels: list[dict[str, Any]]) -> str:
|
|
147
|
+
"""Extract pipeline status from PR labels."""
|
|
148
|
+
for label in labels:
|
|
149
|
+
name = label.get("name", "")
|
|
150
|
+
if name in _PIPELINE_LABELS:
|
|
151
|
+
return _PIPELINE_LABELS[name]
|
|
152
|
+
return "unknown"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def _fetch_all_comments(
|
|
156
|
+
client: GitCodeClient, owner: str, repo: str, pr_number: int,
|
|
157
|
+
) -> list[dict[str, Any]]:
|
|
158
|
+
"""Fetch all PR comments, handling pagination."""
|
|
159
|
+
all_comments: list[dict[str, Any]] = []
|
|
160
|
+
page = 1
|
|
161
|
+
per_page = 100
|
|
162
|
+
while True:
|
|
163
|
+
comments = await client.get(
|
|
164
|
+
f"/repos/{owner}/{repo}/pulls/{pr_number}/comments",
|
|
165
|
+
page=page,
|
|
166
|
+
per_page=per_page,
|
|
167
|
+
)
|
|
168
|
+
all_comments.extend(comments)
|
|
169
|
+
if len(comments) < per_page:
|
|
170
|
+
break
|
|
171
|
+
page += 1
|
|
172
|
+
return all_comments
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def register_pipeline_tools(mcp: FastMCP) -> None:
|
|
176
|
+
|
|
177
|
+
@mcp.tool()
|
|
178
|
+
async def trigger_pr_pipeline(
|
|
179
|
+
owner: str,
|
|
180
|
+
repo: str,
|
|
181
|
+
pr_number: int,
|
|
182
|
+
) -> str:
|
|
183
|
+
"""Trigger CI/CD pipeline for a GitCode PR by posting a 'compile' comment.
|
|
184
|
+
|
|
185
|
+
The cann-robot bot will pick up the comment and start the pipeline.
|
|
186
|
+
Use get_pr_pipeline_summary to check the result later (pipelines
|
|
187
|
+
take minutes to tens of minutes to complete).
|
|
188
|
+
|
|
189
|
+
IMPORTANT: Do not call this repeatedly — a second 'compile' comment
|
|
190
|
+
will abort the running pipeline and restart it.
|
|
191
|
+
|
|
192
|
+
Workflow: if you only have a branch name, first call
|
|
193
|
+
list_pull_requests(owner, repo, state="open", head="<branch>")
|
|
194
|
+
to find the PR number.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
owner: Repository namespace (e.g. "cann")
|
|
198
|
+
repo: Repository name (e.g. "ge")
|
|
199
|
+
pr_number: PR number
|
|
200
|
+
"""
|
|
201
|
+
client = _get_client()
|
|
202
|
+
result = await client.post(
|
|
203
|
+
f"/repos/{owner}/{repo}/pulls/{pr_number}/comments",
|
|
204
|
+
json={"body": "compile"},
|
|
205
|
+
)
|
|
206
|
+
return _text_result({
|
|
207
|
+
"pr": f"{owner}/{repo}#{pr_number}",
|
|
208
|
+
"action": "pipeline_triggered",
|
|
209
|
+
"message": "Posted 'compile' comment. Pipeline will start shortly. "
|
|
210
|
+
"Use get_pr_pipeline_summary to check status.",
|
|
211
|
+
"comment_id": result.get("id"),
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
@mcp.tool()
|
|
215
|
+
async def get_pr_pipeline_summary(
|
|
216
|
+
owner: str,
|
|
217
|
+
repo: str,
|
|
218
|
+
pr_number: int,
|
|
219
|
+
) -> str:
|
|
220
|
+
"""Get CI/CD pipeline summary for a GitCode Pull Request.
|
|
221
|
+
|
|
222
|
+
Returns pipeline status (from PR labels: running/passed/failed)
|
|
223
|
+
and the latest pipeline result parsed from cann-robot bot comments.
|
|
224
|
+
Only uses GitCode API (Level 1), no openLiBing token required.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
owner: Repository namespace (e.g. "cann")
|
|
228
|
+
repo: Repository name (e.g. "ge")
|
|
229
|
+
pr_number: PR number
|
|
230
|
+
"""
|
|
231
|
+
client = _get_client()
|
|
232
|
+
|
|
233
|
+
# Fetch PR details for label-based status
|
|
234
|
+
pr_detail = await client.get(
|
|
235
|
+
f"/repos/{owner}/{repo}/pulls/{pr_number}",
|
|
236
|
+
)
|
|
237
|
+
pipeline_status = _get_pipeline_status(pr_detail.get("labels", []))
|
|
238
|
+
|
|
239
|
+
# Fetch all comments (paginated) for pipeline results
|
|
240
|
+
all_comments = await _fetch_all_comments(client, owner, repo, pr_number)
|
|
241
|
+
|
|
242
|
+
pipeline_results: list[dict[str, Any]] = []
|
|
243
|
+
for comment in all_comments:
|
|
244
|
+
user_login = comment.get("user", {}).get("login", "")
|
|
245
|
+
body = comment.get("body", "")
|
|
246
|
+
|
|
247
|
+
if user_login != "cann-robot":
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
parsed = _parse_pipeline_comment(body)
|
|
251
|
+
if parsed is not None:
|
|
252
|
+
pipeline_results.append(parsed)
|
|
253
|
+
|
|
254
|
+
base_result: dict[str, Any] = {
|
|
255
|
+
"pr": f"{owner}/{repo}#{pr_number}",
|
|
256
|
+
"pipeline_status": pipeline_status,
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if not pipeline_results:
|
|
260
|
+
return _text_result({
|
|
261
|
+
**base_result,
|
|
262
|
+
"pipeline_found": False,
|
|
263
|
+
"message": "No pipeline comments found from cann-robot",
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
# Return the latest pipeline result (last comment)
|
|
267
|
+
latest = pipeline_results[-1]
|
|
268
|
+
return _text_result({
|
|
269
|
+
**base_result,
|
|
270
|
+
"pipeline_found": True,
|
|
271
|
+
"total_pipeline_comments": len(pipeline_results),
|
|
272
|
+
**latest,
|
|
273
|
+
})
|
{cann_gitcode_mcp-0.1.0 → cann_gitcode_mcp-0.2.0}/src/cann_gitcode_mcp/tools/pull_request.py
RENAMED
|
@@ -70,6 +70,8 @@ def register_pull_request_tools(mcp: FastMCP) -> None:
|
|
|
70
70
|
owner: str,
|
|
71
71
|
repo: str,
|
|
72
72
|
state: str | None = None,
|
|
73
|
+
head: str | None = None,
|
|
74
|
+
base: str | None = None,
|
|
73
75
|
sort: str | None = None,
|
|
74
76
|
direction: str | None = None,
|
|
75
77
|
page: int | None = None,
|
|
@@ -77,10 +79,17 @@ def register_pull_request_tools(mcp: FastMCP) -> None:
|
|
|
77
79
|
) -> str:
|
|
78
80
|
"""List Pull Requests of a GitCode repository.
|
|
79
81
|
|
|
82
|
+
Tip: use `head` to find the PR associated with a local branch.
|
|
83
|
+
For example, after `git branch --show-current` returns "fix-bug",
|
|
84
|
+
call list_pull_requests(owner, repo, state="open", head="fix-bug")
|
|
85
|
+
to locate the PR for that branch.
|
|
86
|
+
|
|
80
87
|
Args:
|
|
81
88
|
owner: Repository namespace
|
|
82
89
|
repo: Repository name
|
|
83
90
|
state: Filter by state (open/closed/merged/all, default open)
|
|
91
|
+
head: Filter by source branch name (e.g. "my-feature")
|
|
92
|
+
base: Filter by target branch name (e.g. "master")
|
|
84
93
|
sort: Sort by (created/updated/popularity)
|
|
85
94
|
direction: Sort direction (asc/desc)
|
|
86
95
|
page: Page number
|
|
@@ -89,7 +98,8 @@ def register_pull_request_tools(mcp: FastMCP) -> None:
|
|
|
89
98
|
client = _get_client()
|
|
90
99
|
params = {
|
|
91
100
|
k: v for k, v in {
|
|
92
|
-
"state": state, "
|
|
101
|
+
"state": state, "head": head, "base": base,
|
|
102
|
+
"sort": sort, "direction": direction,
|
|
93
103
|
"page": page, "per_page": per_page,
|
|
94
104
|
}.items() if v is not None
|
|
95
105
|
}
|
|
@@ -171,3 +181,30 @@ def register_pull_request_tools(mcp: FastMCP) -> None:
|
|
|
171
181
|
f"/repos/{owner}/{repo}/pulls/{number}/files",
|
|
172
182
|
)
|
|
173
183
|
return _text_result(result)
|
|
184
|
+
|
|
185
|
+
@mcp.tool()
|
|
186
|
+
async def list_pull_request_comments(
|
|
187
|
+
owner: str,
|
|
188
|
+
repo: str,
|
|
189
|
+
number: int,
|
|
190
|
+
page: int | None = None,
|
|
191
|
+
per_page: int | None = None,
|
|
192
|
+
) -> str:
|
|
193
|
+
"""List all comments on a GitCode Pull Request.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
owner: Repository namespace
|
|
197
|
+
repo: Repository name
|
|
198
|
+
number: PR number
|
|
199
|
+
page: Page number
|
|
200
|
+
per_page: Items per page (default 20)
|
|
201
|
+
"""
|
|
202
|
+
client = _get_client()
|
|
203
|
+
params = {
|
|
204
|
+
k: v for k, v in {"page": page, "per_page": per_page}.items()
|
|
205
|
+
if v is not None
|
|
206
|
+
}
|
|
207
|
+
result = await client.get(
|
|
208
|
+
f"/repos/{owner}/{repo}/pulls/{number}/comments", **params,
|
|
209
|
+
)
|
|
210
|
+
return _text_result(result)
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from unittest.mock import AsyncMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from mcp.server.fastmcp import FastMCP
|
|
8
|
+
|
|
9
|
+
from cann_gitcode_mcp.tools.pipeline import (
|
|
10
|
+
_get_pipeline_status,
|
|
11
|
+
_parse_pipeline_comment,
|
|
12
|
+
register_pipeline_tools,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture(autouse=True)
|
|
17
|
+
def reset_client():
|
|
18
|
+
import cann_gitcode_mcp.tools.pipeline as pipeline_mod
|
|
19
|
+
pipeline_mod._client = None
|
|
20
|
+
yield
|
|
21
|
+
pipeline_mod._client = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def mock_client():
|
|
26
|
+
client = AsyncMock()
|
|
27
|
+
with patch("cann_gitcode_mcp.tools.pipeline._get_client", return_value=client):
|
|
28
|
+
yield client
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def mcp_server():
|
|
33
|
+
server = FastMCP("test")
|
|
34
|
+
register_pipeline_tools(server)
|
|
35
|
+
return server
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# -- Sample bot comment matching the research doc format --
|
|
39
|
+
|
|
40
|
+
PIPELINE_COMMENT_BODY = """\
|
|
41
|
+
流水线任务触发成功
|
|
42
|
+
任务链接 [<a href='https://www.openlibing.com/apps/pipelineDetail?pipelineId=8033cdebd5e5420e9165181589392a80&pipelineRunId=aaa5c9122dbb44b0bbdd2890218a92f2&projectName=CANN'>aaa5c9122dbb44b0bbdd2890218a92f2</a>]
|
|
43
|
+
<table>
|
|
44
|
+
<tr><th>任务名称</th><th>状态</th><th>日志</th><th>下载链接</th></tr>
|
|
45
|
+
<tr>
|
|
46
|
+
<td><strong>Check_Pr</strong></td>
|
|
47
|
+
<td>✅ SUCCESS</td>
|
|
48
|
+
<td><a href=https://www.openlibing.com/apps/pipelineDetail?pipelineId=8033cdebd5e5420e9165181589392a80&pipelineRunId=aaa5c9122dbb44b0bbdd2890218a92f2&projectName=CANN>>>>>></a></td>
|
|
49
|
+
<td><a href=></a></td>
|
|
50
|
+
</tr>
|
|
51
|
+
<tr>
|
|
52
|
+
<td><strong>Compile_X86_compiler</strong></td>
|
|
53
|
+
<td>❌ FAILED</td>
|
|
54
|
+
<td><a href=https://www.openlibing.com/apps/pipelineDetail?pipelineId=8033cdebd5e5420e9165181589392a80&pipelineRunId=aaa5c9122dbb44b0bbdd2890218a92f2&projectName=CANN>>>>>></a></td>
|
|
55
|
+
<td><a href=></a></td>
|
|
56
|
+
</tr>
|
|
57
|
+
<tr>
|
|
58
|
+
<td><strong>Compile_X86_executor</strong></td>
|
|
59
|
+
<td>✅ SUCCESS</td>
|
|
60
|
+
<td><a href=https://www.openlibing.com/apps/pipelineDetail?pipelineId=8033cdebd5e5420e9165181589392a80&pipelineRunId=aaa5c9122dbb44b0bbdd2890218a92f2&projectName=CANN>>>>>></a></td>
|
|
61
|
+
<td><a href=https://ascend-ci.obs.cn-north-4.myhuaweicloud.com/ge/package/1601/cann-ge-executor_linux-x86_64.run>>>>>></a></td>
|
|
62
|
+
</tr>
|
|
63
|
+
</table>
|
|
64
|
+
[2026-03-28 16:45:30] CI执行失败
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
APPROVAL_COMMENT_BODY = """\
|
|
68
|
+
CLA签名通过
|
|
69
|
+
lgtm: 0/2
|
|
70
|
+
approve: 0/1
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TestParsePipelineComment:
|
|
75
|
+
def test_parses_pipeline_comment(self):
|
|
76
|
+
result = _parse_pipeline_comment(PIPELINE_COMMENT_BODY)
|
|
77
|
+
assert result is not None
|
|
78
|
+
assert result["pipeline_id"] == "8033cdebd5e5420e9165181589392a80"
|
|
79
|
+
assert result["pipeline_run_id"] == "aaa5c9122dbb44b0bbdd2890218a92f2"
|
|
80
|
+
assert result["conclusion"] == "CI执行失败"
|
|
81
|
+
assert result["timestamp"] == "2026-03-28 16:45:30"
|
|
82
|
+
|
|
83
|
+
tasks = result["tasks"]
|
|
84
|
+
assert len(tasks) == 3
|
|
85
|
+
|
|
86
|
+
assert tasks[0]["name"] == "Check_Pr"
|
|
87
|
+
assert tasks[0]["status"] == "SUCCESS"
|
|
88
|
+
|
|
89
|
+
assert tasks[1]["name"] == "Compile_X86_compiler"
|
|
90
|
+
assert tasks[1]["status"] == "FAILED"
|
|
91
|
+
|
|
92
|
+
assert tasks[2]["name"] == "Compile_X86_executor"
|
|
93
|
+
assert tasks[2]["status"] == "SUCCESS"
|
|
94
|
+
assert "download_url" in tasks[2]
|
|
95
|
+
assert "ascend-ci" in tasks[2]["download_url"]
|
|
96
|
+
|
|
97
|
+
def test_returns_none_for_non_pipeline_comment(self):
|
|
98
|
+
assert _parse_pipeline_comment(APPROVAL_COMMENT_BODY) is None
|
|
99
|
+
|
|
100
|
+
def test_returns_none_for_empty_string(self):
|
|
101
|
+
assert _parse_pipeline_comment("") is None
|
|
102
|
+
|
|
103
|
+
def test_extracts_openlibing_url(self):
|
|
104
|
+
result = _parse_pipeline_comment(PIPELINE_COMMENT_BODY)
|
|
105
|
+
assert result is not None
|
|
106
|
+
assert "openlibing_url" in result
|
|
107
|
+
assert "pipelineDetail" in result["openlibing_url"]
|
|
108
|
+
|
|
109
|
+
def test_success_comment(self):
|
|
110
|
+
body = PIPELINE_COMMENT_BODY.replace("CI执行失败", "CI执行成功").replace(
|
|
111
|
+
"❌ FAILED", "✅ SUCCESS"
|
|
112
|
+
)
|
|
113
|
+
result = _parse_pipeline_comment(body)
|
|
114
|
+
assert result is not None
|
|
115
|
+
assert result["conclusion"] == "CI执行成功"
|
|
116
|
+
assert all(t["status"] == "SUCCESS" for t in result["tasks"])
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TestGetPipelineStatus:
|
|
120
|
+
def test_running(self):
|
|
121
|
+
labels = [{"name": "ci-pipeline-running"}, {"name": "enhancement"}]
|
|
122
|
+
assert _get_pipeline_status(labels) == "running"
|
|
123
|
+
|
|
124
|
+
def test_passed(self):
|
|
125
|
+
labels = [{"name": "ci-pipeline-passed"}]
|
|
126
|
+
assert _get_pipeline_status(labels) == "passed"
|
|
127
|
+
|
|
128
|
+
def test_failed(self):
|
|
129
|
+
labels = [{"name": "ci-pipeline-failed"}, {"name": "cann-cla/yes"}]
|
|
130
|
+
assert _get_pipeline_status(labels) == "failed"
|
|
131
|
+
|
|
132
|
+
def test_unknown_when_no_pipeline_label(self):
|
|
133
|
+
labels = [{"name": "enhancement"}, {"name": "cann-cla/yes"}]
|
|
134
|
+
assert _get_pipeline_status(labels) == "unknown"
|
|
135
|
+
|
|
136
|
+
def test_empty_labels(self):
|
|
137
|
+
assert _get_pipeline_status([]) == "unknown"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TestTriggerPrPipeline:
|
|
141
|
+
async def test_posts_compile_comment(self, mock_client, mcp_server):
|
|
142
|
+
mock_client.post.return_value = {"id": 999, "body": "compile"}
|
|
143
|
+
|
|
144
|
+
tool_fn = mcp_server._tool_manager._tools["trigger_pr_pipeline"].fn
|
|
145
|
+
result = await tool_fn(owner="cann", repo="ge", pr_number=1601)
|
|
146
|
+
|
|
147
|
+
parsed = json.loads(result)
|
|
148
|
+
assert parsed["action"] == "pipeline_triggered"
|
|
149
|
+
assert parsed["comment_id"] == 999
|
|
150
|
+
assert parsed["pr"] == "cann/ge#1601"
|
|
151
|
+
|
|
152
|
+
mock_client.post.assert_called_once_with(
|
|
153
|
+
"/repos/cann/ge/pulls/1601/comments",
|
|
154
|
+
json={"body": "compile"},
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TestGetPrPipelineSummary:
|
|
159
|
+
def _setup_mock(self, mock_client, labels, comments):
|
|
160
|
+
"""Configure mock for get_pr_pipeline_summary: PR detail + paginated comments."""
|
|
161
|
+
mock_client.get.side_effect = [
|
|
162
|
+
# First call: PR detail
|
|
163
|
+
{"labels": labels},
|
|
164
|
+
# Second call: comments page 1 (return all, simulating < per_page)
|
|
165
|
+
comments,
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
async def test_returns_pipeline_summary_with_status(self, mock_client, mcp_server):
|
|
169
|
+
self._setup_mock(
|
|
170
|
+
mock_client,
|
|
171
|
+
labels=[{"name": "ci-pipeline-failed"}, {"name": "enhancement"}],
|
|
172
|
+
comments=[
|
|
173
|
+
{"user": {"login": "someone"}, "body": "regular comment"},
|
|
174
|
+
{"user": {"login": "cann-robot"}, "body": APPROVAL_COMMENT_BODY},
|
|
175
|
+
{"user": {"login": "cann-robot"}, "body": PIPELINE_COMMENT_BODY},
|
|
176
|
+
],
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
tool_fn = mcp_server._tool_manager._tools["get_pr_pipeline_summary"].fn
|
|
180
|
+
result = await tool_fn(owner="cann", repo="ge", pr_number=1601)
|
|
181
|
+
|
|
182
|
+
parsed = json.loads(result)
|
|
183
|
+
assert parsed["pipeline_found"] is True
|
|
184
|
+
assert parsed["pipeline_status"] == "failed"
|
|
185
|
+
assert parsed["pr"] == "cann/ge#1601"
|
|
186
|
+
assert parsed["conclusion"] == "CI执行失败"
|
|
187
|
+
assert len(parsed["tasks"]) == 3
|
|
188
|
+
|
|
189
|
+
async def test_running_status(self, mock_client, mcp_server):
|
|
190
|
+
self._setup_mock(
|
|
191
|
+
mock_client,
|
|
192
|
+
labels=[{"name": "ci-pipeline-running"}],
|
|
193
|
+
comments=[],
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
tool_fn = mcp_server._tool_manager._tools["get_pr_pipeline_summary"].fn
|
|
197
|
+
result = await tool_fn(owner="cann", repo="ge", pr_number=1601)
|
|
198
|
+
|
|
199
|
+
parsed = json.loads(result)
|
|
200
|
+
assert parsed["pipeline_status"] == "running"
|
|
201
|
+
assert parsed["pipeline_found"] is False
|
|
202
|
+
|
|
203
|
+
async def test_no_pipeline_comments(self, mock_client, mcp_server):
|
|
204
|
+
self._setup_mock(
|
|
205
|
+
mock_client,
|
|
206
|
+
labels=[],
|
|
207
|
+
comments=[{"user": {"login": "someone"}, "body": "just a comment"}],
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
tool_fn = mcp_server._tool_manager._tools["get_pr_pipeline_summary"].fn
|
|
211
|
+
result = await tool_fn(owner="cann", repo="ge", pr_number=100)
|
|
212
|
+
|
|
213
|
+
parsed = json.loads(result)
|
|
214
|
+
assert parsed["pipeline_found"] is False
|
|
215
|
+
assert parsed["pipeline_status"] == "unknown"
|
|
216
|
+
|
|
217
|
+
async def test_multiple_pipeline_comments_returns_latest(self, mock_client, mcp_server):
|
|
218
|
+
early_body = PIPELINE_COMMENT_BODY.replace(
|
|
219
|
+
"2026-03-28 16:45:30", "2026-03-28 10:00:00"
|
|
220
|
+
)
|
|
221
|
+
self._setup_mock(
|
|
222
|
+
mock_client,
|
|
223
|
+
labels=[{"name": "ci-pipeline-failed"}],
|
|
224
|
+
comments=[
|
|
225
|
+
{"user": {"login": "cann-robot"}, "body": early_body},
|
|
226
|
+
{"user": {"login": "cann-robot"}, "body": PIPELINE_COMMENT_BODY},
|
|
227
|
+
],
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
tool_fn = mcp_server._tool_manager._tools["get_pr_pipeline_summary"].fn
|
|
231
|
+
result = await tool_fn(owner="cann", repo="ge", pr_number=1601)
|
|
232
|
+
|
|
233
|
+
parsed = json.loads(result)
|
|
234
|
+
assert parsed["total_pipeline_comments"] == 2
|
|
235
|
+
assert parsed["timestamp"] == "2026-03-28 16:45:30"
|
|
236
|
+
|
|
237
|
+
async def test_pagination_fetches_all_pages(self, mock_client, mcp_server):
|
|
238
|
+
"""Verify that comments are fetched across multiple pages."""
|
|
239
|
+
mock_client.get.side_effect = [
|
|
240
|
+
# PR detail
|
|
241
|
+
{"labels": [{"name": "ci-pipeline-failed"}]},
|
|
242
|
+
# Page 1: 100 non-pipeline comments (simulating full page)
|
|
243
|
+
[{"user": {"login": "someone"}, "body": "comment"}] * 100,
|
|
244
|
+
# Page 2: pipeline comment (< 100 items, so pagination stops)
|
|
245
|
+
[{"user": {"login": "cann-robot"}, "body": PIPELINE_COMMENT_BODY}],
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
tool_fn = mcp_server._tool_manager._tools["get_pr_pipeline_summary"].fn
|
|
249
|
+
result = await tool_fn(owner="cann", repo="ge", pr_number=1601)
|
|
250
|
+
|
|
251
|
+
parsed = json.loads(result)
|
|
252
|
+
assert parsed["pipeline_found"] is True
|
|
253
|
+
assert parsed["conclusion"] == "CI执行失败"
|
|
254
|
+
# Verify 3 get calls: PR detail + 2 comment pages
|
|
255
|
+
assert mock_client.get.call_count == 3
|
|
256
|
+
|
|
257
|
+
async def test_empty_comments(self, mock_client, mcp_server):
|
|
258
|
+
self._setup_mock(mock_client, labels=[], comments=[])
|
|
259
|
+
|
|
260
|
+
tool_fn = mcp_server._tool_manager._tools["get_pr_pipeline_summary"].fn
|
|
261
|
+
result = await tool_fn(owner="cann", repo="ge", pr_number=1)
|
|
262
|
+
|
|
263
|
+
parsed = json.loads(result)
|
|
264
|
+
assert parsed["pipeline_found"] is False
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class TestListPullRequestComments:
|
|
268
|
+
"""Test list_pull_request_comments added to pull_request tools."""
|
|
269
|
+
|
|
270
|
+
async def test_list_comments(self):
|
|
271
|
+
from cann_gitcode_mcp.tools.pull_request import register_pull_request_tools
|
|
272
|
+
import cann_gitcode_mcp.tools.pull_request as pr_mod
|
|
273
|
+
pr_mod._client = None
|
|
274
|
+
|
|
275
|
+
server = FastMCP("test")
|
|
276
|
+
register_pull_request_tools(server)
|
|
277
|
+
|
|
278
|
+
client = AsyncMock()
|
|
279
|
+
client.get.return_value = [
|
|
280
|
+
{"id": 1, "body": "comment 1", "user": {"login": "alice"}},
|
|
281
|
+
{"id": 2, "body": "comment 2", "user": {"login": "bob"}},
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
with patch("cann_gitcode_mcp.tools.pull_request._get_client", return_value=client):
|
|
285
|
+
tool_fn = server._tool_manager._tools["list_pull_request_comments"].fn
|
|
286
|
+
result = await tool_fn(owner="org", repo="repo", number=42)
|
|
287
|
+
|
|
288
|
+
parsed = json.loads(result)
|
|
289
|
+
assert len(parsed) == 2
|
|
290
|
+
assert parsed[0]["body"] == "comment 1"
|
|
291
|
+
|
|
292
|
+
client.get.assert_called_once_with(
|
|
293
|
+
"/repos/org/repo/pulls/42/comments",
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
pr_mod._client = None
|
|
@@ -101,6 +101,35 @@ class TestListPullRequests:
|
|
|
101
101
|
state="closed", page=2, per_page=10,
|
|
102
102
|
)
|
|
103
103
|
|
|
104
|
+
async def test_list_prs_filter_by_head(self, mock_client, mcp_server):
|
|
105
|
+
mock_client.get.return_value = [
|
|
106
|
+
{"number": 5, "title": "feat: new", "state": "open"},
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
tool_fn = mcp_server._tool_manager._tools["list_pull_requests"].fn
|
|
110
|
+
result = await tool_fn(
|
|
111
|
+
owner="org", repo="repo", state="open", head="my-feature",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
mock_client.get.assert_called_once_with(
|
|
115
|
+
"/repos/org/repo/pulls",
|
|
116
|
+
state="open", head="my-feature",
|
|
117
|
+
)
|
|
118
|
+
parsed = json.loads(result)
|
|
119
|
+
assert len(parsed) == 1
|
|
120
|
+
assert parsed[0]["number"] == 5
|
|
121
|
+
|
|
122
|
+
async def test_list_prs_filter_by_base(self, mock_client, mcp_server):
|
|
123
|
+
mock_client.get.return_value = []
|
|
124
|
+
|
|
125
|
+
tool_fn = mcp_server._tool_manager._tools["list_pull_requests"].fn
|
|
126
|
+
await tool_fn(owner="org", repo="repo", base="master")
|
|
127
|
+
|
|
128
|
+
mock_client.get.assert_called_once_with(
|
|
129
|
+
"/repos/org/repo/pulls",
|
|
130
|
+
base="master",
|
|
131
|
+
)
|
|
132
|
+
|
|
104
133
|
|
|
105
134
|
class TestGetPullRequest:
|
|
106
135
|
async def test_get_pr(self, mock_client, mcp_server):
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
from cann_gitcode_mcp.server import mcp
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
def
|
|
5
|
-
"""Verify all
|
|
4
|
+
def test_all_tools_registered():
|
|
5
|
+
"""Verify all tools are registered on the MCP server."""
|
|
6
6
|
tools = mcp._tool_manager._tools
|
|
7
7
|
expected_tools = [
|
|
8
|
+
# PR tools
|
|
8
9
|
"create_pull_request",
|
|
9
10
|
"list_pull_requests",
|
|
10
11
|
"get_pull_request",
|
|
11
12
|
"merge_pull_request",
|
|
12
13
|
"comment_pull_request",
|
|
13
14
|
"get_pull_request_files",
|
|
15
|
+
"list_pull_request_comments",
|
|
16
|
+
# Pipeline tools
|
|
17
|
+
"get_pr_pipeline_summary",
|
|
18
|
+
"trigger_pr_pipeline",
|
|
14
19
|
]
|
|
15
20
|
for tool_name in expected_tools:
|
|
16
21
|
assert tool_name in tools, f"Tool '{tool_name}' not registered"
|
cann_gitcode_mcp-0.1.0/README.md
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
# cann-gitcode-mcp
|
|
2
|
-
|
|
3
|
-
[](https://pypi.org/project/cann-gitcode-mcp/)
|
|
4
|
-
[](https://pypi.org/project/cann-gitcode-mcp/)
|
|
5
|
-
[](https://github.com/shengnan666/cann-gitcode-mcp/blob/main/LICENSE)
|
|
6
|
-
|
|
7
|
-
CANN GitCode MCP 服务器 —— 为 CANN 开发者提供的 GitCode 平台 MCP 工具集。
|
|
8
|
-
|
|
9
|
-
## 简介
|
|
10
|
-
|
|
11
|
-
`cann-gitcode-mcp` 是一个基于 [MCP(Model Context Protocol)](https://modelcontextprotocol.io/) 的服务器,将 GitCode API 操作封装为 MCP 工具,供 Claude Code 及其他 MCP 客户端直接调用。
|
|
12
|
-
|
|
13
|
-
CANN 社区的代码托管在 GitCode 平台,本项目为 CANN 开发者提供在 AI 助手中操作 GitCode 仓库(Pull Request、流水线、Issue 等)的能力,无需离开对话界面即可完成常见研发工作流。
|
|
14
|
-
|
|
15
|
-
## 安装
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
pip install cann-gitcode-mcp
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## 配置
|
|
22
|
-
|
|
23
|
-
### 1. 获取 GitCode API Token
|
|
24
|
-
|
|
25
|
-
在 [GitCode 用户设置](https://gitcode.com/profile/tokens) 中生成个人访问令牌。
|
|
26
|
-
|
|
27
|
-
### 2. 设置环境变量
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
export GITCODE_API_TOKEN=your_token_here
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
### 3. 与 Claude Code 集成
|
|
34
|
-
|
|
35
|
-
在 Claude Code 的 MCP 配置文件中添加:
|
|
36
|
-
|
|
37
|
-
```json
|
|
38
|
-
{
|
|
39
|
-
"mcpServers": {
|
|
40
|
-
"gitcode": {
|
|
41
|
-
"command": "cann-gitcode-mcp",
|
|
42
|
-
"env": {
|
|
43
|
-
"GITCODE_API_TOKEN": "your_token_here"
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
或使用模块方式:
|
|
51
|
-
|
|
52
|
-
```json
|
|
53
|
-
{
|
|
54
|
-
"mcpServers": {
|
|
55
|
-
"gitcode": {
|
|
56
|
-
"command": "python",
|
|
57
|
-
"args": ["-m", "cann_gitcode_mcp"],
|
|
58
|
-
"env": {
|
|
59
|
-
"GITCODE_API_TOKEN": "your_token_here"
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
配置完成后重启 Claude Code,即可在对话中使用所有 GitCode 工具。
|
|
67
|
-
|
|
68
|
-
## 可用工具
|
|
69
|
-
|
|
70
|
-
### Pull Request
|
|
71
|
-
|
|
72
|
-
| 工具名 | 说明 |
|
|
73
|
-
|--------|------|
|
|
74
|
-
| `create_pull_request` | 创建 Pull Request |
|
|
75
|
-
| `list_pull_requests` | 列出仓库的 Pull Request(支持状态、排序等过滤) |
|
|
76
|
-
| `get_pull_request` | 获取指定 PR 的详细信息 |
|
|
77
|
-
| `merge_pull_request` | 合并 Pull Request |
|
|
78
|
-
| `comment_pull_request` | 在 PR 上发表评论 |
|
|
79
|
-
| `get_pull_request_files` | 获取 PR 的变更文件列表 |
|
|
80
|
-
|
|
81
|
-
## 环境变量
|
|
82
|
-
|
|
83
|
-
| 变量 | 必填 | 说明 |
|
|
84
|
-
|------|------|------|
|
|
85
|
-
| `GITCODE_API_TOKEN` | 是 | GitCode 个人访问令牌 |
|
|
86
|
-
| `GITCODE_API_BASE_URL` | 否 | API 地址覆盖(默认 `https://gitcode.com/api/v5`) |
|
|
87
|
-
|
|
88
|
-
## 路线图
|
|
89
|
-
|
|
90
|
-
- **Issue 工具**:Issue 增删改查、评论管理
|
|
91
|
-
- **仓库工具**:分支管理、文件读取、提交历史
|
|
92
|
-
- **流水线/CI 工具**:触发 CANN CI 流水线、查询流水线状态
|
|
93
|
-
|
|
94
|
-
## 开发
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
# 安装开发依赖
|
|
98
|
-
pip install -e ".[dev]"
|
|
99
|
-
|
|
100
|
-
# 运行测试
|
|
101
|
-
pytest
|
|
102
|
-
|
|
103
|
-
# 构建包
|
|
104
|
-
python -m build
|
|
105
|
-
|
|
106
|
-
# 上传到 PyPI
|
|
107
|
-
twine upload dist/*
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## 版本规则
|
|
111
|
-
|
|
112
|
-
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/):
|
|
113
|
-
|
|
114
|
-
- `0.x.y` — 早期开发阶段,接口可能不兼容变更
|
|
115
|
-
- `1.0.0` — 工具集稳定后的首个正式版本
|
|
116
|
-
- MAJOR.MINOR.PATCH — 不兼容变更.新功能.修复
|
|
117
|
-
|
|
118
|
-
## 许可证
|
|
119
|
-
|
|
120
|
-
[Apache License 2.0](LICENSE)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|