mirror of
https://gitea.com/gitea/gitea-mcp.git
synced 2026-02-06 06:45:12 +00:00
added support for get_pull_request_diff (#119)
This function call is needed to be able to do AI code review to actually get the diff from the PR. Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/119 Reviewed-by: silverwind <silverwind@noreply.gitea.com> Reviewed-by: hiifong <f@f.style> Co-authored-by: Gustav <tvarsis@hotmail.com> Co-committed-by: Gustav <tvarsis@hotmail.com>
This commit is contained in:
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
|
||||||
@@ -209,6 +209,7 @@ 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 |
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ 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"
|
||||||
@@ -40,6 +41,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"),
|
||||||
@@ -167,6 +177,10 @@ 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,
|
||||||
@@ -239,6 +253,43 @@ func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp
|
|||||||
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, ok := req.GetArguments()["index"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return to.ErrorResult(fmt.Errorf("index is required"))
|
||||||
|
}
|
||||||
|
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, int64(index), gitea_sdk.PullRequestDiffOptions{
|
||||||
|
Binary: binary,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v diff err: %v", owner, repo, int64(index), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"diff": string(diffBytes),
|
||||||
|
"binary": binary,
|
||||||
|
"index": int64(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)
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user