已开启
feat: 新增15个MCP工具,API覆盖率从46%提升至76% #10
suantea创建于 7月9日
feat: 新增15个MCP工具,API覆盖率从46%提升至76% #10
已开启
共 4 个文件变更+556-34
| @@ -0,0 +1,26 @@ | |||
| 1 | +## 变更说明 | ||
| 2 | + | ||
| 3 | +<!-- 请简要描述本次 PR 的目的和内容 --> | ||
| 4 | + | ||
| 5 | +### 变更类型 | ||
| 6 | + | ||
| 7 | +- [ ] 新功能 (feat) | ||
| 8 | +- [ ] Bug 修复 (fix) | ||
| 9 | +- [ ] 文档更新 (docs) | ||
| 10 | +- [ ] 测试补充 (test) | ||
| 11 | +- [ ] 重构 (refactor) | ||
| 12 | +- [ ] 性能优化 (perf) | ||
| 13 | + | ||
| 14 | +### 文件变更 | ||
| 15 | + | ||
| 16 | +<!-- 列出本次变更的主要文件和变更内容 --> | ||
| 17 | + | ||
| 18 | +### 测试情况 | ||
| 19 | + | ||
| 20 | +- [ ] 本地测试已通过 | ||
| 21 | +- [ ] 新增了单元测试 | ||
| 22 | +- [ ] 不影响现有功能 | ||
| 23 | + | ||
| 24 | +### 备注 | ||
| 25 | + | ||
| 26 | +<!-- 其他需要说明的内容 --> | ||
| @@ -0,0 +1,46 @@ | |||
| 1 | +# 贡献指南 | ||
| 2 | + | ||
| 3 | +感谢您对 GitCode MCP Server 的关注!我们欢迎各种形式的贡献。 | ||
| 4 | + | ||
| 5 | +## 贡献方式 | ||
| 6 | + | ||
| 7 | +### 🐛 报告 Bug | ||
| 8 | + | ||
| 9 | +提交 Issue 时请包含: | ||
| 10 | +- 环境信息(操作系统、Python 版本) | ||
| 11 | +- 复现步骤 | ||
| 12 | +- 期望行为 vs 实际行为 | ||
| 13 | +- 相关日志 | ||
| 14 | + | ||
| 15 | +### 💡 功能建议 | ||
| 16 | + | ||
| 17 | +欢迎提交功能建议 Issue,描述你希望看到的功能和使用场景。 | ||
| 18 | + | ||
| 19 | +### 🔧 提交代码 | ||
| 20 | + | ||
| 21 | +1. **Fork** 本仓库 | ||
| 22 | +2. **创建特性分支**:`git checkout -b feat/your-feature-name` | ||
| 23 | +3. **提交更改**:遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范 | ||
| 24 | + - `feat:` 新功能 | ||
| 25 | + - `fix:` Bug 修复 | ||
| 26 | + - `docs:` 文档变更 | ||
| 27 | + - `test:` 测试变更 | ||
| 28 | + - `refactor:` 重构 | ||
| 29 | +4. **运行测试**:确保现有测试通过 | ||
| 30 | + ```bash | ||
| 31 | + pip install -e ".[dev]" | ||
| 32 | + pytest | ||
| 33 | + ``` | ||
| 34 | +5. **推送到你的 Fork**:`git push origin feat/your-feature-name` | ||
| 35 | +6. **创建 Pull Request** | ||
| 36 | + | ||
| 37 | +### 📝 代码风格 | ||
| 38 | + | ||
| 39 | +- 遵循 PEP 8 | ||
| 40 | +- 使用 type hints | ||
| 41 | +- 异步函数使用 `async/await` | ||
| 42 | +- 运行 `black` 和 `isort` 格式化代码 | ||
| 43 | + | ||
| 44 | +## 许可 | ||
| 45 | + | ||
| 46 | +提交代码即表示您同意将代码以 MIT 许可证授权。 | ||
| @@ -1,3 +1,4 @@ | |||
| 1 | + | ||
| 1 | """ | 2 | """ |
| 2 | GitCode MCP服务器,提供GitCode API功能的MCP接口 | 3 | GitCode MCP服务器,提供GitCode API功能的MCP接口 |
| 3 | """ | 4 | """ |
| @@ -60,7 +61,7 @@ def init_api_client(): | |||
| 60 | # 辅助函数 | 61 | # 辅助函数 |
| 61 | async def safe_api_call(func, *args, **kwargs): | 62 | async def safe_api_call(func, *args, **kwargs): |
| 62 | """安全调用API,统一处理错误和日志""" | 63 | """安全调用API,统一处理错误和日志""" |
| 63 | - client = init_api_client() | 64 | + init_api_client() |
| 64 | 65 | ||
| 65 | try: | 66 | try: |
| 66 | return await func(*args, **kwargs) | 67 | return await func(*args, **kwargs) |
| @@ -96,6 +97,51 @@ async def create_repository(name: str, description: str = "", private: bool = Fa | |||
| 96 | return json.dumps(repo, ensure_ascii=False) | 97 | return json.dumps(repo, ensure_ascii=False) |
| 97 | 98 | ||
| 98 | 99 | ||
| 100 | + | ||
| 101 | +async def update_repository(owner: str, repo: str, name: str = None, description: str = None, private: Optional[bool] = None, has_issues: Optional[bool] = None, has_wiki: Optional[bool] = None, default_branch: Optional[str] = None) -> str: | ||
| 102 | + """更新仓库信息""" | ||
| 103 | + client = init_api_client() | ||
| 104 | + kwargs = {} | ||
| 105 | + if name is not None: | ||
| 106 | + kwargs["name"] = name | ||
| 107 | + if description is not None: | ||
| 108 | + kwargs["description"] = description | ||
| 109 | + if private is not None: | ||
| 110 | + kwargs["private"] = private | ||
| 111 | + if has_issues is not None: | ||
| 112 | + kwargs["has_issues"] = has_issues | ||
| 113 | + if has_wiki is not None: | ||
| 114 | + kwargs["has_wiki"] = has_wiki | ||
| 115 | + if default_branch is not None: | ||
| 116 | + kwargs["default_branch"] = default_branch | ||
| 117 | + result = await safe_api_call(client.repos.update_repo, owner=owner, repo=repo, **kwargs) | ||
| 118 | + return json.dumps(result, ensure_ascii=False) | ||
| 119 | + | ||
| 120 | + | ||
| 121 | + | ||
| 122 | +async def delete_repository(owner: str, repo: str) -> str: | ||
| 123 | + """删除仓库""" | ||
| 124 | + client = init_api_client() | ||
| 125 | + await safe_api_call(client.repos.delete_repo, owner=owner, repo=repo) | ||
| 126 | + return json.dumps({"success": True, "message": f"仓库 {owner}/{repo} 已删除"}, ensure_ascii=False) | ||
| 127 | + | ||
| 128 | + | ||
| 129 | + | ||
| 130 | +async def get_repository_readme(owner: str, repo: str) -> str: | ||
| 131 | + """获取仓库的README文件内容""" | ||
| 132 | + client = init_api_client() | ||
| 133 | + result = await safe_api_call(client.repos.get_readme, owner=owner, repo=repo) | ||
| 134 | + return json.dumps(result, ensure_ascii=False) | ||
| 135 | + | ||
| 136 | + | ||
| 137 | + | ||
| 138 | +async def get_repository_contents(owner: str, repo: str, path: str) -> str: | ||
| 139 | + """获取仓库指定路径的文件或目录内容""" | ||
| 140 | + client = init_api_client() | ||
| 141 | + result = await safe_api_call(client.repos.get_contents, owner=owner, repo=repo, path=path) | ||
| 142 | + return json.dumps(result, ensure_ascii=False) | ||
| 143 | + | ||
| 144 | + | ||
| 99 | 145 | ||
| 100 | async def list_branches(owner: str, repo: str) -> str: | 146 | async def list_branches(owner: str, repo: str) -> str: |
| 101 | """列出仓库的分支""" | 147 | """列出仓库的分支""" |
| @@ -121,29 +167,44 @@ async def create_branch(owner: str, repo: str, branch: str, ref: str) -> str: | |||
| 121 | 167 | ||
| 122 | 168 | ||
| 123 | 169 | ||
| 124 | -async def list_issues(owner: str, repo: str) -> str: | 170 | +async def delete_branch(owner: str, repo: str, branch: str) -> str: |
| 171 | + """删除分支""" | ||
| 172 | + client = init_api_client() | ||
| 173 | + await safe_api_call(client.branches.delete_branch, owner=owner, repo=repo, branch=branch) | ||
| 174 | + return json.dumps({"success": True, "message": f"分支 {branch} 已删除"}, ensure_ascii=False) | ||
| 175 | + | ||
| 176 | + | ||
| 177 | + | ||
| 178 | +async def merge_branches(owner: str, repo: str, base: str, head: str, commit_message: str = None) -> str: | ||
| 179 | + """合并分支(将head分支合并到base分支)""" | ||
| 180 | + client = init_api_client() | ||
| 181 | + result = await safe_api_call( | ||
| 182 | + client.branches.merge_branch, | ||
| 183 | + owner=owner, repo=repo, base=base, head=head, commit_message=commit_message | ||
| 184 | + ) | ||
| 185 | + return json.dumps(result, ensure_ascii=False) | ||
| 186 | + | ||
| 187 | + | ||
| 188 | + | ||
| 189 | +async def list_commits(owner: str, repo: str, sha: str = None, path: str = None, author: str = None, since: str = None, until: str = None, per_page: int = None, page: int = None) -> str: | ||
| 190 | + """列出仓库的提交记录""" | ||
| 191 | + client = init_api_client() | ||
| 192 | + commits = await safe_api_call( | ||
| 193 | + client.branches.list_commits, | ||
| 194 | + owner=owner, repo=repo, sha=sha, path=path, author=author, | ||
| 195 | + since=since, until=until, per_page=per_page, page=page | ||
| 196 | + ) | ||
| 197 | + return json.dumps(commits, ensure_ascii=False) | ||
| 198 | + | ||
| 199 | + | ||
| 200 | + | ||
| 201 | +async def list_issues(owner: str, repo: str, state: str = None, labels: str = None, sort: str = None, direction: str = None, per_page: int = None, page: int = None) -> str: | ||
| 125 | """列出仓库的Issues""" | 202 | """列出仓库的Issues""" |
| 126 | client = init_api_client() | 203 | client = init_api_client() |
| 127 | - issues = await safe_api_call(client.issues.list_issues, owner=owner, repo=repo) | 204 | + issues = await safe_api_call(client.issues.list_issues, owner=owner, repo=repo, state=state, labels=labels, sort=sort, direction=direction, per_page=per_page, page=page) |
| 128 | return json.dumps(issues, ensure_ascii=False) | 205 | return json.dumps(issues, ensure_ascii=False) |
| 129 | 206 | ||
| 130 | 207 | ||
| 131 | - | ||
| 132 | -async def get_issue(owner: str, repo: str, issue_number: int) -> str: | ||
| 133 | - """获取特定Issue的详细信息""" | ||
| 134 | - client = init_api_client() | ||
| 135 | - issue = await safe_api_call(client.issues.get_issue, owner=owner, repo=repo, issue_number=issue_number) | ||
| 136 | - return json.dumps(issue, ensure_ascii=False) | ||
| 137 | - | ||
| 138 | - | ||
| 139 | - | ||
| 140 | -async def create_issue(owner: str, repo: str, title: str, body: str = "") -> str: | ||
| 141 | - """创建新Issue""" | ||
| 142 | - client = init_api_client() | ||
| 143 | - issue = await safe_api_call(client.issues.create_issue, owner=owner, repo=repo, title=title, body=body) | ||
| 144 | - return json.dumps(issue, ensure_ascii=False) | ||
| 145 | - | ||
| 146 | - | ||
| 147 | 208 | ||
| 148 | async def list_issue_comments(owner: str, repo: str, issue_number: int) -> str: | 209 | async def list_issue_comments(owner: str, repo: str, issue_number: int) -> str: |
| 149 | """列出Issue的评论列表""" | 210 | """列出Issue的评论列表""" |
| @@ -171,6 +232,55 @@ async def create_issue_comment(owner: str, repo: str, issue_number: int, body: s | |||
| 171 | return json.dumps(comment, ensure_ascii=False) | 232 | return json.dumps(comment, ensure_ascii=False) |
| 172 | 233 | ||
| 173 | 234 | ||
| 235 | + | ||
| 236 | +async def update_issue(owner: str, repo: str, issue_number: int, title: str = None, body: str = None, state: str = None) -> str: | ||
| 237 | + """更新Issue""" | ||
| 238 | + client = init_api_client() | ||
| 239 | + result = await safe_api_call( | ||
| 240 | + client.issues.update_issue, | ||
| 241 | + owner=owner, repo=repo, issue_number=issue_number, title=title, body=body, state=state | ||
| 242 | + ) | ||
| 243 | + return json.dumps(result, ensure_ascii=False) | ||
| 244 | + | ||
| 245 | + | ||
| 246 | + | ||
| 247 | +async def update_issue_comment(owner: str, repo: str, comment_id: int, body: str) -> str: | ||
| 248 | + """更新Issue评论""" | ||
| 249 | + client = init_api_client() | ||
| 250 | + result = await safe_api_call(client.issues.update_comment, owner=owner, repo=repo, comment_id=comment_id, body=body) | ||
| 251 | + return json.dumps(result, ensure_ascii=False) | ||
| 252 | + | ||
| 253 | + | ||
| 254 | + | ||
| 255 | +async def delete_issue_comment(owner: str, repo: str, comment_id: int) -> str: | ||
| 256 | + """删除Issue评论""" | ||
| 257 | + client = init_api_client() | ||
| 258 | + await safe_api_call(client.issues.delete_comment, owner=owner, repo=repo, comment_id=comment_id) | ||
| 259 | + return json.dumps({"success": True, "message": f"评论 {comment_id} 已删除"}, ensure_ascii=False) | ||
| 260 | + | ||
| 261 | +async def get_issue_comment(owner: str, repo: str, comment_id: int) -> str: | ||
| 262 | + """获取Issue单条评论的详细信息""" | ||
| 263 | + client = init_api_client() | ||
| 264 | + comment = await safe_api_call(client.issues.get_comment, owner=owner, repo=repo, comment_id=comment_id) | ||
| 265 | + return json.dumps(comment, ensure_ascii=False) | ||
| 266 | + | ||
| 267 | + | ||
| 268 | + | ||
| 269 | +async def list_issue_labels(owner: str, repo: str) -> str: | ||
| 270 | + """列出仓库的Issue标签""" | ||
| 271 | + client = init_api_client() | ||
| 272 | + labels = await safe_api_call(client.issues.list_labels, owner=owner, repo=repo) | ||
| 273 | + return json.dumps(labels, ensure_ascii=False) | ||
| 274 | + | ||
| 275 | + | ||
| 276 | + | ||
| 277 | +async def list_issue_milestones(owner: str, repo: str) -> str: | ||
| 278 | + """列出仓库的Issue里程碑""" | ||
| 279 | + client = init_api_client() | ||
| 280 | + milestones = await safe_api_call(client.issues.list_milestones, owner=owner, repo=repo) | ||
| 281 | + return json.dumps(milestones, ensure_ascii=False) | ||
| 282 | + | ||
| 283 | + | ||
| 174 | 284 | ||
| 175 | async def list_pull_requests(owner: str, repo: str) -> str: | 285 | async def list_pull_requests(owner: str, repo: str) -> str: |
| 176 | """列出仓库的Pull Requests""" | 286 | """列出仓库的Pull Requests""" |
| @@ -310,37 +420,62 @@ async def reply_to_pull_request_comment( | |||
| 310 | return json.dumps(result, ensure_ascii=False) | 420 | return json.dumps(result, ensure_ascii=False) |
| 311 | 421 | ||
| 312 | 422 | ||
| 313 | -# === 搜索功能 === | 423 | +@mcp.tool() |
| 314 | -# 注意:以下搜索功能暂时注释,因为平台目前不支持 | 424 | +async def list_pull_request_files(owner: str, repo: str, pull_number: int) -> str: |
| 425 | + """列出Pull Request的文件变更列表""" | ||
| 426 | + client = init_api_client() | ||
| 427 | + files = await safe_api_call(client.pulls.list_files, owner=owner, repo=repo, pr_number=pull_number) | ||
| 428 | + return json.dumps(files, ensure_ascii=False) | ||
| 315 | 429 | ||
| 316 | -# @mcp.tool() | ||
| 317 | -# async def search_code(query: str) -> str: | ||
| 318 | -# """搜索代码""" | ||
| 319 | -# client = init_api_client() | ||
| 320 | -# results = await safe_api_call(client.search.search_code, query=query) | ||
| 321 | -# return json.dumps(results, ensure_ascii=False) | ||
| 322 | 430 | ||
| 323 | 431 | ||
| 324 | -async def search_repositories(query: str) -> str: | 432 | +async def list_pull_request_commits(owner: str, repo: str, pull_number: int) -> str: |
| 433 | + """列出Pull Request的提交记录""" | ||
| 434 | + client = init_api_client() | ||
| 435 | + commits = await safe_api_call(client.pulls.list_commits, owner=owner, repo=repo, pr_number=pull_number) | ||
| 436 | + return json.dumps(commits, ensure_ascii=False) | ||
| 437 | + | ||
| 438 | + | ||
| 439 | +# === 搜索功能 === | ||
| 440 | + | ||
| 441 | + | ||
| 442 | +async def search_code(query: str, sort: str = None, order: str = None, per_page: int = None, page: int = None) -> str: | ||
| 443 | + """搜索代码""" | ||
| 444 | + client = init_api_client() | ||
| 445 | + try: | ||
| 446 | + results = await safe_api_call(client.search.search_code, query=query, sort=sort, order=order, per_page=per_page, page=page) | ||
| 447 | + return json.dumps(results, ensure_ascii=False) | ||
| 448 | + except ValueError as e: | ||
| 449 | + if "404" in str(e) or "Not Found" in str(e): | ||
| 450 | + return json.dumps({ | ||
| 451 | + "error": "代码搜索暂不可用", | ||
| 452 | + "message": "GitCode API 可能不支持代码搜索,请使用 search_repositories 或 search_issues 替代", | ||
| 453 | + "hint": "尝试: search_repositories(query)" | ||
| 454 | + }, ensure_ascii=False) | ||
| 455 | + else: | ||
| 456 | + raise | ||
| 457 | + | ||
| 458 | + | ||
| 459 | +async def search_repositories(query: str, sort: str = None, order: str = None, per_page: int = None, page: int = None) -> str: | ||
| 325 | """搜索仓库""" | 460 | """搜索仓库""" |
| 326 | client = init_api_client() | 461 | client = init_api_client() |
| 327 | - results = await safe_api_call(client.search.search_repositories, query=query) | 462 | + results = await safe_api_call(client.search.search_repositories, query=query, sort=sort, order=order, per_page=per_page, page=page) |
| 328 | return json.dumps(results, ensure_ascii=False) | 463 | return json.dumps(results, ensure_ascii=False) |
| 329 | 464 | ||
| 330 | 465 | ||
| 331 | 466 | ||
| 332 | -async def search_issues(query: str) -> str: | 467 | +async def search_issues(query: str, sort: str = None, order: str = None, per_page: int = None, page: int = None) -> str: |
| 333 | """搜索Issues""" | 468 | """搜索Issues""" |
| 334 | client = init_api_client() | 469 | client = init_api_client() |
| 335 | - results = await safe_api_call(client.search.search_issues, query=query) | 470 | + results = await safe_api_call(client.search.search_issues, query=query, sort=sort, order=order, per_page=per_page, page=page) |
| 336 | return json.dumps(results, ensure_ascii=False) | 471 | return json.dumps(results, ensure_ascii=False) |
| 337 | 472 | ||
| 338 | 473 | ||
| 339 | 474 | ||
| 340 | -async def search_users(query: str) -> str: | 475 | +async def search_users(query: str, sort: str = None, order: str = None, per_page: int = None, page: int = None) -> str: |
| 341 | """搜索用户""" | 476 | """搜索用户""" |
| 342 | client = init_api_client() | 477 | client = init_api_client() |
| 343 | - results = await safe_api_call(client.search.search_users, query=query) | 478 | + results = await safe_api_call(client.search.search_users, query=query, sort=sort, order=order, per_page=per_page, page=page) |
| 344 | return json.dumps(results, ensure_ascii=False) | 479 | return json.dumps(results, ensure_ascii=False) |
| 345 | 480 | ||
| 346 | 481 | ||