mirror of
https://gitea.com/gitea/gitea-mcp.git
synced 2026-03-25 14:25:13 +00:00
Add missing tool parameters from Gitea SDK
Expose additional parameters that the Gitea SDK supports but were not yet wired through the MCP tool definitions: - list_issues: labels, since, before filters - issue_write: labels and deadline on create, deadline/remove_deadline on update - pull_request_write: labels/deadline on create/update, remove_deadline on update, force_merge/merge_when_checks_succeed/head_commit_id on merge - list_branches: page/perPage pagination - create_repo: trust_model, object_format_name - label_write: is_archived on create/edit Also adds params.GetOptionalTime helper for RFC3339 timestamp parsing and tests for the most important new parameters. Co-Authored-By: Claude (claude-opus-4-6) <noreply@anthropic.com>
This commit is contained in:
@@ -67,9 +67,15 @@ var (
|
||||
mcp.WithNumber("milestone", mcp.Description("milestone number (for 'update')")),
|
||||
mcp.WithString("state", mcp.Description("PR state (for 'update')"), mcp.Enum("open", "closed")),
|
||||
mcp.WithBoolean("allow_maintainer_edit", mcp.Description("allow maintainer to edit (for 'update')")),
|
||||
mcp.WithArray("labels", mcp.Description("array of label IDs (for 'create', 'update')"), mcp.Items(map[string]any{"type": "number"})),
|
||||
mcp.WithString("deadline", mcp.Description("due date in ISO 8601 format (for 'create', 'update')")),
|
||||
mcp.WithBoolean("remove_deadline", mcp.Description("unset due date (for 'update')")),
|
||||
mcp.WithString("merge_style", mcp.Description("merge style (for 'merge')"), mcp.Enum("merge", "rebase", "rebase-merge", "squash", "fast-forward-only"), mcp.DefaultString("merge")),
|
||||
mcp.WithString("message", mcp.Description("merge commit message (for 'merge') or dismissal reason")),
|
||||
mcp.WithBoolean("delete_branch", mcp.Description("delete branch after merge (for 'merge')")),
|
||||
mcp.WithBoolean("force_merge", mcp.Description("force merge even if checks are not passing (for 'merge')")),
|
||||
mcp.WithBoolean("merge_when_checks_succeed", mcp.Description("auto-merge when checks succeed (for 'merge')")),
|
||||
mcp.WithString("head_commit_id", mcp.Description("expected head commit SHA for merge conflict detection (for 'merge')")),
|
||||
mcp.WithArray("reviewers", mcp.Description("reviewer usernames (for 'add_reviewers', 'remove_reviewers')"), mcp.Items(map[string]any{"type": "string"})),
|
||||
mcp.WithArray("team_reviewers", mcp.Description("team reviewer names (for 'add_reviewers', 'remove_reviewers')"), mcp.Items(map[string]any{"type": "string"})),
|
||||
mcp.WithBoolean("draft", mcp.Description("mark PR as draft (for 'create', 'update'). Gitea uses a 'WIP: ' title prefix for drafts.")),
|
||||
@@ -331,12 +337,17 @@ func createPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Cal
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
pr, _, err := client.CreatePullRequest(owner, repo, gitea_sdk.CreatePullRequestOption{
|
||||
opt := gitea_sdk.CreatePullRequestOption{
|
||||
Title: title,
|
||||
Body: body,
|
||||
Head: head,
|
||||
Base: base,
|
||||
})
|
||||
}
|
||||
if labelIDs, err := params.GetInt64Slice(args, "labels"); err == nil {
|
||||
opt.Labels = labelIDs
|
||||
}
|
||||
opt.Deadline = params.GetOptionalTime(args, "deadline")
|
||||
pr, _, err := client.CreatePullRequest(owner, repo, opt)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("create %v/%v/pull_request err: %v", owner, repo, err))
|
||||
}
|
||||
@@ -751,11 +762,18 @@ func mergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
|
||||
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
||||
}
|
||||
|
||||
forceMerge, _ := args["force_merge"].(bool)
|
||||
mergeWhenChecksSucceed, _ := args["merge_when_checks_succeed"].(bool)
|
||||
headCommitID, _ := args["head_commit_id"].(string)
|
||||
|
||||
opt := gitea_sdk.MergePullRequestOption{
|
||||
Style: gitea_sdk.MergeStyle(mergeStyle),
|
||||
Title: title,
|
||||
Message: message,
|
||||
DeleteBranchAfterMerge: deleteBranch,
|
||||
ForceMerge: forceMerge,
|
||||
MergeWhenChecksSucceed: mergeWhenChecksSucceed,
|
||||
HeadCommitId: headCommitID,
|
||||
}
|
||||
|
||||
merged, resp, err := client.MergePullRequest(owner, repo, index, opt)
|
||||
@@ -842,6 +860,13 @@ func editPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
|
||||
if allowMaintainerEdit, ok := args["allow_maintainer_edit"].(bool); ok {
|
||||
opt.AllowMaintainerEdit = new(allowMaintainerEdit)
|
||||
}
|
||||
if labelIDs, err := params.GetInt64Slice(args, "labels"); err == nil {
|
||||
opt.Labels = labelIDs
|
||||
}
|
||||
opt.Deadline = params.GetOptionalTime(args, "deadline")
|
||||
if removeDeadline, ok := args["remove_deadline"].(bool); ok {
|
||||
opt.RemoveDeadline = &removeDeadline
|
||||
}
|
||||
|
||||
client, err := gitea.ClientFromContext(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -254,6 +254,168 @@ func Test_mergePullRequestFn(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_mergePullRequestFn_newParams(t *testing.T) {
|
||||
const (
|
||||
owner = "octo"
|
||||
repo = "demo"
|
||||
index = 8
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
gotBody map[string]any
|
||||
)
|
||||
|
||||
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("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index):
|
||||
mu.Lock()
|
||||
var body map[string]any
|
||||
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||
gotBody = body
|
||||
mu.Unlock()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
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),
|
||||
"merge_style": "merge",
|
||||
"force_merge": true,
|
||||
"merge_when_checks_succeed": true,
|
||||
"head_commit_id": "abc123",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := mergePullRequestFn(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("mergePullRequestFn() error = %v", err)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if gotBody["force_merge"] != true {
|
||||
t.Fatalf("expected force_merge true, got %v", gotBody["force_merge"])
|
||||
}
|
||||
if gotBody["merge_when_checks_succeed"] != true {
|
||||
t.Fatalf("expected merge_when_checks_succeed true, got %v", gotBody["merge_when_checks_succeed"])
|
||||
}
|
||||
if gotBody["head_commit_id"] != "abc123" {
|
||||
t.Fatalf("expected head_commit_id 'abc123', got %v", gotBody["head_commit_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func Test_createPullRequestFn_labels(t *testing.T) {
|
||||
const (
|
||||
owner = "octo"
|
||||
repo = "demo"
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
gotBody map[string]any
|
||||
)
|
||||
|
||||
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("/api/v1/repos/%s/%s/pulls", owner, repo):
|
||||
mu.Lock()
|
||||
var body map[string]any
|
||||
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||
gotBody = body
|
||||
mu.Unlock()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"number":1,"title":"test","state":"open"}`))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
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,
|
||||
"title": "test",
|
||||
"body": "body",
|
||||
"head": "feature",
|
||||
"base": "main",
|
||||
"labels": []any{float64(1), float64(2)},
|
||||
"deadline": "2026-06-01T00:00:00Z",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := createPullRequestFn(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("createPullRequestFn() error = %v", err)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
labels, ok := gotBody["labels"].([]any)
|
||||
if !ok || len(labels) != 2 {
|
||||
t.Fatalf("expected 2 labels, got %v", gotBody["labels"])
|
||||
}
|
||||
if labels[0] != float64(1) || labels[1] != float64(2) {
|
||||
t.Fatalf("expected labels [1,2], got %v", labels)
|
||||
}
|
||||
if gotBody["due_date"] == nil {
|
||||
t.Fatalf("expected due_date to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_applyDraftPrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user