diff --git a/cmd/gitea-mcp/main.go b/cmd/gitea-mcp/main.go index 504ee3e..d1674b5 100644 --- a/cmd/gitea-mcp/main.go +++ b/cmd/gitea-mcp/main.go @@ -32,6 +32,7 @@ func main() { reg.Register(tools.NewFileRead(giteaClient, ownerAllow)) reg.Register(tools.NewFileWriteBranch(giteaClient, ownerAllow)) reg.Register(tools.NewPRCreate(giteaClient, ownerAllow)) + reg.Register(tools.NewPRGet(giteaClient, ownerAllow)) mcpSrv := mcp.NewServer(mcp.ServerOptions{ Registry: reg, diff --git a/internal/tools/pr_get.go b/internal/tools/pr_get.go new file mode 100644 index 0000000..87334c0 --- /dev/null +++ b/internal/tools/pr_get.go @@ -0,0 +1,68 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + + "gitea.d-ma.be/mathias/gitea-mcp/internal/allowlist" + "gitea.d-ma.be/mathias/gitea-mcp/internal/gitea" + "gitea.d-ma.be/mathias/gitea-mcp/internal/registry" +) + +type PRGet struct { + c *gitea.Client + a *allowlist.Allowlist +} + +func NewPRGet(c *gitea.Client, a *allowlist.Allowlist) *PRGet { return &PRGet{c: c, a: a} } + +func (t *PRGet) Descriptor() registry.ToolDescriptor { + return registry.ToolDescriptor{ + Name: "pr_get", + Description: "Get a pull request by number.", + InputSchema: json.RawMessage(`{ + "type":"object", + "properties":{ + "owner":{"type":"string"}, + "name":{"type":"string"}, + "number":{"type":"integer","minimum":1} + }, + "required":["owner","name","number"] + }`), + } +} + +type prGetArgs struct { + Owner string `json:"owner"` + Name string `json:"name"` + Number int `json:"number"` +} + +func (t *PRGet) Call(ctx context.Context, raw json.RawMessage) (json.RawMessage, error) { + var args prGetArgs + if err := parseArgs(raw, &args); err != nil { + return nil, err + } + if err := t.a.Check(args.Owner); err != nil { + return nil, err + } + if args.Number < 1 { + return nil, fmt.Errorf("number must be >= 1: %w", gitea.ErrValidation) + } + + pr, err := t.c.GetPullRequest(ctx, args.Owner, args.Name, args.Number) + if err != nil { + return nil, err + } + + return textOK(map[string]any{ + "number": pr.Number, + "title": pr.Title, + "html_url": pr.HTMLURL, + "head": pr.Head.Ref, + "base": pr.Base.Ref, + "state": pr.State, + "draft": pr.Draft, + }) +} diff --git a/internal/tools/pr_get_test.go b/internal/tools/pr_get_test.go new file mode 100644 index 0000000..c230b13 --- /dev/null +++ b/internal/tools/pr_get_test.go @@ -0,0 +1,61 @@ +package tools_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "gitea.d-ma.be/mathias/gitea-mcp/internal/allowlist" + "gitea.d-ma.be/mathias/gitea-mcp/internal/gitea" + "gitea.d-ma.be/mathias/gitea-mcp/internal/tools" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPRGetTool(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v1/repos/o/r/pulls/42", r.URL.Path) + assert.Equal(t, http.MethodGet, r.Method) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{ + "number": 42, + "title": "Fix bug Y", + "body": "Fixes Y", + "html_url": "http://example.com/pulls/42", + "state": "open", + "draft": true, + "head": {"ref": "fix/y"}, + "base": {"ref": "main"} + }`)) + })) + defer srv.Close() + + tool := tools.NewPRGet(gitea.NewClient(srv.URL, "tok"), allowlist.New([]string{"o"})) + out, err := tool.Call(context.Background(), json.RawMessage(`{"owner":"o","name":"r","number":42}`)) + require.NoError(t, err) + + var result map[string]any + require.NoError(t, json.Unmarshal(out, &result)) + assert.Equal(t, float64(42), result["number"]) + assert.Equal(t, "Fix bug Y", result["title"]) + assert.Equal(t, "http://example.com/pulls/42", result["html_url"]) + assert.Equal(t, "fix/y", result["head"]) + assert.Equal(t, "main", result["base"]) + assert.Equal(t, "open", result["state"]) + assert.Equal(t, true, result["draft"]) +} + +func TestPRGetAllowlistRejects(t *testing.T) { + tool := tools.NewPRGet(gitea.NewClient("http://unused", ""), allowlist.New([]string{"allowed"})) + _, err := tool.Call(context.Background(), json.RawMessage(`{"owner":"evil","name":"r","number":1}`)) + require.Error(t, err) +} + +func TestPRGetRequiresValidNumber(t *testing.T) { + tool := tools.NewPRGet(gitea.NewClient("http://unused", ""), allowlist.New([]string{"o"})) + _, err := tool.Call(context.Background(), json.RawMessage(`{"owner":"o","name":"r","number":0}`)) + require.Error(t, err) + assert.ErrorIs(t, err, gitea.ErrValidation) +}