From c3db4fb65f6925c8bedb4454a8fe585af22e2223 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 5 Mar 2026 05:56:23 +0000 Subject: [PATCH] feat: slim tool responses (#141) Reduce token usage by slimming tool responses. Instead of returning full Gitea SDK objects (with nested user/repo objects, avatars, permissions, etc.), each operation now has a colocated `slim.go` that extracts only the fields an LLM needs. List endpoints return even fewer fields than single-item endpoints. Other changes: - Add `params` helpers to DRY parameter extraction across 40+ handlers - Remove `{"Result": ...}` wrapper for flatter responses - Reduce default pageSize from 100 to 30 Fixes: https://gitea.com/gitea/gitea-mcp/issues/128 *Created by Claude on behalf of @silverwind* Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/141 Reviewed-by: Lunny Xiao Co-authored-by: silverwind Co-committed-by: silverwind --- operation/actions/logs.go | 32 +- operation/actions/runs.go | 142 ++++---- operation/actions/secrets.go | 76 ++--- operation/actions/slim.go | 92 +++++ operation/actions/variables.go | 128 ++++--- operation/issue/issue.go | 144 ++++---- operation/issue/slim.go | 116 +++++++ operation/issue/slim_test.go | 69 ++++ operation/label/label.go | 192 ++++++----- operation/label/slim.go | 26 ++ operation/label/slim_test.go | 25 ++ operation/milestone/milestone.go | 94 +++-- operation/milestone/slim.go | 28 ++ operation/pull/pull.go | 453 +++++++++++-------------- operation/pull/pull_test.go | 44 +-- operation/pull/slim.go | 191 +++++++++++ operation/pull/slim_test.go | 124 +++++++ operation/repo/branch.go | 59 ++-- operation/repo/commit.go | 30 +- operation/repo/file.go | 135 ++++---- operation/repo/release.go | 135 +++----- operation/repo/repo.go | 58 ++-- operation/repo/slim.go | 201 +++++++++++ operation/repo/slim_test.go | 142 ++++++++ operation/repo/tag.go | 100 +++--- operation/search/search.go | 58 ++-- operation/search/slim.go | 88 +++++ operation/timetracking/slim.go | 47 +++ operation/timetracking/timetracking.go | 113 +++--- operation/user/slim.go | 42 +++ operation/user/slim_test.go | 39 +++ operation/user/user.go | 19 +- operation/wiki/wiki.go | 140 ++++---- pkg/params/params.go | 41 +++ pkg/to/to.go | 7 +- 35 files changed, 2274 insertions(+), 1156 deletions(-) create mode 100644 operation/actions/slim.go create mode 100644 operation/issue/slim.go create mode 100644 operation/issue/slim_test.go create mode 100644 operation/label/slim.go create mode 100644 operation/label/slim_test.go create mode 100644 operation/milestone/slim.go create mode 100644 operation/pull/slim.go create mode 100644 operation/pull/slim_test.go create mode 100644 operation/repo/slim.go create mode 100644 operation/repo/slim_test.go create mode 100644 operation/search/slim.go create mode 100644 operation/timetracking/slim.go create mode 100644 operation/user/slim.go create mode 100644 operation/user/slim_test.go diff --git a/operation/actions/logs.go b/operation/actions/logs.go index cd99efd..b9ca705 100644 --- a/operation/actions/logs.go +++ b/operation/actions/logs.go @@ -109,17 +109,17 @@ func limitBytes(data []byte, maxBytes int) ([]byte, bool) { func GetRepoActionJobLogPreviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetRepoActionJobLogPreviewFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } jobID, err := params.GetIndex(req.GetArguments(), "job_id") - if err != nil || jobID <= 0 { - return to.ErrorResult(errors.New("job_id is required")) + if err != nil { + return to.ErrorResult(err) } tailLines := int(params.GetOptionalInt(req.GetArguments(), "tail_lines", 200)) maxBytes := int(params.GetOptionalInt(req.GetArguments(), "max_bytes", 65536)) @@ -144,17 +144,17 @@ func GetRepoActionJobLogPreviewFn(ctx context.Context, req mcp.CallToolRequest) func DownloadRepoActionJobLogFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called DownloadRepoActionJobLogFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } jobID, err := params.GetIndex(req.GetArguments(), "job_id") - if err != nil || jobID <= 0 { - return to.ErrorResult(errors.New("job_id is required")) + if err != nil { + return to.ErrorResult(err) } outputPath, _ := req.GetArguments()["output_path"].(string) diff --git a/operation/actions/runs.go b/operation/actions/runs.go index 1fb632b..da9d915 100644 --- a/operation/actions/runs.go +++ b/operation/actions/runs.go @@ -39,7 +39,7 @@ var ( mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(50), mcp.Min(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30), mcp.Min(1)), ) GetRepoActionWorkflowTool = mcp.NewTool( @@ -66,7 +66,7 @@ var ( mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(50), mcp.Min(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30), mcp.Min(1)), mcp.WithString("status", mcp.Description("optional status filter")), ) @@ -100,7 +100,7 @@ var ( mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(50), mcp.Min(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30), mcp.Min(1)), mcp.WithString("status", mcp.Description("optional status filter")), ) @@ -111,7 +111,7 @@ var ( mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithNumber("run_id", mcp.Required(), mcp.Description("run ID")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(50), mcp.Min(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30), mcp.Min(1)), ) ) @@ -148,22 +148,21 @@ func doJSONWithFallback(ctx context.Context, method string, paths []string, quer func ListRepoActionWorkflowsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListRepoActionWorkflowsFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { return to.ErrorResult(errors.New("repo is required")) } - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 50) + page, pageSize := params.GetPagination(req.GetArguments(), 30) query := url.Values{} - query.Set("page", strconv.Itoa(int(page))) - query.Set("limit", strconv.Itoa(int(pageSize))) + query.Set("page", strconv.Itoa(page)) + query.Set("limit", strconv.Itoa(pageSize)) var result any - err := doJSONWithFallback(ctx, "GET", + err = doJSONWithFallback(ctx, "GET", []string{ fmt.Sprintf("repos/%s/%s/actions/workflows", url.PathEscape(owner), url.PathEscape(repo)), }, @@ -172,26 +171,26 @@ func ListRepoActionWorkflowsFn(ctx context.Context, req mcp.CallToolRequest) (*m if err != nil { return to.ErrorResult(fmt.Errorf("list action workflows err: %v", err)) } - return to.TextResult(result) + return to.TextResult(slimActionWorkflows(result)) } func GetRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetRepoActionWorkflowFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { return to.ErrorResult(errors.New("repo is required")) } - workflowID, ok := req.GetArguments()["workflow_id"].(string) - if !ok || workflowID == "" { + workflowID, err := params.GetString(req.GetArguments(), "workflow_id") + if err != nil || workflowID == "" { return to.ErrorResult(errors.New("workflow_id is required")) } var result any - err := doJSONWithFallback(ctx, "GET", + err = doJSONWithFallback(ctx, "GET", []string{ fmt.Sprintf("repos/%s/%s/actions/workflows/%s", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(workflowID)), }, @@ -200,25 +199,25 @@ func GetRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp if err != nil { return to.ErrorResult(fmt.Errorf("get action workflow err: %v", err)) } - return to.TextResult(result) + return to.TextResult(slimActionWorkflow(result)) } func DispatchRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called DispatchRepoActionWorkflowFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { return to.ErrorResult(errors.New("repo is required")) } - workflowID, ok := req.GetArguments()["workflow_id"].(string) - if !ok || workflowID == "" { + workflowID, err := params.GetString(req.GetArguments(), "workflow_id") + if err != nil || workflowID == "" { return to.ErrorResult(errors.New("workflow_id is required")) } - ref, ok := req.GetArguments()["ref"].(string) - if !ok || ref == "" { + ref, err := params.GetString(req.GetArguments(), "ref") + if err != nil || ref == "" { return to.ErrorResult(errors.New("ref is required")) } @@ -239,7 +238,7 @@ func DispatchRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) body["inputs"] = inputs } - err := doJSONWithFallback(ctx, "POST", + err = doJSONWithFallback(ctx, "POST", []string{ fmt.Sprintf("repos/%s/%s/actions/workflows/%s/dispatches", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(workflowID)), fmt.Sprintf("repos/%s/%s/actions/workflows/%s/dispatch", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(workflowID)), @@ -258,27 +257,26 @@ func DispatchRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) func ListRepoActionRunsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListRepoActionRunsFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { return to.ErrorResult(errors.New("repo is required")) } - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 50) + page, pageSize := params.GetPagination(req.GetArguments(), 30) statusFilter, _ := req.GetArguments()["status"].(string) query := url.Values{} - query.Set("page", strconv.Itoa(int(page))) - query.Set("limit", strconv.Itoa(int(pageSize))) + query.Set("page", strconv.Itoa(page)) + query.Set("limit", strconv.Itoa(pageSize)) if statusFilter != "" { query.Set("status", statusFilter) } var result any - err := doJSONWithFallback(ctx, "GET", + err = doJSONWithFallback(ctx, "GET", []string{ fmt.Sprintf("repos/%s/%s/actions/runs", url.PathEscape(owner), url.PathEscape(repo)), }, @@ -287,17 +285,17 @@ func ListRepoActionRunsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca if err != nil { return to.ErrorResult(fmt.Errorf("list action runs err: %v", err)) } - return to.TextResult(result) + return to.TextResult(slimActionRuns(result)) } func GetRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetRepoActionRunFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { return to.ErrorResult(errors.New("repo is required")) } runID, err := params.GetIndex(req.GetArguments(), "run_id") @@ -315,17 +313,17 @@ func GetRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call if err != nil { return to.ErrorResult(fmt.Errorf("get action run err: %v", err)) } - return to.TextResult(result) + return to.TextResult(slimActionRun(result)) } func CancelRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CancelRepoActionRunFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { return to.ErrorResult(errors.New("repo is required")) } runID, err := params.GetIndex(req.GetArguments(), "run_id") @@ -347,12 +345,12 @@ func CancelRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.C func RerunRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called RerunRepoActionRunFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { return to.ErrorResult(errors.New("repo is required")) } runID, err := params.GetIndex(req.GetArguments(), "run_id") @@ -379,27 +377,26 @@ func RerunRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca func ListRepoActionJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListRepoActionJobsFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { return to.ErrorResult(errors.New("repo is required")) } - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 50) + page, pageSize := params.GetPagination(req.GetArguments(), 30) statusFilter, _ := req.GetArguments()["status"].(string) query := url.Values{} - query.Set("page", strconv.Itoa(int(page))) - query.Set("limit", strconv.Itoa(int(pageSize))) + query.Set("page", strconv.Itoa(page)) + query.Set("limit", strconv.Itoa(pageSize)) if statusFilter != "" { query.Set("status", statusFilter) } var result any - err := doJSONWithFallback(ctx, "GET", + err = doJSONWithFallback(ctx, "GET", []string{ fmt.Sprintf("repos/%s/%s/actions/jobs", url.PathEscape(owner), url.PathEscape(repo)), }, @@ -408,29 +405,28 @@ func ListRepoActionJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca if err != nil { return to.ErrorResult(fmt.Errorf("list action jobs err: %v", err)) } - return to.TextResult(result) + return to.TextResult(slimActionJobs(result)) } func ListRepoActionRunJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListRepoActionRunJobsFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { return to.ErrorResult(errors.New("repo is required")) } runID, err := params.GetIndex(req.GetArguments(), "run_id") if err != nil || runID <= 0 { return to.ErrorResult(errors.New("run_id is required")) } - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 50) + page, pageSize := params.GetPagination(req.GetArguments(), 30) query := url.Values{} - query.Set("page", strconv.Itoa(int(page))) - query.Set("limit", strconv.Itoa(int(pageSize))) + query.Set("page", strconv.Itoa(page)) + query.Set("limit", strconv.Itoa(pageSize)) var result any err = doJSONWithFallback(ctx, "GET", @@ -442,5 +438,5 @@ func ListRepoActionRunJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp if err != nil { return to.ErrorResult(fmt.Errorf("list action run jobs err: %v", err)) } - return to.TextResult(result) + return to.TextResult(slimActionJobs(result)) } diff --git a/operation/actions/secrets.go b/operation/actions/secrets.go index 586cda1..92d85b0 100644 --- a/operation/actions/secrets.go +++ b/operation/actions/secrets.go @@ -39,7 +39,7 @@ var ( mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100), mcp.Min(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30), mcp.Min(1)), ) UpsertRepoActionSecretTool = mcp.NewTool( @@ -65,7 +65,7 @@ var ( mcp.WithDescription("List organization Actions secrets (metadata only; secret values are never returned)"), mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100), mcp.Min(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30), mcp.Min(1)), ) UpsertOrgActionSecretTool = mcp.NewTool( @@ -97,16 +97,15 @@ func init() { func ListRepoActionSecretsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListRepoActionSecretsFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { return to.ErrorResult(errors.New("repo is required")) } - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + page, pageSize := params.GetPagination(req.GetArguments(), 30) client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -114,7 +113,7 @@ func ListRepoActionSecretsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp } secrets, _, err := client.ListRepoActionSecret(owner, repo, gitea_sdk.ListRepoActionSecretOption{ - ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + ListOptions: gitea_sdk.ListOptions{Page: page, PageSize: pageSize}, }) if err != nil { return to.ErrorResult(fmt.Errorf("list repo action secrets err: %v", err)) @@ -136,20 +135,20 @@ func ListRepoActionSecretsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp func UpsertRepoActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called UpsertRepoActionSecretFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { return to.ErrorResult(errors.New("repo is required")) } - name, ok := req.GetArguments()["name"].(string) - if !ok || name == "" { + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { return to.ErrorResult(errors.New("name is required")) } - data, ok := req.GetArguments()["data"].(string) - if !ok || data == "" { + data, err := params.GetString(req.GetArguments(), "data") + if err != nil || data == "" { return to.ErrorResult(errors.New("data is required")) } description, _ := req.GetArguments()["description"].(string) @@ -171,16 +170,16 @@ func UpsertRepoActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mc func DeleteRepoActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called DeleteRepoActionSecretFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { return to.ErrorResult(errors.New("repo is required")) } - secretName, ok := req.GetArguments()["secretName"].(string) - if !ok || secretName == "" { + secretName, err := params.GetString(req.GetArguments(), "secretName") + if err != nil || secretName == "" { return to.ErrorResult(errors.New("secretName is required")) } @@ -197,12 +196,11 @@ func DeleteRepoActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mc func ListOrgActionSecretsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListOrgActionSecretsFn") - org, ok := req.GetArguments()["org"].(string) - if !ok || org == "" { + org, err := params.GetString(req.GetArguments(), "org") + if err != nil || org == "" { return to.ErrorResult(errors.New("org is required")) } - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + page, pageSize := params.GetPagination(req.GetArguments(), 30) client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -210,7 +208,7 @@ func ListOrgActionSecretsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp. } secrets, _, err := client.ListOrgActionSecret(org, gitea_sdk.ListOrgActionSecretOption{ - ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + ListOptions: gitea_sdk.ListOptions{Page: page, PageSize: pageSize}, }) if err != nil { return to.ErrorResult(fmt.Errorf("list org action secrets err: %v", err)) @@ -232,16 +230,16 @@ func ListOrgActionSecretsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp. func UpsertOrgActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called UpsertOrgActionSecretFn") - org, ok := req.GetArguments()["org"].(string) - if !ok || org == "" { + org, err := params.GetString(req.GetArguments(), "org") + if err != nil || org == "" { return to.ErrorResult(errors.New("org is required")) } - name, ok := req.GetArguments()["name"].(string) - if !ok || name == "" { + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { return to.ErrorResult(errors.New("name is required")) } - data, ok := req.GetArguments()["data"].(string) - if !ok || data == "" { + data, err := params.GetString(req.GetArguments(), "data") + if err != nil || data == "" { return to.ErrorResult(errors.New("data is required")) } description, _ := req.GetArguments()["description"].(string) @@ -263,18 +261,18 @@ func UpsertOrgActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp func DeleteOrgActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called DeleteOrgActionSecretFn") - org, ok := req.GetArguments()["org"].(string) - if !ok || org == "" { + org, err := params.GetString(req.GetArguments(), "org") + if err != nil || org == "" { return to.ErrorResult(errors.New("org is required")) } - secretName, ok := req.GetArguments()["secretName"].(string) - if !ok || secretName == "" { + secretName, err := params.GetString(req.GetArguments(), "secretName") + if err != nil || secretName == "" { return to.ErrorResult(errors.New("secretName is required")) } escapedOrg := url.PathEscape(org) escapedSecret := url.PathEscape(secretName) - _, err := gitea.DoJSON(ctx, "DELETE", fmt.Sprintf("orgs/%s/actions/secrets/%s", escapedOrg, escapedSecret), nil, nil, nil) + _, err = gitea.DoJSON(ctx, "DELETE", fmt.Sprintf("orgs/%s/actions/secrets/%s", escapedOrg, escapedSecret), nil, nil, nil) if err != nil { return to.ErrorResult(fmt.Errorf("delete org action secret err: %v", err)) } diff --git a/operation/actions/slim.go b/operation/actions/slim.go new file mode 100644 index 0000000..3ecd07b --- /dev/null +++ b/operation/actions/slim.go @@ -0,0 +1,92 @@ +package actions + +func pick(m map[string]any, keys ...string) map[string]any { + out := make(map[string]any, len(keys)) + for _, k := range keys { + if v, ok := m[k]; ok { + out[k] = v + } + } + return out +} + +func slimPaginated(raw any, itemFn func(map[string]any) map[string]any) any { + m, ok := raw.(map[string]any) + if !ok { + return raw + } + result := make(map[string]any) + if tc, ok := m["total_count"]; ok { + result["total_count"] = tc + } + for key, val := range m { + if key == "total_count" { + continue + } + arr, ok := val.([]any) + if !ok { + continue + } + slimmed := make([]any, 0, len(arr)) + for _, item := range arr { + if im, ok := item.(map[string]any); ok { + slimmed = append(slimmed, itemFn(im)) + } + } + result[key] = slimmed + break + } + return result +} + +func slimRun(m map[string]any) map[string]any { + return pick(m, "id", "name", "head_branch", "head_sha", "run_number", + "event", "status", "conclusion", "workflow_id", + "html_url", "created_at", "updated_at") +} + +func slimJob(m map[string]any) map[string]any { + out := pick(m, "id", "run_id", "name", "workflow_name", + "status", "conclusion", "html_url", + "started_at", "completed_at") + if steps, ok := m["steps"].([]any); ok { + slim := make([]any, 0, len(steps)) + for _, s := range steps { + if sm, ok := s.(map[string]any); ok { + slim = append(slim, pick(sm, "name", "number", "status", "conclusion")) + } + } + out["steps"] = slim + } + return out +} + +func slimWorkflow(m map[string]any) map[string]any { + return pick(m, "id", "name", "path", "state", "html_url", "created_at", "updated_at") +} + +func slimActionRun(raw any) any { + if m, ok := raw.(map[string]any); ok { + return slimRun(m) + } + return raw +} + +func slimActionRuns(raw any) any { + return slimPaginated(raw, slimRun) +} + +func slimActionJobs(raw any) any { + return slimPaginated(raw, slimJob) +} + +func slimActionWorkflow(raw any) any { + if m, ok := raw.(map[string]any); ok { + return slimWorkflow(m) + } + return raw +} + +func slimActionWorkflows(raw any) any { + return slimPaginated(raw, slimWorkflow) +} diff --git a/operation/actions/variables.go b/operation/actions/variables.go index dfeacfc..e2d579a 100644 --- a/operation/actions/variables.go +++ b/operation/actions/variables.go @@ -38,7 +38,7 @@ var ( mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100), mcp.Min(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30), mcp.Min(1)), ) GetRepoActionVariableTool = mcp.NewTool( @@ -80,7 +80,7 @@ var ( mcp.WithDescription("List organization Actions variables"), mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100), mcp.Min(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30), mcp.Min(1)), ) GetOrgActionVariableTool = mcp.NewTool( @@ -132,23 +132,22 @@ func init() { func ListRepoActionVariablesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListRepoActionVariablesFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { return to.ErrorResult(errors.New("repo is required")) } - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + page, pageSize := params.GetPagination(req.GetArguments(), 30) query := url.Values{} - query.Set("page", strconv.Itoa(int(page))) - query.Set("limit", strconv.Itoa(int(pageSize))) + query.Set("page", strconv.Itoa(page)) + query.Set("limit", strconv.Itoa(pageSize)) var result any - _, err := gitea.DoJSON(ctx, "GET", fmt.Sprintf("repos/%s/%s/actions/variables", url.PathEscape(owner), url.PathEscape(repo)), query, nil, &result) + _, err = gitea.DoJSON(ctx, "GET", fmt.Sprintf("repos/%s/%s/actions/variables", url.PathEscape(owner), url.PathEscape(repo)), query, nil, &result) if err != nil { return to.ErrorResult(fmt.Errorf("list repo action variables err: %v", err)) } @@ -157,16 +156,16 @@ func ListRepoActionVariablesFn(ctx context.Context, req mcp.CallToolRequest) (*m func GetRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetRepoActionVariableFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { return to.ErrorResult(errors.New("repo is required")) } - name, ok := req.GetArguments()["name"].(string) - if !ok || name == "" { + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { return to.ErrorResult(errors.New("name is required")) } @@ -183,20 +182,20 @@ func GetRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp func CreateRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CreateRepoActionVariableFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { return to.ErrorResult(errors.New("repo is required")) } - name, ok := req.GetArguments()["name"].(string) - if !ok || name == "" { + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { return to.ErrorResult(errors.New("name is required")) } - value, ok := req.GetArguments()["value"].(string) - if !ok || value == "" { + value, err := params.GetString(req.GetArguments(), "value") + if err != nil || value == "" { return to.ErrorResult(errors.New("value is required")) } @@ -213,20 +212,20 @@ func CreateRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (* func UpdateRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called UpdateRepoActionVariableFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { return to.ErrorResult(errors.New("repo is required")) } - name, ok := req.GetArguments()["name"].(string) - if !ok || name == "" { + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { return to.ErrorResult(errors.New("name is required")) } - value, ok := req.GetArguments()["value"].(string) - if !ok || value == "" { + value, err := params.GetString(req.GetArguments(), "value") + if err != nil || value == "" { return to.ErrorResult(errors.New("value is required")) } @@ -243,16 +242,16 @@ func UpdateRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (* func DeleteRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called DeleteRepoActionVariableFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok || owner == "" { + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok || repo == "" { + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { return to.ErrorResult(errors.New("repo is required")) } - name, ok := req.GetArguments()["name"].(string) - if !ok || name == "" { + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { return to.ErrorResult(errors.New("name is required")) } @@ -269,19 +268,18 @@ func DeleteRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (* func ListOrgActionVariablesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListOrgActionVariablesFn") - org, ok := req.GetArguments()["org"].(string) - if !ok || org == "" { + org, err := params.GetString(req.GetArguments(), "org") + if err != nil || org == "" { return to.ErrorResult(errors.New("org is required")) } - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + page, pageSize := params.GetPagination(req.GetArguments(), 30) client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } variables, _, err := client.ListOrgActionVariable(org, gitea_sdk.ListOrgActionVariableOption{ - ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + ListOptions: gitea_sdk.ListOptions{Page: page, PageSize: pageSize}, }) if err != nil { return to.ErrorResult(fmt.Errorf("list org action variables err: %v", err)) @@ -291,12 +289,12 @@ func ListOrgActionVariablesFn(ctx context.Context, req mcp.CallToolRequest) (*mc func GetOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetOrgActionVariableFn") - org, ok := req.GetArguments()["org"].(string) - if !ok || org == "" { + org, err := params.GetString(req.GetArguments(), "org") + if err != nil || org == "" { return to.ErrorResult(errors.New("org is required")) } - name, ok := req.GetArguments()["name"].(string) - if !ok || name == "" { + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { return to.ErrorResult(errors.New("name is required")) } @@ -313,16 +311,16 @@ func GetOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp. func CreateOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CreateOrgActionVariableFn") - org, ok := req.GetArguments()["org"].(string) - if !ok || org == "" { + org, err := params.GetString(req.GetArguments(), "org") + if err != nil || org == "" { return to.ErrorResult(errors.New("org is required")) } - name, ok := req.GetArguments()["name"].(string) - if !ok || name == "" { + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { return to.ErrorResult(errors.New("name is required")) } - value, ok := req.GetArguments()["value"].(string) - if !ok || value == "" { + value, err := params.GetString(req.GetArguments(), "value") + if err != nil || value == "" { return to.ErrorResult(errors.New("value is required")) } description, _ := req.GetArguments()["description"].(string) @@ -344,16 +342,16 @@ func CreateOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*m func UpdateOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called UpdateOrgActionVariableFn") - org, ok := req.GetArguments()["org"].(string) - if !ok || org == "" { + org, err := params.GetString(req.GetArguments(), "org") + if err != nil || org == "" { return to.ErrorResult(errors.New("org is required")) } - name, ok := req.GetArguments()["name"].(string) - if !ok || name == "" { + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { return to.ErrorResult(errors.New("name is required")) } - value, ok := req.GetArguments()["value"].(string) - if !ok || value == "" { + value, err := params.GetString(req.GetArguments(), "value") + if err != nil || value == "" { return to.ErrorResult(errors.New("value is required")) } description, _ := req.GetArguments()["description"].(string) @@ -374,16 +372,16 @@ func UpdateOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*m func DeleteOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called DeleteOrgActionVariableFn") - org, ok := req.GetArguments()["org"].(string) - if !ok || org == "" { + org, err := params.GetString(req.GetArguments(), "org") + if err != nil || org == "" { return to.ErrorResult(errors.New("org is required")) } - name, ok := req.GetArguments()["name"].(string) - if !ok || name == "" { + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { return to.ErrorResult(errors.New("name is required")) } - _, err := gitea.DoJSON(ctx, "DELETE", fmt.Sprintf("orgs/%s/actions/variables/%s", url.PathEscape(org), url.PathEscape(name)), nil, nil, nil) + _, err = gitea.DoJSON(ctx, "DELETE", fmt.Sprintf("orgs/%s/actions/variables/%s", url.PathEscape(org), url.PathEscape(name)), nil, nil, nil) if err != nil { return to.ErrorResult(fmt.Errorf("delete org action variable err: %v", err)) } diff --git a/operation/issue/issue.go b/operation/issue/issue.go index a380a84..6b9deb4 100644 --- a/operation/issue/issue.go +++ b/operation/issue/issue.go @@ -2,7 +2,6 @@ package issue import ( "context" - "errors" "fmt" "gitea.com/gitea/gitea-mcp/pkg/gitea" @@ -44,7 +43,7 @@ var ( mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("state", mcp.Description("issue state"), mcp.DefaultString("all")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30)), ) CreateIssueTool = mcp.NewTool( @@ -129,13 +128,13 @@ func init() { func GetIssueByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetIssueByIndexFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } index, err := params.GetIndex(req.GetArguments(), "index") if err != nil { @@ -150,30 +149,29 @@ func GetIssueByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT return to.ErrorResult(fmt.Errorf("get %v/%v/issue/%v err: %v", owner, repo, index, err)) } - return to.TextResult(issue) + return to.TextResult(slimIssue(issue)) } func ListRepoIssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListIssuesFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } state, ok := req.GetArguments()["state"].(string) if !ok { state = "all" } - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + page, pageSize := params.GetPagination(req.GetArguments(), 30) opt := gitea_sdk.ListIssueOption{ State: gitea_sdk.StateType(state), ListOptions: gitea_sdk.ListOptions{ - Page: int(page), - PageSize: int(pageSize), + Page: page, + PageSize: pageSize, }, } client, err := gitea.ClientFromContext(ctx) @@ -184,26 +182,26 @@ func ListRepoIssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo if err != nil { return to.ErrorResult(fmt.Errorf("get %v/%v/issues err: %v", owner, repo, err)) } - return to.TextResult(issues) + return to.TextResult(slimIssues(issues)) } func CreateIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CreateIssueFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } - title, ok := req.GetArguments()["title"].(string) - if !ok { - return to.ErrorResult(errors.New("title is required")) + title, err := params.GetString(req.GetArguments(), "title") + if err != nil { + return to.ErrorResult(err) } - body, ok := req.GetArguments()["body"].(string) - if !ok { - return to.ErrorResult(errors.New("body is required")) + body, err := params.GetString(req.GetArguments(), "body") + if err != nil { + return to.ErrorResult(err) } client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -217,26 +215,26 @@ func CreateIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR return to.ErrorResult(fmt.Errorf("create %v/%v/issue err: %v", owner, repo, err)) } - return to.TextResult(issue) + return to.TextResult(slimIssue(issue)) } func CreateIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CreateIssueCommentFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } index, err := params.GetIndex(req.GetArguments(), "index") if err != nil { return to.ErrorResult(err) } - body, ok := req.GetArguments()["body"].(string) - if !ok { - return to.ErrorResult(errors.New("body is required")) + body, err := params.GetString(req.GetArguments(), "body") + if err != nil { + return to.ErrorResult(err) } opt := gitea_sdk.CreateIssueCommentOption{ Body: body, @@ -250,18 +248,18 @@ func CreateIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca return to.ErrorResult(fmt.Errorf("create %v/%v/issue/%v/comment err: %v", owner, repo, index, err)) } - return to.TextResult(issueComment) + return to.TextResult(slimComment(issueComment)) } func EditIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called EditIssueFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } index, err := params.GetIndex(req.GetArguments(), "index") if err != nil { @@ -278,17 +276,7 @@ func EditIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes if ok { opt.Body = new(body) } - var assignees []string - if assigneesArg, exists := req.GetArguments()["assignees"]; exists { - if assigneesSlice, ok := assigneesArg.([]any); ok { - for _, assignee := range assigneesSlice { - if assigneeStr, ok := assignee.(string); ok { - assignees = append(assignees, assigneeStr) - } - } - } - } - opt.Assignees = assignees + opt.Assignees = params.GetStringSlice(req.GetArguments(), "assignees") if val, exists := req.GetArguments()["milestone"]; exists { if milestone, ok := params.ToInt64(val); ok { opt.Milestone = new(milestone) @@ -308,26 +296,26 @@ func EditIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes return to.ErrorResult(fmt.Errorf("edit %v/%v/issue/%v err: %v", owner, repo, index, err)) } - return to.TextResult(issue) + return to.TextResult(slimIssue(issue)) } func EditIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called EditIssueCommentFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } commentID, err := params.GetIndex(req.GetArguments(), "commentID") if err != nil { return to.ErrorResult(err) } - body, ok := req.GetArguments()["body"].(string) - if !ok { - return to.ErrorResult(errors.New("body is required")) + body, err := params.GetString(req.GetArguments(), "body") + if err != nil { + return to.ErrorResult(err) } opt := gitea_sdk.EditIssueCommentOption{ Body: body, @@ -341,18 +329,18 @@ func EditIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return to.ErrorResult(fmt.Errorf("edit %v/%v/issues/comments/%v err: %v", owner, repo, commentID, err)) } - return to.TextResult(issueComment) + return to.TextResult(slimComment(issueComment)) } func GetIssueCommentsByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetIssueCommentsByIndexFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } index, err := params.GetIndex(req.GetArguments(), "index") if err != nil { @@ -368,5 +356,5 @@ func GetIssueCommentsByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.ErrorResult(fmt.Errorf("get %v/%v/issues/%v/comments err: %v", owner, repo, index, err)) } - return to.TextResult(issue) + return to.TextResult(slimComments(issue)) } diff --git a/operation/issue/slim.go b/operation/issue/slim.go new file mode 100644 index 0000000..6916703 --- /dev/null +++ b/operation/issue/slim.go @@ -0,0 +1,116 @@ +package issue + +import ( + gitea_sdk "code.gitea.io/sdk/gitea" +) + +func userLogin(u *gitea_sdk.User) string { + if u == nil { + return "" + } + return u.UserName +} + +func userLogins(users []*gitea_sdk.User) []string { + if len(users) == 0 { + return nil + } + out := make([]string, 0, len(users)) + for _, u := range users { + if u != nil { + out = append(out, u.UserName) + } + } + return out +} + +func labelNames(labels []*gitea_sdk.Label) []string { + if len(labels) == 0 { + return nil + } + out := make([]string, 0, len(labels)) + for _, l := range labels { + if l != nil { + out = append(out, l.Name) + } + } + return out +} + +func slimIssue(i *gitea_sdk.Issue) map[string]any { + if i == nil { + return nil + } + m := map[string]any{ + "number": i.Index, + "title": i.Title, + "body": i.Body, + "state": i.State, + "html_url": i.HTMLURL, + "user": userLogin(i.Poster), + "labels": labelNames(i.Labels), + "comments": i.Comments, + "created_at": i.Created, + "updated_at": i.Updated, + "closed_at": i.Closed, + } + if len(i.Assignees) > 0 { + m["assignees"] = userLogins(i.Assignees) + } + if i.Milestone != nil { + m["milestone"] = map[string]any{ + "id": i.Milestone.ID, + "title": i.Milestone.Title, + } + } + if i.PullRequest != nil { + m["is_pull"] = true + } + return m +} + +func slimIssues(issues []*gitea_sdk.Issue) []map[string]any { + out := make([]map[string]any, 0, len(issues)) + for _, i := range issues { + if i == nil { + continue + } + m := map[string]any{ + "number": i.Index, + "title": i.Title, + "state": i.State, + "html_url": i.HTMLURL, + "user": userLogin(i.Poster), + "comments": i.Comments, + "created_at": i.Created, + "updated_at": i.Updated, + } + if len(i.Labels) > 0 { + m["labels"] = labelNames(i.Labels) + } + out = append(out, m) + } + return out +} + +func slimComment(c *gitea_sdk.Comment) map[string]any { + if c == nil { + return nil + } + return map[string]any{ + "id": c.ID, + "body": c.Body, + "user": userLogin(c.Poster), + "html_url": c.HTMLURL, + "created_at": c.Created, + "updated_at": c.Updated, + } +} + +func slimComments(comments []*gitea_sdk.Comment) []map[string]any { + out := make([]map[string]any, 0, len(comments)) + for _, c := range comments { + out = append(out, slimComment(c)) + } + return out +} diff --git a/operation/issue/slim_test.go b/operation/issue/slim_test.go new file mode 100644 index 0000000..12a421e --- /dev/null +++ b/operation/issue/slim_test.go @@ -0,0 +1,69 @@ +package issue + +import ( + "testing" + + gitea_sdk "code.gitea.io/sdk/gitea" +) + +func TestSlimIssue(t *testing.T) { + i := &gitea_sdk.Issue{ + Index: 42, + Title: "Bug report", + Body: "Something is broken", + State: "open", + HTMLURL: "https://gitea.com/org/repo/issues/42", + Poster: &gitea_sdk.User{UserName: "alice"}, + Labels: []*gitea_sdk.Label{{Name: "bug"}}, + Milestone: &gitea_sdk.Milestone{ + ID: 1, + Title: "v1.0", + }, + PullRequest: &gitea_sdk.PullRequestMeta{HasMerged: false}, + } + + m := slimIssue(i) + + if m["number"] != int64(42) { + t.Errorf("expected number 42, got %v", m["number"]) + } + if m["body"] != "Something is broken" { + t.Errorf("expected body, got %v", m["body"]) + } + if m["is_pull"] != true { + t.Error("expected is_pull true for issue with PullRequest") + } + + ms := m["milestone"].(map[string]any) + if ms["title"] != "v1.0" { + t.Errorf("expected milestone title v1.0, got %v", ms["title"]) + } +} + +func TestSlimIssues_ListIsSlimmer(t *testing.T) { + i := &gitea_sdk.Issue{ + Index: 1, + Title: "Issue", + State: "open", + Body: "Full body", + Poster: &gitea_sdk.User{UserName: "alice"}, + Labels: []*gitea_sdk.Label{{Name: "enhancement"}}, + } + + single := slimIssue(i) + list := slimIssues([]*gitea_sdk.Issue{i}) + + // Single has body, list does not + if _, ok := single["body"]; !ok { + t.Error("single issue should have body") + } + if _, ok := list[0]["body"]; ok { + t.Error("list issue should not have body") + } +} + +func TestSlimIssues_Nil(t *testing.T) { + if r := slimIssues(nil); len(r) != 0 { + t.Errorf("expected empty slice, got %v", r) + } +} diff --git a/operation/label/label.go b/operation/label/label.go index acb6128..67a8581 100644 --- a/operation/label/label.go +++ b/operation/label/label.go @@ -41,7 +41,7 @@ var ( 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)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30)), ) GetRepoLabelTool = mcp.NewTool( @@ -121,7 +121,7 @@ var ( mcp.WithDescription("Lists labels defined at organization level"), mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30)), ) CreateOrgLabelTool = mcp.NewTool( @@ -210,21 +210,20 @@ func init() { func ListRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListRepoLabelsFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + page, pageSize := params.GetPagination(req.GetArguments(), 30) opt := gitea_sdk.ListLabelsOptions{ ListOptions: gitea_sdk.ListOptions{ - Page: int(page), - PageSize: int(pageSize), + Page: page, + PageSize: pageSize, }, } client, err := gitea.ClientFromContext(ctx) @@ -235,18 +234,18 @@ func ListRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo if err != nil { return to.ErrorResult(fmt.Errorf("list %v/%v/labels err: %v", owner, repo, err)) } - return to.TextResult(labels) + return to.TextResult(slimLabels(labels)) } func GetRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetRepoLabelFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } id, err := params.GetIndex(req.GetArguments(), "id") if err != nil { @@ -261,26 +260,26 @@ func GetRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool if err != nil { return to.ErrorResult(fmt.Errorf("get %v/%v/label/%v err: %v", owner, repo, id, err)) } - return to.TextResult(label) + return to.TextResult(slimLabel(label)) } func CreateRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CreateRepoLabelFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } - name, ok := req.GetArguments()["name"].(string) - if !ok { - return to.ErrorResult(errors.New("name is required")) + name, err := params.GetString(req.GetArguments(), "name") + if err != nil { + return to.ErrorResult(err) } - color, ok := req.GetArguments()["color"].(string) - if !ok { - return to.ErrorResult(errors.New("color is required")) + color, err := params.GetString(req.GetArguments(), "color") + if err != nil { + return to.ErrorResult(err) } description, _ := req.GetArguments()["description"].(string) // Optional @@ -298,18 +297,18 @@ func CreateRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT if err != nil { return to.ErrorResult(fmt.Errorf("create %v/%v/label err: %v", owner, repo, err)) } - return to.TextResult(label) + return to.TextResult(slimLabel(label)) } func EditRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called EditRepoLabelFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } id, err := params.GetIndex(req.GetArguments(), "id") if err != nil { @@ -335,18 +334,18 @@ func EditRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo if err != nil { return to.ErrorResult(fmt.Errorf("edit %v/%v/label/%v err: %v", owner, repo, id, err)) } - return to.TextResult(label) + return to.TextResult(slimLabel(label)) } func DeleteRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called DeleteRepoLabelFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } id, err := params.GetIndex(req.GetArguments(), "id") if err != nil { @@ -366,13 +365,13 @@ func DeleteRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT func AddIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called AddIssueLabelsFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } index, err := params.GetIndex(req.GetArguments(), "index") if err != nil { @@ -403,18 +402,18 @@ func AddIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo if err != nil { 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(slimLabels(issueLabels)) } func ReplaceIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ReplaceIssueLabelsFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } index, err := params.GetIndex(req.GetArguments(), "index") if err != nil { @@ -445,18 +444,18 @@ func ReplaceIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca if err != nil { 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(slimLabels(issueLabels)) } func ClearIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ClearIssueLabelsFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } index, err := params.GetIndex(req.GetArguments(), "index") if err != nil { @@ -476,13 +475,13 @@ func ClearIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call func RemoveIssueLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called RemoveIssueLabelFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } index, err := params.GetIndex(req.GetArguments(), "index") if err != nil { @@ -506,17 +505,16 @@ func RemoveIssueLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call func ListOrgLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListOrgLabelsFn") - org, ok := req.GetArguments()["org"].(string) - if !ok { - return to.ErrorResult(errors.New("org is required")) + org, err := params.GetString(req.GetArguments(), "org") + if err != nil { + return to.ErrorResult(err) } - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + page, pageSize := params.GetPagination(req.GetArguments(), 30) opt := gitea_sdk.ListOrgLabelsOptions{ ListOptions: gitea_sdk.ListOptions{ - Page: int(page), - PageSize: int(pageSize), + Page: page, + PageSize: pageSize, }, } client, err := gitea.ClientFromContext(ctx) @@ -527,22 +525,22 @@ func ListOrgLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo if err != nil { return to.ErrorResult(fmt.Errorf("list %v/labels err: %v", org, err)) } - return to.TextResult(labels) + return to.TextResult(slimLabels(labels)) } func CreateOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CreateOrgLabelFn") - org, ok := req.GetArguments()["org"].(string) - if !ok { - return to.ErrorResult(errors.New("org is required")) + org, err := params.GetString(req.GetArguments(), "org") + if err != nil { + return to.ErrorResult(err) } - name, ok := req.GetArguments()["name"].(string) - if !ok { - return to.ErrorResult(errors.New("name is required")) + name, err := params.GetString(req.GetArguments(), "name") + if err != nil { + return to.ErrorResult(err) } - color, ok := req.GetArguments()["color"].(string) - if !ok { - return to.ErrorResult(errors.New("color is required")) + color, err := params.GetString(req.GetArguments(), "color") + if err != nil { + return to.ErrorResult(err) } description, _ := req.GetArguments()["description"].(string) exclusive, _ := req.GetArguments()["exclusive"].(bool) @@ -562,14 +560,14 @@ func CreateOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo if err != nil { return to.ErrorResult(fmt.Errorf("create %v/labels err: %v", org, err)) } - return to.TextResult(label) + return to.TextResult(slimLabel(label)) } func EditOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called EditOrgLabelFn") - org, ok := req.GetArguments()["org"].(string) - if !ok { - return to.ErrorResult(errors.New("org is required")) + org, err := params.GetString(req.GetArguments(), "org") + if err != nil { + return to.ErrorResult(err) } id, err := params.GetIndex(req.GetArguments(), "id") if err != nil { @@ -598,14 +596,14 @@ func EditOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool if err != nil { return to.ErrorResult(fmt.Errorf("edit %v/labels/%v err: %v", org, id, err)) } - return to.TextResult(label) + return to.TextResult(slimLabel(label)) } func DeleteOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called DeleteOrgLabelFn") - org, ok := req.GetArguments()["org"].(string) - if !ok { - return to.ErrorResult(errors.New("org is required")) + org, err := params.GetString(req.GetArguments(), "org") + if err != nil { + return to.ErrorResult(err) } id, err := params.GetIndex(req.GetArguments(), "id") if err != nil { diff --git a/operation/label/slim.go b/operation/label/slim.go new file mode 100644 index 0000000..315105a --- /dev/null +++ b/operation/label/slim.go @@ -0,0 +1,26 @@ +package label + +import ( + gitea_sdk "code.gitea.io/sdk/gitea" +) + +func slimLabel(l *gitea_sdk.Label) map[string]any { + if l == nil { + return nil + } + return map[string]any{ + "id": l.ID, + "name": l.Name, + "color": l.Color, + "description": l.Description, + "exclusive": l.Exclusive, + } +} + +func slimLabels(labels []*gitea_sdk.Label) []map[string]any { + out := make([]map[string]any, 0, len(labels)) + for _, l := range labels { + out = append(out, slimLabel(l)) + } + return out +} diff --git a/operation/label/slim_test.go b/operation/label/slim_test.go new file mode 100644 index 0000000..9eec9bd --- /dev/null +++ b/operation/label/slim_test.go @@ -0,0 +1,25 @@ +package label + +import ( + "testing" + + gitea_sdk "code.gitea.io/sdk/gitea" +) + +func TestSlimLabel(t *testing.T) { + l := &gitea_sdk.Label{ + ID: 1, + Name: "bug", + Color: "#d73a4a", + Description: "Something isn't working", + Exclusive: false, + } + + m := slimLabel(l) + if m["name"] != "bug" { + t.Errorf("expected name bug, got %v", m["name"]) + } + if m["color"] != "#d73a4a" { + t.Errorf("expected color, got %v", m["color"]) + } +} diff --git a/operation/milestone/milestone.go b/operation/milestone/milestone.go index 8f853f3..40d49a9 100644 --- a/operation/milestone/milestone.go +++ b/operation/milestone/milestone.go @@ -2,7 +2,6 @@ package milestone import ( "context" - "errors" "fmt" "gitea.com/gitea/gitea-mcp/pkg/gitea" @@ -43,7 +42,7 @@ var ( mcp.WithString("state", mcp.Description("milestone state"), mcp.DefaultString("all")), mcp.WithString("name", mcp.Description("milestone name")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30)), ) CreateMilestoneTool = mcp.NewTool( @@ -102,13 +101,13 @@ func init() { func GetMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetMilestoneFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } id, err := params.GetIndex(req.GetArguments(), "id") if err != nil { @@ -123,35 +122,28 @@ func GetMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool return to.ErrorResult(fmt.Errorf("get %v/%v/milestone/%v err: %v", owner, repo, id, err)) } - return to.TextResult(milestone) + return to.TextResult(slimMilestone(milestone)) } func ListMilestonesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListMilestonesFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } - state, ok := req.GetArguments()["state"].(string) - if !ok { - state = "all" - } - name, ok := req.GetArguments()["name"].(string) - if !ok { - name = "" - } - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + state := params.GetOptionalString(req.GetArguments(), "state", "all") + name := params.GetOptionalString(req.GetArguments(), "name", "") + page, pageSize := params.GetPagination(req.GetArguments(), 30) opt := gitea_sdk.ListMilestoneOption{ State: gitea_sdk.StateType(state), Name: name, ListOptions: gitea_sdk.ListOptions{ - Page: int(page), - PageSize: int(pageSize), + Page: page, + PageSize: pageSize, }, } client, err := gitea.ClientFromContext(ctx) @@ -162,22 +154,22 @@ func ListMilestonesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo if err != nil { return to.ErrorResult(fmt.Errorf("get %v/%v/milestones err: %v", owner, repo, err)) } - return to.TextResult(milestones) + return to.TextResult(slimMilestones(milestones)) } func CreateMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CreateMilestoneFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } - title, ok := req.GetArguments()["title"].(string) - if !ok { - return to.ErrorResult(errors.New("title is required")) + title, err := params.GetString(req.GetArguments(), "title") + if err != nil { + return to.ErrorResult(err) } opt := gitea_sdk.CreateMilestoneOption{ @@ -198,18 +190,18 @@ func CreateMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT return to.ErrorResult(fmt.Errorf("create %v/%v/milestone err: %v", owner, repo, err)) } - return to.TextResult(milestone) + return to.TextResult(slimMilestone(milestone)) } func EditMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called EditMilestoneFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } id, err := params.GetIndex(req.GetArguments(), "id") if err != nil { @@ -240,18 +232,18 @@ func EditMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo return to.ErrorResult(fmt.Errorf("edit %v/%v/milestone/%v err: %v", owner, repo, id, err)) } - return to.TextResult(milestone) + return to.TextResult(slimMilestone(milestone)) } func DeleteMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called DeleteMilestoneFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } id, err := params.GetIndex(req.GetArguments(), "id") if err != nil { diff --git a/operation/milestone/slim.go b/operation/milestone/slim.go new file mode 100644 index 0000000..207ad27 --- /dev/null +++ b/operation/milestone/slim.go @@ -0,0 +1,28 @@ +package milestone + +import ( + gitea_sdk "code.gitea.io/sdk/gitea" +) + +func slimMilestone(m *gitea_sdk.Milestone) map[string]any { + if m == nil { + return nil + } + return map[string]any{ + "id": m.ID, + "title": m.Title, + "description": m.Description, + "state": m.State, + "open_issues": m.OpenIssues, + "closed_issues": m.ClosedIssues, + "due_on": m.Deadline, + } +} + +func slimMilestones(milestones []*gitea_sdk.Milestone) []map[string]any { + out := make([]map[string]any, 0, len(milestones)) + for _, m := range milestones { + out = append(out, slimMilestone(m)) + } + return out +} diff --git a/operation/pull/pull.go b/operation/pull/pull.go index d55df30..8e3b46a 100644 --- a/operation/pull/pull.go +++ b/operation/pull/pull.go @@ -2,7 +2,6 @@ package pull import ( "context" - "errors" "fmt" "gitea.com/gitea/gitea-mcp/pkg/gitea" @@ -63,7 +62,7 @@ var ( mcp.WithString("sort", mcp.Description("sort"), mcp.Enum("oldest", "recentupdate", "leastupdate", "mostcomment", "leastcomment", "priority"), mcp.DefaultString("recentupdate")), mcp.WithNumber("milestone", mcp.Description("milestone")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30)), ) CreatePullRequestTool = mcp.NewTool( @@ -104,7 +103,7 @@ var ( 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)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30)), ) GetPullRequestReviewTool = mcp.NewTool( @@ -269,15 +268,16 @@ func init() { func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetPullRequestByIndexFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "index") + index, err := params.GetIndex(args, "index") if err != nil { return to.ErrorResult(err) } @@ -290,24 +290,25 @@ func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v err: %v", owner, repo, index, err)) } - return to.TextResult(pr) + return to.TextResult(slimPullRequest(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(errors.New("owner is required")) - } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) - } - index, err := params.GetIndex(req.GetArguments(), "index") + args := req.GetArguments() + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - binary, _ := req.GetArguments()["binary"].(bool) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) + } + index, err := params.GetIndex(args, "index") + if err != nil { + return to.ErrorResult(err) + } + binary, _ := args["binary"].(bool) client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -320,41 +321,31 @@ func GetPullRequestDiffFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v diff err: %v", owner, repo, index, err)) } - result := map[string]any{ - "diff": string(diffBytes), - "binary": binary, - "index": index, - "repo": repo, - "owner": owner, - } - return to.TextResult(result) + return to.TextResult(string(diffBytes)) } func ListRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListRepoPullRequests") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - state, _ := req.GetArguments()["state"].(string) - sort, ok := req.GetArguments()["sort"].(string) - if !ok { - sort = "recentupdate" - } - milestone := params.GetOptionalInt(req.GetArguments(), "milestone", 0) - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + state, _ := args["state"].(string) + sort := params.GetOptionalString(args, "sort", "recentupdate") + milestone := params.GetOptionalInt(args, "milestone", 0) + page, pageSize := params.GetPagination(args, 30) opt := gitea_sdk.ListPullRequestsOptions{ State: gitea_sdk.StateType(state), Sort: sort, Milestone: milestone, ListOptions: gitea_sdk.ListOptions{ - Page: int(page), - PageSize: int(pageSize), + Page: page, + PageSize: pageSize, }, } client, err := gitea.ClientFromContext(ctx) @@ -366,34 +357,35 @@ func ListRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp. return to.ErrorResult(fmt.Errorf("list %v/%v/pull_requests err: %v", owner, repo, err)) } - return to.TextResult(pullRequests) + return to.TextResult(slimPullRequests(pullRequests)) } func CreatePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CreatePullRequestFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - title, ok := req.GetArguments()["title"].(string) - if !ok { - return to.ErrorResult(errors.New("title is required")) + title, err := params.GetString(args, "title") + if err != nil { + return to.ErrorResult(err) } - body, ok := req.GetArguments()["body"].(string) - if !ok { - return to.ErrorResult(errors.New("body is required")) + body, err := params.GetString(args, "body") + if err != nil { + return to.ErrorResult(err) } - head, ok := req.GetArguments()["head"].(string) - if !ok { - return to.ErrorResult(errors.New("head is required")) + head, err := params.GetString(args, "head") + if err != nil { + return to.ErrorResult(err) } - base, ok := req.GetArguments()["base"].(string) - if !ok { - return to.ErrorResult(errors.New("base is required")) + base, err := params.GetString(args, "base") + if err != nil { + return to.ErrorResult(err) } client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -409,45 +401,27 @@ func CreatePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Cal return to.ErrorResult(fmt.Errorf("create %v/%v/pull_request err: %v", owner, repo, err)) } - return to.TextResult(pr) + return to.TextResult(slimPullRequest(pr)) } func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CreatePullRequestReviewerFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "index") + index, err := params.GetIndex(args, "index") if err != nil { return to.ErrorResult(err) } - var reviewers []string - if reviewersArg, exists := req.GetArguments()["reviewers"]; exists { - if reviewersSlice, ok := reviewersArg.([]any); 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.([]any); ok { - for _, teamReviewer := range teamReviewersSlice { - if teamReviewerStr, ok := teamReviewer.(string); ok { - teamReviewers = append(teamReviewers, teamReviewerStr) - } - } - } - } + reviewers := params.GetStringSlice(args, "reviewers") + teamReviewers := params.GetStringSlice(args, "team_reviewers") client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -476,40 +450,22 @@ func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) ( 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(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "index") + index, err := params.GetIndex(args, "index") if err != nil { return to.ErrorResult(err) } - var reviewers []string - if reviewersArg, exists := req.GetArguments()["reviewers"]; exists { - if reviewersSlice, ok := reviewersArg.([]any); 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.([]any); ok { - for _, teamReviewer := range teamReviewersSlice { - if teamReviewerStr, ok := teamReviewer.(string); ok { - teamReviewers = append(teamReviewers, teamReviewerStr) - } - } - } - } + reviewers := params.GetStringSlice(args, "reviewers") + teamReviewers := params.GetStringSlice(args, "team_reviewers") client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -537,20 +493,20 @@ func DeletePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) ( 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(errors.New("owner is required")) - } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) - } - index, err := params.GetIndex(req.GetArguments(), "index") + args := req.GetArguments() + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) + } + index, err := params.GetIndex(args, "index") + if err != nil { + return to.ErrorResult(err) + } + page, pageSize := params.GetPagination(args, 30) client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -559,32 +515,33 @@ func ListPullRequestReviewsFn(ctx context.Context, req mcp.CallToolRequest) (*mc reviews, _, err := client.ListPullReviews(owner, repo, index, gitea_sdk.ListPullReviewsOptions{ ListOptions: gitea_sdk.ListOptions{ - Page: int(page), - PageSize: int(pageSize), + Page: page, + PageSize: 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) + return to.TextResult(slimReviews(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(errors.New("owner is required")) - } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) - } - index, err := params.GetIndex(req.GetArguments(), "index") + args := req.GetArguments() + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - reviewID, err := params.GetIndex(req.GetArguments(), "review_id") + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) + } + index, err := params.GetIndex(args, "index") + if err != nil { + return to.ErrorResult(err) + } + reviewID, err := params.GetIndex(args, "review_id") if err != nil { return to.ErrorResult(err) } @@ -599,24 +556,25 @@ func GetPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp. return to.ErrorResult(fmt.Errorf("get review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err)) } - return to.TextResult(review) + return to.TextResult(slimReview(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(errors.New("owner is required")) - } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) - } - index, err := params.GetIndex(req.GetArguments(), "index") + args := req.GetArguments() + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - reviewID, err := params.GetIndex(req.GetArguments(), "review_id") + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) + } + index, err := params.GetIndex(args, "index") + if err != nil { + return to.ErrorResult(err) + } + reviewID, err := params.GetIndex(args, "review_id") if err != nil { return to.ErrorResult(err) } @@ -631,38 +589,39 @@ func ListPullRequestReviewCommentsFn(ctx context.Context, req mcp.CallToolReques return to.ErrorResult(fmt.Errorf("list review comments for review %v on %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err)) } - return to.TextResult(comments) + return to.TextResult(slimReviewComments(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(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "index") + index, err := params.GetIndex(args, "index") if err != nil { return to.ErrorResult(err) } opt := gitea_sdk.CreatePullReviewOptions{} - if state, ok := req.GetArguments()["state"].(string); ok { + if state, ok := args["state"].(string); ok { opt.State = gitea_sdk.ReviewStateType(state) } - if body, ok := req.GetArguments()["body"].(string); ok { + if body, ok := args["body"].(string); ok { opt.Body = body } - if commitID, ok := req.GetArguments()["commit_id"].(string); ok { + if commitID, ok := args["commit_id"].(string); ok { opt.CommitID = commitID } // Parse inline comments - if commentsArg, exists := req.GetArguments()["comments"]; exists { + if commentsArg, exists := args["comments"]; exists { if commentsSlice, ok := commentsArg.([]any); ok { for _, comment := range commentsSlice { if commentMap, ok := comment.(map[string]any); ok { @@ -695,36 +654,37 @@ func CreatePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.ErrorResult(fmt.Errorf("create review for %v/%v/pr/%v err: %v", owner, repo, index, err)) } - return to.TextResult(review) + return to.TextResult(slimReview(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(errors.New("owner is required")) - } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) - } - index, err := params.GetIndex(req.GetArguments(), "index") + args := req.GetArguments() + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - reviewID, err := params.GetIndex(req.GetArguments(), "review_id") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - state, ok := req.GetArguments()["state"].(string) - if !ok { - return to.ErrorResult(errors.New("state is required")) + index, err := params.GetIndex(args, "index") + if err != nil { + return to.ErrorResult(err) + } + reviewID, err := params.GetIndex(args, "review_id") + if err != nil { + return to.ErrorResult(err) + } + state, err := params.GetString(args, "state") + if err != nil { + return to.ErrorResult(err) } opt := gitea_sdk.SubmitPullReviewOptions{ State: gitea_sdk.ReviewStateType(state), } - if body, ok := req.GetArguments()["body"].(string); ok { + if body, ok := args["body"].(string); ok { opt.Body = body } @@ -738,24 +698,25 @@ func SubmitPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.ErrorResult(fmt.Errorf("submit review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err)) } - return to.TextResult(review) + return to.TextResult(slimReview(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(errors.New("owner is required")) - } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) - } - index, err := params.GetIndex(req.GetArguments(), "index") + args := req.GetArguments() + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - reviewID, err := params.GetIndex(req.GetArguments(), "review_id") + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) + } + index, err := params.GetIndex(args, "index") + if err != nil { + return to.ErrorResult(err) + } + reviewID, err := params.GetIndex(args, "review_id") if err != nil { return to.ErrorResult(err) } @@ -782,25 +743,26 @@ func DeletePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m 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(errors.New("owner is required")) - } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) - } - index, err := params.GetIndex(req.GetArguments(), "index") + args := req.GetArguments() + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - reviewID, err := params.GetIndex(req.GetArguments(), "review_id") + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) + } + index, err := params.GetIndex(args, "index") + if err != nil { + return to.ErrorResult(err) + } + reviewID, err := params.GetIndex(args, "review_id") if err != nil { return to.ErrorResult(err) } opt := gitea_sdk.DismissPullReviewOptions{} - if message, ok := req.GetArguments()["message"].(string); ok { + if message, ok := args["message"].(string); ok { opt.Message = message } @@ -826,38 +788,24 @@ func DismissPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (* func MergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called MergePullRequestFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "index") + index, err := params.GetIndex(args, "index") if err != nil { return to.ErrorResult(err) } - mergeStyle := "merge" - if style, exists := req.GetArguments()["merge_style"].(string); exists && style != "" { - mergeStyle = style - } - - title := "" - if t, exists := req.GetArguments()["title"].(string); exists { - title = t - } - - message := "" - if msg, exists := req.GetArguments()["message"].(string); exists { - message = msg - } - - deleteBranch := false - if del, exists := req.GetArguments()["delete_branch"].(bool); exists { - deleteBranch = del - } + mergeStyle := params.GetOptionalString(args, "merge_style", "merge") + title, _ := args["title"].(string) + message, _ := args["message"].(string) + deleteBranch, _ := args["delete_branch"].(bool) client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -897,53 +845,46 @@ func MergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call func EditPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called EditPullRequestFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - index, err := params.GetIndex(req.GetArguments(), "index") + index, err := params.GetIndex(args, "index") if err != nil { return to.ErrorResult(err) } opt := gitea_sdk.EditPullRequestOption{} - if title, ok := req.GetArguments()["title"].(string); ok { + if title, ok := args["title"].(string); ok { opt.Title = title } - if body, ok := req.GetArguments()["body"].(string); ok { + if body, ok := args["body"].(string); ok { opt.Body = new(body) } - if base, ok := req.GetArguments()["base"].(string); ok { + if base, ok := args["base"].(string); ok { opt.Base = base } - if assignee, ok := req.GetArguments()["assignee"].(string); ok { + if assignee, ok := args["assignee"].(string); ok { opt.Assignee = assignee } - if assigneesArg, exists := req.GetArguments()["assignees"]; exists { - if assigneesSlice, ok := assigneesArg.([]any); ok { - var assignees []string - for _, a := range assigneesSlice { - if s, ok := a.(string); ok { - assignees = append(assignees, s) - } - } - opt.Assignees = assignees - } + if assignees := params.GetStringSlice(args, "assignees"); assignees != nil { + opt.Assignees = assignees } - if val, exists := req.GetArguments()["milestone"]; exists { + if val, exists := args["milestone"]; exists { if milestone, ok := params.ToInt64(val); ok { opt.Milestone = milestone } } - if state, ok := req.GetArguments()["state"].(string); ok { + if state, ok := args["state"].(string); ok { opt.State = new(gitea_sdk.StateType(state)) } - if allowMaintainerEdit, ok := req.GetArguments()["allow_maintainer_edit"].(bool); ok { + if allowMaintainerEdit, ok := args["allow_maintainer_edit"].(bool); ok { opt.AllowMaintainerEdit = new(allowMaintainerEdit) } @@ -957,5 +898,5 @@ func EditPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT return to.ErrorResult(fmt.Errorf("edit %v/%v/pr/%v err: %v", owner, repo, index, err)) } - return to.TextResult(pr) + return to.TextResult(slimPullRequest(pr)) } diff --git a/operation/pull/pull_test.go b/operation/pull/pull_test.go index a6cdcdc..ab123bf 100644 --- a/operation/pull/pull_test.go +++ b/operation/pull/pull_test.go @@ -116,13 +116,11 @@ func TestEditPullRequestFn(t *testing.T) { t.Fatalf("expected text content, got %T", result.Content[0]) } - var parsed struct { - Result map[string]any `json:"Result"` - } + var parsed map[string]any if err := json.Unmarshal([]byte(textContent.Text), &parsed); err != nil { t.Fatalf("unmarshal result text: %v", err) } - if got := parsed.Result["title"].(string); got != "WIP: my feature" { + if got := parsed["title"].(string); got != "WIP: my feature" { t.Fatalf("result title = %q, want %q", got, "WIP: my feature") } }) @@ -239,20 +237,18 @@ func TestMergePullRequestFn(t *testing.T) { t.Fatalf("expected text content, got %T", result.Content[0]) } - var parsed struct { - Result map[string]any `json:"Result"` - } + var parsed map[string]any if err := json.Unmarshal([]byte(textContent.Text), &parsed); err != nil { t.Fatalf("unmarshal result text: %v", err) } - if parsed.Result["merged"] != true { - t.Fatalf("expected merged=true, got %v", parsed.Result["merged"]) + if parsed["merged"] != true { + t.Fatalf("expected merged=true, got %v", parsed["merged"]) } - if parsed.Result["merge_style"] != "squash" { - t.Fatalf("expected merge_style 'squash', got %v", parsed.Result["merge_style"]) + if parsed["merge_style"] != "squash" { + t.Fatalf("expected merge_style 'squash', got %v", parsed["merge_style"]) } - if parsed.Result["branch_deleted"] != true { - t.Fatalf("expected branch_deleted=true, got %v", parsed.Result["branch_deleted"]) + if parsed["branch_deleted"] != true { + t.Fatalf("expected branch_deleted=true, got %v", parsed["branch_deleted"]) } }) } @@ -370,27 +366,13 @@ func TestGetPullRequestDiffFn(t *testing.T) { t.Fatalf("expected text content, got %T", result.Content[0]) } - var parsed struct { - Result map[string]any `json:"Result"` - } + // The diff response is now a plain string + var parsed string 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) + if parsed != diffRaw { + t.Fatalf("diff = %q, want %q", parsed, diffRaw) } }) } diff --git a/operation/pull/slim.go b/operation/pull/slim.go new file mode 100644 index 0000000..0bf8416 --- /dev/null +++ b/operation/pull/slim.go @@ -0,0 +1,191 @@ +package pull + +import ( + gitea_sdk "code.gitea.io/sdk/gitea" +) + +func userLogin(u *gitea_sdk.User) string { + if u == nil { + return "" + } + return u.UserName +} + +func userLogins(users []*gitea_sdk.User) []string { + if len(users) == 0 { + return nil + } + out := make([]string, 0, len(users)) + for _, u := range users { + if u != nil { + out = append(out, u.UserName) + } + } + return out +} + +func labelNames(labels []*gitea_sdk.Label) []string { + if len(labels) == 0 { + return nil + } + out := make([]string, 0, len(labels)) + for _, l := range labels { + if l != nil { + out = append(out, l.Name) + } + } + return out +} + +func repoRef(r *gitea_sdk.Repository) map[string]any { + if r == nil { + return nil + } + return map[string]any{ + "full_name": r.FullName, + "description": r.Description, + } +} + +func slimPullRequest(pr *gitea_sdk.PullRequest) map[string]any { + if pr == nil { + return nil + } + m := map[string]any{ + "number": pr.Index, + "title": pr.Title, + "body": pr.Body, + "state": pr.State, + "draft": pr.Draft, + "merged": pr.HasMerged, + "mergeable": pr.Mergeable, + "html_url": pr.HTMLURL, + "user": userLogin(pr.Poster), + "labels": labelNames(pr.Labels), + "comments": pr.Comments, + "created_at": pr.Created, + "updated_at": pr.Updated, + "closed_at": pr.Closed, + } + if pr.HasMerged { + m["merged_at"] = pr.Merged + m["merge_commit_sha"] = pr.MergedCommitID + m["merged_by"] = userLogin(pr.MergedBy) + } + if pr.Head != nil { + head := map[string]any{"ref": pr.Head.Ref, "sha": pr.Head.Sha} + if pr.Head.Repository != nil { + head["repo"] = repoRef(pr.Head.Repository) + } + m["head"] = head + } + if pr.Base != nil { + base := map[string]any{"ref": pr.Base.Ref, "sha": pr.Base.Sha} + if pr.Base.Repository != nil { + base["repo"] = repoRef(pr.Base.Repository) + } + m["base"] = base + } + if pr.Additions != nil { + m["additions"] = *pr.Additions + } + if pr.Deletions != nil { + m["deletions"] = *pr.Deletions + } + if pr.ChangedFiles != nil { + m["changed_files"] = *pr.ChangedFiles + } + if len(pr.Assignees) > 0 { + m["assignees"] = userLogins(pr.Assignees) + } + if pr.Milestone != nil { + m["milestone"] = pr.Milestone.Title + } + if pr.ReviewComments > 0 { + m["review_comments"] = pr.ReviewComments + } + return m +} + +func slimPullRequests(prs []*gitea_sdk.PullRequest) []map[string]any { + out := make([]map[string]any, 0, len(prs)) + for _, pr := range prs { + if pr == nil { + continue + } + m := map[string]any{ + "number": pr.Index, + "title": pr.Title, + "state": pr.State, + "draft": pr.Draft, + "merged": pr.HasMerged, + "html_url": pr.HTMLURL, + "user": userLogin(pr.Poster), + "created_at": pr.Created, + "updated_at": pr.Updated, + } + if pr.Head != nil { + m["head"] = pr.Head.Ref + } + if pr.Base != nil { + m["base"] = pr.Base.Ref + } + if len(pr.Labels) > 0 { + m["labels"] = labelNames(pr.Labels) + } + out = append(out, m) + } + return out +} + +func slimReview(r *gitea_sdk.PullReview) map[string]any { + if r == nil { + return nil + } + return map[string]any{ + "id": r.ID, + "state": r.State, + "body": r.Body, + "user": userLogin(r.Reviewer), + "comments_count": r.CodeCommentsCount, + "submitted_at": r.Submitted, + "html_url": r.HTMLURL, + "stale": r.Stale, + "official": r.Official, + "dismissed": r.Dismissed, + } +} + +func slimReviews(reviews []*gitea_sdk.PullReview) []map[string]any { + out := make([]map[string]any, 0, len(reviews)) + for _, r := range reviews { + out = append(out, slimReview(r)) + } + return out +} + +func slimReviewComment(c *gitea_sdk.PullReviewComment) map[string]any { + if c == nil { + return nil + } + return map[string]any{ + "id": c.ID, + "body": c.Body, + "path": c.Path, + "position": c.LineNum, + "old_position": c.OldLineNum, + "diff_hunk": c.DiffHunk, + "user": userLogin(c.Reviewer), + "html_url": c.HTMLURL, + "created_at": c.Created, + "updated_at": c.Updated, + } +} + +func slimReviewComments(comments []*gitea_sdk.PullReviewComment) []map[string]any { + out := make([]map[string]any, 0, len(comments)) + for _, c := range comments { + out = append(out, slimReviewComment(c)) + } + return out +} diff --git a/operation/pull/slim_test.go b/operation/pull/slim_test.go new file mode 100644 index 0000000..104c932 --- /dev/null +++ b/operation/pull/slim_test.go @@ -0,0 +1,124 @@ +package pull + +import ( + "testing" + "time" + + gitea_sdk "code.gitea.io/sdk/gitea" +) + +func TestSlimPullRequest(t *testing.T) { + now := time.Now() + additions := 10 + deletions := 5 + changedFiles := 3 + pr := &gitea_sdk.PullRequest{ + Index: 1, + Title: "Fix bug", + Body: "Fixes #123", + State: "open", + Draft: false, + HasMerged: false, + Mergeable: true, + HTMLURL: "https://gitea.com/org/repo/pulls/1", + Poster: &gitea_sdk.User{UserName: "bob"}, + Labels: []*gitea_sdk.Label{ + {Name: "bug"}, + {Name: "priority"}, + }, + Comments: 2, + Created: &now, + Updated: &now, + Additions: &additions, + Deletions: &deletions, + ChangedFiles: &changedFiles, + Head: &gitea_sdk.PRBranchInfo{ + Ref: "fix-branch", + Sha: "abc123", + }, + Base: &gitea_sdk.PRBranchInfo{ + Ref: "main", + Sha: "def456", + }, + Assignees: []*gitea_sdk.User{ + {UserName: "alice"}, + }, + Milestone: &gitea_sdk.Milestone{Title: "v1.0"}, + } + + m := slimPullRequest(pr) + + if m["number"] != int64(1) { + t.Errorf("expected number 1, got %v", m["number"]) + } + if m["title"] != "Fix bug" { + t.Errorf("expected title Fix bug, got %v", m["title"]) + } + if m["user"] != "bob" { + t.Errorf("expected user bob, got %v", m["user"]) + } + if m["additions"] != 10 { + t.Errorf("expected additions 10, got %v", m["additions"]) + } + if m["milestone"] != "v1.0" { + t.Errorf("expected milestone v1.0, got %v", m["milestone"]) + } + + labels := m["labels"].([]string) + if len(labels) != 2 || labels[0] != "bug" { + t.Errorf("expected labels [bug priority], got %v", labels) + } + + head := m["head"].(map[string]any) + if head["ref"] != "fix-branch" { + t.Errorf("expected head ref fix-branch, got %v", head["ref"]) + } + + assignees := m["assignees"].([]string) + if len(assignees) != 1 || assignees[0] != "alice" { + t.Errorf("expected assignees [alice], got %v", assignees) + } + + // merged fields should not be present for unmerged PR + if _, ok := m["merged_at"]; ok { + t.Error("merged_at should not be present for unmerged PR") + } +} + +func TestSlimPullRequests_ListIsSlimmer(t *testing.T) { + pr := &gitea_sdk.PullRequest{ + Index: 1, + Title: "PR title", + State: "open", + HTMLURL: "https://gitea.com/org/repo/pulls/1", + Poster: &gitea_sdk.User{UserName: "bob"}, + Body: "Full body text here", + Head: &gitea_sdk.PRBranchInfo{Ref: "feature"}, + Base: &gitea_sdk.PRBranchInfo{Ref: "main"}, + } + + single := slimPullRequest(pr) + list := slimPullRequests([]*gitea_sdk.PullRequest{pr}) + + // Single has body, list does not + if _, ok := single["body"]; !ok { + t.Error("single PR should have body") + } + if _, ok := list[0]["body"]; ok { + t.Error("list PR should not have body") + } + + // List has head as string ref, single has head as map + if _, ok := single["head"].(map[string]any); !ok { + t.Error("single PR head should be a map") + } + if list[0]["head"] != "feature" { + t.Errorf("list PR head should be string ref, got %v", list[0]["head"]) + } +} + +func TestSlimPullRequests_Nil(t *testing.T) { + if r := slimPullRequests(nil); len(r) != 0 { + t.Errorf("expected empty slice, got %v", r) + } +} diff --git a/operation/repo/branch.go b/operation/repo/branch.go index e4dac5b..95099bd 100644 --- a/operation/repo/branch.go +++ b/operation/repo/branch.go @@ -2,11 +2,11 @@ package repo import ( "context" - "errors" "fmt" "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_sdk "code.gitea.io/sdk/gitea" @@ -63,19 +63,20 @@ func init() { func CreateBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CreateBranchFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - branch, ok := req.GetArguments()["branch"].(string) - if !ok { - return to.ErrorResult(errors.New("branch is required")) + branch, err := params.GetString(args, "branch") + if err != nil { + return to.ErrorResult(err) } - oldBranch, _ := req.GetArguments()["old_branch"].(string) + oldBranch, _ := args["old_branch"].(string) client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -94,17 +95,18 @@ func CreateBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool func DeleteBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called DeleteBranchFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - branch, ok := req.GetArguments()["branch"].(string) - if !ok { - return to.ErrorResult(errors.New("branch is required")) + branch, err := params.GetString(args, "branch") + if err != nil { + return to.ErrorResult(err) } client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -120,18 +122,19 @@ func DeleteBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool func ListBranchesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListBranchesFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } opt := gitea_sdk.ListRepoBranchesOptions{ ListOptions: gitea_sdk.ListOptions{ Page: 1, - PageSize: 100, + PageSize: 30, }, } client, err := gitea.ClientFromContext(ctx) @@ -143,5 +146,5 @@ func ListBranchesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool return to.ErrorResult(fmt.Errorf("list branches error: %v", err)) } - return to.TextResult(branches) + return to.TextResult(slimBranches(branches)) } diff --git a/operation/repo/commit.go b/operation/repo/commit.go index e66cb4e..b372870 100644 --- a/operation/repo/commit.go +++ b/operation/repo/commit.go @@ -2,7 +2,6 @@ package repo import ( "context" - "errors" "fmt" "gitea.com/gitea/gitea-mcp/pkg/gitea" @@ -27,7 +26,7 @@ var ListRepoCommitsTool = mcp.NewTool( mcp.WithString("sha", mcp.Description("SHA or branch to start listing commits from")), mcp.WithString("path", mcp.Description("path indicates that only commits that include the path's file/dir should be returned.")), mcp.WithNumber("page", mcp.Required(), mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("page_size", mcp.Required(), mcp.Description("page size"), mcp.DefaultNumber(50), mcp.Min(1)), + mcp.WithNumber("page_size", mcp.Required(), mcp.Description("page size"), mcp.DefaultNumber(30), mcp.Min(1)), ) func init() { @@ -39,24 +38,25 @@ func init() { func ListRepoCommitsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListRepoCommitsFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) - } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) - } - page, err := params.GetIndex(req.GetArguments(), "page") + args := req.GetArguments() + owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } - pageSize, err := params.GetIndex(req.GetArguments(), "page_size") + repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } - sha, _ := req.GetArguments()["sha"].(string) - path, _ := req.GetArguments()["path"].(string) + page, err := params.GetIndex(args, "page") + if err != nil { + return to.ErrorResult(err) + } + pageSize, err := params.GetIndex(args, "page_size") + if err != nil { + return to.ErrorResult(err) + } + sha, _ := args["sha"].(string) + path, _ := args["path"].(string) opt := gitea_sdk.ListCommitOptions{ ListOptions: gitea_sdk.ListOptions{ Page: int(page), @@ -73,5 +73,5 @@ func ListRepoCommitsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT if err != nil { return to.ErrorResult(fmt.Errorf("list repo commits err: %v", err)) } - return to.TextResult(commits) + return to.TextResult(slimCommits(commits)) } diff --git a/operation/repo/file.go b/operation/repo/file.go index 623678f..f6e10e1 100644 --- a/operation/repo/file.go +++ b/operation/repo/file.go @@ -6,11 +6,11 @@ import ( "context" "encoding/base64" "encoding/json" - "errors" "fmt" "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_sdk "code.gitea.io/sdk/gitea" @@ -112,18 +112,19 @@ type ContentLine struct { func GetFileContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetFileFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - ref, _ := req.GetArguments()["ref"].(string) - filePath, ok := req.GetArguments()["filePath"].(string) - if !ok { - return to.ErrorResult(errors.New("filePath is required")) + ref, _ := args["ref"].(string) + filePath, err := params.GetString(args, "filePath") + if err != nil { + return to.ErrorResult(err) } client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -133,7 +134,7 @@ func GetFileContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo if err != nil { return to.ErrorResult(fmt.Errorf("get file err: %v", err)) } - withLines, _ := req.GetArguments()["withLines"].(bool) + withLines, _ := args["withLines"].(bool) if withLines { rawContent, err := base64.StdEncoding.DecodeString(*content.Content) if err != nil { @@ -170,23 +171,24 @@ func GetFileContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo contentStr := string(contentBytes) content.Content = &contentStr } - return to.TextResult(content) + return to.TextResult(slimContents(content)) } func GetDirContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetDirContentFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - ref, _ := req.GetArguments()["ref"].(string) - filePath, ok := req.GetArguments()["filePath"].(string) - if !ok { - return to.ErrorResult(errors.New("filePath is required")) + ref, _ := args["ref"].(string) + filePath, err := params.GetString(args, "filePath") + if err != nil { + return to.ErrorResult(err) } client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -196,26 +198,27 @@ func GetDirContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo if err != nil { return to.ErrorResult(fmt.Errorf("get dir content err: %v", err)) } - return to.TextResult(content) + return to.TextResult(slimDirEntries(content)) } func CreateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CreateFileFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - filePath, ok := req.GetArguments()["filePath"].(string) - if !ok { - return to.ErrorResult(errors.New("filePath is required")) + filePath, err := params.GetString(args, "filePath") + if err != nil { + return to.ErrorResult(err) } - content, _ := req.GetArguments()["content"].(string) - message, _ := req.GetArguments()["message"].(string) - branchName, _ := req.GetArguments()["branch_name"].(string) + content, _ := args["content"].(string) + message, _ := args["message"].(string) + branchName, _ := args["branch_name"].(string) opt := gitea_sdk.CreateFileOptions{ Content: base64.StdEncoding.EncodeToString([]byte(content)), FileOptions: gitea_sdk.FileOptions{ @@ -237,25 +240,26 @@ func CreateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe func UpdateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called UpdateFileFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - filePath, ok := req.GetArguments()["filePath"].(string) - if !ok { - return to.ErrorResult(errors.New("filePath is required")) + filePath, err := params.GetString(args, "filePath") + if err != nil { + return to.ErrorResult(err) } - sha, ok := req.GetArguments()["sha"].(string) - if !ok { - return to.ErrorResult(errors.New("sha is required")) + sha, err := params.GetString(args, "sha") + if err != nil { + return to.ErrorResult(err) } - content, _ := req.GetArguments()["content"].(string) - message, _ := req.GetArguments()["message"].(string) - branchName, _ := req.GetArguments()["branch_name"].(string) + content, _ := args["content"].(string) + message, _ := args["message"].(string) + branchName, _ := args["branch_name"].(string) opt := gitea_sdk.UpdateFileOptions{ SHA: sha, @@ -278,23 +282,24 @@ func UpdateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe func DeleteFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called DeleteFileFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - filePath, ok := req.GetArguments()["filePath"].(string) - if !ok { - return to.ErrorResult(errors.New("filePath is required")) + filePath, err := params.GetString(args, "filePath") + if err != nil { + return to.ErrorResult(err) } - message, _ := req.GetArguments()["message"].(string) - branchName, _ := req.GetArguments()["branch_name"].(string) - sha, ok := req.GetArguments()["sha"].(string) - if !ok { - return to.ErrorResult(errors.New("sha is required")) + message, _ := args["message"].(string) + branchName, _ := args["branch_name"].(string) + sha, err := params.GetString(args, "sha") + if err != nil { + return to.ErrorResult(err) } opt := gitea_sdk.DeleteFileOptions{ FileOptions: gitea_sdk.FileOptions{ diff --git a/operation/repo/release.go b/operation/repo/release.go index e8ec44c..c70189e 100644 --- a/operation/repo/release.go +++ b/operation/repo/release.go @@ -2,9 +2,7 @@ package repo import ( "context" - "errors" "fmt" - "time" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" @@ -96,44 +94,32 @@ func init() { }) } -// To avoid return too many tokens, we need to provide at least information as possible -// llm can call get release to get more information -type ListReleaseResult struct { - ID int64 `json:"id"` - TagName string `json:"tag_name"` - Target string `json:"target_commitish"` - Title string `json:"title"` - IsDraft bool `json:"draft"` - IsPrerelease bool `json:"prerelease"` - CreatedAt time.Time `json:"created_at"` - PublishedAt time.Time `json:"published_at"` -} - func CreateReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CreateReleasesFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return nil, errors.New("owner is required") + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return nil, errors.New("repo is required") + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - tagName, ok := req.GetArguments()["tag_name"].(string) - if !ok { - return nil, errors.New("tag_name is required") + tagName, err := params.GetString(args, "tag_name") + if err != nil { + return to.ErrorResult(err) } - target, ok := req.GetArguments()["target"].(string) - if !ok { - return nil, errors.New("target is required") + target, err := params.GetString(args, "target") + if err != nil { + return to.ErrorResult(err) } - title, ok := req.GetArguments()["title"].(string) - if !ok { - return nil, errors.New("title is required") + title, err := params.GetString(args, "title") + if err != nil { + return to.ErrorResult(err) } - isDraft, _ := req.GetArguments()["is_draft"].(bool) - isPreRelease, _ := req.GetArguments()["is_pre_release"].(bool) - body, _ := req.GetArguments()["body"].(string) + isDraft, _ := args["is_draft"].(bool) + isPreRelease, _ := args["is_pre_release"].(bool) + body, _ := args["body"].(string) client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -156,15 +142,16 @@ func CreateReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo func DeleteReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called DeleteReleaseFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return nil, errors.New("owner is required") + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return nil, errors.New("repo is required") + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - id, err := params.GetIndex(req.GetArguments(), "id") + id, err := params.GetIndex(args, "id") if err != nil { return to.ErrorResult(err) } @@ -183,15 +170,16 @@ func DeleteReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo func GetReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetReleaseFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return nil, errors.New("owner is required") + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return nil, errors.New("repo is required") + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - id, err := params.GetIndex(req.GetArguments(), "id") + id, err := params.GetIndex(args, "id") if err != nil { return to.ErrorResult(err) } @@ -205,18 +193,19 @@ func GetReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe return nil, fmt.Errorf("get release error: %v", err) } - return to.TextResult(release) + return to.TextResult(slimRelease(release)) } func GetLatestReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetLatestReleaseFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return nil, errors.New("owner is required") + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return nil, errors.New("repo is required") + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } client, err := gitea.ClientFromContext(ctx) @@ -228,31 +217,32 @@ func GetLatestReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return nil, fmt.Errorf("get latest release error: %v", err) } - return to.TextResult(release) + return to.TextResult(slimRelease(release)) } func ListReleasesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListReleasesFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return nil, errors.New("owner is required") + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return nil, errors.New("repo is required") + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } var pIsDraft *bool - isDraft, ok := req.GetArguments()["is_draft"].(bool) + isDraft, ok := args["is_draft"].(bool) if ok { pIsDraft = new(isDraft) } var pIsPreRelease *bool - isPreRelease, ok := req.GetArguments()["is_pre_release"].(bool) + isPreRelease, ok := args["is_pre_release"].(bool) if ok { pIsPreRelease = new(isPreRelease) } - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 20) + page := params.GetOptionalInt(args, "page", 1) + pageSize := params.GetOptionalInt(args, "pageSize", 20) client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -270,18 +260,5 @@ func ListReleasesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool return nil, fmt.Errorf("list releases error: %v", err) } - results := make([]ListReleaseResult, len(releases)) - for _, release := range releases { - results = append(results, ListReleaseResult{ - ID: release.ID, - TagName: release.TagName, - Target: release.Target, - Title: release.Title, - IsDraft: release.IsDraft, - IsPrerelease: release.IsPrerelease, - CreatedAt: release.CreatedAt, - PublishedAt: release.PublishedAt, - }) - } - return to.TextResult(results) + return to.TextResult(slimReleases(releases)) } diff --git a/operation/repo/repo.go b/operation/repo/repo.go index 18dc18c..dde7099 100644 --- a/operation/repo/repo.go +++ b/operation/repo/repo.go @@ -2,7 +2,6 @@ package repo import ( "context" - "errors" "fmt" "gitea.com/gitea/gitea-mcp/pkg/gitea" @@ -54,7 +53,7 @@ var ( ListMyReposToolName, mcp.WithDescription("List my repositories"), mcp.WithNumber("page", mcp.Required(), mcp.Description("Page number"), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("pageSize", mcp.Required(), mcp.Description("Page size number"), mcp.DefaultNumber(100), mcp.Min(1)), + mcp.WithNumber("pageSize", mcp.Required(), mcp.Description("Page size number"), mcp.DefaultNumber(30), mcp.Min(1)), ) ) @@ -108,20 +107,21 @@ func RegisterTool(s *server.MCPServer) { func CreateRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CreateRepoFn") - name, ok := req.GetArguments()["name"].(string) - if !ok { - return to.ErrorResult(errors.New("repository name is required")) + args := req.GetArguments() + name, err := params.GetString(args, "name") + if err != nil { + return to.ErrorResult(err) } - description, _ := req.GetArguments()["description"].(string) - private, _ := req.GetArguments()["private"].(bool) - issueLabels, _ := req.GetArguments()["issue_labels"].(string) - autoInit, _ := req.GetArguments()["auto_init"].(bool) - template, _ := req.GetArguments()["template"].(bool) - gitignores, _ := req.GetArguments()["gitignores"].(string) - license, _ := req.GetArguments()["license"].(string) - readme, _ := req.GetArguments()["readme"].(string) - defaultBranch, _ := req.GetArguments()["default_branch"].(string) - organization, _ := req.GetArguments()["organization"].(string) + description, _ := args["description"].(string) + private, _ := args["private"].(bool) + issueLabels, _ := args["issue_labels"].(string) + autoInit, _ := args["auto_init"].(bool) + template, _ := args["template"].(bool) + gitignores, _ := args["gitignores"].(string) + license, _ := args["license"].(string) + readme, _ := args["readme"].(string) + defaultBranch, _ := args["default_branch"].(string) + organization, _ := args["organization"].(string) opt := gitea_sdk.CreateRepoOption{ Name: name, @@ -152,25 +152,26 @@ func CreateRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe return to.ErrorResult(fmt.Errorf("create repository '%s' err: %v", name, err)) } } - return to.TextResult(repo) + return to.TextResult(slimRepo(repo)) } func ForkRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ForkRepoFn") - user, ok := req.GetArguments()["user"].(string) - if !ok { - return to.ErrorResult(errors.New("user name is required")) + args := req.GetArguments() + user, err := params.GetString(args, "user") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repository name is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - organization, ok := req.GetArguments()["organization"].(string) + organization, ok := args["organization"].(string) organizationPtr := new(organization) if !ok || organization == "" { organizationPtr = nil } - name, ok := req.GetArguments()["name"].(string) + name, ok := args["name"].(string) namePtr := new(name) if !ok || name == "" { namePtr = nil @@ -192,12 +193,11 @@ func ForkRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResu func ListMyReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListMyReposFn") - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + page, pageSize := params.GetPagination(req.GetArguments(), 30) opt := gitea_sdk.ListReposOptions{ ListOptions: gitea_sdk.ListOptions{ - Page: int(page), - PageSize: int(pageSize), + Page: page, + PageSize: pageSize, }, } client, err := gitea.ClientFromContext(ctx) @@ -209,5 +209,5 @@ func ListMyReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR return to.ErrorResult(fmt.Errorf("list my repositories error: %v", err)) } - return to.TextResult(repos) + return to.TextResult(slimRepos(repos)) } diff --git a/operation/repo/slim.go b/operation/repo/slim.go new file mode 100644 index 0000000..5f3b418 --- /dev/null +++ b/operation/repo/slim.go @@ -0,0 +1,201 @@ +package repo + +import ( + gitea_sdk "code.gitea.io/sdk/gitea" +) + +func userLogin(u *gitea_sdk.User) string { + if u == nil { + return "" + } + return u.UserName +} + +func slimRepo(r *gitea_sdk.Repository) map[string]any { + if r == nil { + return nil + } + m := map[string]any{ + "id": r.ID, + "full_name": r.FullName, + "description": r.Description, + "html_url": r.HTMLURL, + "clone_url": r.CloneURL, + "ssh_url": r.SSHURL, + "default_branch": r.DefaultBranch, + "private": r.Private, + "fork": r.Fork, + "archived": r.Archived, + "language": r.Language, + "stars_count": r.Stars, + "forks_count": r.Forks, + "open_issues_count": r.OpenIssues, + "open_pr_counter": r.OpenPulls, + "created_at": r.Created, + "updated_at": r.Updated, + } + if r.Owner != nil { + m["owner"] = r.Owner.UserName + } + if len(r.Topics) > 0 { + m["topics"] = r.Topics + } + return m +} + +func slimRepos(repos []*gitea_sdk.Repository) []map[string]any { + out := make([]map[string]any, 0, len(repos)) + for _, r := range repos { + out = append(out, slimRepo(r)) + } + return out +} + +func slimBranch(b *gitea_sdk.Branch) map[string]any { + if b == nil { + return nil + } + m := map[string]any{ + "name": b.Name, + "protected": b.Protected, + } + if b.Commit != nil { + m["commit_sha"] = b.Commit.ID + } + return m +} + +func slimBranches(branches []*gitea_sdk.Branch) []map[string]any { + out := make([]map[string]any, 0, len(branches)) + for _, b := range branches { + out = append(out, slimBranch(b)) + } + return out +} + +func slimCommit(c *gitea_sdk.Commit) map[string]any { + if c == nil { + return nil + } + m := map[string]any{ + "sha": c.SHA, + "html_url": c.HTMLURL, + "created": c.Created, + } + if c.RepoCommit != nil { + m["message"] = c.RepoCommit.Message + if c.RepoCommit.Author != nil { + m["author"] = map[string]any{ + "name": c.RepoCommit.Author.Name, + "email": c.RepoCommit.Author.Email, + "date": c.RepoCommit.Author.Date, + } + } + } + return m +} + +func slimCommits(commits []*gitea_sdk.Commit) []map[string]any { + out := make([]map[string]any, 0, len(commits)) + for _, c := range commits { + out = append(out, slimCommit(c)) + } + return out +} + +func slimTag(t *gitea_sdk.Tag) map[string]any { + if t == nil { + return nil + } + m := map[string]any{ + "name": t.Name, + "message": t.Message, + } + if t.Commit != nil { + m["commit_sha"] = t.Commit.SHA + } + return m +} + +func slimTags(tags []*gitea_sdk.Tag) []map[string]any { + out := make([]map[string]any, 0, len(tags)) + for _, t := range tags { + m := map[string]any{ + "name": t.Name, + } + if t.Commit != nil { + m["commit_sha"] = t.Commit.SHA + } + out = append(out, m) + } + return out +} + +func slimRelease(r *gitea_sdk.Release) map[string]any { + if r == nil { + return nil + } + return map[string]any{ + "id": r.ID, + "tag_name": r.TagName, + "target": r.Target, + "title": r.Title, + "body": r.Note, + "draft": r.IsDraft, + "prerelease": r.IsPrerelease, + "html_url": r.HTMLURL, + "author": userLogin(r.Publisher), + "created_at": r.CreatedAt, + "published_at": r.PublishedAt, + } +} + +func slimReleases(releases []*gitea_sdk.Release) []map[string]any { + out := make([]map[string]any, 0, len(releases)) + for _, r := range releases { + out = append(out, slimRelease(r)) + } + return out +} + +func slimContents(c *gitea_sdk.ContentsResponse) map[string]any { + if c == nil { + return nil + } + m := map[string]any{ + "name": c.Name, + "path": c.Path, + "sha": c.SHA, + "type": c.Type, + "size": c.Size, + } + if c.Content != nil { + m["content"] = *c.Content + } + if c.Encoding != nil { + m["encoding"] = *c.Encoding + } + if c.HTMLURL != nil { + m["html_url"] = *c.HTMLURL + } + if c.DownloadURL != nil { + m["download_url"] = *c.DownloadURL + } + return m +} + +func slimDirEntries(entries []*gitea_sdk.ContentsResponse) []map[string]any { + out := make([]map[string]any, 0, len(entries)) + for _, c := range entries { + if c == nil { + continue + } + out = append(out, map[string]any{ + "name": c.Name, + "path": c.Path, + "type": c.Type, + "size": c.Size, + }) + } + return out +} diff --git a/operation/repo/slim_test.go b/operation/repo/slim_test.go new file mode 100644 index 0000000..60bbc7a --- /dev/null +++ b/operation/repo/slim_test.go @@ -0,0 +1,142 @@ +package repo + +import ( + "testing" + + gitea_sdk "code.gitea.io/sdk/gitea" +) + +func TestSlimRepo(t *testing.T) { + r := &gitea_sdk.Repository{ + ID: 1, + FullName: "org/repo", + Description: "A test repo", + HTMLURL: "https://gitea.com/org/repo", + CloneURL: "https://gitea.com/org/repo.git", + SSHURL: "git@gitea.com:org/repo.git", + DefaultBranch: "main", + Private: false, + Fork: false, + Archived: false, + Language: "Go", + Stars: 10, + Forks: 2, + Owner: &gitea_sdk.User{UserName: "org"}, + Topics: []string{"mcp", "gitea"}, + } + + m := slimRepo(r) + + if m["full_name"] != "org/repo" { + t.Errorf("expected full_name org/repo, got %v", m["full_name"]) + } + if m["owner"] != "org" { + t.Errorf("expected owner org, got %v", m["owner"]) + } + topics := m["topics"].([]string) + if len(topics) != 2 { + t.Errorf("expected 2 topics, got %d", len(topics)) + } +} + +func TestSlimTag(t *testing.T) { + tag := &gitea_sdk.Tag{ + Name: "v1.0.0", + Message: "Release v1.0.0", + Commit: &gitea_sdk.CommitMeta{SHA: "abc123"}, + } + + m := slimTag(tag) + if m["name"] != "v1.0.0" { + t.Errorf("expected name v1.0.0, got %v", m["name"]) + } + if m["message"] != "Release v1.0.0" { + t.Errorf("expected message, got %v", m["message"]) + } + + // List variant omits message + list := slimTags([]*gitea_sdk.Tag{tag}) + if _, ok := list[0]["message"]; ok { + t.Error("Tags list should omit message") + } + if list[0]["name"] != "v1.0.0" { + t.Errorf("expected name in list, got %v", list[0]["name"]) + } +} + +func TestSlimRelease(t *testing.T) { + r := &gitea_sdk.Release{ + ID: 1, + TagName: "v1.0.0", + Title: "First Release", + Note: "Release notes", + IsDraft: false, + Publisher: &gitea_sdk.User{UserName: "alice"}, + } + + m := slimRelease(r) + if m["tag_name"] != "v1.0.0" { + t.Errorf("expected tag_name v1.0.0, got %v", m["tag_name"]) + } + if m["body"] != "Release notes" { + t.Errorf("expected body from Note field, got %v", m["body"]) + } + if m["author"] != "alice" { + t.Errorf("expected author alice, got %v", m["author"]) + } +} + +func TestSlimContents(t *testing.T) { + content := "package main" + encoding := "base64" + htmlURL := "https://gitea.com/org/repo/src/branch/main/main.go" + c := &gitea_sdk.ContentsResponse{ + Name: "main.go", + Path: "main.go", + SHA: "abc123", + Type: "file", + Size: 12, + Content: &content, + Encoding: &encoding, + HTMLURL: &htmlURL, + } + + m := slimContents(c) + if m["name"] != "main.go" { + t.Errorf("expected name main.go, got %v", m["name"]) + } + if m["content"] != "package main" { + t.Errorf("expected content, got %v", m["content"]) + } +} + +func TestSlimDirEntries(t *testing.T) { + entries := []*gitea_sdk.ContentsResponse{ + {Name: "src", Path: "src", Type: "dir", Size: 0}, + {Name: "main.go", Path: "main.go", Type: "file", Size: 100}, + } + + result := slimDirEntries(entries) + if len(result) != 2 { + t.Fatalf("expected 2 entries, got %d", len(result)) + } + if result[0]["name"] != "src" { + t.Errorf("expected first entry name src, got %v", result[0]["name"]) + } + // Dir entries should not have content + if _, ok := result[0]["content"]; ok { + t.Error("dir entries should not have content field") + } +} + +func TestSlimTags_Nil(t *testing.T) { + if r := slimTags(nil); len(r) != 0 { + t.Errorf("expected empty slice, got %v", r) + } +} + +func TestSlimReleases_Nil(t *testing.T) { + if r := slimReleases(nil); len(r) != 0 { + t.Errorf("expected empty slice, got %v", r) + } +} diff --git a/operation/repo/tag.go b/operation/repo/tag.go index 42803df..6dcf494 100644 --- a/operation/repo/tag.go +++ b/operation/repo/tag.go @@ -2,7 +2,6 @@ package repo import ( "context" - "errors" "fmt" "gitea.com/gitea/gitea-mcp/pkg/gitea" @@ -78,31 +77,23 @@ func init() { }) } -// To avoid return too many tokens, we need to provide at least information as possible -// llm can call get tag to get more information -type ListTagResult struct { - ID string `json:"id"` - Name string `json:"name"` - Commit *gitea_sdk.CommitMeta `json:"commit"` - // message may be a long text, so we should not provide it here -} - func CreateTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CreateTagFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return nil, errors.New("owner is required") + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return nil, errors.New("repo is required") + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - tagName, ok := req.GetArguments()["tag_name"].(string) - if !ok { - return nil, errors.New("tag_name is required") + tagName, err := params.GetString(args, "tag_name") + if err != nil { + return to.ErrorResult(err) } - target, _ := req.GetArguments()["target"].(string) - message, _ := req.GetArguments()["message"].(string) + target, _ := args["target"].(string) + message, _ := args["message"].(string) client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -122,17 +113,18 @@ func CreateTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes func DeleteTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called DeleteTagFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return nil, errors.New("owner is required") + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return nil, errors.New("repo is required") + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - tagName, ok := req.GetArguments()["tag_name"].(string) - if !ok { - return nil, errors.New("tag_name is required") + tagName, err := params.GetString(args, "tag_name") + if err != nil { + return to.ErrorResult(err) } client, err := gitea.ClientFromContext(ctx) @@ -149,17 +141,18 @@ func DeleteTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes func GetTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetTagFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return nil, errors.New("owner is required") + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return nil, errors.New("repo is required") + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - tagName, ok := req.GetArguments()["tag_name"].(string) - if !ok { - return nil, errors.New("tag_name is required") + tagName, err := params.GetString(args, "tag_name") + if err != nil { + return to.ErrorResult(err) } client, err := gitea.ClientFromContext(ctx) @@ -171,21 +164,22 @@ func GetTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult return nil, fmt.Errorf("get tag error: %v", err) } - return to.TextResult(tag) + return to.TextResult(slimTag(tag)) } func ListTagsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListTagsFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return nil, errors.New("owner is required") + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return nil, errors.New("repo is required") + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 20) + page := params.GetOptionalInt(args, "page", 1) + pageSize := params.GetOptionalInt(args, "pageSize", 20) client, err := gitea.ClientFromContext(ctx) if err != nil { @@ -201,13 +195,5 @@ func ListTagsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResu return nil, fmt.Errorf("list tags error: %v", err) } - results := make([]ListTagResult, 0, len(tags)) - for _, tag := range tags { - results = append(results, ListTagResult{ - ID: tag.ID, - Name: tag.Name, - Commit: tag.Commit, - }) - } - return to.TextResult(results) + return to.TextResult(slimTags(tags)) } diff --git a/operation/search/search.go b/operation/search/search.go index bbb6127..1472591 100644 --- a/operation/search/search.go +++ b/operation/search/search.go @@ -2,7 +2,6 @@ package search import ( "context" - "errors" "fmt" "gitea.com/gitea/gitea-mcp/pkg/gitea" @@ -30,7 +29,7 @@ var ( mcp.WithDescription("search users"), mcp.WithString("keyword", mcp.Required(), mcp.Description("Keyword")), mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)), - mcp.WithNumber("pageSize", mcp.Description("PageSize"), mcp.DefaultNumber(100)), + mcp.WithNumber("pageSize", mcp.Description("PageSize"), mcp.DefaultNumber(30)), ) SearOrgTeamsTool = mcp.NewTool( @@ -40,7 +39,7 @@ var ( mcp.WithString("query", mcp.Required(), mcp.Description("search organization teams")), mcp.WithBoolean("includeDescription", mcp.Description("include description?")), mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)), - mcp.WithNumber("pageSize", mcp.Description("PageSize"), mcp.DefaultNumber(100)), + mcp.WithNumber("pageSize", mcp.Description("PageSize"), mcp.DefaultNumber(30)), ) SearchReposTool = mcp.NewTool( @@ -55,7 +54,7 @@ var ( mcp.WithString("sort", mcp.Description("Sort")), mcp.WithString("order", mcp.Description("Order")), mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)), - mcp.WithNumber("pageSize", mcp.Description("PageSize"), mcp.DefaultNumber(100)), + mcp.WithNumber("pageSize", mcp.Description("PageSize"), mcp.DefaultNumber(30)), ) ) @@ -76,17 +75,16 @@ func init() { func UsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called UsersFn") - keyword, ok := req.GetArguments()["keyword"].(string) - if !ok { - return to.ErrorResult(errors.New("keyword is required")) + keyword, err := params.GetString(req.GetArguments(), "keyword") + if err != nil { + return to.ErrorResult(err) } - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + page, pageSize := params.GetPagination(req.GetArguments(), 30) opt := gitea_sdk.SearchUsersOption{ KeyWord: keyword, ListOptions: gitea_sdk.ListOptions{ - Page: int(page), - PageSize: int(pageSize), + Page: page, + PageSize: pageSize, }, } client, err := gitea.ClientFromContext(ctx) @@ -97,28 +95,27 @@ func UsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, if err != nil { return to.ErrorResult(fmt.Errorf("search users err: %v", err)) } - return to.TextResult(users) + return to.TextResult(slimUserDetails(users)) } func OrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called OrgTeamsFn") - org, ok := req.GetArguments()["org"].(string) - if !ok { - return to.ErrorResult(errors.New("organization is required")) + org, err := params.GetString(req.GetArguments(), "org") + if err != nil { + return to.ErrorResult(err) } - query, ok := req.GetArguments()["query"].(string) - if !ok { - return to.ErrorResult(errors.New("query is required")) + query, err := params.GetString(req.GetArguments(), "query") + if err != nil { + return to.ErrorResult(err) } includeDescription, _ := req.GetArguments()["includeDescription"].(bool) - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + page, pageSize := params.GetPagination(req.GetArguments(), 30) opt := gitea_sdk.SearchTeamsOptions{ Query: query, IncludeDescription: includeDescription, ListOptions: gitea_sdk.ListOptions{ - Page: int(page), - PageSize: int(pageSize), + Page: page, + PageSize: pageSize, }, } client, err := gitea.ClientFromContext(ctx) @@ -129,14 +126,14 @@ func OrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResu if err != nil { return to.ErrorResult(fmt.Errorf("search organization teams error: %v", err)) } - return to.TextResult(teams) + return to.TextResult(slimTeams(teams)) } func ReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ReposFn") - keyword, ok := req.GetArguments()["keyword"].(string) - if !ok { - return to.ErrorResult(errors.New("keyword is required")) + keyword, err := params.GetString(req.GetArguments(), "keyword") + if err != nil { + return to.ErrorResult(err) } keywordIsTopic, _ := req.GetArguments()["keywordIsTopic"].(bool) keywordInDescription, _ := req.GetArguments()["keywordInDescription"].(bool) @@ -153,8 +150,7 @@ func ReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, } sort, _ := req.GetArguments()["sort"].(string) order, _ := req.GetArguments()["order"].(string) - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + page, pageSize := params.GetPagination(req.GetArguments(), 30) opt := gitea_sdk.SearchRepoOptions{ Keyword: keyword, KeywordIsTopic: keywordIsTopic, @@ -165,8 +161,8 @@ func ReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, Sort: sort, Order: order, ListOptions: gitea_sdk.ListOptions{ - Page: int(page), - PageSize: int(pageSize), + Page: page, + PageSize: pageSize, }, } client, err := gitea.ClientFromContext(ctx) @@ -177,5 +173,5 @@ func ReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, if err != nil { return to.ErrorResult(fmt.Errorf("search repos error: %v", err)) } - return to.TextResult(repos) + return to.TextResult(slimRepos(repos)) } diff --git a/operation/search/slim.go b/operation/search/slim.go new file mode 100644 index 0000000..a37a6b5 --- /dev/null +++ b/operation/search/slim.go @@ -0,0 +1,88 @@ +package search + +import ( + gitea_sdk "code.gitea.io/sdk/gitea" +) + +func slimUserDetail(u *gitea_sdk.User) map[string]any { + if u == nil { + return nil + } + return map[string]any{ + "id": u.ID, + "login": u.UserName, + "full_name": u.FullName, + "email": u.Email, + "avatar_url": u.AvatarURL, + "html_url": u.HTMLURL, + "is_admin": u.IsAdmin, + } +} + +func slimUserDetails(users []*gitea_sdk.User) []map[string]any { + out := make([]map[string]any, 0, len(users)) + for _, u := range users { + out = append(out, slimUserDetail(u)) + } + return out +} + +func slimTeam(t *gitea_sdk.Team) map[string]any { + if t == nil { + return nil + } + return map[string]any{ + "id": t.ID, + "name": t.Name, + "description": t.Description, + "permission": t.Permission, + } +} + +func slimTeams(teams []*gitea_sdk.Team) []map[string]any { + out := make([]map[string]any, 0, len(teams)) + for _, t := range teams { + out = append(out, slimTeam(t)) + } + return out +} + +func slimRepo(r *gitea_sdk.Repository) map[string]any { + if r == nil { + return nil + } + m := map[string]any{ + "id": r.ID, + "full_name": r.FullName, + "description": r.Description, + "html_url": r.HTMLURL, + "clone_url": r.CloneURL, + "ssh_url": r.SSHURL, + "default_branch": r.DefaultBranch, + "private": r.Private, + "fork": r.Fork, + "archived": r.Archived, + "language": r.Language, + "stars_count": r.Stars, + "forks_count": r.Forks, + "open_issues_count": r.OpenIssues, + "open_pr_counter": r.OpenPulls, + "created_at": r.Created, + "updated_at": r.Updated, + } + if r.Owner != nil { + m["owner"] = r.Owner.UserName + } + if len(r.Topics) > 0 { + m["topics"] = r.Topics + } + return m +} + +func slimRepos(repos []*gitea_sdk.Repository) []map[string]any { + out := make([]map[string]any, 0, len(repos)) + for _, r := range repos { + out = append(out, slimRepo(r)) + } + return out +} diff --git a/operation/timetracking/slim.go b/operation/timetracking/slim.go new file mode 100644 index 0000000..b360a48 --- /dev/null +++ b/operation/timetracking/slim.go @@ -0,0 +1,47 @@ +package timetracking + +import ( + gitea_sdk "code.gitea.io/sdk/gitea" +) + +func slimStopWatch(s *gitea_sdk.StopWatch) map[string]any { + if s == nil { + return nil + } + return map[string]any{ + "issue_index": s.IssueIndex, + "issue_title": s.IssueTitle, + "repo_name": s.RepoName, + "repo_owner": s.RepoOwnerName, + "created": s.Created, + "seconds": s.Seconds, + } +} + +func slimStopWatches(watches []*gitea_sdk.StopWatch) []map[string]any { + out := make([]map[string]any, 0, len(watches)) + for _, s := range watches { + out = append(out, slimStopWatch(s)) + } + return out +} + +func slimTrackedTime(t *gitea_sdk.TrackedTime) map[string]any { + if t == nil { + return nil + } + return map[string]any{ + "id": t.ID, + "time": t.Time, + "user_name": t.UserName, + "created": t.Created, + } +} + +func slimTrackedTimes(times []*gitea_sdk.TrackedTime) []map[string]any { + out := make([]map[string]any, 0, len(times)) + for _, t := range times { + out = append(out, slimTrackedTime(t)) + } + return out +} diff --git a/operation/timetracking/timetracking.go b/operation/timetracking/timetracking.go index fb82b8b..fb601fc 100644 --- a/operation/timetracking/timetracking.go +++ b/operation/timetracking/timetracking.go @@ -3,7 +3,6 @@ package timetracking import ( "context" - "errors" "fmt" gitea_sdk "code.gitea.io/sdk/gitea" @@ -73,7 +72,7 @@ var ( 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)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30)), ) AddTrackedTimeTool = mcp.NewTool( @@ -100,7 +99,7 @@ var ( 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)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30)), ) GetMyTimesTool = mcp.NewTool( @@ -128,13 +127,13 @@ func init() { 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(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } index, err := params.GetIndex(req.GetArguments(), "index") if err != nil { @@ -153,13 +152,13 @@ func StartStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo 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(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } index, err := params.GetIndex(req.GetArguments(), "index") if err != nil { @@ -178,13 +177,13 @@ func StopStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo 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(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } index, err := params.GetIndex(req.GetArguments(), "index") if err != nil { @@ -214,27 +213,26 @@ func GetMyStopwatchesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call if len(stopwatches) == 0 { return to.TextResult("No active stopwatches") } - return to.TextResult(stopwatches) + return to.TextResult(slimStopWatches(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(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } index, err := params.GetIndex(req.GetArguments(), "index") if err != nil { return to.ErrorResult(err) } - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + page, pageSize := params.GetPagination(req.GetArguments(), 30) client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) @@ -242,8 +240,8 @@ func ListTrackedTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call times, _, err := client.ListIssueTrackedTimes(owner, repo, index, gitea_sdk.ListTrackedTimesOptions{ ListOptions: gitea_sdk.ListOptions{ - Page: int(page), - PageSize: int(pageSize), + Page: page, + PageSize: pageSize, }, }) if err != nil { @@ -252,18 +250,18 @@ func ListTrackedTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call if len(times) == 0 { return to.TextResult(fmt.Sprintf("No tracked times for issue %s/%s#%d", owner, repo, index)) } - return to.TextResult(times) + return to.TextResult(slimTrackedTimes(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(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } index, err := params.GetIndex(req.GetArguments(), "index") if err != nil { @@ -284,18 +282,18 @@ func AddTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo 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) + return to.TextResult(slimTrackedTime(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(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } index, err := params.GetIndex(req.GetArguments(), "index") @@ -319,25 +317,24 @@ func DeleteTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Cal 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(errors.New("owner is required")) + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil { + return to.ErrorResult(err) } - page := params.GetOptionalInt(req.GetArguments(), "page", 1) - pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + page, pageSize := params.GetPagination(req.GetArguments(), 30) 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), + Page: page, + PageSize: pageSize, }, }) if err != nil { @@ -346,7 +343,7 @@ func ListRepoTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo if len(times) == 0 { return to.TextResult(fmt.Sprintf("No tracked times for repository %s/%s", owner, repo)) } - return to.TextResult(times) + return to.TextResult(slimTrackedTimes(times)) } func GetMyTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -362,5 +359,5 @@ func GetMyTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe if len(times) == 0 { return to.TextResult("No tracked times found") } - return to.TextResult(times) + return to.TextResult(slimTrackedTimes(times)) } diff --git a/operation/user/slim.go b/operation/user/slim.go new file mode 100644 index 0000000..421a20f --- /dev/null +++ b/operation/user/slim.go @@ -0,0 +1,42 @@ +package user + +import ( + gitea_sdk "code.gitea.io/sdk/gitea" +) + +func slimUserDetail(u *gitea_sdk.User) map[string]any { + if u == nil { + return nil + } + return map[string]any{ + "id": u.ID, + "login": u.UserName, + "full_name": u.FullName, + "email": u.Email, + "avatar_url": u.AvatarURL, + "html_url": u.HTMLURL, + "is_admin": u.IsAdmin, + } +} + +func slimOrg(o *gitea_sdk.Organization) map[string]any { + if o == nil { + return nil + } + return map[string]any{ + "id": o.ID, + "name": o.Name, + "full_name": o.FullName, + "description": o.Description, + "avatar_url": o.AvatarURL, + "website": o.Website, + } +} + +func slimOrgs(orgs []*gitea_sdk.Organization) []map[string]any { + out := make([]map[string]any, 0, len(orgs)) + for _, o := range orgs { + out = append(out, slimOrg(o)) + } + return out +} diff --git a/operation/user/slim_test.go b/operation/user/slim_test.go new file mode 100644 index 0000000..bc32887 --- /dev/null +++ b/operation/user/slim_test.go @@ -0,0 +1,39 @@ +package user + +import ( + "testing" + + gitea_sdk "code.gitea.io/sdk/gitea" +) + +func TestSlimUserDetail(t *testing.T) { + u := &gitea_sdk.User{ + ID: 42, + UserName: "alice", + FullName: "Alice Smith", + Email: "alice@example.com", + AvatarURL: "https://gitea.com/avatars/42", + HTMLURL: "https://gitea.com/alice", + IsAdmin: true, + } + m := slimUserDetail(u) + + if m["id"] != int64(42) { + t.Errorf("expected id 42, got %v", m["id"]) + } + if m["login"] != "alice" { + t.Errorf("expected login alice, got %v", m["login"]) + } + if m["full_name"] != "Alice Smith" { + t.Errorf("expected full_name Alice Smith, got %v", m["full_name"]) + } + if m["is_admin"] != true { + t.Errorf("expected is_admin true, got %v", m["is_admin"]) + } +} + +func TestSlimUserDetail_Nil(t *testing.T) { + if m := slimUserDetail(nil); m != nil { + t.Errorf("expected nil for nil user, got %v", m) + } +} diff --git a/operation/user/user.go b/operation/user/user.go index 21447bf..b093dd0 100644 --- a/operation/user/user.go +++ b/operation/user/user.go @@ -24,7 +24,7 @@ const ( // defaultPage is the default starting page number used for paginated organization listings. defaultPage = 1 // defaultPageSize is the default number of organizations per page for paginated queries. - defaultPageSize = 100 + defaultPageSize = 30 ) // Tool is the MCP tool manager instance for registering all MCP tools in this package. @@ -66,16 +66,6 @@ func registerTools() { } } -// getIntArg parses an integer argument from the MCP request arguments map. -// Returns def if missing, not a number, or less than 1. Used for pagination arguments. -func getIntArg(req mcp.CallToolRequest, name string, def int) int { - v := params.GetOptionalInt(req.GetArguments(), name, int64(def)) - if v < 1 { - return def - } - return int(v) -} - // GetUserInfoFn is the handler for "get_my_user_info" MCP tool requests. // Logs invocation, fetches current user info from gitea, wraps result for MCP. func GetUserInfoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -88,7 +78,7 @@ func GetUserInfoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR if err != nil { return to.ErrorResult(fmt.Errorf("get user info err: %v", err)) } - return to.TextResult(user) + return to.TextResult(slimUserDetail(user)) } // GetUserOrgsFn is the handler for "get_user_orgs" MCP tool requests. @@ -96,8 +86,7 @@ func GetUserInfoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR // performs Gitea organization listing, and wraps the result for MCP. func GetUserOrgsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("[User] Called GetUserOrgsFn") - page := getIntArg(req, "page", defaultPage) - pageSize := getIntArg(req, "pageSize", defaultPageSize) + page, pageSize := params.GetPagination(req.GetArguments(), defaultPageSize) opt := gitea_sdk.ListOrgsOptions{ ListOptions: gitea_sdk.ListOptions{ @@ -113,5 +102,5 @@ func GetUserOrgsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR if err != nil { return to.ErrorResult(fmt.Errorf("get user orgs err: %v", err)) } - return to.TextResult(orgs) + return to.TextResult(slimOrgs(orgs)) } diff --git a/operation/wiki/wiki.go b/operation/wiki/wiki.go index c0bf9e8..9ed42b6 100644 --- a/operation/wiki/wiki.go +++ b/operation/wiki/wiki.go @@ -2,12 +2,12 @@ package wiki import ( "context" - "errors" "fmt" "net/url" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" + "gitea.com/gitea/gitea-mcp/pkg/params" "gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/tool" @@ -109,18 +109,19 @@ func init() { func ListWikiPagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListWikiPagesFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } // Use direct HTTP request because SDK does not support yet wikis var result any - _, err := gitea.DoJSON(ctx, "GET", fmt.Sprintf("repos/%s/%s/wiki/pages", url.QueryEscape(owner), url.QueryEscape(repo)), nil, nil, &result) + _, err = gitea.DoJSON(ctx, "GET", fmt.Sprintf("repos/%s/%s/wiki/pages", url.QueryEscape(owner), url.QueryEscape(repo)), nil, nil, &result) if err != nil { return to.ErrorResult(fmt.Errorf("list wiki pages err: %v", err)) } @@ -130,21 +131,22 @@ func ListWikiPagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo func GetWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetWikiPageFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - pageName, ok := req.GetArguments()["pageName"].(string) - if !ok { - return to.ErrorResult(errors.New("pageName is required")) + pageName, err := params.GetString(args, "pageName") + if err != nil { + return to.ErrorResult(err) } var result any - _, err := gitea.DoJSON(ctx, "GET", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil, nil, &result) + _, err = gitea.DoJSON(ctx, "GET", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil, nil, &result) if err != nil { return to.ErrorResult(fmt.Errorf("get wiki page err: %v", err)) } @@ -154,21 +156,22 @@ func GetWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR func GetWikiRevisionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetWikiRevisionsFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - pageName, ok := req.GetArguments()["pageName"].(string) - if !ok { - return to.ErrorResult(errors.New("pageName is required")) + pageName, err := params.GetString(args, "pageName") + if err != nil { + return to.ErrorResult(err) } var result any - _, err := gitea.DoJSON(ctx, "GET", fmt.Sprintf("repos/%s/%s/wiki/revisions/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil, nil, &result) + _, err = gitea.DoJSON(ctx, "GET", fmt.Sprintf("repos/%s/%s/wiki/revisions/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil, nil, &result) if err != nil { return to.ErrorResult(fmt.Errorf("get wiki revisions err: %v", err)) } @@ -178,24 +181,25 @@ func GetWikiRevisionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call func CreateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CreateWikiPageFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - title, ok := req.GetArguments()["title"].(string) - if !ok { - return to.ErrorResult(errors.New("title is required")) + title, err := params.GetString(args, "title") + if err != nil { + return to.ErrorResult(err) } - contentBase64, ok := req.GetArguments()["content_base64"].(string) - if !ok { - return to.ErrorResult(errors.New("content_base64 is required")) + contentBase64, err := params.GetString(args, "content_base64") + if err != nil { + return to.ErrorResult(err) } - message, _ := req.GetArguments()["message"].(string) + message, _ := args["message"].(string) if message == "" { message = fmt.Sprintf("Create wiki page '%s'", title) } @@ -207,7 +211,7 @@ func CreateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo } var result any - _, err := gitea.DoJSON(ctx, "POST", fmt.Sprintf("repos/%s/%s/wiki/new", url.QueryEscape(owner), url.QueryEscape(repo)), nil, requestBody, &result) + _, err = gitea.DoJSON(ctx, "POST", fmt.Sprintf("repos/%s/%s/wiki/new", url.QueryEscape(owner), url.QueryEscape(repo)), nil, requestBody, &result) if err != nil { return to.ErrorResult(fmt.Errorf("create wiki page err: %v", err)) } @@ -217,21 +221,22 @@ func CreateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo func UpdateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called UpdateWikiPageFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - pageName, ok := req.GetArguments()["pageName"].(string) - if !ok { - return to.ErrorResult(errors.New("pageName is required")) + pageName, err := params.GetString(args, "pageName") + if err != nil { + return to.ErrorResult(err) } - contentBase64, ok := req.GetArguments()["content_base64"].(string) - if !ok { - return to.ErrorResult(errors.New("content_base64 is required")) + contentBase64, err := params.GetString(args, "content_base64") + if err != nil { + return to.ErrorResult(err) } requestBody := map[string]string{ @@ -239,21 +244,21 @@ func UpdateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo } // If title is given, use it. Otherwise, keep current page name - if title, ok := req.GetArguments()["title"].(string); ok && title != "" { + if title, ok := args["title"].(string); ok && title != "" { requestBody["title"] = title } else { // Utiliser pageName comme fallback pour éviter "unnamed" requestBody["title"] = pageName } - if message, ok := req.GetArguments()["message"].(string); ok && message != "" { + if message, ok := args["message"].(string); ok && message != "" { requestBody["message"] = message } else { requestBody["message"] = fmt.Sprintf("Update wiki page '%s'", pageName) } var result any - _, err := gitea.DoJSON(ctx, "PATCH", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil, requestBody, &result) + _, err = gitea.DoJSON(ctx, "PATCH", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil, requestBody, &result) if err != nil { return to.ErrorResult(fmt.Errorf("update wiki page err: %v", err)) } @@ -263,20 +268,21 @@ func UpdateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo func DeleteWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called DeleteWikiPageFn") - owner, ok := req.GetArguments()["owner"].(string) - if !ok { - return to.ErrorResult(errors.New("owner is required")) + args := req.GetArguments() + owner, err := params.GetString(args, "owner") + if err != nil { + return to.ErrorResult(err) } - repo, ok := req.GetArguments()["repo"].(string) - if !ok { - return to.ErrorResult(errors.New("repo is required")) + repo, err := params.GetString(args, "repo") + if err != nil { + return to.ErrorResult(err) } - pageName, ok := req.GetArguments()["pageName"].(string) - if !ok { - return to.ErrorResult(errors.New("pageName is required")) + pageName, err := params.GetString(args, "pageName") + if err != nil { + return to.ErrorResult(err) } - _, err := gitea.DoJSON(ctx, "DELETE", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil, nil, nil) + _, err = gitea.DoJSON(ctx, "DELETE", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil, nil, nil) if err != nil { return to.ErrorResult(fmt.Errorf("delete wiki page err: %v", err)) } diff --git a/pkg/params/params.go b/pkg/params/params.go index 5ace6a3..f12abf6 100644 --- a/pkg/params/params.go +++ b/pkg/params/params.go @@ -5,6 +5,47 @@ import ( "strconv" ) +// GetString extracts a required string parameter from MCP tool arguments. +func GetString(args map[string]any, key string) (string, error) { + val, ok := args[key].(string) + if !ok { + return "", fmt.Errorf("%s is required", key) + } + return val, nil +} + +// GetOptionalString extracts an optional string parameter with a default value. +func GetOptionalString(args map[string]any, key, defaultVal string) string { + if val, ok := args[key].(string); ok { + return val + } + return defaultVal +} + +// GetStringSlice extracts an optional string slice parameter from MCP tool arguments. +func GetStringSlice(args map[string]any, key string) []string { + val, ok := args[key] + if !ok { + return nil + } + sliceVal, ok := val.([]any) + if !ok { + return nil + } + out := make([]string, 0, len(sliceVal)) + for _, item := range sliceVal { + if s, ok := item.(string); ok { + out = append(out, s) + } + } + return out +} + +// GetPagination extracts page and pageSize parameters, returning them as ints. +func GetPagination(args map[string]any, defaultPageSize int64) (page, pageSize int) { + return int(GetOptionalInt(args, "page", 1)), int(GetOptionalInt(args, "pageSize", defaultPageSize)) +} + // ToInt64 converts a value to int64, accepting both float64 (JSON number) and // string representations. Returns false if the value cannot be converted. func ToInt64(val any) (int64, bool) { diff --git a/pkg/to/to.go b/pkg/to/to.go index 9622c80..412d98f 100644 --- a/pkg/to/to.go +++ b/pkg/to/to.go @@ -8,13 +8,8 @@ import ( "github.com/mark3labs/mcp-go/mcp" ) -type textResult struct { - Result any -} - func TextResult(v any) (*mcp.CallToolResult, error) { - result := textResult{v} - resultBytes, err := json.Marshal(result) + resultBytes, err := json.Marshal(v) if err != nil { return nil, fmt.Errorf("marshal result err: %v", err) }