mirror of
https://gitea.com/gitea/gitea-mcp.git
synced 2026-01-17 05:02:43 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dbfc62042 | ||
|
|
e851f542f5 | ||
|
|
b8f2377f47 | ||
|
|
017ca94a86 | ||
|
|
17119bcab6 | ||
|
|
8b06d7154e | ||
|
|
bdd9fb1816 |
63
BUILDING.md
Normal file
63
BUILDING.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Building gitea-mcp on Windows
|
||||||
|
|
||||||
|
This project includes PowerShell and batch scripts to build the gitea-mcp application on Windows systems.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Go 1.24 or later
|
||||||
|
- Git (for version information)
|
||||||
|
- PowerShell 5.1 or later (included with Windows 10/11)
|
||||||
|
|
||||||
|
## Build Scripts
|
||||||
|
|
||||||
|
### PowerShell Script (`build.ps1`)
|
||||||
|
|
||||||
|
The main build script that replicates all Makefile functionality:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Show help
|
||||||
|
.\build.ps1 help
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
.\build.ps1 build
|
||||||
|
|
||||||
|
# Install the application
|
||||||
|
.\build.ps1 install
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
.\build.ps1 clean
|
||||||
|
|
||||||
|
# Run in development mode (hot reload)
|
||||||
|
.\build.ps1 dev
|
||||||
|
|
||||||
|
# Update vendor dependencies
|
||||||
|
.\build.ps1 vendor
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch File Wrapper (`build.bat`)
|
||||||
|
|
||||||
|
A simple wrapper to run the PowerShell script:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
# Run with default help target
|
||||||
|
build.bat
|
||||||
|
|
||||||
|
# Run specific target
|
||||||
|
build.bat build
|
||||||
|
build.bat install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Targets
|
||||||
|
|
||||||
|
- **help** - Print help message
|
||||||
|
- **build** - Build the application executable
|
||||||
|
- **install** - Build and install to GOPATH/bin
|
||||||
|
- **uninstall** - Remove executable from GOPATH/bin
|
||||||
|
- **clean** - Remove build artifacts
|
||||||
|
- **air** - Install air for hot reload development
|
||||||
|
- **dev** - Run with hot reload development
|
||||||
|
- **vendor** - Tidy and verify Go module dependencies
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
The build process creates `gitea-mcp.exe` in the project directory.
|
||||||
135
README.md
135
README.md
@@ -164,56 +164,91 @@ list all my repositories
|
|||||||
|
|
||||||
The Gitea MCP Server supports the following tools:
|
The Gitea MCP Server supports the following tools:
|
||||||
|
|
||||||
| Tool | Scope | Description |
|
| Tool | Scope | Description |
|
||||||
| :--------------------------: | :----------: | :------------------------------------------------------: |
|
| :-------------------------------: | :----------: | :------------------------------------------------------: |
|
||||||
| get_my_user_info | User | Get the information of the authenticated user |
|
| get_my_user_info | User | Get the information of the authenticated user |
|
||||||
| get_user_orgs | User | Get organizations associated with the authenticated user |
|
| get_user_orgs | User | Get organizations associated with the authenticated user |
|
||||||
| create_repo | Repository | Create a new repository |
|
| create_repo | Repository | Create a new repository |
|
||||||
| fork_repo | Repository | Fork a repository |
|
| fork_repo | Repository | Fork a repository |
|
||||||
| list_my_repos | Repository | List all repositories owned by the authenticated user |
|
| list_my_repos | Repository | List all repositories owned by the authenticated user |
|
||||||
| create_branch | Branch | Create a new branch |
|
| create_branch | Branch | Create a new branch |
|
||||||
| delete_branch | Branch | Delete a branch |
|
| delete_branch | Branch | Delete a branch |
|
||||||
| list_branches | Branch | List all branches in a repository |
|
| list_branches | Branch | List all branches in a repository |
|
||||||
| create_release | Release | Create a new release in a repository |
|
| create_release | Release | Create a new release in a repository |
|
||||||
| delete_release | Release | Delete a release from a repository |
|
| delete_release | Release | Delete a release from a repository |
|
||||||
| get_release | Release | Get a release |
|
| get_release | Release | Get a release |
|
||||||
| get_latest_release | Release | Get the latest release in a repository |
|
| get_latest_release | Release | Get the latest release in a repository |
|
||||||
| list_releases | Release | List all releases in a repository |
|
| list_releases | Release | List all releases in a repository |
|
||||||
| create_tag | Tag | Create a new tag |
|
| create_tag | Tag | Create a new tag |
|
||||||
| delete_tag | Tag | Delete a tag |
|
| delete_tag | Tag | Delete a tag |
|
||||||
| get_tag | Tag | Get a tag |
|
| get_tag | Tag | Get a tag |
|
||||||
| list_tags | Tag | List all tags in a repository |
|
| list_tags | Tag | List all tags in a repository |
|
||||||
| list_repo_commits | Commit | List all commits in a repository |
|
| list_repo_commits | Commit | List all commits in a repository |
|
||||||
| get_file_content | File | Get the content and metadata of a file |
|
| get_file_content | File | Get the content and metadata of a file |
|
||||||
| get_dir_content | File | Get a list of entries in a directory |
|
| get_dir_content | File | Get a list of entries in a directory |
|
||||||
| create_file | File | Create a new file |
|
| create_file | File | Create a new file |
|
||||||
| update_file | File | Update an existing file |
|
| update_file | File | Update an existing file |
|
||||||
| delete_file | File | Delete a file |
|
| delete_file | File | Delete a file |
|
||||||
| get_issue_by_index | Issue | Get an issue by its index |
|
| get_issue_by_index | Issue | Get an issue by its index |
|
||||||
| list_repo_issues | Issue | List all issues in a repository |
|
| list_repo_issues | Issue | List all issues in a repository |
|
||||||
| create_issue | Issue | Create a new issue |
|
| create_issue | Issue | Create a new issue |
|
||||||
| create_issue_comment | Issue | Create a comment on an issue |
|
| create_issue_comment | Issue | Create a comment on an issue |
|
||||||
| edit_issue | Issue | Edit a issue |
|
| edit_issue | Issue | Edit a issue |
|
||||||
| edit_issue_comment | Issue | Edit a comment on an issue |
|
| edit_issue_comment | Issue | Edit a comment on an issue |
|
||||||
| get_issue_comments_by_index | Issue | Get comments of an issue by its index |
|
| get_issue_comments_by_index | Issue | Get comments of an issue by its index |
|
||||||
| get_pull_request_by_index | Pull Request | Get a pull request by its index |
|
| get_pull_request_by_index | Pull Request | Get a pull request by its index |
|
||||||
| list_repo_pull_requests | Pull Request | List all pull requests in a repository |
|
| list_repo_pull_requests | Pull Request | List all pull requests in a repository |
|
||||||
| create_pull_request | Pull Request | Create a new pull request |
|
| create_pull_request | Pull Request | Create a new pull request |
|
||||||
| create_pull_request_reviewer | Pull Request | Add reviewers to a pull request |
|
| create_pull_request_reviewer | Pull Request | Add reviewers to a pull request |
|
||||||
| search_users | User | Search for users |
|
| delete_pull_request_reviewer | Pull Request | Remove reviewers from a pull request |
|
||||||
| search_org_teams | Organization | Search for teams in an organization |
|
| list_pull_request_reviews | Pull Request | List all reviews for a pull request |
|
||||||
| list_org_labels | Organization | List labels defined at organization level |
|
| get_pull_request_review | Pull Request | Get a specific review by ID |
|
||||||
| create_org_label | Organization | Create a label in an organization |
|
| list_pull_request_review_comments | Pull Request | List inline comments for a review |
|
||||||
| edit_org_label | Organization | Edit a label in an organization |
|
| create_pull_request_review | Pull Request | Create a review with optional inline comments |
|
||||||
| delete_org_label | Organization | Delete a label in an organization |
|
| submit_pull_request_review | Pull Request | Submit a pending review |
|
||||||
| search_repos | Repository | Search for repositories |
|
| delete_pull_request_review | Pull Request | Delete a review |
|
||||||
| get_gitea_mcp_server_version | Server | Get the version of the Gitea MCP Server |
|
| dismiss_pull_request_review | Pull Request | Dismiss a review with optional message |
|
||||||
| list_wiki_pages | Wiki | List all wiki pages in a repository |
|
| search_users | User | Search for users |
|
||||||
| get_wiki_page | Wiki | Get a wiki page content and metadata |
|
| search_org_teams | Organization | Search for teams in an organization |
|
||||||
| get_wiki_revisions | Wiki | Get revisions history of a wiki page |
|
| list_org_labels | Organization | List labels defined at organization level |
|
||||||
| create_wiki_page | Wiki | Create a new wiki page |
|
| create_org_label | Organization | Create a label in an organization |
|
||||||
| update_wiki_page | Wiki | Update an existing wiki page |
|
| edit_org_label | Organization | Edit a label in an organization |
|
||||||
| delete_wiki_page | Wiki | Delete a wiki page |
|
| delete_org_label | Organization | Delete a label in an organization |
|
||||||
|
| search_repos | Repository | Search for repositories |
|
||||||
|
| list_repo_action_secrets | Actions | List repository Actions secrets (metadata only) |
|
||||||
|
| upsert_repo_action_secret | Actions | Create/update (upsert) a repository Actions secret |
|
||||||
|
| delete_repo_action_secret | Actions | Delete a repository Actions secret |
|
||||||
|
| list_org_action_secrets | Actions | List organization Actions secrets (metadata only) |
|
||||||
|
| upsert_org_action_secret | Actions | Create/update (upsert) an organization Actions secret |
|
||||||
|
| delete_org_action_secret | Actions | Delete an organization Actions secret |
|
||||||
|
| list_repo_action_variables | Actions | List repository Actions variables |
|
||||||
|
| get_repo_action_variable | Actions | Get a repository Actions variable |
|
||||||
|
| create_repo_action_variable | Actions | Create a repository Actions variable |
|
||||||
|
| update_repo_action_variable | Actions | Update a repository Actions variable |
|
||||||
|
| delete_repo_action_variable | Actions | Delete a repository Actions variable |
|
||||||
|
| list_org_action_variables | Actions | List organization Actions variables |
|
||||||
|
| get_org_action_variable | Actions | Get an organization Actions variable |
|
||||||
|
| create_org_action_variable | Actions | Create an organization Actions variable |
|
||||||
|
| update_org_action_variable | Actions | Update an organization Actions variable |
|
||||||
|
| delete_org_action_variable | Actions | Delete an organization Actions variable |
|
||||||
|
| list_repo_action_workflows | Actions | List repository Actions workflows |
|
||||||
|
| get_repo_action_workflow | Actions | Get a repository Actions workflow |
|
||||||
|
| dispatch_repo_action_workflow | Actions | Trigger (dispatch) a repository Actions workflow |
|
||||||
|
| list_repo_action_runs | Actions | List repository Actions runs |
|
||||||
|
| get_repo_action_run | Actions | Get a repository Actions run |
|
||||||
|
| cancel_repo_action_run | Actions | Cancel a repository Actions run |
|
||||||
|
| rerun_repo_action_run | Actions | Rerun a repository Actions run |
|
||||||
|
| list_repo_action_jobs | Actions | List repository Actions jobs |
|
||||||
|
| list_repo_action_run_jobs | Actions | List Actions jobs for a run |
|
||||||
|
| get_repo_action_job_log_preview | Actions | Get a job log preview (tail/limited) |
|
||||||
|
| download_repo_action_job_log | Actions | Download a job log to a file |
|
||||||
|
| get_gitea_mcp_server_version | Server | Get the version of the Gitea MCP Server |
|
||||||
|
| list_wiki_pages | Wiki | List all wiki pages in a repository |
|
||||||
|
| get_wiki_page | Wiki | Get a wiki page content and metadata |
|
||||||
|
| get_wiki_revisions | Wiki | Get revisions history of a wiki page |
|
||||||
|
| create_wiki_page | Wiki | Create a new wiki page |
|
||||||
|
| update_wiki_page | Wiki | Update an existing wiki page |
|
||||||
|
| delete_wiki_page | Wiki | Delete a wiki page |
|
||||||
|
|
||||||
## 🐛 Debugging
|
## 🐛 Debugging
|
||||||
|
|
||||||
|
|||||||
108
README.zh-cn.md
108
README.zh-cn.md
@@ -164,56 +164,64 @@ cp gitea-mcp /usr/local/bin/
|
|||||||
|
|
||||||
Gitea MCP 服务器支持以下工具:
|
Gitea MCP 服务器支持以下工具:
|
||||||
|
|
||||||
| 工具 | 范围 | 描述 |
|
| 工具 | 范围 | 描述 |
|
||||||
| :--------------------------: | :------: | :------------------------: |
|
| :-------------------------------: | :------: | :------------------------: |
|
||||||
| get_my_user_info | 用户 | 获取已认证用户信息 |
|
| get_my_user_info | 用户 | 获取已认证用户信息 |
|
||||||
| get_user_orgs | 用户 | 获取已认证用户关联组织 |
|
| get_user_orgs | 用户 | 获取已认证用户关联组织 |
|
||||||
| create_repo | 仓库 | 创建新仓库 |
|
| create_repo | 仓库 | 创建新仓库 |
|
||||||
| fork_repo | 仓库 | 复刻仓库 |
|
| fork_repo | 仓库 | 复刻仓库 |
|
||||||
| list_my_repos | 仓库 | 列出用户所有仓库 |
|
| list_my_repos | 仓库 | 列出用户所有仓库 |
|
||||||
| create_branch | 分支 | 创建新分支 |
|
| create_branch | 分支 | 创建新分支 |
|
||||||
| delete_branch | 分支 | 删除分支 |
|
| delete_branch | 分支 | 删除分支 |
|
||||||
| list_branches | 分支 | 列出所有分支 |
|
| list_branches | 分支 | 列出所有分支 |
|
||||||
| create_release | 版本发布 | 创建新版本发布 |
|
| create_release | 版本发布 | 创建新版本发布 |
|
||||||
| delete_release | 版本发布 | 删除版本发布 |
|
| delete_release | 版本发布 | 删除版本发布 |
|
||||||
| get_release | 版本发布 | 获取版本发布 |
|
| get_release | 版本发布 | 获取版本发布 |
|
||||||
| get_latest_release | 版本发布 | 获取最新版本发布 |
|
| get_latest_release | 版本发布 | 获取最新版本发布 |
|
||||||
| list_releases | 版本发布 | 列出所有版本发布 |
|
| list_releases | 版本发布 | 列出所有版本发布 |
|
||||||
| create_tag | 标签 | 创建新标签 |
|
| create_tag | 标签 | 创建新标签 |
|
||||||
| delete_tag | 标签 | 删除标签 |
|
| delete_tag | 标签 | 删除标签 |
|
||||||
| get_tag | 标签 | 获取标签 |
|
| get_tag | 标签 | 获取标签 |
|
||||||
| list_tags | 标签 | 列出所有标签 |
|
| list_tags | 标签 | 列出所有标签 |
|
||||||
| list_repo_commits | 提交 | 列出所有提交 |
|
| list_repo_commits | 提交 | 列出所有提交 |
|
||||||
| get_file_content | 文件 | 获取文件内容和元数据 |
|
| get_file_content | 文件 | 获取文件内容和元数据 |
|
||||||
| get_dir_content | 文件 | 获取目录内容列表 |
|
| get_dir_content | 文件 | 获取目录内容列表 |
|
||||||
| create_file | 文件 | 创建新文件 |
|
| create_file | 文件 | 创建新文件 |
|
||||||
| update_file | 文件 | 更新现有文件 |
|
| update_file | 文件 | 更新现有文件 |
|
||||||
| delete_file | 文件 | 删除文件 |
|
| delete_file | 文件 | 删除文件 |
|
||||||
| get_issue_by_index | 问题 | 按索引获取问题 |
|
| get_issue_by_index | 问题 | 按索引获取问题 |
|
||||||
| list_repo_issues | 问题 | 列出所有问题 |
|
| list_repo_issues | 问题 | 列出所有问题 |
|
||||||
| create_issue | 问题 | 创建新问题 |
|
| create_issue | 问题 | 创建新问题 |
|
||||||
| create_issue_comment | 问题 | 在问题上创建评论 |
|
| create_issue_comment | 问题 | 在问题上创建评论 |
|
||||||
| edit_issue | 问题 | 编辑问题 |
|
| edit_issue | 问题 | 编辑问题 |
|
||||||
| edit_issue_comment | 问题 | 编辑问题评论 |
|
| edit_issue_comment | 问题 | 编辑问题评论 |
|
||||||
| get_issue_comments_by_index | 问题 | 按索引获取问题评论 |
|
| get_issue_comments_by_index | 问题 | 按索引获取问题评论 |
|
||||||
| get_pull_request_by_index | 拉取请求 | 按索引获取拉取请求 |
|
| get_pull_request_by_index | 拉取请求 | 按索引获取拉取请求 |
|
||||||
| list_repo_pull_requests | 拉取请求 | 列出所有拉取请求 |
|
| list_repo_pull_requests | 拉取请求 | 列出所有拉取请求 |
|
||||||
| create_pull_request | 拉取请求 | 创建新拉取请求 |
|
| create_pull_request | 拉取请求 | 创建新拉取请求 |
|
||||||
| create_pull_request_reviewer | 拉取请求 | 为拉取请求添加审查者 |
|
| create_pull_request_reviewer | 拉取请求 | 为拉取请求添加审查者 |
|
||||||
| search_users | 用户 | 搜索用户 |
|
| delete_pull_request_reviewer | 拉取请求 | 移除拉取请求的审查者 |
|
||||||
| search_org_teams | 组织 | 搜索组织团队 |
|
| list_pull_request_reviews | 拉取请求 | 列出拉取请求的所有审查 |
|
||||||
| list_org_labels | 组织 | 列出组织标签 |
|
| get_pull_request_review | 拉取请求 | 按 ID 获取特定审查 |
|
||||||
| create_org_label | 组织 | 创建组织标签 |
|
| list_pull_request_review_comments | 拉取请求 | 列出审查的行内评论 |
|
||||||
| edit_org_label | 组织 | 编辑组织标签 |
|
| create_pull_request_review | 拉取请求 | 创建审查(可含行内评论) |
|
||||||
| delete_org_label | 组织 | 删除组织标签 |
|
| submit_pull_request_review | 拉取请求 | 提交待处理的审查 |
|
||||||
| search_repos | 仓库 | 搜索仓库 |
|
| delete_pull_request_review | 拉取请求 | 删除审查 |
|
||||||
| get_gitea_mcp_server_version | 服务器 | 获取 Gitea MCP 服务器版本 |
|
| dismiss_pull_request_review | 拉取请求 | 驳回审查(可附消息) |
|
||||||
| list_wiki_pages | Wiki | 列出所有 Wiki 页面 |
|
| search_users | 用户 | 搜索用户 |
|
||||||
| get_wiki_page | Wiki | 获取 Wiki 页面内容和元数据 |
|
| search_org_teams | 组织 | 搜索组织团队 |
|
||||||
| get_wiki_revisions | Wiki | 获取 Wiki 修订历史 |
|
| list_org_labels | 组织 | 列出组织标签 |
|
||||||
| create_wiki_page | Wiki | 创建新 Wiki 页面 |
|
| create_org_label | 组织 | 创建组织标签 |
|
||||||
| update_wiki_page | Wiki | 更新现有 Wiki 页面 |
|
| edit_org_label | 组织 | 编辑组织标签 |
|
||||||
| delete_wiki_page | Wiki | 删除 Wiki 页面 |
|
| delete_org_label | 组织 | 删除组织标签 |
|
||||||
|
| search_repos | 仓库 | 搜索仓库 |
|
||||||
|
| get_gitea_mcp_server_version | 服务器 | 获取 Gitea MCP 服务器版本 |
|
||||||
|
| list_wiki_pages | Wiki | 列出所有 Wiki 页面 |
|
||||||
|
| get_wiki_page | Wiki | 获取 Wiki 页面内容和元数据 |
|
||||||
|
| get_wiki_revisions | Wiki | 获取 Wiki 修订历史 |
|
||||||
|
| create_wiki_page | Wiki | 创建新 Wiki 页面 |
|
||||||
|
| update_wiki_page | Wiki | 更新现有 Wiki 页面 |
|
||||||
|
| delete_wiki_page | Wiki | 删除 Wiki 页面 |
|
||||||
|
|
||||||
## 🐛 调试
|
## 🐛 调试
|
||||||
|
|
||||||
|
|||||||
108
README.zh-tw.md
108
README.zh-tw.md
@@ -164,56 +164,64 @@ cp gitea-mcp /usr/local/bin/
|
|||||||
|
|
||||||
Gitea MCP 伺服器支援以下工具:
|
Gitea MCP 伺服器支援以下工具:
|
||||||
|
|
||||||
| 工具 | 範圍 | 描述 |
|
| 工具 | 範圍 | 描述 |
|
||||||
| :--------------------------: | :------: | :--------------------------: |
|
| :-------------------------------: | :------: | :--------------------------: |
|
||||||
| get_my_user_info | 用戶 | 取得已認證用戶資訊 |
|
| get_my_user_info | 用戶 | 取得已認證用戶資訊 |
|
||||||
| get_user_orgs | 用戶 | 取得已認證用戶所屬組織 |
|
| get_user_orgs | 用戶 | 取得已認證用戶所屬組織 |
|
||||||
| create_repo | 倉庫 | 創建新倉庫 |
|
| create_repo | 倉庫 | 創建新倉庫 |
|
||||||
| fork_repo | 倉庫 | 復刻倉庫 |
|
| fork_repo | 倉庫 | 復刻倉庫 |
|
||||||
| list_my_repos | 倉庫 | 列出用戶所有倉庫 |
|
| list_my_repos | 倉庫 | 列出用戶所有倉庫 |
|
||||||
| create_branch | 分支 | 創建新分支 |
|
| create_branch | 分支 | 創建新分支 |
|
||||||
| delete_branch | 分支 | 刪除分支 |
|
| delete_branch | 分支 | 刪除分支 |
|
||||||
| list_branches | 分支 | 列出所有分支 |
|
| list_branches | 分支 | 列出所有分支 |
|
||||||
| create_release | 版本發布 | 創建新版本發布 |
|
| create_release | 版本發布 | 創建新版本發布 |
|
||||||
| delete_release | 版本發布 | 刪除版本發布 |
|
| delete_release | 版本發布 | 刪除版本發布 |
|
||||||
| get_release | 版本發布 | 取得版本發布 |
|
| get_release | 版本發布 | 取得版本發布 |
|
||||||
| get_latest_release | 版本發布 | 取得最新版本發布 |
|
| get_latest_release | 版本發布 | 取得最新版本發布 |
|
||||||
| list_releases | 版本發布 | 列出所有版本發布 |
|
| list_releases | 版本發布 | 列出所有版本發布 |
|
||||||
| create_tag | 標籤 | 創建新標籤 |
|
| create_tag | 標籤 | 創建新標籤 |
|
||||||
| delete_tag | 標籤 | 刪除標籤 |
|
| delete_tag | 標籤 | 刪除標籤 |
|
||||||
| get_tag | 標籤 | 取得標籤 |
|
| get_tag | 標籤 | 取得標籤 |
|
||||||
| list_tags | 標籤 | 列出所有標籤 |
|
| list_tags | 標籤 | 列出所有標籤 |
|
||||||
| list_repo_commits | 提交 | 列出所有提交 |
|
| list_repo_commits | 提交 | 列出所有提交 |
|
||||||
| get_file_content | 文件 | 取得文件內容與中繼資料 |
|
| get_file_content | 文件 | 取得文件內容與中繼資料 |
|
||||||
| get_dir_content | 文件 | 取得目錄內容列表 |
|
| get_dir_content | 文件 | 取得目錄內容列表 |
|
||||||
| create_file | 文件 | 創建新文件 |
|
| create_file | 文件 | 創建新文件 |
|
||||||
| update_file | 文件 | 更新現有文件 |
|
| update_file | 文件 | 更新現有文件 |
|
||||||
| delete_file | 文件 | 刪除文件 |
|
| delete_file | 文件 | 刪除文件 |
|
||||||
| get_issue_by_index | 問題 | 依索引取得問題 |
|
| get_issue_by_index | 問題 | 依索引取得問題 |
|
||||||
| list_repo_issues | 問題 | 列出所有問題 |
|
| list_repo_issues | 問題 | 列出所有問題 |
|
||||||
| create_issue | 問題 | 創建新問題 |
|
| create_issue | 問題 | 創建新問題 |
|
||||||
| create_issue_comment | 問題 | 在問題上創建評論 |
|
| create_issue_comment | 問題 | 在問題上創建評論 |
|
||||||
| edit_issue | 問題 | 編輯問題 |
|
| edit_issue | 問題 | 編輯問題 |
|
||||||
| edit_issue_comment | 問題 | 編輯問題評論 |
|
| edit_issue_comment | 問題 | 編輯問題評論 |
|
||||||
| get_issue_comments_by_index | 問題 | 依索引取得問題評論 |
|
| get_issue_comments_by_index | 問題 | 依索引取得問題評論 |
|
||||||
| get_pull_request_by_index | 拉取請求 | 依索引取得拉取請求 |
|
| get_pull_request_by_index | 拉取請求 | 依索引取得拉取請求 |
|
||||||
| list_repo_pull_requests | 拉取請求 | 列出所有拉取請求 |
|
| list_repo_pull_requests | 拉取請求 | 列出所有拉取請求 |
|
||||||
| create_pull_request | 拉取請求 | 創建新拉取請求 |
|
| create_pull_request | 拉取請求 | 創建新拉取請求 |
|
||||||
| create_pull_request_reviewer | 拉取請求 | 為拉取請求添加審查者 |
|
| create_pull_request_reviewer | 拉取請求 | 為拉取請求添加審查者 |
|
||||||
| search_users | 用戶 | 搜尋用戶 |
|
| delete_pull_request_reviewer | 拉取請求 | 移除拉取請求的審查者 |
|
||||||
| search_org_teams | 組織 | 搜尋組織團隊 |
|
| list_pull_request_reviews | 拉取請求 | 列出拉取請求的所有審查 |
|
||||||
| list_org_labels | 組織 | 列出組織標籤 |
|
| get_pull_request_review | 拉取請求 | 依 ID 取得特定審查 |
|
||||||
| create_org_label | 組織 | 創建組織標籤 |
|
| list_pull_request_review_comments | 拉取請求 | 列出審查的行內評論 |
|
||||||
| edit_org_label | 組織 | 編輯組織標籤 |
|
| create_pull_request_review | 拉取請求 | 創建審查(可含行內評論) |
|
||||||
| delete_org_label | 組織 | 刪除組織標籤 |
|
| submit_pull_request_review | 拉取請求 | 提交待處理的審查 |
|
||||||
| search_repos | 倉庫 | 搜尋倉庫 |
|
| delete_pull_request_review | 拉取請求 | 刪除審查 |
|
||||||
| get_gitea_mcp_server_version | 伺服器 | 取得 Gitea MCP 伺服器版本 |
|
| dismiss_pull_request_review | 拉取請求 | 駁回審查(可附訊息) |
|
||||||
| list_wiki_pages | Wiki | 列出所有 Wiki 頁面 |
|
| search_users | 用戶 | 搜尋用戶 |
|
||||||
| get_wiki_page | Wiki | 取得 Wiki 頁面內容與中繼資料 |
|
| search_org_teams | 組織 | 搜尋組織團隊 |
|
||||||
| get_wiki_revisions | Wiki | 取得 Wiki 修訂歷史 |
|
| list_org_labels | 組織 | 列出組織標籤 |
|
||||||
| create_wiki_page | Wiki | 創建新 Wiki 頁面 |
|
| create_org_label | 組織 | 創建組織標籤 |
|
||||||
| update_wiki_page | Wiki | 更新現有 Wiki 頁面 |
|
| edit_org_label | 組織 | 編輯組織標籤 |
|
||||||
| delete_wiki_page | Wiki | 刪除 Wiki 頁面 |
|
| delete_org_label | 組織 | 刪除組織標籤 |
|
||||||
|
| search_repos | 倉庫 | 搜尋倉庫 |
|
||||||
|
| get_gitea_mcp_server_version | 伺服器 | 取得 Gitea MCP 伺服器版本 |
|
||||||
|
| list_wiki_pages | Wiki | 列出所有 Wiki 頁面 |
|
||||||
|
| get_wiki_page | Wiki | 取得 Wiki 頁面內容與中繼資料 |
|
||||||
|
| get_wiki_revisions | Wiki | 取得 Wiki 修訂歷史 |
|
||||||
|
| create_wiki_page | Wiki | 創建新 Wiki 頁面 |
|
||||||
|
| update_wiki_page | Wiki | 更新現有 Wiki 頁面 |
|
||||||
|
| delete_wiki_page | Wiki | 刪除 Wiki 頁面 |
|
||||||
|
|
||||||
## 🐛 調試
|
## 🐛 調試
|
||||||
|
|
||||||
|
|||||||
2
build.bat
Normal file
2
build.bat
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
@echo off
|
||||||
|
powershell -ExecutionPolicy Bypass -File "%~dp0build.ps1" %*
|
||||||
220
build.ps1
Normal file
220
build.ps1
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
|
||||||
|
# PowerShell build script for gitea-mcp
|
||||||
|
# Replicates the functionality of the Makefile
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$Target = "help"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
$EXECUTABLE = "gitea-mcp.exe"
|
||||||
|
$VERSION = & git describe --tags --always 2>$null | ForEach-Object { $_ -replace '-', '+' -replace '^v', '' }
|
||||||
|
if (-not $VERSION) { $VERSION = "dev" }
|
||||||
|
$LDFLAGS = "-X `"main.Version=$VERSION`""
|
||||||
|
|
||||||
|
# Colors for output (Windows PowerShell compatible)
|
||||||
|
$CYAN = "Cyan"
|
||||||
|
$RESET = "White"
|
||||||
|
|
||||||
|
function Write-Header {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host "=== $Message ===" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Info {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host $Message -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Success {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host $Message -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Error {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host $Message -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-Help {
|
||||||
|
Write-Host "Usage: .\build.ps1 [target]" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Targets:" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
Write-Host ("{0,-30}" -f "help") -ForegroundColor Cyan -NoNewline
|
||||||
|
Write-Host " Print this help message."
|
||||||
|
Write-Host ("{0,-30}" -f "build") -ForegroundColor Cyan -NoNewline
|
||||||
|
Write-Host " Build the application."
|
||||||
|
Write-Host ("{0,-30}" -f "install") -ForegroundColor Cyan -NoNewline
|
||||||
|
Write-Host " Install the application."
|
||||||
|
Write-Host ("{0,-30}" -f "uninstall") -ForegroundColor Cyan -NoNewline
|
||||||
|
Write-Host " Uninstall the application."
|
||||||
|
Write-Host ("{0,-30}" -f "clean") -ForegroundColor Cyan -NoNewline
|
||||||
|
Write-Host " Clean the build artifacts."
|
||||||
|
Write-Host ("{0,-30}" -f "air") -ForegroundColor Cyan -NoNewline
|
||||||
|
Write-Host " Install air for hot reload."
|
||||||
|
Write-Host ("{0,-30}" -f "dev") -ForegroundColor Cyan -NoNewline
|
||||||
|
Write-Host " Run the application with hot reload."
|
||||||
|
Write-Host ("{0,-30}" -f "vendor") -ForegroundColor Cyan -NoNewline
|
||||||
|
Write-Host " Tidy and verify module dependencies."
|
||||||
|
}
|
||||||
|
|
||||||
|
function Build-App {
|
||||||
|
Write-Header "Building application"
|
||||||
|
|
||||||
|
$ldflags = "-s -w $LDFLAGS"
|
||||||
|
Write-Info "go build -v -ldflags '$ldflags' -o $EXECUTABLE"
|
||||||
|
|
||||||
|
try {
|
||||||
|
& go build -v -ldflags $ldflags -o $EXECUTABLE
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Success "Build successful: $EXECUTABLE"
|
||||||
|
} else {
|
||||||
|
Write-Error "Build failed with exit code: $LASTEXITCODE"
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Error "Build failed: $_"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Install-App {
|
||||||
|
Write-Header "Installing application"
|
||||||
|
|
||||||
|
# First build the application
|
||||||
|
Build-App
|
||||||
|
|
||||||
|
$GOPATH = $env:GOPATH
|
||||||
|
if (-not $GOPATH) {
|
||||||
|
$GOPATH = Join-Path $env:USERPROFILE "go"
|
||||||
|
}
|
||||||
|
|
||||||
|
$installDir = Join-Path $GOPATH "bin"
|
||||||
|
$installPath = Join-Path $installDir $EXECUTABLE
|
||||||
|
|
||||||
|
Write-Info "Installing $EXECUTABLE to $installPath"
|
||||||
|
|
||||||
|
# Create directory if it doesn't exist
|
||||||
|
if (-not (Test-Path $installDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy the executable
|
||||||
|
if (Test-Path $EXECUTABLE) {
|
||||||
|
Copy-Item $EXECUTABLE $installPath -Force
|
||||||
|
Write-Success "Installed $EXECUTABLE to $installPath"
|
||||||
|
Write-Info "Please add $installDir to your PATH if it is not already there."
|
||||||
|
} else {
|
||||||
|
Write-Error "Executable not found. Please build first."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Uninstall-App {
|
||||||
|
Write-Header "Uninstalling application"
|
||||||
|
|
||||||
|
$GOPATH = $env:GOPATH
|
||||||
|
if (-not $GOPATH) {
|
||||||
|
$GOPATH = Join-Path $env:USERPROFILE "go"
|
||||||
|
}
|
||||||
|
|
||||||
|
$installPath = Join-Path $GOPATH "bin" $EXECUTABLE
|
||||||
|
|
||||||
|
Write-Info "Uninstalling $EXECUTABLE from $installPath"
|
||||||
|
|
||||||
|
if (Test-Path $installPath) {
|
||||||
|
Remove-Item $installPath -Force
|
||||||
|
Write-Success "Uninstalled $EXECUTABLE from $installPath"
|
||||||
|
} else {
|
||||||
|
Write-Warning "$EXECUTABLE not found at $installPath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Clean-Build {
|
||||||
|
Write-Header "Cleaning build artifacts"
|
||||||
|
|
||||||
|
Write-Info "Cleaning up $EXECUTABLE"
|
||||||
|
|
||||||
|
if (Test-Path $EXECUTABLE) {
|
||||||
|
Remove-Item $EXECUTABLE -Force
|
||||||
|
Write-Success "Cleaned up $EXECUTABLE"
|
||||||
|
} else {
|
||||||
|
Write-Warning "$EXECUTABLE not found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Install-Air {
|
||||||
|
Write-Header "Installing air for hot reload"
|
||||||
|
|
||||||
|
# Check if air is already installed
|
||||||
|
$airPath = Get-Command air -ErrorAction SilentlyContinue
|
||||||
|
if ($airPath) {
|
||||||
|
Write-Success "air is already installed"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info "Installing github.com/air-verse/air@latest"
|
||||||
|
try {
|
||||||
|
& go install github.com/air-verse/air@latest
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Success "air installed successfully"
|
||||||
|
} else {
|
||||||
|
Write-Error "Failed to install air"
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Error "Failed to install air: $_"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Start-Dev {
|
||||||
|
Write-Header "Starting development mode with hot reload"
|
||||||
|
|
||||||
|
# Install air first
|
||||||
|
Install-Air
|
||||||
|
|
||||||
|
Write-Info "Starting air with build configuration"
|
||||||
|
& air --build.cmd "go build -o $EXECUTABLE" --build.bin "./$EXECUTABLE"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-Vendor {
|
||||||
|
Write-Header "Tidying and verifying module dependencies"
|
||||||
|
|
||||||
|
Write-Info "Running go mod tidy"
|
||||||
|
& go mod tidy
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error "go mod tidy failed"
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info "Running go mod verify"
|
||||||
|
& go mod verify
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error "go mod verify failed"
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Success "Dependencies updated successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main execution logic
|
||||||
|
switch ($Target.ToLower()) {
|
||||||
|
"help" { Get-Help }
|
||||||
|
"build" { Build-App }
|
||||||
|
"install" { Install-App }
|
||||||
|
"uninstall" { Uninstall-App }
|
||||||
|
"clean" { Clean-Build }
|
||||||
|
"air" { Install-Air }
|
||||||
|
"dev" { Start-Dev }
|
||||||
|
"vendor" { Update-Vendor }
|
||||||
|
default {
|
||||||
|
Write-Error "Unknown target: $Target"
|
||||||
|
Write-Host ""
|
||||||
|
Get-Help
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
10
operation/actions/actions.go
Normal file
10
operation/actions/actions.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/tool"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tool is the registry for all Actions-related MCP tools.
|
||||||
|
var Tool = tool.New()
|
||||||
|
|
||||||
|
|
||||||
198
operation/actions/logs.go
Normal file
198
operation/actions/logs.go
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
|
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GetRepoActionJobLogPreviewToolName = "get_repo_action_job_log_preview"
|
||||||
|
DownloadRepoActionJobLogToolName = "download_repo_action_job_log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
GetRepoActionJobLogPreviewTool = mcp.NewTool(
|
||||||
|
GetRepoActionJobLogPreviewToolName,
|
||||||
|
mcp.WithDescription("Get a repository Actions job log preview (tail/limited for chat-friendly output)"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("job_id", mcp.Required(), mcp.Description("job ID")),
|
||||||
|
mcp.WithNumber("tail_lines", mcp.Description("number of lines from the end of the log"), mcp.DefaultNumber(200), mcp.Min(1)),
|
||||||
|
mcp.WithNumber("max_bytes", mcp.Description("max bytes to return"), mcp.DefaultNumber(65536), mcp.Min(1024)),
|
||||||
|
)
|
||||||
|
|
||||||
|
DownloadRepoActionJobLogTool = mcp.NewTool(
|
||||||
|
DownloadRepoActionJobLogToolName,
|
||||||
|
mcp.WithDescription("Download a repository Actions job log to a file on the MCP server filesystem"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("job_id", mcp.Required(), mcp.Description("job ID")),
|
||||||
|
mcp.WithString("output_path", mcp.Description("optional output file path; if omitted, uses ~/.gitea-mcp/artifacts/actions-logs/...")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Tool.RegisterRead(server.ServerTool{Tool: GetRepoActionJobLogPreviewTool, Handler: GetRepoActionJobLogPreviewFn})
|
||||||
|
Tool.RegisterRead(server.ServerTool{Tool: DownloadRepoActionJobLogTool, Handler: DownloadRepoActionJobLogFn})
|
||||||
|
}
|
||||||
|
|
||||||
|
func logPaths(owner, repo string, jobID int64) []string {
|
||||||
|
// Primary candidate endpoints, plus a few commonly-seen variants across versions.
|
||||||
|
// We try these in order; 404/405 falls through.
|
||||||
|
return []string{
|
||||||
|
fmt.Sprintf("repos/%s/%s/actions/jobs/%d/logs", url.PathEscape(owner), url.PathEscape(repo), jobID),
|
||||||
|
fmt.Sprintf("repos/%s/%s/actions/jobs/%d/log", url.PathEscape(owner), url.PathEscape(repo), jobID),
|
||||||
|
fmt.Sprintf("repos/%s/%s/actions/tasks/%d/log", url.PathEscape(owner), url.PathEscape(repo), jobID),
|
||||||
|
fmt.Sprintf("repos/%s/%s/actions/task/%d/log", url.PathEscape(owner), url.PathEscape(repo), jobID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchJobLogBytes(ctx context.Context, owner, repo string, jobID int64) ([]byte, string, error) {
|
||||||
|
var lastErr error
|
||||||
|
for _, p := range logPaths(owner, repo, jobID) {
|
||||||
|
b, _, err := gitea.DoBytes(ctx, "GET", p, nil, nil, "text/plain")
|
||||||
|
if err == nil {
|
||||||
|
return b, p, nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
var httpErr *gitea.HTTPError
|
||||||
|
if errors.As(err, &httpErr) && (httpErr.StatusCode == 404 || httpErr.StatusCode == 405) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, p, err
|
||||||
|
}
|
||||||
|
return nil, "", lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func tailByLines(data []byte, tailLines int) []byte {
|
||||||
|
if tailLines <= 0 || len(data) == 0 {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
// Find the start index of the last N lines by scanning backwards.
|
||||||
|
lines := 0
|
||||||
|
i := len(data) - 1
|
||||||
|
for i >= 0 {
|
||||||
|
if data[i] == '\n' {
|
||||||
|
lines++
|
||||||
|
if lines > tailLines {
|
||||||
|
return data[i+1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func limitBytes(data []byte, maxBytes int) ([]byte, bool) {
|
||||||
|
if maxBytes <= 0 {
|
||||||
|
return data, false
|
||||||
|
}
|
||||||
|
if len(data) <= maxBytes {
|
||||||
|
return data, false
|
||||||
|
}
|
||||||
|
// Keep the tail so the most recent log content is preserved.
|
||||||
|
return data[len(data)-maxBytes:], true
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRepoActionJobLogPreviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetRepoActionJobLogPreviewFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
jobIDFloat, ok := req.GetArguments()["job_id"].(float64)
|
||||||
|
if !ok || jobIDFloat <= 0 {
|
||||||
|
return to.ErrorResult(fmt.Errorf("job_id is required"))
|
||||||
|
}
|
||||||
|
tailLinesFloat, _ := req.GetArguments()["tail_lines"].(float64)
|
||||||
|
maxBytesFloat, _ := req.GetArguments()["max_bytes"].(float64)
|
||||||
|
tailLines := int(tailLinesFloat)
|
||||||
|
if tailLines <= 0 {
|
||||||
|
tailLines = 200
|
||||||
|
}
|
||||||
|
maxBytes := int(maxBytesFloat)
|
||||||
|
if maxBytes <= 0 {
|
||||||
|
maxBytes = 65536
|
||||||
|
}
|
||||||
|
|
||||||
|
jobID := int64(jobIDFloat)
|
||||||
|
raw, usedPath, err := fetchJobLogBytes(ctx, owner, repo, jobID)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get job log err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
tailed := tailByLines(raw, tailLines)
|
||||||
|
limited, truncated := limitBytes(tailed, maxBytes)
|
||||||
|
|
||||||
|
return to.TextResult(map[string]any{
|
||||||
|
"endpoint": usedPath,
|
||||||
|
"job_id": jobID,
|
||||||
|
"bytes": len(raw),
|
||||||
|
"tail_lines": tailLines,
|
||||||
|
"max_bytes": maxBytes,
|
||||||
|
"truncated": truncated,
|
||||||
|
"log": string(limited),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadRepoActionJobLogFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called DownloadRepoActionJobLogFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
jobIDFloat, ok := req.GetArguments()["job_id"].(float64)
|
||||||
|
if !ok || jobIDFloat <= 0 {
|
||||||
|
return to.ErrorResult(fmt.Errorf("job_id is required"))
|
||||||
|
}
|
||||||
|
outputPath, _ := req.GetArguments()["output_path"].(string)
|
||||||
|
jobID := int64(jobIDFloat)
|
||||||
|
|
||||||
|
raw, usedPath, err := fetchJobLogBytes(ctx, owner, repo, jobID)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("download job log err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if outputPath == "" {
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
if home == "" {
|
||||||
|
home = os.TempDir()
|
||||||
|
}
|
||||||
|
outputPath = filepath.Join(home, ".gitea-mcp", "artifacts", "actions-logs", owner, repo, fmt.Sprintf("%d.log", jobID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(outputPath), 0o700); err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("create output dir err: %v", err))
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(outputPath, raw, 0o600); err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("write log file err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(map[string]any{
|
||||||
|
"endpoint": usedPath,
|
||||||
|
"job_id": jobID,
|
||||||
|
"path": outputPath,
|
||||||
|
"bytes": len(raw),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
24
operation/actions/logs_test.go
Normal file
24
operation/actions/logs_test.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestTailByLines(t *testing.T) {
|
||||||
|
in := []byte("a\nb\nc\nd\n")
|
||||||
|
got := string(tailByLines(in, 2))
|
||||||
|
if got != "c\nd\n" {
|
||||||
|
t.Fatalf("tailByLines(...,2) = %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLimitBytesKeepsTail(t *testing.T) {
|
||||||
|
in := []byte("0123456789")
|
||||||
|
out, truncated := limitBytes(in, 4)
|
||||||
|
if !truncated {
|
||||||
|
t.Fatalf("expected truncated=true")
|
||||||
|
}
|
||||||
|
if string(out) != "6789" {
|
||||||
|
t.Fatalf("limitBytes tail = %q, want %q", string(out), "6789")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
468
operation/actions/runs.go
Normal file
468
operation/actions/runs.go
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
|
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ListRepoActionWorkflowsToolName = "list_repo_action_workflows"
|
||||||
|
GetRepoActionWorkflowToolName = "get_repo_action_workflow"
|
||||||
|
DispatchRepoActionWorkflowToolName = "dispatch_repo_action_workflow"
|
||||||
|
|
||||||
|
ListRepoActionRunsToolName = "list_repo_action_runs"
|
||||||
|
GetRepoActionRunToolName = "get_repo_action_run"
|
||||||
|
CancelRepoActionRunToolName = "cancel_repo_action_run"
|
||||||
|
RerunRepoActionRunToolName = "rerun_repo_action_run"
|
||||||
|
|
||||||
|
ListRepoActionJobsToolName = "list_repo_action_jobs"
|
||||||
|
ListRepoActionRunJobsToolName = "list_repo_action_run_jobs"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ListRepoActionWorkflowsTool = mcp.NewTool(
|
||||||
|
ListRepoActionWorkflowsToolName,
|
||||||
|
mcp.WithDescription("List repository Actions workflows"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(50), mcp.Min(1)),
|
||||||
|
)
|
||||||
|
|
||||||
|
GetRepoActionWorkflowTool = mcp.NewTool(
|
||||||
|
GetRepoActionWorkflowToolName,
|
||||||
|
mcp.WithDescription("Get a repository Actions workflow by ID"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("workflow_id", mcp.Required(), mcp.Description("workflow ID or filename (e.g. 'my-workflow.yml')")),
|
||||||
|
)
|
||||||
|
|
||||||
|
DispatchRepoActionWorkflowTool = mcp.NewTool(
|
||||||
|
DispatchRepoActionWorkflowToolName,
|
||||||
|
mcp.WithDescription("Trigger (dispatch) a repository Actions workflow"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("workflow_id", mcp.Required(), mcp.Description("workflow ID or filename (e.g. 'my-workflow.yml')")),
|
||||||
|
mcp.WithString("ref", mcp.Required(), mcp.Description("git ref (branch or tag)")),
|
||||||
|
mcp.WithObject("inputs", mcp.Description("workflow inputs object")),
|
||||||
|
)
|
||||||
|
|
||||||
|
ListRepoActionRunsTool = mcp.NewTool(
|
||||||
|
ListRepoActionRunsToolName,
|
||||||
|
mcp.WithDescription("List repository Actions workflow runs"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(50), mcp.Min(1)),
|
||||||
|
mcp.WithString("status", mcp.Description("optional status filter")),
|
||||||
|
)
|
||||||
|
|
||||||
|
GetRepoActionRunTool = mcp.NewTool(
|
||||||
|
GetRepoActionRunToolName,
|
||||||
|
mcp.WithDescription("Get a repository Actions run by ID"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("run_id", mcp.Required(), mcp.Description("run ID")),
|
||||||
|
)
|
||||||
|
|
||||||
|
CancelRepoActionRunTool = mcp.NewTool(
|
||||||
|
CancelRepoActionRunToolName,
|
||||||
|
mcp.WithDescription("Cancel a repository Actions run"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("run_id", mcp.Required(), mcp.Description("run ID")),
|
||||||
|
)
|
||||||
|
|
||||||
|
RerunRepoActionRunTool = mcp.NewTool(
|
||||||
|
RerunRepoActionRunToolName,
|
||||||
|
mcp.WithDescription("Rerun a repository Actions run"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("run_id", mcp.Required(), mcp.Description("run ID")),
|
||||||
|
)
|
||||||
|
|
||||||
|
ListRepoActionJobsTool = mcp.NewTool(
|
||||||
|
ListRepoActionJobsToolName,
|
||||||
|
mcp.WithDescription("List repository Actions jobs"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(50), mcp.Min(1)),
|
||||||
|
mcp.WithString("status", mcp.Description("optional status filter")),
|
||||||
|
)
|
||||||
|
|
||||||
|
ListRepoActionRunJobsTool = mcp.NewTool(
|
||||||
|
ListRepoActionRunJobsToolName,
|
||||||
|
mcp.WithDescription("List Actions jobs for a specific run"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("run_id", mcp.Required(), mcp.Description("run ID")),
|
||||||
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(50), mcp.Min(1)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionWorkflowsTool, Handler: ListRepoActionWorkflowsFn})
|
||||||
|
Tool.RegisterRead(server.ServerTool{Tool: GetRepoActionWorkflowTool, Handler: GetRepoActionWorkflowFn})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{Tool: DispatchRepoActionWorkflowTool, Handler: DispatchRepoActionWorkflowFn})
|
||||||
|
|
||||||
|
Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionRunsTool, Handler: ListRepoActionRunsFn})
|
||||||
|
Tool.RegisterRead(server.ServerTool{Tool: GetRepoActionRunTool, Handler: GetRepoActionRunFn})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{Tool: CancelRepoActionRunTool, Handler: CancelRepoActionRunFn})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{Tool: RerunRepoActionRunTool, Handler: RerunRepoActionRunFn})
|
||||||
|
|
||||||
|
Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionJobsTool, Handler: ListRepoActionJobsFn})
|
||||||
|
Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionRunJobsTool, Handler: ListRepoActionRunJobsFn})
|
||||||
|
}
|
||||||
|
|
||||||
|
func doJSONWithFallback(ctx context.Context, method string, paths []string, query url.Values, body any, respOut any) (string, int, error) {
|
||||||
|
var lastErr error
|
||||||
|
for _, p := range paths {
|
||||||
|
status, err := gitea.DoJSON(ctx, method, p, query, body, respOut)
|
||||||
|
if err == nil {
|
||||||
|
return p, status, nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
var httpErr *gitea.HTTPError
|
||||||
|
if errors.As(err, &httpErr) && (httpErr.StatusCode == 404 || httpErr.StatusCode == 405) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return p, status, err
|
||||||
|
}
|
||||||
|
return "", 0, lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListRepoActionWorkflowsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListRepoActionWorkflowsFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
page, _ := req.GetArguments()["page"].(float64)
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, _ := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = 50
|
||||||
|
}
|
||||||
|
query := url.Values{}
|
||||||
|
query.Set("page", fmt.Sprintf("%d", int(page)))
|
||||||
|
query.Set("limit", fmt.Sprintf("%d", int(pageSize)))
|
||||||
|
|
||||||
|
var result any
|
||||||
|
_, _, err := doJSONWithFallback(ctx, "GET",
|
||||||
|
[]string{
|
||||||
|
fmt.Sprintf("repos/%s/%s/actions/workflows", url.PathEscape(owner), url.PathEscape(repo)),
|
||||||
|
},
|
||||||
|
query, nil, &result,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("list action workflows err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetRepoActionWorkflowFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
workflowID, ok := req.GetArguments()["workflow_id"].(string)
|
||||||
|
if !ok || workflowID == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("workflow_id is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var result any
|
||||||
|
_, _, err := doJSONWithFallback(ctx, "GET",
|
||||||
|
[]string{
|
||||||
|
fmt.Sprintf("repos/%s/%s/actions/workflows/%s", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(workflowID)),
|
||||||
|
},
|
||||||
|
nil, nil, &result,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get action workflow err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DispatchRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called DispatchRepoActionWorkflowFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
workflowID, ok := req.GetArguments()["workflow_id"].(string)
|
||||||
|
if !ok || workflowID == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("workflow_id is required"))
|
||||||
|
}
|
||||||
|
ref, ok := req.GetArguments()["ref"].(string)
|
||||||
|
if !ok || ref == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("ref is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var inputs map[string]any
|
||||||
|
if raw, exists := req.GetArguments()["inputs"]; exists {
|
||||||
|
if m, ok := raw.(map[string]any); ok {
|
||||||
|
inputs = m
|
||||||
|
} else if m, ok := raw.(map[string]interface{}); ok {
|
||||||
|
inputs = make(map[string]any, len(m))
|
||||||
|
for k, v := range m {
|
||||||
|
inputs[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body := map[string]any{
|
||||||
|
"ref": ref,
|
||||||
|
}
|
||||||
|
if inputs != nil {
|
||||||
|
body["inputs"] = inputs
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err := doJSONWithFallback(ctx, "POST",
|
||||||
|
[]string{
|
||||||
|
fmt.Sprintf("repos/%s/%s/actions/workflows/%s/dispatches", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(workflowID)),
|
||||||
|
fmt.Sprintf("repos/%s/%s/actions/workflows/%s/dispatch", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(workflowID)),
|
||||||
|
},
|
||||||
|
nil, body, nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
var httpErr *gitea.HTTPError
|
||||||
|
if errors.As(err, &httpErr) && (httpErr.StatusCode == 404 || httpErr.StatusCode == 405) {
|
||||||
|
return to.ErrorResult(fmt.Errorf("workflow dispatch not supported on this Gitea version (endpoint returned %d). Check https://docs.gitea.com/api/1.24/ for available Actions endpoints", httpErr.StatusCode))
|
||||||
|
}
|
||||||
|
return to.ErrorResult(fmt.Errorf("dispatch action workflow err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(map[string]any{"message": "workflow dispatched"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListRepoActionRunsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListRepoActionRunsFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
page, _ := req.GetArguments()["page"].(float64)
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, _ := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = 50
|
||||||
|
}
|
||||||
|
statusFilter, _ := req.GetArguments()["status"].(string)
|
||||||
|
|
||||||
|
query := url.Values{}
|
||||||
|
query.Set("page", fmt.Sprintf("%d", int(page)))
|
||||||
|
query.Set("limit", fmt.Sprintf("%d", int(pageSize)))
|
||||||
|
if statusFilter != "" {
|
||||||
|
query.Set("status", statusFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result any
|
||||||
|
_, _, err := doJSONWithFallback(ctx, "GET",
|
||||||
|
[]string{
|
||||||
|
fmt.Sprintf("repos/%s/%s/actions/runs", url.PathEscape(owner), url.PathEscape(repo)),
|
||||||
|
},
|
||||||
|
query, nil, &result,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("list action runs err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetRepoActionRunFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
runID, ok := req.GetArguments()["run_id"].(float64)
|
||||||
|
if !ok || runID <= 0 {
|
||||||
|
return to.ErrorResult(fmt.Errorf("run_id is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var result any
|
||||||
|
_, _, err := doJSONWithFallback(ctx, "GET",
|
||||||
|
[]string{
|
||||||
|
fmt.Sprintf("repos/%s/%s/actions/runs/%d", url.PathEscape(owner), url.PathEscape(repo), int64(runID)),
|
||||||
|
},
|
||||||
|
nil, nil, &result,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get action run err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CancelRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called CancelRepoActionRunFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
runID, ok := req.GetArguments()["run_id"].(float64)
|
||||||
|
if !ok || runID <= 0 {
|
||||||
|
return to.ErrorResult(fmt.Errorf("run_id is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err := doJSONWithFallback(ctx, "POST",
|
||||||
|
[]string{
|
||||||
|
fmt.Sprintf("repos/%s/%s/actions/runs/%d/cancel", url.PathEscape(owner), url.PathEscape(repo), int64(runID)),
|
||||||
|
},
|
||||||
|
nil, nil, nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("cancel action run err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(map[string]any{"message": "run cancellation requested"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func RerunRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called RerunRepoActionRunFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
runID, ok := req.GetArguments()["run_id"].(float64)
|
||||||
|
if !ok || runID <= 0 {
|
||||||
|
return to.ErrorResult(fmt.Errorf("run_id is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err := doJSONWithFallback(ctx, "POST",
|
||||||
|
[]string{
|
||||||
|
fmt.Sprintf("repos/%s/%s/actions/runs/%d/rerun", url.PathEscape(owner), url.PathEscape(repo), int64(runID)),
|
||||||
|
fmt.Sprintf("repos/%s/%s/actions/runs/%d/rerun-failed-jobs", url.PathEscape(owner), url.PathEscape(repo), int64(runID)),
|
||||||
|
},
|
||||||
|
nil, nil, nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
var httpErr *gitea.HTTPError
|
||||||
|
if errors.As(err, &httpErr) && (httpErr.StatusCode == 404 || httpErr.StatusCode == 405) {
|
||||||
|
return to.ErrorResult(fmt.Errorf("workflow rerun not supported on this Gitea version (endpoint returned %d). Check https://docs.gitea.com/api/1.24/ for available Actions endpoints", httpErr.StatusCode))
|
||||||
|
}
|
||||||
|
return to.ErrorResult(fmt.Errorf("rerun action run err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(map[string]any{"message": "run rerun requested"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListRepoActionJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListRepoActionJobsFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
page, _ := req.GetArguments()["page"].(float64)
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, _ := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = 50
|
||||||
|
}
|
||||||
|
statusFilter, _ := req.GetArguments()["status"].(string)
|
||||||
|
|
||||||
|
query := url.Values{}
|
||||||
|
query.Set("page", fmt.Sprintf("%d", int(page)))
|
||||||
|
query.Set("limit", fmt.Sprintf("%d", int(pageSize)))
|
||||||
|
if statusFilter != "" {
|
||||||
|
query.Set("status", statusFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result any
|
||||||
|
_, _, err := doJSONWithFallback(ctx, "GET",
|
||||||
|
[]string{
|
||||||
|
fmt.Sprintf("repos/%s/%s/actions/jobs", url.PathEscape(owner), url.PathEscape(repo)),
|
||||||
|
},
|
||||||
|
query, nil, &result,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("list action jobs err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListRepoActionRunJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListRepoActionRunJobsFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
runID, ok := req.GetArguments()["run_id"].(float64)
|
||||||
|
if !ok || runID <= 0 {
|
||||||
|
return to.ErrorResult(fmt.Errorf("run_id is required"))
|
||||||
|
}
|
||||||
|
page, _ := req.GetArguments()["page"].(float64)
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, _ := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
query := url.Values{}
|
||||||
|
query.Set("page", fmt.Sprintf("%d", int(page)))
|
||||||
|
query.Set("limit", fmt.Sprintf("%d", int(pageSize)))
|
||||||
|
|
||||||
|
var result any
|
||||||
|
_, _, err := doJSONWithFallback(ctx, "GET",
|
||||||
|
[]string{
|
||||||
|
fmt.Sprintf("repos/%s/%s/actions/runs/%d/jobs", url.PathEscape(owner), url.PathEscape(repo), int64(runID)),
|
||||||
|
},
|
||||||
|
query, nil, &result,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("list action run jobs err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(result)
|
||||||
|
}
|
||||||
292
operation/actions/secrets.go
Normal file
292
operation/actions/secrets.go
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
|
|
||||||
|
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ListRepoActionSecretsToolName = "list_repo_action_secrets"
|
||||||
|
UpsertRepoActionSecretToolName = "upsert_repo_action_secret"
|
||||||
|
DeleteRepoActionSecretToolName = "delete_repo_action_secret"
|
||||||
|
ListOrgActionSecretsToolName = "list_org_action_secrets"
|
||||||
|
UpsertOrgActionSecretToolName = "upsert_org_action_secret"
|
||||||
|
DeleteOrgActionSecretToolName = "delete_org_action_secret"
|
||||||
|
)
|
||||||
|
|
||||||
|
type secretMeta struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ListRepoActionSecretsTool = mcp.NewTool(
|
||||||
|
ListRepoActionSecretsToolName,
|
||||||
|
mcp.WithDescription("List repository Actions secrets (metadata only; secret values are never returned)"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100), mcp.Min(1)),
|
||||||
|
)
|
||||||
|
|
||||||
|
UpsertRepoActionSecretTool = mcp.NewTool(
|
||||||
|
UpsertRepoActionSecretToolName,
|
||||||
|
mcp.WithDescription("Create or update (upsert) a repository Actions secret"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("name", mcp.Required(), mcp.Description("secret name")),
|
||||||
|
mcp.WithString("data", mcp.Required(), mcp.Description("secret value")),
|
||||||
|
mcp.WithString("description", mcp.Description("secret description")),
|
||||||
|
)
|
||||||
|
|
||||||
|
DeleteRepoActionSecretTool = mcp.NewTool(
|
||||||
|
DeleteRepoActionSecretToolName,
|
||||||
|
mcp.WithDescription("Delete a repository Actions secret"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("secretName", mcp.Required(), mcp.Description("secret name")),
|
||||||
|
)
|
||||||
|
|
||||||
|
ListOrgActionSecretsTool = mcp.NewTool(
|
||||||
|
ListOrgActionSecretsToolName,
|
||||||
|
mcp.WithDescription("List organization Actions secrets (metadata only; secret values are never returned)"),
|
||||||
|
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
|
||||||
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100), mcp.Min(1)),
|
||||||
|
)
|
||||||
|
|
||||||
|
UpsertOrgActionSecretTool = mcp.NewTool(
|
||||||
|
UpsertOrgActionSecretToolName,
|
||||||
|
mcp.WithDescription("Create or update (upsert) an organization Actions secret"),
|
||||||
|
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
|
||||||
|
mcp.WithString("name", mcp.Required(), mcp.Description("secret name")),
|
||||||
|
mcp.WithString("data", mcp.Required(), mcp.Description("secret value")),
|
||||||
|
mcp.WithString("description", mcp.Description("secret description")),
|
||||||
|
)
|
||||||
|
|
||||||
|
DeleteOrgActionSecretTool = mcp.NewTool(
|
||||||
|
DeleteOrgActionSecretToolName,
|
||||||
|
mcp.WithDescription("Delete an organization Actions secret"),
|
||||||
|
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
|
||||||
|
mcp.WithString("secretName", mcp.Required(), mcp.Description("secret name")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionSecretsTool, Handler: ListRepoActionSecretsFn})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{Tool: UpsertRepoActionSecretTool, Handler: UpsertRepoActionSecretFn})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{Tool: DeleteRepoActionSecretTool, Handler: DeleteRepoActionSecretFn})
|
||||||
|
|
||||||
|
Tool.RegisterRead(server.ServerTool{Tool: ListOrgActionSecretsTool, Handler: ListOrgActionSecretsFn})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{Tool: UpsertOrgActionSecretTool, Handler: UpsertOrgActionSecretFn})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{Tool: DeleteOrgActionSecretTool, Handler: DeleteOrgActionSecretFn})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListRepoActionSecretsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListRepoActionSecretsFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
page, _ := req.GetArguments()["page"].(float64)
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, _ := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, _, err := client.ListRepoActionSecret(owner, repo, gitea_sdk.ListRepoActionSecretOption{
|
||||||
|
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("list repo action secrets err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
metas := make([]secretMeta, 0, len(secrets))
|
||||||
|
for _, s := range secrets {
|
||||||
|
if s == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
metas = append(metas, secretMeta{
|
||||||
|
Name: s.Name,
|
||||||
|
Description: s.Description,
|
||||||
|
CreatedAt: s.Created,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return to.TextResult(metas)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpsertRepoActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called UpsertRepoActionSecretFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
name, ok := req.GetArguments()["name"].(string)
|
||||||
|
if !ok || name == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("name is required"))
|
||||||
|
}
|
||||||
|
data, ok := req.GetArguments()["data"].(string)
|
||||||
|
if !ok || data == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("data is required"))
|
||||||
|
}
|
||||||
|
description, _ := req.GetArguments()["description"].(string)
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
resp, err := client.CreateRepoActionSecret(owner, repo, gitea_sdk.CreateSecretOption{
|
||||||
|
Name: name,
|
||||||
|
Data: data,
|
||||||
|
Description: description,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("upsert repo action secret err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(map[string]any{"message": "secret upserted", "status": resp.StatusCode})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteRepoActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called DeleteRepoActionSecretFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
secretName, ok := req.GetArguments()["secretName"].(string)
|
||||||
|
if !ok || secretName == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("secretName is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
resp, err := client.DeleteRepoActionSecret(owner, repo, secretName)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("delete repo action secret err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(map[string]any{"message": "secret deleted", "status": resp.StatusCode})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListOrgActionSecretsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListOrgActionSecretsFn")
|
||||||
|
org, ok := req.GetArguments()["org"].(string)
|
||||||
|
if !ok || org == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("org is required"))
|
||||||
|
}
|
||||||
|
page, _ := req.GetArguments()["page"].(float64)
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, _ := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, _, err := client.ListOrgActionSecret(org, gitea_sdk.ListOrgActionSecretOption{
|
||||||
|
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("list org action secrets err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
metas := make([]secretMeta, 0, len(secrets))
|
||||||
|
for _, s := range secrets {
|
||||||
|
if s == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
metas = append(metas, secretMeta{
|
||||||
|
Name: s.Name,
|
||||||
|
Description: s.Description,
|
||||||
|
CreatedAt: s.Created,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return to.TextResult(metas)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpsertOrgActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called UpsertOrgActionSecretFn")
|
||||||
|
org, ok := req.GetArguments()["org"].(string)
|
||||||
|
if !ok || org == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("org is required"))
|
||||||
|
}
|
||||||
|
name, ok := req.GetArguments()["name"].(string)
|
||||||
|
if !ok || name == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("name is required"))
|
||||||
|
}
|
||||||
|
data, ok := req.GetArguments()["data"].(string)
|
||||||
|
if !ok || data == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("data is required"))
|
||||||
|
}
|
||||||
|
description, _ := req.GetArguments()["description"].(string)
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
resp, err := client.CreateOrgActionSecret(org, gitea_sdk.CreateSecretOption{
|
||||||
|
Name: name,
|
||||||
|
Data: data,
|
||||||
|
Description: description,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("upsert org action secret err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(map[string]any{"message": "secret upserted", "status": resp.StatusCode})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteOrgActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called DeleteOrgActionSecretFn")
|
||||||
|
org, ok := req.GetArguments()["org"].(string)
|
||||||
|
if !ok || org == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("org is required"))
|
||||||
|
}
|
||||||
|
secretName, ok := req.GetArguments()["secretName"].(string)
|
||||||
|
if !ok || secretName == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("secretName is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
escapedOrg := url.PathEscape(org)
|
||||||
|
escapedSecret := url.PathEscape(secretName)
|
||||||
|
_, err := gitea.DoJSON(ctx, "DELETE", fmt.Sprintf("orgs/%s/actions/secrets/%s", escapedOrg, escapedSecret), nil, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("delete org action secret err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(map[string]any{"message": "secret deleted"})
|
||||||
|
}
|
||||||
402
operation/actions/variables.go
Normal file
402
operation/actions/variables.go
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
|
|
||||||
|
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ListRepoActionVariablesToolName = "list_repo_action_variables"
|
||||||
|
GetRepoActionVariableToolName = "get_repo_action_variable"
|
||||||
|
CreateRepoActionVariableToolName = "create_repo_action_variable"
|
||||||
|
UpdateRepoActionVariableToolName = "update_repo_action_variable"
|
||||||
|
DeleteRepoActionVariableToolName = "delete_repo_action_variable"
|
||||||
|
|
||||||
|
ListOrgActionVariablesToolName = "list_org_action_variables"
|
||||||
|
GetOrgActionVariableToolName = "get_org_action_variable"
|
||||||
|
CreateOrgActionVariableToolName = "create_org_action_variable"
|
||||||
|
UpdateOrgActionVariableToolName = "update_org_action_variable"
|
||||||
|
DeleteOrgActionVariableToolName = "delete_org_action_variable"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ListRepoActionVariablesTool = mcp.NewTool(
|
||||||
|
ListRepoActionVariablesToolName,
|
||||||
|
mcp.WithDescription("List repository Actions variables"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100), mcp.Min(1)),
|
||||||
|
)
|
||||||
|
|
||||||
|
GetRepoActionVariableTool = mcp.NewTool(
|
||||||
|
GetRepoActionVariableToolName,
|
||||||
|
mcp.WithDescription("Get a repository Actions variable by name"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("name", mcp.Required(), mcp.Description("variable name")),
|
||||||
|
)
|
||||||
|
|
||||||
|
CreateRepoActionVariableTool = mcp.NewTool(
|
||||||
|
CreateRepoActionVariableToolName,
|
||||||
|
mcp.WithDescription("Create a repository Actions variable"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("name", mcp.Required(), mcp.Description("variable name")),
|
||||||
|
mcp.WithString("value", mcp.Required(), mcp.Description("variable value")),
|
||||||
|
)
|
||||||
|
|
||||||
|
UpdateRepoActionVariableTool = mcp.NewTool(
|
||||||
|
UpdateRepoActionVariableToolName,
|
||||||
|
mcp.WithDescription("Update a repository Actions variable"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("name", mcp.Required(), mcp.Description("variable name")),
|
||||||
|
mcp.WithString("value", mcp.Required(), mcp.Description("new variable value")),
|
||||||
|
)
|
||||||
|
|
||||||
|
DeleteRepoActionVariableTool = mcp.NewTool(
|
||||||
|
DeleteRepoActionVariableToolName,
|
||||||
|
mcp.WithDescription("Delete a repository Actions variable"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("name", mcp.Required(), mcp.Description("variable name")),
|
||||||
|
)
|
||||||
|
|
||||||
|
ListOrgActionVariablesTool = mcp.NewTool(
|
||||||
|
ListOrgActionVariablesToolName,
|
||||||
|
mcp.WithDescription("List organization Actions variables"),
|
||||||
|
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
|
||||||
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100), mcp.Min(1)),
|
||||||
|
)
|
||||||
|
|
||||||
|
GetOrgActionVariableTool = mcp.NewTool(
|
||||||
|
GetOrgActionVariableToolName,
|
||||||
|
mcp.WithDescription("Get an organization Actions variable by name"),
|
||||||
|
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
|
||||||
|
mcp.WithString("name", mcp.Required(), mcp.Description("variable name")),
|
||||||
|
)
|
||||||
|
|
||||||
|
CreateOrgActionVariableTool = mcp.NewTool(
|
||||||
|
CreateOrgActionVariableToolName,
|
||||||
|
mcp.WithDescription("Create an organization Actions variable"),
|
||||||
|
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
|
||||||
|
mcp.WithString("name", mcp.Required(), mcp.Description("variable name")),
|
||||||
|
mcp.WithString("value", mcp.Required(), mcp.Description("variable value")),
|
||||||
|
mcp.WithString("description", mcp.Description("variable description")),
|
||||||
|
)
|
||||||
|
|
||||||
|
UpdateOrgActionVariableTool = mcp.NewTool(
|
||||||
|
UpdateOrgActionVariableToolName,
|
||||||
|
mcp.WithDescription("Update an organization Actions variable"),
|
||||||
|
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
|
||||||
|
mcp.WithString("name", mcp.Required(), mcp.Description("variable name")),
|
||||||
|
mcp.WithString("value", mcp.Required(), mcp.Description("new variable value")),
|
||||||
|
mcp.WithString("description", mcp.Description("new variable description")),
|
||||||
|
)
|
||||||
|
|
||||||
|
DeleteOrgActionVariableTool = mcp.NewTool(
|
||||||
|
DeleteOrgActionVariableToolName,
|
||||||
|
mcp.WithDescription("Delete an organization Actions variable"),
|
||||||
|
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
|
||||||
|
mcp.WithString("name", mcp.Required(), mcp.Description("variable name")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionVariablesTool, Handler: ListRepoActionVariablesFn})
|
||||||
|
Tool.RegisterRead(server.ServerTool{Tool: GetRepoActionVariableTool, Handler: GetRepoActionVariableFn})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{Tool: CreateRepoActionVariableTool, Handler: CreateRepoActionVariableFn})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{Tool: UpdateRepoActionVariableTool, Handler: UpdateRepoActionVariableFn})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{Tool: DeleteRepoActionVariableTool, Handler: DeleteRepoActionVariableFn})
|
||||||
|
|
||||||
|
Tool.RegisterRead(server.ServerTool{Tool: ListOrgActionVariablesTool, Handler: ListOrgActionVariablesFn})
|
||||||
|
Tool.RegisterRead(server.ServerTool{Tool: GetOrgActionVariableTool, Handler: GetOrgActionVariableFn})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{Tool: CreateOrgActionVariableTool, Handler: CreateOrgActionVariableFn})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{Tool: UpdateOrgActionVariableTool, Handler: UpdateOrgActionVariableFn})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{Tool: DeleteOrgActionVariableTool, Handler: DeleteOrgActionVariableFn})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListRepoActionVariablesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListRepoActionVariablesFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
page, _ := req.GetArguments()["page"].(float64)
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, _ := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
query := url.Values{}
|
||||||
|
query.Set("page", fmt.Sprintf("%d", int(page)))
|
||||||
|
query.Set("limit", fmt.Sprintf("%d", int(pageSize)))
|
||||||
|
|
||||||
|
var result any
|
||||||
|
_, err := gitea.DoJSON(ctx, "GET", fmt.Sprintf("repos/%s/%s/actions/variables", url.PathEscape(owner), url.PathEscape(repo)), query, nil, &result)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("list repo action variables err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetRepoActionVariableFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
name, ok := req.GetArguments()["name"].(string)
|
||||||
|
if !ok || name == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("name is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
variable, _, err := client.GetRepoActionVariable(owner, repo, name)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get repo action variable err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(variable)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called CreateRepoActionVariableFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
name, ok := req.GetArguments()["name"].(string)
|
||||||
|
if !ok || name == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("name is required"))
|
||||||
|
}
|
||||||
|
value, ok := req.GetArguments()["value"].(string)
|
||||||
|
if !ok || value == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("value is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
resp, err := client.CreateRepoActionVariable(owner, repo, name, value)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("create repo action variable err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(map[string]any{"message": "variable created", "status": resp.StatusCode})
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called UpdateRepoActionVariableFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
name, ok := req.GetArguments()["name"].(string)
|
||||||
|
if !ok || name == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("name is required"))
|
||||||
|
}
|
||||||
|
value, ok := req.GetArguments()["value"].(string)
|
||||||
|
if !ok || value == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("value is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
resp, err := client.UpdateRepoActionVariable(owner, repo, name, value)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("update repo action variable err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(map[string]any{"message": "variable updated", "status": resp.StatusCode})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called DeleteRepoActionVariableFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok || owner == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok || repo == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
name, ok := req.GetArguments()["name"].(string)
|
||||||
|
if !ok || name == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("name is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
resp, err := client.DeleteRepoActionVariable(owner, repo, name)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("delete repo action variable err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(map[string]any{"message": "variable deleted", "status": resp.StatusCode})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListOrgActionVariablesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListOrgActionVariablesFn")
|
||||||
|
org, ok := req.GetArguments()["org"].(string)
|
||||||
|
if !ok || org == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("org is required"))
|
||||||
|
}
|
||||||
|
page, _ := req.GetArguments()["page"].(float64)
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, _ := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
variables, _, err := client.ListOrgActionVariable(org, gitea_sdk.ListOrgActionVariableOption{
|
||||||
|
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("list org action variables err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetOrgActionVariableFn")
|
||||||
|
org, ok := req.GetArguments()["org"].(string)
|
||||||
|
if !ok || org == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("org is required"))
|
||||||
|
}
|
||||||
|
name, ok := req.GetArguments()["name"].(string)
|
||||||
|
if !ok || name == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("name is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
variable, _, err := client.GetOrgActionVariable(org, name)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get org action variable err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(variable)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called CreateOrgActionVariableFn")
|
||||||
|
org, ok := req.GetArguments()["org"].(string)
|
||||||
|
if !ok || org == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("org is required"))
|
||||||
|
}
|
||||||
|
name, ok := req.GetArguments()["name"].(string)
|
||||||
|
if !ok || name == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("name is required"))
|
||||||
|
}
|
||||||
|
value, ok := req.GetArguments()["value"].(string)
|
||||||
|
if !ok || value == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("value is required"))
|
||||||
|
}
|
||||||
|
description, _ := req.GetArguments()["description"].(string)
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
resp, err := client.CreateOrgActionVariable(org, gitea_sdk.CreateOrgActionVariableOption{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Description: description,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("create org action variable err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(map[string]any{"message": "variable created", "status": resp.StatusCode})
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called UpdateOrgActionVariableFn")
|
||||||
|
org, ok := req.GetArguments()["org"].(string)
|
||||||
|
if !ok || org == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("org is required"))
|
||||||
|
}
|
||||||
|
name, ok := req.GetArguments()["name"].(string)
|
||||||
|
if !ok || name == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("name is required"))
|
||||||
|
}
|
||||||
|
value, ok := req.GetArguments()["value"].(string)
|
||||||
|
if !ok || value == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("value is required"))
|
||||||
|
}
|
||||||
|
description, _ := req.GetArguments()["description"].(string)
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
resp, err := client.UpdateOrgActionVariable(org, name, gitea_sdk.UpdateOrgActionVariableOption{
|
||||||
|
Value: value,
|
||||||
|
Description: description,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("update org action variable err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(map[string]any{"message": "variable updated", "status": resp.StatusCode})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called DeleteOrgActionVariableFn")
|
||||||
|
org, ok := req.GetArguments()["org"].(string)
|
||||||
|
if !ok || org == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("org is required"))
|
||||||
|
}
|
||||||
|
name, ok := req.GetArguments()["name"].(string)
|
||||||
|
if !ok || name == "" {
|
||||||
|
return to.ErrorResult(fmt.Errorf("name is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := gitea.DoJSON(ctx, "DELETE", fmt.Sprintf("orgs/%s/actions/variables/%s", url.PathEscape(org), url.PathEscape(name)), nil, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("delete org action variable err: %v", err))
|
||||||
|
}
|
||||||
|
return to.TextResult(map[string]any{"message": "variable deleted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -283,10 +283,17 @@ func EditIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes
|
|||||||
if ok {
|
if ok {
|
||||||
opt.Body = ptr.To(body)
|
opt.Body = ptr.To(body)
|
||||||
}
|
}
|
||||||
assignees, ok := req.GetArguments()["assignees"].([]string)
|
var assignees []string
|
||||||
if ok {
|
if assigneesArg, exists := req.GetArguments()["assignees"]; exists {
|
||||||
opt.Assignees = assignees
|
if assigneesSlice, ok := assigneesArg.([]interface{}); ok {
|
||||||
|
for _, assignee := range assigneesSlice {
|
||||||
|
if assigneeStr, ok := assignee.(string); ok {
|
||||||
|
assignees = append(assignees, assigneeStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
opt.Assignees = assignees
|
||||||
milestone, ok := req.GetArguments()["milestone"].(float64)
|
milestone, ok := req.GetArguments()["milestone"].(float64)
|
||||||
if ok {
|
if ok {
|
||||||
opt.Milestone = ptr.To(int64(milestone))
|
opt.Milestone = ptr.To(int64(milestone))
|
||||||
|
|||||||
275
operation/milestone/milestone.go
Normal file
275
operation/milestone/milestone.go
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
package milestone
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/ptr"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/tool"
|
||||||
|
|
||||||
|
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Tool = tool.New()
|
||||||
|
|
||||||
|
const (
|
||||||
|
GetMilestoneToolName = "get_milestone"
|
||||||
|
ListMilestonesToolName = "list_milestones"
|
||||||
|
CreateMilestoneToolName = "create_milestone"
|
||||||
|
EditMilestoneToolName = "edit_milestone"
|
||||||
|
DeleteMilestoneToolName = "delete_milestone"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
GetMilestoneTool = mcp.NewTool(
|
||||||
|
GetMilestoneToolName,
|
||||||
|
mcp.WithDescription("get milestone by id"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("id", mcp.Required(), mcp.Description("milestone id")),
|
||||||
|
)
|
||||||
|
|
||||||
|
ListMilestonesTool = mcp.NewTool(
|
||||||
|
ListMilestonesToolName,
|
||||||
|
mcp.WithDescription("List milestones"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("state", mcp.Description("milestone state"), mcp.DefaultString("all")),
|
||||||
|
mcp.WithString("name", mcp.Description("milestone name")),
|
||||||
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
|
||||||
|
)
|
||||||
|
|
||||||
|
CreateMilestoneTool = mcp.NewTool(
|
||||||
|
CreateMilestoneToolName,
|
||||||
|
mcp.WithDescription("create milestone"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithString("title", mcp.Required(), mcp.Description("milestone title")),
|
||||||
|
mcp.WithString("description", mcp.Description("milestone description")),
|
||||||
|
mcp.WithString("due_on", mcp.Description("due date")),
|
||||||
|
)
|
||||||
|
|
||||||
|
EditMilestoneTool = mcp.NewTool(
|
||||||
|
EditMilestoneToolName,
|
||||||
|
mcp.WithDescription("edit milestone"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("id", mcp.Required(), mcp.Description("milestone id")),
|
||||||
|
mcp.WithString("title", mcp.Description("milestone title")),
|
||||||
|
mcp.WithString("description", mcp.Description("milestone description")),
|
||||||
|
mcp.WithString("due_on", mcp.Description("due date")),
|
||||||
|
mcp.WithString("state", mcp.Description("milestone state, one of open, closed")),
|
||||||
|
)
|
||||||
|
|
||||||
|
DeleteMilestoneTool = mcp.NewTool(
|
||||||
|
DeleteMilestoneToolName,
|
||||||
|
mcp.WithDescription("delete milestone"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("id", mcp.Required(), mcp.Description("milestone id")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: GetMilestoneTool,
|
||||||
|
Handler: GetMilestoneFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: ListMilestonesTool,
|
||||||
|
Handler: ListMilestonesFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: CreateMilestoneTool,
|
||||||
|
Handler: CreateMilestoneFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: EditMilestoneTool,
|
||||||
|
Handler: EditMilestoneFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: DeleteMilestoneTool,
|
||||||
|
Handler: DeleteMilestoneFn,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetMilestoneFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
id, ok := req.GetArguments()["id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("id is required"))
|
||||||
|
}
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
milestone, _, err := client.GetMilestone(owner, repo, int64(id))
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get %v/%v/milestone/%v err: %v", owner, repo, int64(id), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(milestone)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListMilestonesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListMilestonesFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
state, ok := req.GetArguments()["state"].(string)
|
||||||
|
if !ok {
|
||||||
|
state = "all"
|
||||||
|
}
|
||||||
|
name, ok := req.GetArguments()["name"].(string)
|
||||||
|
if !ok {
|
||||||
|
name = ""
|
||||||
|
}
|
||||||
|
page, ok := req.GetArguments()["page"].(float64)
|
||||||
|
if !ok {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, ok := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if !ok {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
opt := gitea_sdk.ListMilestoneOption{
|
||||||
|
State: gitea_sdk.StateType(state),
|
||||||
|
Name: name,
|
||||||
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
|
Page: int(page),
|
||||||
|
PageSize: int(pageSize),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
milestones, _, err := client.ListRepoMilestones(owner, repo, opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get %v/%v/milestones err: %v", owner, repo, err))
|
||||||
|
}
|
||||||
|
return to.TextResult(milestones)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called CreateMilestoneFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
title, ok := req.GetArguments()["title"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("title is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
opt := gitea_sdk.CreateMilestoneOption{
|
||||||
|
Title: title,
|
||||||
|
}
|
||||||
|
|
||||||
|
description, ok := req.GetArguments()["description"].(string)
|
||||||
|
if ok {
|
||||||
|
opt.Description = description
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
milestone, _, err := client.CreateMilestone(owner, repo, opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("create %v/%v/milestone err: %v", owner, repo, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(milestone)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EditMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called EditMilestoneFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
id, ok := req.GetArguments()["id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("id is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
opt := gitea_sdk.EditMilestoneOption{}
|
||||||
|
|
||||||
|
title, ok := req.GetArguments()["title"].(string)
|
||||||
|
if ok {
|
||||||
|
opt.Title = title
|
||||||
|
}
|
||||||
|
description, ok := req.GetArguments()["description"].(string)
|
||||||
|
if ok {
|
||||||
|
opt.Description = ptr.To(description)
|
||||||
|
}
|
||||||
|
state, ok := req.GetArguments()["state"].(string)
|
||||||
|
if ok {
|
||||||
|
opt.State = ptr.To(gitea_sdk.StateType(state))
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
milestone, _, err := client.EditMilestone(owner, repo, int64(id), opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("edit %v/%v/milestone/%v err: %v", owner, repo, int64(id), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(milestone)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called DeleteMilestoneFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
id, ok := req.GetArguments()["id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("id is required"))
|
||||||
|
}
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
_, err = client.DeleteMilestone(owner, repo, int64(id))
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("delete %v/%v/milestone/%v err: %v", owner, repo, int64(id), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult("Milestone deleted successfully")
|
||||||
|
}
|
||||||
@@ -12,6 +12,9 @@ import (
|
|||||||
|
|
||||||
"gitea.com/gitea/gitea-mcp/operation/issue"
|
"gitea.com/gitea/gitea-mcp/operation/issue"
|
||||||
"gitea.com/gitea/gitea-mcp/operation/label"
|
"gitea.com/gitea/gitea-mcp/operation/label"
|
||||||
|
"gitea.com/gitea/gitea-mcp/operation/milestone"
|
||||||
|
"gitea.com/gitea/gitea-mcp/operation/timetracking"
|
||||||
|
"gitea.com/gitea/gitea-mcp/operation/actions"
|
||||||
"gitea.com/gitea/gitea-mcp/operation/pull"
|
"gitea.com/gitea/gitea-mcp/operation/pull"
|
||||||
"gitea.com/gitea/gitea-mcp/operation/repo"
|
"gitea.com/gitea/gitea-mcp/operation/repo"
|
||||||
"gitea.com/gitea/gitea-mcp/operation/search"
|
"gitea.com/gitea/gitea-mcp/operation/search"
|
||||||
@@ -31,6 +34,9 @@ func RegisterTool(s *server.MCPServer) {
|
|||||||
// User Tool
|
// User Tool
|
||||||
s.AddTools(user.Tool.Tools()...)
|
s.AddTools(user.Tool.Tools()...)
|
||||||
|
|
||||||
|
// Actions Tool
|
||||||
|
s.AddTools(actions.Tool.Tools()...)
|
||||||
|
|
||||||
// Repo Tool
|
// Repo Tool
|
||||||
s.AddTools(repo.Tool.Tools()...)
|
s.AddTools(repo.Tool.Tools()...)
|
||||||
|
|
||||||
@@ -40,6 +46,9 @@ func RegisterTool(s *server.MCPServer) {
|
|||||||
// Label Tool
|
// Label Tool
|
||||||
s.AddTools(label.Tool.Tools()...)
|
s.AddTools(label.Tool.Tools()...)
|
||||||
|
|
||||||
|
// Milestone Tool
|
||||||
|
s.AddTools(milestone.Tool.Tools()...)
|
||||||
|
|
||||||
// Pull Tool
|
// Pull Tool
|
||||||
s.AddTools(pull.Tool.Tools()...)
|
s.AddTools(pull.Tool.Tools()...)
|
||||||
|
|
||||||
@@ -52,6 +61,9 @@ func RegisterTool(s *server.MCPServer) {
|
|||||||
// Wiki Tool
|
// Wiki Tool
|
||||||
s.AddTools(wiki.Tool.Tools()...)
|
s.AddTools(wiki.Tool.Tools()...)
|
||||||
|
|
||||||
|
// Time Tracking Tool
|
||||||
|
s.AddTools(timetracking.Tool.Tools()...)
|
||||||
|
|
||||||
s.DeleteTools("")
|
s.DeleteTools("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,18 @@ import (
|
|||||||
var Tool = tool.New()
|
var Tool = tool.New()
|
||||||
|
|
||||||
const (
|
const (
|
||||||
GetPullRequestByIndexToolName = "get_pull_request_by_index"
|
GetPullRequestByIndexToolName = "get_pull_request_by_index"
|
||||||
ListRepoPullRequestsToolName = "list_repo_pull_requests"
|
ListRepoPullRequestsToolName = "list_repo_pull_requests"
|
||||||
CreatePullRequestToolName = "create_pull_request"
|
CreatePullRequestToolName = "create_pull_request"
|
||||||
CreatePullRequestReviewerToolName = "create_pull_request_reviewer"
|
CreatePullRequestReviewerToolName = "create_pull_request_reviewer"
|
||||||
|
DeletePullRequestReviewerToolName = "delete_pull_request_reviewer"
|
||||||
|
ListPullRequestReviewsToolName = "list_pull_request_reviews"
|
||||||
|
GetPullRequestReviewToolName = "get_pull_request_review"
|
||||||
|
ListPullRequestReviewCommentsToolName = "list_pull_request_review_comments"
|
||||||
|
CreatePullRequestReviewToolName = "create_pull_request_review"
|
||||||
|
SubmitPullRequestReviewToolName = "submit_pull_request_review"
|
||||||
|
DeletePullRequestReviewToolName = "delete_pull_request_review"
|
||||||
|
DismissPullRequestReviewToolName = "dismiss_pull_request_review"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -64,6 +72,94 @@ var (
|
|||||||
mcp.WithArray("reviewers", mcp.Description("list of reviewer usernames"), mcp.Items(map[string]interface{}{"type": "string"})),
|
mcp.WithArray("reviewers", mcp.Description("list of reviewer usernames"), mcp.Items(map[string]interface{}{"type": "string"})),
|
||||||
mcp.WithArray("team_reviewers", mcp.Description("list of team reviewer names"), mcp.Items(map[string]interface{}{"type": "string"})),
|
mcp.WithArray("team_reviewers", mcp.Description("list of team reviewer names"), mcp.Items(map[string]interface{}{"type": "string"})),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DeletePullRequestReviewerTool = mcp.NewTool(
|
||||||
|
DeletePullRequestReviewerToolName,
|
||||||
|
mcp.WithDescription("remove reviewer requests from a pull request"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
|
||||||
|
mcp.WithArray("reviewers", mcp.Description("list of reviewer usernames to remove"), mcp.Items(map[string]interface{}{"type": "string"})),
|
||||||
|
mcp.WithArray("team_reviewers", mcp.Description("list of team reviewer names to remove"), mcp.Items(map[string]interface{}{"type": "string"})),
|
||||||
|
)
|
||||||
|
|
||||||
|
ListPullRequestReviewsTool = mcp.NewTool(
|
||||||
|
ListPullRequestReviewsToolName,
|
||||||
|
mcp.WithDescription("list all reviews for a pull request"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
|
||||||
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
|
||||||
|
)
|
||||||
|
|
||||||
|
GetPullRequestReviewTool = mcp.NewTool(
|
||||||
|
GetPullRequestReviewToolName,
|
||||||
|
mcp.WithDescription("get a specific review for a pull request"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
|
||||||
|
mcp.WithNumber("review_id", mcp.Required(), mcp.Description("review ID")),
|
||||||
|
)
|
||||||
|
|
||||||
|
ListPullRequestReviewCommentsTool = mcp.NewTool(
|
||||||
|
ListPullRequestReviewCommentsToolName,
|
||||||
|
mcp.WithDescription("list all comments for a specific pull request review"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
|
||||||
|
mcp.WithNumber("review_id", mcp.Required(), mcp.Description("review ID")),
|
||||||
|
)
|
||||||
|
|
||||||
|
CreatePullRequestReviewTool = mcp.NewTool(
|
||||||
|
CreatePullRequestReviewToolName,
|
||||||
|
mcp.WithDescription("create a review for a pull request"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
|
||||||
|
mcp.WithString("state", mcp.Description("review state"), mcp.Enum("APPROVED", "REQUEST_CHANGES", "COMMENT", "PENDING")),
|
||||||
|
mcp.WithString("body", mcp.Description("review body/comment")),
|
||||||
|
mcp.WithString("commit_id", mcp.Description("commit SHA to review")),
|
||||||
|
mcp.WithArray("comments", mcp.Description("inline review comments (objects with path, body, old_line_num, new_line_num)"), mcp.Items(map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"path": map[string]interface{}{"type": "string", "description": "file path to comment on"},
|
||||||
|
"body": map[string]interface{}{"type": "string", "description": "comment body"},
|
||||||
|
"old_line_num": map[string]interface{}{"type": "number", "description": "line number in the old file (for deletions/changes)"},
|
||||||
|
"new_line_num": map[string]interface{}{"type": "number", "description": "line number in the new file (for additions/changes)"},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
SubmitPullRequestReviewTool = mcp.NewTool(
|
||||||
|
SubmitPullRequestReviewToolName,
|
||||||
|
mcp.WithDescription("submit a pending pull request review"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
|
||||||
|
mcp.WithNumber("review_id", mcp.Required(), mcp.Description("review ID")),
|
||||||
|
mcp.WithString("state", mcp.Required(), mcp.Description("final review state"), mcp.Enum("APPROVED", "REQUEST_CHANGES", "COMMENT")),
|
||||||
|
mcp.WithString("body", mcp.Description("submission message")),
|
||||||
|
)
|
||||||
|
|
||||||
|
DeletePullRequestReviewTool = mcp.NewTool(
|
||||||
|
DeletePullRequestReviewToolName,
|
||||||
|
mcp.WithDescription("delete a pull request review"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
|
||||||
|
mcp.WithNumber("review_id", mcp.Required(), mcp.Description("review ID")),
|
||||||
|
)
|
||||||
|
|
||||||
|
DismissPullRequestReviewTool = mcp.NewTool(
|
||||||
|
DismissPullRequestReviewToolName,
|
||||||
|
mcp.WithDescription("dismiss a pull request review"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
|
||||||
|
mcp.WithNumber("review_id", mcp.Required(), mcp.Description("review ID")),
|
||||||
|
mcp.WithString("message", mcp.Description("dismissal reason")),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -75,6 +171,18 @@ func init() {
|
|||||||
Tool: ListRepoPullRequestsTool,
|
Tool: ListRepoPullRequestsTool,
|
||||||
Handler: ListRepoPullRequestsFn,
|
Handler: ListRepoPullRequestsFn,
|
||||||
})
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: ListPullRequestReviewsTool,
|
||||||
|
Handler: ListPullRequestReviewsFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: GetPullRequestReviewTool,
|
||||||
|
Handler: GetPullRequestReviewFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: ListPullRequestReviewCommentsTool,
|
||||||
|
Handler: ListPullRequestReviewCommentsFn,
|
||||||
|
})
|
||||||
Tool.RegisterWrite(server.ServerTool{
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
Tool: CreatePullRequestTool,
|
Tool: CreatePullRequestTool,
|
||||||
Handler: CreatePullRequestFn,
|
Handler: CreatePullRequestFn,
|
||||||
@@ -83,6 +191,26 @@ func init() {
|
|||||||
Tool: CreatePullRequestReviewerTool,
|
Tool: CreatePullRequestReviewerTool,
|
||||||
Handler: CreatePullRequestReviewerFn,
|
Handler: CreatePullRequestReviewerFn,
|
||||||
})
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: DeletePullRequestReviewerTool,
|
||||||
|
Handler: DeletePullRequestReviewerFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: CreatePullRequestReviewTool,
|
||||||
|
Handler: CreatePullRequestReviewFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: SubmitPullRequestReviewTool,
|
||||||
|
Handler: SubmitPullRequestReviewFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: DeletePullRequestReviewTool,
|
||||||
|
Handler: DeletePullRequestReviewFn,
|
||||||
|
})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{
|
||||||
|
Tool: DismissPullRequestReviewTool,
|
||||||
|
Handler: DismissPullRequestReviewFn,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
@@ -260,3 +388,359 @@ func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (
|
|||||||
|
|
||||||
return to.TextResult(successMsg)
|
return to.TextResult(successMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeletePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called DeletePullRequestReviewerFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var reviewers []string
|
||||||
|
if reviewersArg, exists := req.GetArguments()["reviewers"]; exists {
|
||||||
|
if reviewersSlice, ok := reviewersArg.([]interface{}); ok {
|
||||||
|
for _, reviewer := range reviewersSlice {
|
||||||
|
if reviewerStr, ok := reviewer.(string); ok {
|
||||||
|
reviewers = append(reviewers, reviewerStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var teamReviewers []string
|
||||||
|
if teamReviewersArg, exists := req.GetArguments()["team_reviewers"]; exists {
|
||||||
|
if teamReviewersSlice, ok := teamReviewersArg.([]interface{}); ok {
|
||||||
|
for _, teamReviewer := range teamReviewersSlice {
|
||||||
|
if teamReviewerStr, ok := teamReviewer.(string); ok {
|
||||||
|
teamReviewers = append(teamReviewers, teamReviewerStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.DeleteReviewRequests(owner, repo, int64(index), gitea_sdk.PullReviewRequestOptions{
|
||||||
|
Reviewers: reviewers,
|
||||||
|
TeamReviewers: teamReviewers,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("delete review requests for %v/%v/pr/%v err: %v", owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
successMsg := map[string]interface{}{
|
||||||
|
"message": "Successfully deleted review requests",
|
||||||
|
"reviewers": reviewers,
|
||||||
|
"team_reviewers": teamReviewers,
|
||||||
|
"pr_index": int64(index),
|
||||||
|
"repository": fmt.Sprintf("%s/%s", owner, repo),
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(successMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListPullRequestReviewsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListPullRequestReviewsFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
page, ok := req.GetArguments()["page"].(float64)
|
||||||
|
if !ok {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, ok := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if !ok {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
reviews, _, err := client.ListPullReviews(owner, repo, int64(index), gitea_sdk.ListPullReviewsOptions{
|
||||||
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
|
Page: int(page),
|
||||||
|
PageSize: int(pageSize),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("list reviews for %v/%v/pr/%v err: %v", owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(reviews)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetPullRequestReviewFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
reviewID, ok := req.GetArguments()["review_id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("review_id is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
review, _, err := client.GetPullReview(owner, repo, int64(index), int64(reviewID))
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get review %v for %v/%v/pr/%v err: %v", int64(reviewID), owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(review)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListPullRequestReviewCommentsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListPullRequestReviewCommentsFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
reviewID, ok := req.GetArguments()["review_id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("review_id is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
comments, _, err := client.ListPullReviewComments(owner, repo, int64(index), int64(reviewID))
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("list review comments for review %v on %v/%v/pr/%v err: %v", int64(reviewID), owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(comments)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreatePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called CreatePullRequestReviewFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
opt := gitea_sdk.CreatePullReviewOptions{}
|
||||||
|
|
||||||
|
if state, ok := req.GetArguments()["state"].(string); ok {
|
||||||
|
opt.State = gitea_sdk.ReviewStateType(state)
|
||||||
|
}
|
||||||
|
if body, ok := req.GetArguments()["body"].(string); ok {
|
||||||
|
opt.Body = body
|
||||||
|
}
|
||||||
|
if commitID, ok := req.GetArguments()["commit_id"].(string); ok {
|
||||||
|
opt.CommitID = commitID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse inline comments
|
||||||
|
if commentsArg, exists := req.GetArguments()["comments"]; exists {
|
||||||
|
if commentsSlice, ok := commentsArg.([]interface{}); ok {
|
||||||
|
for _, comment := range commentsSlice {
|
||||||
|
if commentMap, ok := comment.(map[string]interface{}); ok {
|
||||||
|
reviewComment := gitea_sdk.CreatePullReviewComment{}
|
||||||
|
if path, ok := commentMap["path"].(string); ok {
|
||||||
|
reviewComment.Path = path
|
||||||
|
}
|
||||||
|
if body, ok := commentMap["body"].(string); ok {
|
||||||
|
reviewComment.Body = body
|
||||||
|
}
|
||||||
|
if oldLineNum, ok := commentMap["old_line_num"].(float64); ok {
|
||||||
|
reviewComment.OldLineNum = int64(oldLineNum)
|
||||||
|
}
|
||||||
|
if newLineNum, ok := commentMap["new_line_num"].(float64); ok {
|
||||||
|
reviewComment.NewLineNum = int64(newLineNum)
|
||||||
|
}
|
||||||
|
opt.Comments = append(opt.Comments, reviewComment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
review, _, err := client.CreatePullReview(owner, repo, int64(index), opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("create review for %v/%v/pr/%v err: %v", owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(review)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SubmitPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called SubmitPullRequestReviewFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
reviewID, ok := req.GetArguments()["review_id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("review_id is required"))
|
||||||
|
}
|
||||||
|
state, ok := req.GetArguments()["state"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("state is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
opt := gitea_sdk.SubmitPullReviewOptions{
|
||||||
|
State: gitea_sdk.ReviewStateType(state),
|
||||||
|
}
|
||||||
|
if body, ok := req.GetArguments()["body"].(string); ok {
|
||||||
|
opt.Body = body
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
review, _, err := client.SubmitPullReview(owner, repo, int64(index), int64(reviewID), opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("submit review %v for %v/%v/pr/%v err: %v", int64(reviewID), owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(review)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeletePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called DeletePullRequestReviewFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
reviewID, ok := req.GetArguments()["review_id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("review_id is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.DeletePullReview(owner, repo, int64(index), int64(reviewID))
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("delete review %v for %v/%v/pr/%v err: %v", int64(reviewID), owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
successMsg := map[string]interface{}{
|
||||||
|
"message": "Successfully deleted review",
|
||||||
|
"review_id": int64(reviewID),
|
||||||
|
"pr_index": int64(index),
|
||||||
|
"repository": fmt.Sprintf("%s/%s", owner, repo),
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(successMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DismissPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called DismissPullRequestReviewFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
reviewID, ok := req.GetArguments()["review_id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("review_id is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
opt := gitea_sdk.DismissPullReviewOptions{}
|
||||||
|
if message, ok := req.GetArguments()["message"].(string); ok {
|
||||||
|
opt.Message = message
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.DismissPullReview(owner, repo, int64(index), int64(reviewID), opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("dismiss review %v for %v/%v/pr/%v err: %v", int64(reviewID), owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
successMsg := map[string]interface{}{
|
||||||
|
"message": "Successfully dismissed review",
|
||||||
|
"review_id": int64(reviewID),
|
||||||
|
"pr_index": int64(index),
|
||||||
|
"repository": fmt.Sprintf("%s/%s", owner, repo),
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.TextResult(successMsg)
|
||||||
|
}
|
||||||
|
|||||||
376
operation/timetracking/timetracking.go
Normal file
376
operation/timetracking/timetracking.go
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
// Package timetracking provides MCP tools for Gitea time tracking operations
|
||||||
|
package timetracking
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/tool"
|
||||||
|
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Tool = tool.New()
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Stopwatch tools
|
||||||
|
StartStopwatchToolName = "start_stopwatch"
|
||||||
|
StopStopwatchToolName = "stop_stopwatch"
|
||||||
|
DeleteStopwatchToolName = "delete_stopwatch"
|
||||||
|
GetMyStopwatchesToolName = "get_my_stopwatches"
|
||||||
|
|
||||||
|
// Tracked time tools
|
||||||
|
ListTrackedTimesToolName = "list_tracked_times"
|
||||||
|
AddTrackedTimeToolName = "add_tracked_time"
|
||||||
|
DeleteTrackedTimeToolName = "delete_tracked_time"
|
||||||
|
ListRepoTimesToolName = "list_repo_times"
|
||||||
|
GetMyTimesToolName = "get_my_times"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Stopwatch tools
|
||||||
|
StartStopwatchTool = mcp.NewTool(
|
||||||
|
StartStopwatchToolName,
|
||||||
|
mcp.WithDescription("Start a stopwatch on an issue to track time spent"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
|
||||||
|
)
|
||||||
|
|
||||||
|
StopStopwatchTool = mcp.NewTool(
|
||||||
|
StopStopwatchToolName,
|
||||||
|
mcp.WithDescription("Stop a running stopwatch on an issue and record the tracked time"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
|
||||||
|
)
|
||||||
|
|
||||||
|
DeleteStopwatchTool = mcp.NewTool(
|
||||||
|
DeleteStopwatchToolName,
|
||||||
|
mcp.WithDescription("Delete/cancel a running stopwatch without recording time"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
|
||||||
|
)
|
||||||
|
|
||||||
|
GetMyStopwatchesTool = mcp.NewTool(
|
||||||
|
GetMyStopwatchesToolName,
|
||||||
|
mcp.WithDescription("Get all currently running stopwatches for the authenticated user"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tracked time tools
|
||||||
|
ListTrackedTimesTool = mcp.NewTool(
|
||||||
|
ListTrackedTimesToolName,
|
||||||
|
mcp.WithDescription("List tracked times for a specific issue"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
|
||||||
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
|
||||||
|
)
|
||||||
|
|
||||||
|
AddTrackedTimeTool = mcp.NewTool(
|
||||||
|
AddTrackedTimeToolName,
|
||||||
|
mcp.WithDescription("Manually add tracked time to an issue"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
|
||||||
|
mcp.WithNumber("time", mcp.Required(), mcp.Description("time to add in seconds")),
|
||||||
|
)
|
||||||
|
|
||||||
|
DeleteTrackedTimeTool = mcp.NewTool(
|
||||||
|
DeleteTrackedTimeToolName,
|
||||||
|
mcp.WithDescription("Delete a tracked time entry from an issue"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
|
||||||
|
mcp.WithNumber("id", mcp.Required(), mcp.Description("tracked time entry ID")),
|
||||||
|
)
|
||||||
|
|
||||||
|
ListRepoTimesTool = mcp.NewTool(
|
||||||
|
ListRepoTimesToolName,
|
||||||
|
mcp.WithDescription("List all tracked times for a repository"),
|
||||||
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
|
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
|
||||||
|
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
|
||||||
|
)
|
||||||
|
|
||||||
|
GetMyTimesTool = mcp.NewTool(
|
||||||
|
GetMyTimesToolName,
|
||||||
|
mcp.WithDescription("Get all tracked times for the authenticated user"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Stopwatch tools
|
||||||
|
Tool.RegisterWrite(server.ServerTool{Tool: StartStopwatchTool, Handler: StartStopwatchFn})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{Tool: StopStopwatchTool, Handler: StopStopwatchFn})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{Tool: DeleteStopwatchTool, Handler: DeleteStopwatchFn})
|
||||||
|
Tool.RegisterRead(server.ServerTool{Tool: GetMyStopwatchesTool, Handler: GetMyStopwatchesFn})
|
||||||
|
|
||||||
|
// Tracked time tools
|
||||||
|
Tool.RegisterRead(server.ServerTool{Tool: ListTrackedTimesTool, Handler: ListTrackedTimesFn})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{Tool: AddTrackedTimeTool, Handler: AddTrackedTimeFn})
|
||||||
|
Tool.RegisterWrite(server.ServerTool{Tool: DeleteTrackedTimeTool, Handler: DeleteTrackedTimeFn})
|
||||||
|
Tool.RegisterRead(server.ServerTool{Tool: ListRepoTimesTool, Handler: ListRepoTimesFn})
|
||||||
|
Tool.RegisterRead(server.ServerTool{Tool: GetMyTimesTool, Handler: GetMyTimesFn})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stopwatch handler functions
|
||||||
|
|
||||||
|
func StartStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called StartStopwatchFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
_, err = client.StartIssueStopWatch(owner, repo, int64(index))
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("start stopwatch on %s/%s#%d err: %v", owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
return to.TextResult(fmt.Sprintf("Stopwatch started on issue %s/%s#%d", owner, repo, int64(index)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func StopStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called StopStopwatchFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
_, err = client.StopIssueStopWatch(owner, repo, int64(index))
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("stop stopwatch on %s/%s#%d err: %v", owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
return to.TextResult(fmt.Sprintf("Stopwatch stopped on issue %s/%s#%d - time recorded", owner, repo, int64(index)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called DeleteStopwatchFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
_, err = client.DeleteIssueStopwatch(owner, repo, int64(index))
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("delete stopwatch on %s/%s#%d err: %v", owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
return to.TextResult(fmt.Sprintf("Stopwatch deleted/cancelled on issue %s/%s#%d", owner, repo, int64(index)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMyStopwatchesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetMyStopwatchesFn")
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
stopwatches, _, err := client.GetMyStopwatches()
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get stopwatches err: %v", err))
|
||||||
|
}
|
||||||
|
if len(stopwatches) == 0 {
|
||||||
|
return to.TextResult("No active stopwatches")
|
||||||
|
}
|
||||||
|
return to.TextResult(stopwatches)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracked time handler functions
|
||||||
|
|
||||||
|
func ListTrackedTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListTrackedTimesFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
page, ok := req.GetArguments()["page"].(float64)
|
||||||
|
if !ok {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, ok := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if !ok {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
times, _, err := client.ListIssueTrackedTimes(owner, repo, int64(index), gitea_sdk.ListTrackedTimesOptions{
|
||||||
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
|
Page: int(page),
|
||||||
|
PageSize: int(pageSize),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("list tracked times for %s/%s#%d err: %v", owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
if len(times) == 0 {
|
||||||
|
return to.TextResult(fmt.Sprintf("No tracked times for issue %s/%s#%d", owner, repo, int64(index)))
|
||||||
|
}
|
||||||
|
return to.TextResult(times)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called AddTrackedTimeFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
timeSeconds, ok := req.GetArguments()["time"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("time is required"))
|
||||||
|
}
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
trackedTime, _, err := client.AddTime(owner, repo, int64(index), gitea_sdk.AddTimeOption{
|
||||||
|
Time: int64(timeSeconds),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("add tracked time to %s/%s#%d err: %v", owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
return to.TextResult(trackedTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called DeleteTrackedTimeFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
index, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
id, ok := req.GetArguments()["id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("id is required"))
|
||||||
|
}
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
_, err = client.DeleteTime(owner, repo, int64(index), int64(id))
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("delete tracked time %d from %s/%s#%d err: %v", int64(id), owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
return to.TextResult(fmt.Sprintf("Tracked time entry %d deleted from issue %s/%s#%d", int64(id), owner, repo, int64(index)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListRepoTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called ListRepoTimesFn")
|
||||||
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("owner is required"))
|
||||||
|
}
|
||||||
|
repo, ok := req.GetArguments()["repo"].(string)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
page, ok := req.GetArguments()["page"].(float64)
|
||||||
|
if !ok {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize, ok := req.GetArguments()["pageSize"].(float64)
|
||||||
|
if !ok {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
times, _, err := client.ListRepoTrackedTimes(owner, repo, gitea_sdk.ListTrackedTimesOptions{
|
||||||
|
ListOptions: gitea_sdk.ListOptions{
|
||||||
|
Page: int(page),
|
||||||
|
PageSize: int(pageSize),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("list repo tracked times for %s/%s err: %v", owner, repo, err))
|
||||||
|
}
|
||||||
|
if len(times) == 0 {
|
||||||
|
return to.TextResult(fmt.Sprintf("No tracked times for repository %s/%s", owner, repo))
|
||||||
|
}
|
||||||
|
return to.TextResult(times)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMyTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetMyTimesFn")
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
times, _, err := client.GetMyTrackedTimes()
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get tracked times err: %v", err))
|
||||||
|
}
|
||||||
|
if len(times) == 0 {
|
||||||
|
return to.TextResult("No tracked times found")
|
||||||
|
}
|
||||||
|
return to.TextResult(times)
|
||||||
|
}
|
||||||
@@ -2,14 +2,9 @@ package wiki
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
|
||||||
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
"gitea.com/gitea/gitea-mcp/pkg/gitea"
|
||||||
"gitea.com/gitea/gitea-mcp/pkg/log"
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||||
"gitea.com/gitea/gitea-mcp/pkg/to"
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
@@ -122,13 +117,9 @@ func ListWikiPagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
|
|||||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := gitea.ClientFromContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use direct HTTP request because SDK does not support yet wikis
|
// Use direct HTTP request because SDK does not support yet wikis
|
||||||
result, err := makeWikiAPIRequest(ctx, client, "GET", fmt.Sprintf("repos/%s/%s/wiki/pages", url.QueryEscape(owner), url.QueryEscape(repo)), nil)
|
var result any
|
||||||
|
_, err := gitea.DoJSON(ctx, "GET", fmt.Sprintf("repos/%s/%s/wiki/pages", url.QueryEscape(owner), url.QueryEscape(repo)), nil, nil, &result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("list wiki pages err: %v", err))
|
return to.ErrorResult(fmt.Errorf("list wiki pages err: %v", err))
|
||||||
}
|
}
|
||||||
@@ -151,12 +142,8 @@ func GetWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR
|
|||||||
return to.ErrorResult(fmt.Errorf("pageName is required"))
|
return to.ErrorResult(fmt.Errorf("pageName is required"))
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := gitea.ClientFromContext(ctx)
|
var result any
|
||||||
if err != nil {
|
_, err := gitea.DoJSON(ctx, "GET", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil, nil, &result)
|
||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := makeWikiAPIRequest(ctx, client, "GET", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("get wiki page err: %v", err))
|
return to.ErrorResult(fmt.Errorf("get wiki page err: %v", err))
|
||||||
}
|
}
|
||||||
@@ -179,12 +166,8 @@ func GetWikiRevisionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
|
|||||||
return to.ErrorResult(fmt.Errorf("pageName is required"))
|
return to.ErrorResult(fmt.Errorf("pageName is required"))
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := gitea.ClientFromContext(ctx)
|
var result any
|
||||||
if err != nil {
|
_, err := gitea.DoJSON(ctx, "GET", fmt.Sprintf("repos/%s/%s/wiki/revisions/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil, nil, &result)
|
||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := makeWikiAPIRequest(ctx, client, "GET", fmt.Sprintf("repos/%s/%s/wiki/revisions/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("get wiki revisions err: %v", err))
|
return to.ErrorResult(fmt.Errorf("get wiki revisions err: %v", err))
|
||||||
}
|
}
|
||||||
@@ -222,12 +205,8 @@ func CreateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
|
|||||||
"message": message,
|
"message": message,
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := gitea.ClientFromContext(ctx)
|
var result any
|
||||||
if err != nil {
|
_, err := gitea.DoJSON(ctx, "POST", fmt.Sprintf("repos/%s/%s/wiki/new", url.QueryEscape(owner), url.QueryEscape(repo)), nil, requestBody, &result)
|
||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := makeWikiAPIRequest(ctx, client, "POST", fmt.Sprintf("repos/%s/%s/wiki/new", url.QueryEscape(owner), url.QueryEscape(repo)), requestBody)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("create wiki page err: %v", err))
|
return to.ErrorResult(fmt.Errorf("create wiki page err: %v", err))
|
||||||
}
|
}
|
||||||
@@ -272,12 +251,8 @@ func UpdateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
|
|||||||
requestBody["message"] = fmt.Sprintf("Update wiki page '%s'", pageName)
|
requestBody["message"] = fmt.Sprintf("Update wiki page '%s'", pageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := gitea.ClientFromContext(ctx)
|
var result any
|
||||||
if err != nil {
|
_, err := gitea.DoJSON(ctx, "PATCH", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil, requestBody, &result)
|
||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := makeWikiAPIRequest(ctx, client, "PATCH", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), requestBody)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("update wiki page err: %v", err))
|
return to.ErrorResult(fmt.Errorf("update wiki page err: %v", err))
|
||||||
}
|
}
|
||||||
@@ -300,64 +275,10 @@ func DeleteWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
|
|||||||
return to.ErrorResult(fmt.Errorf("pageName is required"))
|
return to.ErrorResult(fmt.Errorf("pageName is required"))
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := gitea.ClientFromContext(ctx)
|
_, err := gitea.DoJSON(ctx, "DELETE", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil, nil, nil)
|
||||||
if err != nil {
|
|
||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = makeWikiAPIRequest(ctx, client, "DELETE", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("delete wiki page err: %v", err))
|
return to.ErrorResult(fmt.Errorf("delete wiki page err: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return to.TextResult(map[string]string{"message": "Wiki page deleted successfully"})
|
return to.TextResult(map[string]string{"message": "Wiki page deleted successfully"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to make HTTP requests to Gitea Wiki API
|
|
||||||
func makeWikiAPIRequest(ctx context.Context, client interface{}, method, path string, body interface{}) (interface{}, error) {
|
|
||||||
// Use flags to get base URL and token
|
|
||||||
apiURL := fmt.Sprintf("%s/api/v1/%s", flag.Host, path)
|
|
||||||
|
|
||||||
httpClient := &http.Client{}
|
|
||||||
|
|
||||||
var reqBody io.Reader
|
|
||||||
if body != nil {
|
|
||||||
bodyBytes, err := json.Marshal(body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
|
||||||
}
|
|
||||||
reqBody = strings.NewReader(string(bodyBytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, method, apiURL, reqBody)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("token %s", flag.Token))
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
if body != nil {
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to make request: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
||||||
return nil, fmt.Errorf("API request failed with status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if method == "DELETE" {
|
|
||||||
return map[string]string{"message": "success"}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var result interface{}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|||||||
175
pkg/gitea/rest.go
Normal file
175
pkg/gitea/rest.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
package gitea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
mcpContext "gitea.com/gitea/gitea-mcp/pkg/context"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTTPError struct {
|
||||||
|
StatusCode int
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HTTPError) Error() string {
|
||||||
|
if e.Body == "" {
|
||||||
|
return fmt.Sprintf("request failed with status %d", e.StatusCode)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("request failed with status %d: %s", e.StatusCode, e.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenFromContext(ctx context.Context) string {
|
||||||
|
if ctx != nil {
|
||||||
|
if token, ok := ctx.Value(mcpContext.TokenContextKey).(string); ok && token != "" {
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flag.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRESTHTTPClient() *http.Client {
|
||||||
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
|
if flag.Insecure {
|
||||||
|
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec
|
||||||
|
}
|
||||||
|
return &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAPIURL(path string, query url.Values) (string, error) {
|
||||||
|
host := strings.TrimRight(flag.Host, "/")
|
||||||
|
if host == "" {
|
||||||
|
return "", fmt.Errorf("gitea host is empty")
|
||||||
|
}
|
||||||
|
p := strings.TrimLeft(path, "/")
|
||||||
|
u, err := url.Parse(fmt.Sprintf("%s/api/v1/%s", host, p))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if query != nil {
|
||||||
|
u.RawQuery = query.Encode()
|
||||||
|
}
|
||||||
|
return u.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoJSON performs an API request and decodes a JSON response into respOut (if non-nil).
|
||||||
|
// It returns the HTTP status code.
|
||||||
|
func DoJSON(ctx context.Context, method, path string, query url.Values, body any, respOut any) (int, error) {
|
||||||
|
var bodyReader io.Reader
|
||||||
|
if body != nil {
|
||||||
|
b, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("marshal request body: %w", err)
|
||||||
|
}
|
||||||
|
bodyReader = bytes.NewReader(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := buildAPIURL(path, query)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, u, bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
token := tokenFromContext(ctx)
|
||||||
|
if token != "" {
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
if body != nil {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := newRESTHTTPClient()
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("do request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
bodySnippet, _ := io.ReadAll(io.LimitReader(resp.Body, 8192))
|
||||||
|
return resp.StatusCode, &HTTPError{StatusCode: resp.StatusCode, Body: strings.TrimSpace(string(bodySnippet))}
|
||||||
|
}
|
||||||
|
|
||||||
|
if respOut == nil {
|
||||||
|
io.Copy(io.Discard, resp.Body) // best-effort
|
||||||
|
return resp.StatusCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(respOut); err != nil {
|
||||||
|
return resp.StatusCode, fmt.Errorf("decode response: %w", err)
|
||||||
|
}
|
||||||
|
return resp.StatusCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoBytes performs an API request and returns the raw response bytes.
|
||||||
|
// It returns the HTTP status code.
|
||||||
|
func DoBytes(ctx context.Context, method, path string, query url.Values, body any, accept string) ([]byte, int, error) {
|
||||||
|
var bodyReader io.Reader
|
||||||
|
if body != nil {
|
||||||
|
b, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("marshal request body: %w", err)
|
||||||
|
}
|
||||||
|
bodyReader = bytes.NewReader(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := buildAPIURL(path, query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, u, bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
token := tokenFromContext(ctx)
|
||||||
|
if token != "" {
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
|
||||||
|
}
|
||||||
|
if accept != "" {
|
||||||
|
req.Header.Set("Accept", accept)
|
||||||
|
}
|
||||||
|
if body != nil {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := newRESTHTTPClient()
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("do request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, resp.StatusCode, fmt.Errorf("read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
bodySnippet := respBytes
|
||||||
|
if len(bodySnippet) > 8192 {
|
||||||
|
bodySnippet = bodySnippet[:8192]
|
||||||
|
}
|
||||||
|
return nil, resp.StatusCode, &HTTPError{StatusCode: resp.StatusCode, Body: strings.TrimSpace(string(bodySnippet))}
|
||||||
|
}
|
||||||
|
|
||||||
|
return respBytes, resp.StatusCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
32
pkg/gitea/rest_test.go
Normal file
32
pkg/gitea/rest_test.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package gitea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
mcpContext "gitea.com/gitea/gitea-mcp/pkg/context"
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTokenFromContext(t *testing.T) {
|
||||||
|
orig := flag.Token
|
||||||
|
defer func() { flag.Token = orig }()
|
||||||
|
|
||||||
|
flag.Token = "flag-token"
|
||||||
|
|
||||||
|
t.Run("context token wins", func(t *testing.T) {
|
||||||
|
ctx := context.WithValue(context.Background(), mcpContext.TokenContextKey, "ctx-token")
|
||||||
|
if got := tokenFromContext(ctx); got != "ctx-token" {
|
||||||
|
t.Fatalf("tokenFromContext() = %q, want %q", got, "ctx-token")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fallback to flag token", func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
if got := tokenFromContext(ctx); got != "flag-token" {
|
||||||
|
t.Fatalf("tokenFromContext() = %q, want %q", got, "flag-token")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user