mirror of
https://gitea.com/gitea/gitea-mcp.git
synced 2026-02-27 17:15:13 +00:00
Compare commits
9 Commits
v0.6.0
...
feat/accep
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1620f1d6a9 | ||
|
|
b8630b5b9a | ||
|
|
71dbc9d6da | ||
|
|
1f7392305f | ||
|
|
c3b24d65fe | ||
|
|
dcd01441c5 | ||
|
|
2dbfc62042 | ||
|
|
e851f542f5 | ||
|
|
b8f2377f47 |
71
AGENTS.md
Normal file
71
AGENTS.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file provides guidance to AI coding agents when working with code in this repository.
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
**Build**: `make build` - Build the gitea-mcp binary
|
||||||
|
**Install**: `make install` - Build and install to GOPATH/bin
|
||||||
|
**Clean**: `make clean` - Remove build artifacts
|
||||||
|
**Test**: `go test ./...` - Run all tests
|
||||||
|
**Hot reload**: `make dev` - Start development server with hot reload (requires air)
|
||||||
|
**Dependencies**: `make vendor` - Tidy and verify module dependencies
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
This is a **Gitea MCP (Model Context Protocol) Server** written in Go that provides MCP tools for interacting with Gitea repositories, issues, pull requests, users, and more.
|
||||||
|
|
||||||
|
**Core Components**:
|
||||||
|
|
||||||
|
- `main.go` + `cmd/cmd.go`: CLI entry point and flag parsing
|
||||||
|
- `operation/operation.go`: Main server setup and tool registration
|
||||||
|
- `pkg/tool/tool.go`: Tool registry with read/write categorization
|
||||||
|
- `operation/*/`: Individual tool modules (user, repo, issue, pull, search, wiki, etc.)
|
||||||
|
|
||||||
|
**Transport Modes**:
|
||||||
|
|
||||||
|
- **stdio** (default): Standard input/output for MCP clients
|
||||||
|
- **http**: HTTP server mode on configurable port (default 8080)
|
||||||
|
|
||||||
|
**Authentication**:
|
||||||
|
|
||||||
|
- Global token via `--token` flag or `GITEA_ACCESS_TOKEN` env var
|
||||||
|
- HTTP mode supports per-request Bearer token override in Authorization header
|
||||||
|
- Token precedence: HTTP Authorization header > CLI flag > environment variable
|
||||||
|
|
||||||
|
**Tool Organization**:
|
||||||
|
|
||||||
|
- Tools are categorized as read-only or write operations
|
||||||
|
- `--read-only` flag exposes only read tools
|
||||||
|
- Tool modules register via `Tool.RegisterRead()` and `Tool.RegisterWrite()`
|
||||||
|
|
||||||
|
**Key Configuration**:
|
||||||
|
|
||||||
|
- Default Gitea host: `https://gitea.com` (override with `--host` or `GITEA_HOST`)
|
||||||
|
- Environment variables can override CLI flags: `MCP_MODE`, `GITEA_READONLY`, `GITEA_DEBUG`, `GITEA_INSECURE`
|
||||||
|
- Logs are written to `~/.gitea-mcp/gitea-mcp.log` with rotation
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
The server provides 40+ MCP tools covering:
|
||||||
|
|
||||||
|
- **User**: get_my_user_info, get_user_orgs, search_users
|
||||||
|
- **Repository**: create_repo, fork_repo, list_my_repos, search_repos
|
||||||
|
- **Branches/Tags**: create_branch, delete_branch, list_branches, create_tag, list_tags
|
||||||
|
- **Files**: get_file_content, create_file, update_file, delete_file, get_dir_content
|
||||||
|
- **Issues**: create_issue, list_repo_issues, create_issue_comment, edit_issue
|
||||||
|
- **Pull Requests**: create_pull_request, list_repo_pull_requests, get_pull_request_by_index
|
||||||
|
- **Releases**: create_release, list_releases, get_latest_release
|
||||||
|
- **Wiki**: create_wiki_page, update_wiki_page, list_wiki_pages
|
||||||
|
- **Search**: search_repos, search_users, search_org_teams
|
||||||
|
- **Version**: get_gitea_mcp_server_version
|
||||||
|
|
||||||
|
## Common Development Patterns
|
||||||
|
|
||||||
|
**Testing**: Use `go test ./operation -run TestFunctionName` for specific tests
|
||||||
|
|
||||||
|
**Token Context**: HTTP requests use `pkg/context.TokenContextKey` for request-scoped token access
|
||||||
|
|
||||||
|
**Flag Access**: All packages access configuration via global variables in `pkg/flag/flag.go`
|
||||||
|
|
||||||
|
**Graceful Shutdown**: HTTP mode implements graceful shutdown with 10-second timeout on SIGTERM/SIGINT
|
||||||
23
README.md
23
README.md
@@ -13,6 +13,7 @@
|
|||||||
- [What is Gitea?](#what-is-gitea)
|
- [What is Gitea?](#what-is-gitea)
|
||||||
- [What is MCP?](#what-is-mcp)
|
- [What is MCP?](#what-is-mcp)
|
||||||
- [🚧 Installation](#-installation)
|
- [🚧 Installation](#-installation)
|
||||||
|
- [Usage with Claude Code](#usage-with-claude-code)
|
||||||
- [Usage with VS Code](#usage-with-vs-code)
|
- [Usage with VS Code](#usage-with-vs-code)
|
||||||
- [📥 Download the official binary release](#-download-the-official-binary-release)
|
- [📥 Download the official binary release](#-download-the-official-binary-release)
|
||||||
- [🔧 Build from Source](#-build-from-source)
|
- [🔧 Build from Source](#-build-from-source)
|
||||||
@@ -32,6 +33,17 @@ Model Context Protocol (MCP) is a protocol that allows for the integration of va
|
|||||||
|
|
||||||
## 🚧 Installation
|
## 🚧 Installation
|
||||||
|
|
||||||
|
### Usage with Claude Code
|
||||||
|
|
||||||
|
This method uses `go run` and requires [Go](https://go.dev) to be installed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp add --transport stdio --scope user gitea \
|
||||||
|
--env GITEA_ACCESS_TOKEN=token \
|
||||||
|
--env GITEA_HOST=https://gitea.com \
|
||||||
|
-- go run gitea.com/gitea/gitea-mcp@latest -t stdio
|
||||||
|
```
|
||||||
|
|
||||||
### Usage with VS Code
|
### Usage with VS Code
|
||||||
|
|
||||||
For quick installation, use one of the one-click install buttons at the top of this README.
|
For quick installation, use one of the one-click install buttons at the top of this README.
|
||||||
@@ -165,7 +177,7 @@ 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 |
|
||||||
@@ -197,9 +209,18 @@ The Gitea MCP Server supports the following tools:
|
|||||||
| 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 |
|
||||||
|
| get_pull_request_diff | Pull Request | Get a pull request diff |
|
||||||
| 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 |
|
||||||
|
| delete_pull_request_reviewer | Pull Request | Remove reviewers from a pull request |
|
||||||
|
| list_pull_request_reviews | Pull Request | List all reviews for a pull request |
|
||||||
|
| get_pull_request_review | Pull Request | Get a specific review by ID |
|
||||||
|
| list_pull_request_review_comments | Pull Request | List inline comments for a review |
|
||||||
|
| create_pull_request_review | Pull Request | Create a review with optional inline comments |
|
||||||
|
| submit_pull_request_review | Pull Request | Submit a pending review |
|
||||||
|
| delete_pull_request_review | Pull Request | Delete a review |
|
||||||
|
| dismiss_pull_request_review | Pull Request | Dismiss a review with optional message |
|
||||||
| search_users | User | Search for users |
|
| search_users | User | Search for users |
|
||||||
| search_org_teams | Organization | Search for teams in an organization |
|
| search_org_teams | Organization | Search for teams in an organization |
|
||||||
| list_org_labels | Organization | List labels defined at organization level |
|
| list_org_labels | Organization | List labels defined at organization level |
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
- [什么是 Gitea?](#什么是-gitea)
|
- [什么是 Gitea?](#什么是-gitea)
|
||||||
- [什么是 MCP?](#什么是-mcp)
|
- [什么是 MCP?](#什么是-mcp)
|
||||||
- [🚧 安装](#-安装)
|
- [🚧 安装](#-安装)
|
||||||
|
- [在 Claude Code 中使用](#在-claude-code-中使用)
|
||||||
- [在 VS Code 中使用](#在-vs-code-中使用)
|
- [在 VS Code 中使用](#在-vs-code-中使用)
|
||||||
- [📥 下载官方二进制版本](#-下载官方二进制版本)
|
- [📥 下载官方二进制版本](#-下载官方二进制版本)
|
||||||
- [🔧 从源码构建](#-从源码构建)
|
- [🔧 从源码构建](#-从源码构建)
|
||||||
@@ -32,6 +33,17 @@ Model Context Protocol (MCP) 是一种协议,允许通过聊天界面整合各
|
|||||||
|
|
||||||
## 🚧 安装
|
## 🚧 安装
|
||||||
|
|
||||||
|
### 在 Claude Code 中使用
|
||||||
|
|
||||||
|
此方式使用 `go run`,需要安装 [Go](https://go.dev)。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp add --transport stdio --scope user gitea \
|
||||||
|
--env GITEA_ACCESS_TOKEN=token \
|
||||||
|
--env GITEA_HOST=https://gitea.com \
|
||||||
|
-- go run gitea.com/gitea/gitea-mcp@latest -t stdio
|
||||||
|
```
|
||||||
|
|
||||||
### 在 VS Code 中使用
|
### 在 VS Code 中使用
|
||||||
|
|
||||||
要快速安装,请使用本 README 顶部的安装按钮。
|
要快速安装,请使用本 README 顶部的安装按钮。
|
||||||
@@ -165,7 +177,7 @@ 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 | 仓库 | 创建新仓库 |
|
||||||
@@ -200,6 +212,14 @@ Gitea MCP 服务器支持以下工具:
|
|||||||
| list_repo_pull_requests | 拉取请求 | 列出所有拉取请求 |
|
| list_repo_pull_requests | 拉取请求 | 列出所有拉取请求 |
|
||||||
| create_pull_request | 拉取请求 | 创建新拉取请求 |
|
| create_pull_request | 拉取请求 | 创建新拉取请求 |
|
||||||
| create_pull_request_reviewer | 拉取请求 | 为拉取请求添加审查者 |
|
| create_pull_request_reviewer | 拉取请求 | 为拉取请求添加审查者 |
|
||||||
|
| delete_pull_request_reviewer | 拉取请求 | 移除拉取请求的审查者 |
|
||||||
|
| list_pull_request_reviews | 拉取请求 | 列出拉取请求的所有审查 |
|
||||||
|
| get_pull_request_review | 拉取请求 | 按 ID 获取特定审查 |
|
||||||
|
| list_pull_request_review_comments | 拉取请求 | 列出审查的行内评论 |
|
||||||
|
| create_pull_request_review | 拉取请求 | 创建审查(可含行内评论) |
|
||||||
|
| submit_pull_request_review | 拉取请求 | 提交待处理的审查 |
|
||||||
|
| delete_pull_request_review | 拉取请求 | 删除审查 |
|
||||||
|
| dismiss_pull_request_review | 拉取请求 | 驳回审查(可附消息) |
|
||||||
| search_users | 用户 | 搜索用户 |
|
| search_users | 用户 | 搜索用户 |
|
||||||
| search_org_teams | 组织 | 搜索组织团队 |
|
| search_org_teams | 组织 | 搜索组织团队 |
|
||||||
| list_org_labels | 组织 | 列出组织标签 |
|
| list_org_labels | 组织 | 列出组织标签 |
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
- [什麼是 Gitea?](#什麼是-gitea)
|
- [什麼是 Gitea?](#什麼是-gitea)
|
||||||
- [什麼是 MCP?](#什麼是-mcp)
|
- [什麼是 MCP?](#什麼是-mcp)
|
||||||
- [🚧 安裝](#-安裝)
|
- [🚧 安裝](#-安裝)
|
||||||
|
- [在 Claude Code 中使用](#在-claude-code-中使用)
|
||||||
- [在 VS Code 中使用](#在-vs-code-中使用)
|
- [在 VS Code 中使用](#在-vs-code-中使用)
|
||||||
- [📥 下載官方二進位版本](#-下載官方二進位版本)
|
- [📥 下載官方二進位版本](#-下載官方二進位版本)
|
||||||
- [🔧 從原始碼建置](#-從原始碼建置)
|
- [🔧 從原始碼建置](#-從原始碼建置)
|
||||||
@@ -32,6 +33,17 @@ Model Context Protocol (MCP) 是一種協議,允許透過聊天介面整合各
|
|||||||
|
|
||||||
## 🚧 安裝
|
## 🚧 安裝
|
||||||
|
|
||||||
|
### 在 Claude Code 中使用
|
||||||
|
|
||||||
|
此方式使用 `go run`,需要安裝 [Go](https://go.dev)。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp add --transport stdio --scope user gitea \
|
||||||
|
--env GITEA_ACCESS_TOKEN=token \
|
||||||
|
--env GITEA_HOST=https://gitea.com \
|
||||||
|
-- go run gitea.com/gitea/gitea-mcp@latest -t stdio
|
||||||
|
```
|
||||||
|
|
||||||
### 在 VS Code 中使用
|
### 在 VS Code 中使用
|
||||||
|
|
||||||
欲快速安裝,請使用本 README 頂部的安裝按鈕。
|
欲快速安裝,請使用本 README 頂部的安裝按鈕。
|
||||||
@@ -165,7 +177,7 @@ 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 | 倉庫 | 創建新倉庫 |
|
||||||
@@ -200,6 +212,14 @@ Gitea MCP 伺服器支援以下工具:
|
|||||||
| list_repo_pull_requests | 拉取請求 | 列出所有拉取請求 |
|
| list_repo_pull_requests | 拉取請求 | 列出所有拉取請求 |
|
||||||
| create_pull_request | 拉取請求 | 創建新拉取請求 |
|
| create_pull_request | 拉取請求 | 創建新拉取請求 |
|
||||||
| create_pull_request_reviewer | 拉取請求 | 為拉取請求添加審查者 |
|
| create_pull_request_reviewer | 拉取請求 | 為拉取請求添加審查者 |
|
||||||
|
| delete_pull_request_reviewer | 拉取請求 | 移除拉取請求的審查者 |
|
||||||
|
| list_pull_request_reviews | 拉取請求 | 列出拉取請求的所有審查 |
|
||||||
|
| get_pull_request_review | 拉取請求 | 依 ID 取得特定審查 |
|
||||||
|
| list_pull_request_review_comments | 拉取請求 | 列出審查的行內評論 |
|
||||||
|
| create_pull_request_review | 拉取請求 | 創建審查(可含行內評論) |
|
||||||
|
| submit_pull_request_review | 拉取請求 | 提交待處理的審查 |
|
||||||
|
| delete_pull_request_review | 拉取請求 | 刪除審查 |
|
||||||
|
| dismiss_pull_request_review | 拉取請求 | 駁回審查(可附訊息) |
|
||||||
| search_users | 用戶 | 搜尋用戶 |
|
| search_users | 用戶 | 搜尋用戶 |
|
||||||
| search_org_teams | 組織 | 搜尋組織團隊 |
|
| search_org_teams | 組織 | 搜尋組織團隊 |
|
||||||
| list_org_labels | 組織 | 列出組織標籤 |
|
| list_org_labels | 組織 | 列出組織標籤 |
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ var (
|
|||||||
mcp.WithDescription("Get a repository Actions workflow by ID"),
|
mcp.WithDescription("Get a repository Actions workflow by ID"),
|
||||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
mcp.WithNumber("workflow_id", mcp.Required(), mcp.Description("workflow ID")),
|
mcp.WithString("workflow_id", mcp.Required(), mcp.Description("workflow ID or filename (e.g. 'my-workflow.yml')")),
|
||||||
)
|
)
|
||||||
|
|
||||||
DispatchRepoActionWorkflowTool = mcp.NewTool(
|
DispatchRepoActionWorkflowTool = mcp.NewTool(
|
||||||
@@ -51,7 +51,7 @@ var (
|
|||||||
mcp.WithDescription("Trigger (dispatch) a repository Actions workflow"),
|
mcp.WithDescription("Trigger (dispatch) a repository Actions workflow"),
|
||||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||||
mcp.WithNumber("workflow_id", mcp.Required(), mcp.Description("workflow ID")),
|
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.WithString("ref", mcp.Required(), mcp.Description("git ref (branch or tag)")),
|
||||||
mcp.WithObject("inputs", mcp.Description("workflow inputs object")),
|
mcp.WithObject("inputs", mcp.Description("workflow inputs object")),
|
||||||
)
|
)
|
||||||
@@ -187,15 +187,15 @@ func GetRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp
|
|||||||
if !ok || repo == "" {
|
if !ok || repo == "" {
|
||||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
}
|
}
|
||||||
workflowID, ok := req.GetArguments()["workflow_id"].(float64)
|
workflowID, ok := req.GetArguments()["workflow_id"].(string)
|
||||||
if !ok || workflowID <= 0 {
|
if !ok || workflowID == "" {
|
||||||
return to.ErrorResult(fmt.Errorf("workflow_id is required"))
|
return to.ErrorResult(fmt.Errorf("workflow_id is required"))
|
||||||
}
|
}
|
||||||
|
|
||||||
var result any
|
var result any
|
||||||
_, _, err := doJSONWithFallback(ctx, "GET",
|
_, _, err := doJSONWithFallback(ctx, "GET",
|
||||||
[]string{
|
[]string{
|
||||||
fmt.Sprintf("repos/%s/%s/actions/workflows/%d", url.PathEscape(owner), url.PathEscape(repo), int64(workflowID)),
|
fmt.Sprintf("repos/%s/%s/actions/workflows/%s", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(workflowID)),
|
||||||
},
|
},
|
||||||
nil, nil, &result,
|
nil, nil, &result,
|
||||||
)
|
)
|
||||||
@@ -215,8 +215,8 @@ func DispatchRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest)
|
|||||||
if !ok || repo == "" {
|
if !ok || repo == "" {
|
||||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
}
|
}
|
||||||
workflowID, ok := req.GetArguments()["workflow_id"].(float64)
|
workflowID, ok := req.GetArguments()["workflow_id"].(string)
|
||||||
if !ok || workflowID <= 0 {
|
if !ok || workflowID == "" {
|
||||||
return to.ErrorResult(fmt.Errorf("workflow_id is required"))
|
return to.ErrorResult(fmt.Errorf("workflow_id is required"))
|
||||||
}
|
}
|
||||||
ref, ok := req.GetArguments()["ref"].(string)
|
ref, ok := req.GetArguments()["ref"].(string)
|
||||||
@@ -245,8 +245,8 @@ func DispatchRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest)
|
|||||||
|
|
||||||
_, _, err := doJSONWithFallback(ctx, "POST",
|
_, _, err := doJSONWithFallback(ctx, "POST",
|
||||||
[]string{
|
[]string{
|
||||||
fmt.Sprintf("repos/%s/%s/actions/workflows/%d/dispatches", url.PathEscape(owner), url.PathEscape(repo), int64(workflowID)),
|
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/%d/dispatch", url.PathEscape(owner), url.PathEscape(repo), int64(workflowID)),
|
fmt.Sprintf("repos/%s/%s/actions/workflows/%s/dispatch", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(workflowID)),
|
||||||
},
|
},
|
||||||
nil, body, nil,
|
nil, body, nil,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"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/params"
|
||||||
"gitea.com/gitea/gitea-mcp/pkg/ptr"
|
"gitea.com/gitea/gitea-mcp/pkg/ptr"
|
||||||
"gitea.com/gitea/gitea-mcp/pkg/to"
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
"gitea.com/gitea/gitea-mcp/pkg/tool"
|
"gitea.com/gitea/gitea-mcp/pkg/tool"
|
||||||
@@ -136,17 +137,17 @@ func GetIssueByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
|
|||||||
if !ok {
|
if !ok {
|
||||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
}
|
}
|
||||||
index, ok := req.GetArguments()["index"].(float64)
|
index, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
if !ok {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("index is required"))
|
return to.ErrorResult(err)
|
||||||
}
|
}
|
||||||
client, err := gitea.ClientFromContext(ctx)
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
}
|
}
|
||||||
issue, _, err := client.GetIssue(owner, repo, int64(index))
|
issue, _, err := client.GetIssue(owner, repo, index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("get %v/%v/issue/%v err: %v", owner, repo, int64(index), err))
|
return to.ErrorResult(fmt.Errorf("get %v/%v/issue/%v err: %v", owner, repo, index, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return to.TextResult(issue)
|
return to.TextResult(issue)
|
||||||
@@ -235,9 +236,9 @@ func CreateIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
|
|||||||
if !ok {
|
if !ok {
|
||||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
}
|
}
|
||||||
index, ok := req.GetArguments()["index"].(float64)
|
index, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
if !ok {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("index is required"))
|
return to.ErrorResult(err)
|
||||||
}
|
}
|
||||||
body, ok := req.GetArguments()["body"].(string)
|
body, ok := req.GetArguments()["body"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -250,9 +251,9 @@ func CreateIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
}
|
}
|
||||||
issueComment, _, err := client.CreateIssueComment(owner, repo, int64(index), opt)
|
issueComment, _, err := client.CreateIssueComment(owner, repo, index, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("create %v/%v/issue/%v/comment err: %v", owner, repo, int64(index), err))
|
return to.ErrorResult(fmt.Errorf("create %v/%v/issue/%v/comment err: %v", owner, repo, index, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return to.TextResult(issueComment)
|
return to.TextResult(issueComment)
|
||||||
@@ -268,9 +269,9 @@ func EditIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes
|
|||||||
if !ok {
|
if !ok {
|
||||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
}
|
}
|
||||||
index, ok := req.GetArguments()["index"].(float64)
|
index, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
if !ok {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("index is required"))
|
return to.ErrorResult(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
opt := gitea_sdk.EditIssueOption{}
|
opt := gitea_sdk.EditIssueOption{}
|
||||||
@@ -307,9 +308,9 @@ func EditIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
}
|
}
|
||||||
issue, _, err := client.EditIssue(owner, repo, int64(index), opt)
|
issue, _, err := client.EditIssue(owner, repo, index, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("edit %v/%v/issue/%v err: %v", owner, repo, int64(index), err))
|
return to.ErrorResult(fmt.Errorf("edit %v/%v/issue/%v err: %v", owner, repo, index, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return to.TextResult(issue)
|
return to.TextResult(issue)
|
||||||
@@ -358,18 +359,18 @@ func GetIssueCommentsByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*m
|
|||||||
if !ok {
|
if !ok {
|
||||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
}
|
}
|
||||||
index, ok := req.GetArguments()["index"].(float64)
|
index, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
if !ok {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("index is required"))
|
return to.ErrorResult(err)
|
||||||
}
|
}
|
||||||
opt := gitea_sdk.ListIssueCommentOptions{}
|
opt := gitea_sdk.ListIssueCommentOptions{}
|
||||||
client, err := gitea.ClientFromContext(ctx)
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
}
|
}
|
||||||
issue, _, err := client.ListIssueComments(owner, repo, int64(index), opt)
|
issue, _, err := client.ListIssueComments(owner, repo, index, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("get %v/%v/issues/%v/comments err: %v", owner, repo, int64(index), err))
|
return to.ErrorResult(fmt.Errorf("get %v/%v/issues/%v/comments err: %v", owner, repo, index, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return to.TextResult(issue)
|
return to.TextResult(issue)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"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/params"
|
||||||
"gitea.com/gitea/gitea-mcp/pkg/ptr"
|
"gitea.com/gitea/gitea-mcp/pkg/ptr"
|
||||||
"gitea.com/gitea/gitea-mcp/pkg/to"
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
"gitea.com/gitea/gitea-mcp/pkg/tool"
|
"gitea.com/gitea/gitea-mcp/pkg/tool"
|
||||||
@@ -380,9 +381,9 @@ func AddIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
|
|||||||
if !ok {
|
if !ok {
|
||||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
}
|
}
|
||||||
index, ok := req.GetArguments()["index"].(float64)
|
index, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
if !ok {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("issue index is required"))
|
return to.ErrorResult(err)
|
||||||
}
|
}
|
||||||
labelsRaw, ok := req.GetArguments()["labels"].([]interface{})
|
labelsRaw, ok := req.GetArguments()["labels"].([]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -405,9 +406,9 @@ func AddIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
}
|
}
|
||||||
issueLabels, _, err := client.AddIssueLabels(owner, repo, int64(index), opt)
|
issueLabels, _, err := client.AddIssueLabels(owner, repo, index, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("add labels to %v/%v/issue/%v err: %v", owner, repo, int64(index), err))
|
return to.ErrorResult(fmt.Errorf("add labels to %v/%v/issue/%v err: %v", owner, repo, index, err))
|
||||||
}
|
}
|
||||||
return to.TextResult(issueLabels)
|
return to.TextResult(issueLabels)
|
||||||
}
|
}
|
||||||
@@ -422,9 +423,9 @@ func ReplaceIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
|
|||||||
if !ok {
|
if !ok {
|
||||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
}
|
}
|
||||||
index, ok := req.GetArguments()["index"].(float64)
|
index, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
if !ok {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("issue index is required"))
|
return to.ErrorResult(err)
|
||||||
}
|
}
|
||||||
labelsRaw, ok := req.GetArguments()["labels"].([]interface{})
|
labelsRaw, ok := req.GetArguments()["labels"].([]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -447,9 +448,9 @@ func ReplaceIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
}
|
}
|
||||||
issueLabels, _, err := client.ReplaceIssueLabels(owner, repo, int64(index), opt)
|
issueLabels, _, err := client.ReplaceIssueLabels(owner, repo, index, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("replace labels on %v/%v/issue/%v err: %v", owner, repo, int64(index), err))
|
return to.ErrorResult(fmt.Errorf("replace labels on %v/%v/issue/%v err: %v", owner, repo, index, err))
|
||||||
}
|
}
|
||||||
return to.TextResult(issueLabels)
|
return to.TextResult(issueLabels)
|
||||||
}
|
}
|
||||||
@@ -464,18 +465,18 @@ func ClearIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
|
|||||||
if !ok {
|
if !ok {
|
||||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
}
|
}
|
||||||
index, ok := req.GetArguments()["index"].(float64)
|
index, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
if !ok {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("issue index is required"))
|
return to.ErrorResult(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := gitea.ClientFromContext(ctx)
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
}
|
}
|
||||||
_, err = client.ClearIssueLabels(owner, repo, int64(index))
|
_, err = client.ClearIssueLabels(owner, repo, index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("clear labels on %v/%v/issue/%v err: %v", owner, repo, int64(index), err))
|
return to.ErrorResult(fmt.Errorf("clear labels on %v/%v/issue/%v err: %v", owner, repo, index, err))
|
||||||
}
|
}
|
||||||
return to.TextResult("Labels cleared successfully")
|
return to.TextResult("Labels cleared successfully")
|
||||||
}
|
}
|
||||||
@@ -490,9 +491,9 @@ func RemoveIssueLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
|
|||||||
if !ok {
|
if !ok {
|
||||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
}
|
}
|
||||||
index, ok := req.GetArguments()["index"].(float64)
|
index, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
if !ok {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("issue index is required"))
|
return to.ErrorResult(err)
|
||||||
}
|
}
|
||||||
labelID, ok := req.GetArguments()["label_id"].(float64)
|
labelID, ok := req.GetArguments()["label_id"].(float64)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -503,9 +504,9 @@ func RemoveIssueLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
}
|
}
|
||||||
_, err = client.DeleteIssueLabel(owner, repo, int64(index), int64(labelID))
|
_, err = client.DeleteIssueLabel(owner, repo, index, int64(labelID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("remove label %v from %v/%v/issue/%v err: %v", int64(labelID), owner, repo, int64(index), err))
|
return to.ErrorResult(fmt.Errorf("remove label %v from %v/%v/issue/%v err: %v", int64(labelID), owner, repo, index, err))
|
||||||
}
|
}
|
||||||
return to.TextResult("Label removed successfully")
|
return to.TextResult("Label removed successfully")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ 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/milestone"
|
||||||
|
"gitea.com/gitea/gitea-mcp/operation/timetracking"
|
||||||
"gitea.com/gitea/gitea-mcp/operation/actions"
|
"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"
|
||||||
@@ -60,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("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"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/params"
|
||||||
"gitea.com/gitea/gitea-mcp/pkg/to"
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||||
"gitea.com/gitea/gitea-mcp/pkg/tool"
|
"gitea.com/gitea/gitea-mcp/pkg/tool"
|
||||||
|
|
||||||
@@ -18,9 +19,18 @@ var Tool = tool.New()
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
GetPullRequestByIndexToolName = "get_pull_request_by_index"
|
GetPullRequestByIndexToolName = "get_pull_request_by_index"
|
||||||
|
GetPullRequestDiffToolName = "get_pull_request_diff"
|
||||||
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 (
|
||||||
@@ -32,6 +42,15 @@ var (
|
|||||||
mcp.WithNumber("index", mcp.Required(), mcp.Description("repository pull request index")),
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("repository pull request index")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
GetPullRequestDiffTool = mcp.NewTool(
|
||||||
|
GetPullRequestDiffToolName,
|
||||||
|
mcp.WithDescription("get pull request diff"),
|
||||||
|
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("repository pull request index")),
|
||||||
|
mcp.WithBoolean("binary", mcp.Description("whether to include binary file changes")),
|
||||||
|
)
|
||||||
|
|
||||||
ListRepoPullRequestsTool = mcp.NewTool(
|
ListRepoPullRequestsTool = mcp.NewTool(
|
||||||
ListRepoPullRequestsToolName,
|
ListRepoPullRequestsToolName,
|
||||||
mcp.WithDescription("List repository pull requests"),
|
mcp.WithDescription("List repository pull requests"),
|
||||||
@@ -64,6 +83,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() {
|
||||||
@@ -71,10 +178,26 @@ func init() {
|
|||||||
Tool: GetPullRequestByIndexTool,
|
Tool: GetPullRequestByIndexTool,
|
||||||
Handler: GetPullRequestByIndexFn,
|
Handler: GetPullRequestByIndexFn,
|
||||||
})
|
})
|
||||||
|
Tool.RegisterRead(server.ServerTool{
|
||||||
|
Tool: GetPullRequestDiffTool,
|
||||||
|
Handler: GetPullRequestDiffFn,
|
||||||
|
})
|
||||||
Tool.RegisterRead(server.ServerTool{
|
Tool.RegisterRead(server.ServerTool{
|
||||||
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 +206,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) {
|
||||||
@@ -95,22 +238,59 @@ func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp
|
|||||||
if !ok {
|
if !ok {
|
||||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
}
|
}
|
||||||
index, ok := req.GetArguments()["index"].(float64)
|
index, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
if !ok {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("index is required"))
|
return to.ErrorResult(err)
|
||||||
}
|
}
|
||||||
client, err := gitea.ClientFromContext(ctx)
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
}
|
}
|
||||||
pr, _, err := client.GetPullRequest(owner, repo, int64(index))
|
pr, _, err := client.GetPullRequest(owner, repo, index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v err: %v", owner, repo, int64(index), err))
|
return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v err: %v", owner, repo, index, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return to.TextResult(pr)
|
return to.TextResult(pr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetPullRequestDiffFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
log.Debugf("Called GetPullRequestDiffFn")
|
||||||
|
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, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(err)
|
||||||
|
}
|
||||||
|
binary, _ := req.GetArguments()["binary"].(bool)
|
||||||
|
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
diffBytes, _, err := client.GetPullRequestDiff(owner, repo, index, gitea_sdk.PullRequestDiffOptions{
|
||||||
|
Binary: binary,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v diff err: %v", owner, repo, index, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"diff": string(diffBytes),
|
||||||
|
"binary": binary,
|
||||||
|
"index": index,
|
||||||
|
"repo": repo,
|
||||||
|
"owner": owner,
|
||||||
|
}
|
||||||
|
return to.TextResult(result)
|
||||||
|
}
|
||||||
|
|
||||||
func ListRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
func ListRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
log.Debugf("Called ListRepoPullRequests")
|
log.Debugf("Called ListRepoPullRequests")
|
||||||
owner, ok := req.GetArguments()["owner"].(string)
|
owner, ok := req.GetArguments()["owner"].(string)
|
||||||
@@ -209,9 +389,9 @@ func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (
|
|||||||
if !ok {
|
if !ok {
|
||||||
return to.ErrorResult(fmt.Errorf("repo is required"))
|
return to.ErrorResult(fmt.Errorf("repo is required"))
|
||||||
}
|
}
|
||||||
index, ok := req.GetArguments()["index"].(float64)
|
index, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
if !ok {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("index is required"))
|
return to.ErrorResult(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var reviewers []string
|
var reviewers []string
|
||||||
@@ -241,12 +421,12 @@ func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (
|
|||||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = client.CreateReviewRequests(owner, repo, int64(index), gitea_sdk.PullReviewRequestOptions{
|
_, err = client.CreateReviewRequests(owner, repo, index, gitea_sdk.PullReviewRequestOptions{
|
||||||
Reviewers: reviewers,
|
Reviewers: reviewers,
|
||||||
TeamReviewers: teamReviewers,
|
TeamReviewers: teamReviewers,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return to.ErrorResult(fmt.Errorf("create review requests for %v/%v/pr/%v err: %v", owner, repo, int64(index), err))
|
return to.ErrorResult(fmt.Errorf("create review requests for %v/%v/pr/%v err: %v", owner, repo, index, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a success message instead of the Response object which contains non-serializable functions
|
// Return a success message instead of the Response object which contains non-serializable functions
|
||||||
@@ -254,7 +434,363 @@ func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (
|
|||||||
"message": "Successfully created review requests",
|
"message": "Successfully created review requests",
|
||||||
"reviewers": reviewers,
|
"reviewers": reviewers,
|
||||||
"team_reviewers": teamReviewers,
|
"team_reviewers": teamReviewers,
|
||||||
"pr_index": int64(index),
|
"pr_index": index,
|
||||||
|
"repository": fmt.Sprintf("%s/%s", owner, repo),
|
||||||
|
}
|
||||||
|
|
||||||
|
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, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 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, index, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
successMsg := map[string]interface{}{
|
||||||
|
"message": "Successfully deleted review requests",
|
||||||
|
"reviewers": reviewers,
|
||||||
|
"team_reviewers": teamReviewers,
|
||||||
|
"pr_index": 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, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(err)
|
||||||
|
}
|
||||||
|
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, 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, 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, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(err)
|
||||||
|
}
|
||||||
|
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, 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, 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, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(err)
|
||||||
|
}
|
||||||
|
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, 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, 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, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, index, opt)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("create review for %v/%v/pr/%v err: %v", owner, repo, 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, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(err)
|
||||||
|
}
|
||||||
|
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, 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, 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, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(err)
|
||||||
|
}
|
||||||
|
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, 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, index, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
successMsg := map[string]interface{}{
|
||||||
|
"message": "Successfully deleted review",
|
||||||
|
"review_id": int64(reviewID),
|
||||||
|
"pr_index": 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, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(err)
|
||||||
|
}
|
||||||
|
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, 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, index, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
successMsg := map[string]interface{}{
|
||||||
|
"message": "Successfully dismissed review",
|
||||||
|
"review_id": int64(reviewID),
|
||||||
|
"pr_index": index,
|
||||||
"repository": fmt.Sprintf("%s/%s", owner, repo),
|
"repository": fmt.Sprintf("%s/%s", owner, repo),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
140
operation/pull/pull_test.go
Normal file
140
operation/pull/pull_test.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package pull
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetPullRequestDiffFn(t *testing.T) {
|
||||||
|
const (
|
||||||
|
owner = "octo"
|
||||||
|
repo = "demo"
|
||||||
|
index = 12
|
||||||
|
diffRaw = "diff --git a/file.txt b/file.txt\n+line\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
diffRequested bool
|
||||||
|
binaryValue string
|
||||||
|
)
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/v1/version":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"version":"1.12.0"}`))
|
||||||
|
case fmt.Sprintf("/api/v1/repos/%s/%s", owner, repo):
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"private":false}`))
|
||||||
|
case fmt.Sprintf("/%s/%s/pulls/%d.diff", owner, repo, index):
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
select {
|
||||||
|
case errCh <- fmt.Errorf("unexpected method: %s", r.Method):
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
diffRequested = true
|
||||||
|
binaryValue = r.URL.Query().Get("binary")
|
||||||
|
mu.Unlock()
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
_, _ = w.Write([]byte(diffRaw))
|
||||||
|
default:
|
||||||
|
select {
|
||||||
|
case errCh <- fmt.Errorf("unexpected request path: %s", r.URL.Path):
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server := httptest.NewServer(handler)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
origHost := flag.Host
|
||||||
|
origToken := flag.Token
|
||||||
|
origVersion := flag.Version
|
||||||
|
flag.Host = server.URL
|
||||||
|
flag.Token = ""
|
||||||
|
flag.Version = "test"
|
||||||
|
defer func() {
|
||||||
|
flag.Host = origHost
|
||||||
|
flag.Token = origToken
|
||||||
|
flag.Version = origVersion
|
||||||
|
}()
|
||||||
|
|
||||||
|
req := mcp.CallToolRequest{
|
||||||
|
Params: mcp.CallToolParams{
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"owner": owner,
|
||||||
|
"repo": repo,
|
||||||
|
"index": float64(index),
|
||||||
|
"binary": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := GetPullRequestDiffFn(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetPullRequestDiffFn() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case reqErr := <-errCh:
|
||||||
|
t.Fatalf("handler error: %v", reqErr)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
requested := diffRequested
|
||||||
|
gotBinary := binaryValue
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
if !requested {
|
||||||
|
t.Fatalf("expected diff request to be made")
|
||||||
|
}
|
||||||
|
if gotBinary != "true" {
|
||||||
|
t.Fatalf("expected binary=true query param, got %q", gotBinary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Content) == 0 {
|
||||||
|
t.Fatalf("expected content in result")
|
||||||
|
}
|
||||||
|
|
||||||
|
textContent, ok := mcp.AsTextContent(result.Content[0])
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected text content, got %T", result.Content[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed struct {
|
||||||
|
Result map[string]any `json:"Result"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(textContent.Text), &parsed); err != nil {
|
||||||
|
t.Fatalf("unmarshal result text: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, ok := parsed.Result["diff"].(string); !ok || got != diffRaw {
|
||||||
|
t.Fatalf("diff = %q, want %q", got, diffRaw)
|
||||||
|
}
|
||||||
|
if got, ok := parsed.Result["binary"].(bool); !ok || got != true {
|
||||||
|
t.Fatalf("binary = %v, want true", got)
|
||||||
|
}
|
||||||
|
if got, ok := parsed.Result["index"].(float64); !ok || int64(got) != int64(index) {
|
||||||
|
t.Fatalf("index = %v, want %d", got, index)
|
||||||
|
}
|
||||||
|
if got, ok := parsed.Result["owner"].(string); !ok || got != owner {
|
||||||
|
t.Fatalf("owner = %q, want %q", got, owner)
|
||||||
|
}
|
||||||
|
if got, ok := parsed.Result["repo"].(string); !ok || got != repo {
|
||||||
|
t.Fatalf("repo = %q, want %q", got, repo)
|
||||||
|
}
|
||||||
|
}
|
||||||
377
operation/timetracking/timetracking.go
Normal file
377
operation/timetracking/timetracking.go
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
// 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/params"
|
||||||
|
"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, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(err)
|
||||||
|
}
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
_, err = client.StartIssueStopWatch(owner, repo, index)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("start stopwatch on %s/%s#%d err: %v", owner, repo, index, err))
|
||||||
|
}
|
||||||
|
return to.TextResult(fmt.Sprintf("Stopwatch started on issue %s/%s#%d", owner, repo, 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, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(err)
|
||||||
|
}
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
_, err = client.StopIssueStopWatch(owner, repo, index)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("stop stopwatch on %s/%s#%d err: %v", owner, repo, index, err))
|
||||||
|
}
|
||||||
|
return to.TextResult(fmt.Sprintf("Stopwatch stopped on issue %s/%s#%d - time recorded", owner, repo, 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, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(err)
|
||||||
|
}
|
||||||
|
client, err := gitea.ClientFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||||
|
}
|
||||||
|
_, err = client.DeleteIssueStopwatch(owner, repo, index)
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("delete stopwatch on %s/%s#%d err: %v", owner, repo, index, err))
|
||||||
|
}
|
||||||
|
return to.TextResult(fmt.Sprintf("Stopwatch deleted/cancelled on issue %s/%s#%d", owner, repo, 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, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(err)
|
||||||
|
}
|
||||||
|
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, 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, index, err))
|
||||||
|
}
|
||||||
|
if len(times) == 0 {
|
||||||
|
return to.TextResult(fmt.Sprintf("No tracked times for issue %s/%s#%d", owner, repo, 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, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 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, 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, err := params.GetIndex(req.GetArguments(), "index")
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(err)
|
||||||
|
}
|
||||||
|
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, 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, index, err))
|
||||||
|
}
|
||||||
|
return to.TextResult(fmt.Sprintf("Tracked time entry %d deleted from issue %s/%s#%d", int64(id), owner, repo, 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)
|
||||||
|
}
|
||||||
33
pkg/params/params.go
Normal file
33
pkg/params/params.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package params
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetIndex extracts an index parameter from MCP tool arguments.
|
||||||
|
// It accepts both numeric (float64 from JSON) and string representations.
|
||||||
|
// This provides better UX for LLM callers that may naturally use strings
|
||||||
|
// for identifiers like issue/PR numbers.
|
||||||
|
func GetIndex(args map[string]interface{}, key string) (int64, error) {
|
||||||
|
val, exists := args[key]
|
||||||
|
if !exists {
|
||||||
|
return 0, fmt.Errorf("%s is required", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try float64 (JSON number type)
|
||||||
|
if f, ok := val.(float64); ok {
|
||||||
|
return int64(f), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try string and parse to integer
|
||||||
|
if s, ok := val.(string); ok {
|
||||||
|
i, err := strconv.ParseInt(s, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("%s must be a valid integer (got %q)", key, s)
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, fmt.Errorf("%s must be a number or numeric string", key)
|
||||||
|
}
|
||||||
104
pkg/params/params_test.go
Normal file
104
pkg/params/params_test.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package params
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetIndex(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args map[string]interface{}
|
||||||
|
key string
|
||||||
|
wantIndex int64
|
||||||
|
wantErr bool
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid float64",
|
||||||
|
args: map[string]interface{}{"index": float64(123)},
|
||||||
|
key: "index",
|
||||||
|
wantIndex: 123,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid string",
|
||||||
|
args: map[string]interface{}{"index": "456"},
|
||||||
|
key: "index",
|
||||||
|
wantIndex: 456,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid string with large number",
|
||||||
|
args: map[string]interface{}{"index": "999999"},
|
||||||
|
key: "index",
|
||||||
|
wantIndex: 999999,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing parameter",
|
||||||
|
args: map[string]interface{}{},
|
||||||
|
key: "index",
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "index is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid string (not a number)",
|
||||||
|
args: map[string]interface{}{"index": "abc"},
|
||||||
|
key: "index",
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "must be a valid integer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid string (decimal)",
|
||||||
|
args: map[string]interface{}{"index": "12.34"},
|
||||||
|
key: "index",
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "must be a valid integer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid type (bool)",
|
||||||
|
args: map[string]interface{}{"index": true},
|
||||||
|
key: "index",
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "must be a number or numeric string",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid type (map)",
|
||||||
|
args: map[string]interface{}{"index": map[string]string{"foo": "bar"}},
|
||||||
|
key: "index",
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "must be a number or numeric string",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom key name",
|
||||||
|
args: map[string]interface{}{"pr_index": "789"},
|
||||||
|
key: "pr_index",
|
||||||
|
wantIndex: 789,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gotIndex, err := GetIndex(tt.args, tt.key)
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("GetIndex() expected error but got nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
|
||||||
|
t.Errorf("GetIndex() error = %v, want error containing %q", err, tt.errMsg)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetIndex() unexpected error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if gotIndex != tt.wantIndex {
|
||||||
|
t.Errorf("GetIndex() = %v, want %v", gotIndex, tt.wantIndex)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user