From bba612d238d956e20c20d2f428b90b051996d6fc Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 6 Mar 2026 19:12:15 +0000 Subject: [PATCH] Consolidate tools from 110 to 45 using method dispatch (#143) Consolidate 110 individual MCP tools down to 45 using a method dispatch pattern, aligning tool names with the GitHub MCP server conventions. **Motivation:** LLMs work better with fewer, well-organized tools. The method dispatch pattern (used by GitHub's MCP server) groups related operations under read/write tools with a `method` parameter. **Changes:** - Group related tools into `_read`/`_write` pairs with method dispatch (e.g. `issue_read`, `issue_write`, `pull_request_read`, `pull_request_write`) - Rename tools to match GitHub MCP naming (`get_file_contents`, `create_or_update_file`, `list_issues`, `list_pull_requests`, etc.) - Rename `pageSize` to `perPage` for GitHub MCP compat - Move issue label ops (`add_labels`, `remove_label`, etc.) into `issue_write` - Merge `create_file`/`update_file` into `create_or_update_file` with optional `sha` - Make `delete_file` require `sha` - Add `get_labels` method to `issue_read` - Add shared helpers: `GetInt64Slice`, `GetStringSlice`, `GetPagination` in params package - Unexport all dispatch handler functions - Fix: pass assignees/milestone in `CreateIssue`, bounds check in `GetFileContent` Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/143 Reviewed-by: Lunny Xiao Co-authored-by: silverwind Co-committed-by: silverwind --- CLAUDE.md | 27 +- operation/actions/config.go | 555 +++++++++++++++++++++++++ operation/actions/logs.go | 187 --------- operation/actions/runs.go | 335 ++++++++++----- operation/actions/secrets.go | 280 ------------- operation/actions/variables.go | 389 ----------------- operation/issue/issue.go | 333 ++++++++++----- operation/issue/slim.go | 17 + operation/label/label.go | 417 ++++--------------- operation/milestone/milestone.go | 132 +++--- operation/pull/pull.go | 370 +++++++---------- operation/pull/pull_test.go | 18 +- operation/repo/commit.go | 6 +- operation/repo/file.go | 119 ++---- operation/repo/release.go | 4 +- operation/repo/repo.go | 35 +- operation/repo/tag.go | 4 +- operation/search/search.go | 6 +- operation/timetracking/timetracking.go | 187 ++++----- operation/user/user.go | 10 +- operation/wiki/wiki.go | 161 ++++--- pkg/params/params.go | 21 +- 22 files changed, 1574 insertions(+), 2039 deletions(-) create mode 100644 operation/actions/config.go delete mode 100644 operation/actions/logs.go delete mode 100644 operation/actions/secrets.go delete mode 100644 operation/actions/variables.go diff --git a/CLAUDE.md b/CLAUDE.md index 6607718..9d4ffe6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,17 +47,24 @@ This is a **Gitea MCP (Model Context Protocol) Server** written in Go that provi ## Available Tools -The server provides 40+ MCP tools covering: +The server provides 45 MCP tools covering: -- **User**: get_my_user_info, get_user_orgs, search_users -- **Repository**: create_repo, fork_repo, list_my_repos, search_repos -- **Branches/Tags**: create_branch, delete_branch, list_branches, create_tag, list_tags -- **Files**: get_file_content, create_file, update_file, delete_file, get_dir_content -- **Issues**: create_issue, list_repo_issues, create_issue_comment, edit_issue -- **Pull Requests**: create_pull_request, list_repo_pull_requests, get_pull_request_by_index -- **Releases**: create_release, list_releases, get_latest_release -- **Wiki**: create_wiki_page, update_wiki_page, list_wiki_pages -- **Search**: search_repos, search_users, search_org_teams +- **User**: get_me, get_user_orgs +- **Search**: search_users, search_repos, search_org_teams +- **Repository**: create_repo, fork_repo, list_my_repos +- **Branches**: list_branches, create_branch, delete_branch +- **Tags**: list_tags, get_tag, create_tag, delete_tag +- **Files**: get_file_contents, get_dir_contents, create_or_update_file, delete_file +- **Commits**: list_commits +- **Issues**: list_issues, issue_read, issue_write +- **Pull Requests**: list_pull_requests, pull_request_read, pull_request_write, pull_request_review_write +- **Labels**: label_read, label_write +- **Milestones**: milestone_read, milestone_write +- **Releases**: list_releases, get_release, get_latest_release, create_release, delete_release +- **Wiki**: wiki_read, wiki_write +- **Time Tracking**: timetracking_read, timetracking_write +- **Actions Runs**: actions_run_read, actions_run_write +- **Actions Config**: actions_config_read, actions_config_write - **Version**: get_gitea_mcp_server_version ## Common Development Patterns diff --git a/operation/actions/config.go b/operation/actions/config.go new file mode 100644 index 0000000..478f298 --- /dev/null +++ b/operation/actions/config.go @@ -0,0 +1,555 @@ +package actions + +import ( + "context" + "errors" + "fmt" + "net/url" + "strconv" + "time" + + "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" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ActionsConfigReadToolName = "actions_config_read" + ActionsConfigWriteToolName = "actions_config_write" +) + +type secretMeta struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + CreatedAt time.Time `json:"created_at,omitzero"` +} + +func toSecretMetas(secrets []*gitea_sdk.Secret) []secretMeta { + metas := make([]secretMeta, 0, len(secrets)) + for _, s := range secrets { + if s == nil { + continue + } + metas = append(metas, secretMeta{ + Name: s.Name, + Description: s.Description, + CreatedAt: s.Created, + }) + } + return metas +} + +var ( + ActionsConfigReadTool = mcp.NewTool( + ActionsConfigReadToolName, + mcp.WithDescription("Read Actions secrets and variables configuration."), + mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list_repo_secrets", "list_org_secrets", "list_repo_variables", "get_repo_variable", "list_org_variables", "get_org_variable")), + mcp.WithString("owner", mcp.Description("repository owner (required for repo methods)")), + mcp.WithString("repo", mcp.Description("repository name (required for repo methods)")), + mcp.WithString("org", mcp.Description("organization name (required for org methods)")), + mcp.WithString("name", mcp.Description("variable name (required for get methods)")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)), + mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30), mcp.Min(1)), + ) + + ActionsConfigWriteTool = mcp.NewTool( + ActionsConfigWriteToolName, + mcp.WithDescription("Manage Actions secrets and variables: create, update, or delete."), + mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("upsert_repo_secret", "delete_repo_secret", "upsert_org_secret", "delete_org_secret", "create_repo_variable", "update_repo_variable", "delete_repo_variable", "create_org_variable", "update_org_variable", "delete_org_variable")), + mcp.WithString("owner", mcp.Description("repository owner (required for repo methods)")), + mcp.WithString("repo", mcp.Description("repository name (required for repo methods)")), + mcp.WithString("org", mcp.Description("organization name (required for org methods)")), + mcp.WithString("name", mcp.Description("secret or variable name (required for most methods)")), + mcp.WithString("data", mcp.Description("secret value (required for upsert secret methods)")), + mcp.WithString("value", mcp.Description("variable value (required for create/update variable methods)")), + mcp.WithString("description", mcp.Description("description for secret or variable")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ActionsConfigReadTool, Handler: configReadFn}) + Tool.RegisterWrite(server.ServerTool{Tool: ActionsConfigWriteTool, Handler: configWriteFn}) +} + +func configReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + method, err := params.GetString(req.GetArguments(), "method") + if err != nil { + return to.ErrorResult(err) + } + switch method { + case "list_repo_secrets": + return listRepoActionSecretsFn(ctx, req) + case "list_org_secrets": + return listOrgActionSecretsFn(ctx, req) + case "list_repo_variables": + return listRepoActionVariablesFn(ctx, req) + case "get_repo_variable": + return getRepoActionVariableFn(ctx, req) + case "list_org_variables": + return listOrgActionVariablesFn(ctx, req) + case "get_org_variable": + return getOrgActionVariableFn(ctx, req) + default: + return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) + } +} + +func configWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + method, err := params.GetString(req.GetArguments(), "method") + if err != nil { + return to.ErrorResult(err) + } + switch method { + case "upsert_repo_secret": + return upsertRepoActionSecretFn(ctx, req) + case "delete_repo_secret": + return deleteRepoActionSecretFn(ctx, req) + case "upsert_org_secret": + return upsertOrgActionSecretFn(ctx, req) + case "delete_org_secret": + return deleteOrgActionSecretFn(ctx, req) + case "create_repo_variable": + return createRepoActionVariableFn(ctx, req) + case "update_repo_variable": + return updateRepoActionVariableFn(ctx, req) + case "delete_repo_variable": + return deleteRepoActionVariableFn(ctx, req) + case "create_org_variable": + return createOrgActionVariableFn(ctx, req) + case "update_org_variable": + return updateOrgActionVariableFn(ctx, req) + case "delete_org_variable": + return deleteOrgActionVariableFn(ctx, req) + default: + return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) + } +} + +// Secret functions + +func listRepoActionSecretsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called listRepoActionSecretsFn") + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { + return to.ErrorResult(errors.New("owner is required")) + } + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { + return to.ErrorResult(errors.New("repo is required")) + } + 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)) + } + + secrets, _, err := client.ListRepoActionSecret(owner, repo, gitea_sdk.ListRepoActionSecretOption{ + ListOptions: gitea_sdk.ListOptions{Page: page, PageSize: pageSize}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list repo action secrets err: %v", err)) + } + + return to.TextResult(toSecretMetas(secrets)) +} + +func upsertRepoActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called upsertRepoActionSecretFn") + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { + return to.ErrorResult(errors.New("owner is required")) + } + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { + return to.ErrorResult(errors.New("repo is required")) + } + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { + return to.ErrorResult(errors.New("name is required")) + } + data, err := params.GetString(req.GetArguments(), "data") + if err != nil || data == "" { + return to.ErrorResult(errors.New("data is required")) + } + description, _ := req.GetArguments()["description"].(string) + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + resp, err := client.CreateRepoActionSecret(owner, repo, gitea_sdk.CreateSecretOption{ + Name: name, + Data: data, + Description: description, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("upsert repo action secret err: %v", err)) + } + return to.TextResult(map[string]any{"message": "secret upserted", "status": resp.StatusCode}) +} + +func deleteRepoActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called deleteRepoActionSecretFn") + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { + return to.ErrorResult(errors.New("owner is required")) + } + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { + return to.ErrorResult(errors.New("repo is required")) + } + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { + return to.ErrorResult(errors.New("name is required")) + } + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + resp, err := client.DeleteRepoActionSecret(owner, repo, name) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete repo action secret err: %v", err)) + } + return to.TextResult(map[string]any{"message": "secret deleted", "status": resp.StatusCode}) +} + +func listOrgActionSecretsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called listOrgActionSecretsFn") + org, err := params.GetString(req.GetArguments(), "org") + if err != nil || org == "" { + return to.ErrorResult(errors.New("org is required")) + } + 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)) + } + + secrets, _, err := client.ListOrgActionSecret(org, gitea_sdk.ListOrgActionSecretOption{ + ListOptions: gitea_sdk.ListOptions{Page: page, PageSize: pageSize}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list org action secrets err: %v", err)) + } + + return to.TextResult(toSecretMetas(secrets)) +} + +func upsertOrgActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called upsertOrgActionSecretFn") + org, err := params.GetString(req.GetArguments(), "org") + if err != nil || org == "" { + return to.ErrorResult(errors.New("org is required")) + } + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { + return to.ErrorResult(errors.New("name is required")) + } + data, err := params.GetString(req.GetArguments(), "data") + if err != nil || data == "" { + return to.ErrorResult(errors.New("data is required")) + } + description, _ := req.GetArguments()["description"].(string) + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + resp, err := client.CreateOrgActionSecret(org, gitea_sdk.CreateSecretOption{ + Name: name, + Data: data, + Description: description, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("upsert org action secret err: %v", err)) + } + return to.TextResult(map[string]any{"message": "secret upserted", "status": resp.StatusCode}) +} + +func deleteOrgActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called deleteOrgActionSecretFn") + org, err := params.GetString(req.GetArguments(), "org") + if err != nil || org == "" { + return to.ErrorResult(errors.New("org is required")) + } + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { + return to.ErrorResult(errors.New("name is required")) + } + + escapedOrg := url.PathEscape(org) + escapedSecret := url.PathEscape(name) + _, 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)) + } + return to.TextResult(map[string]any{"message": "secret deleted"}) +} + +// Variable functions + +func listRepoActionVariablesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called listRepoActionVariablesFn") + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { + return to.ErrorResult(errors.New("owner is required")) + } + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { + return to.ErrorResult(errors.New("repo is required")) + } + page, pageSize := params.GetPagination(req.GetArguments(), 30) + + query := url.Values{} + 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) + if err != nil { + return to.ErrorResult(fmt.Errorf("list repo action variables err: %v", err)) + } + return to.TextResult(result) +} + +func getRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called getRepoActionVariableFn") + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { + return to.ErrorResult(errors.New("owner is required")) + } + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { + return to.ErrorResult(errors.New("repo is required")) + } + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { + return to.ErrorResult(errors.New("name is required")) + } + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + variable, _, err := client.GetRepoActionVariable(owner, repo, name) + if err != nil { + return to.ErrorResult(fmt.Errorf("get repo action variable err: %v", err)) + } + return to.TextResult(variable) +} + +func createRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called createRepoActionVariableFn") + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { + return to.ErrorResult(errors.New("owner is required")) + } + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { + return to.ErrorResult(errors.New("repo is required")) + } + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { + return to.ErrorResult(errors.New("name is required")) + } + value, err := params.GetString(req.GetArguments(), "value") + if err != nil || value == "" { + return to.ErrorResult(errors.New("value is required")) + } + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + resp, err := client.CreateRepoActionVariable(owner, repo, name, value) + if err != nil { + return to.ErrorResult(fmt.Errorf("create repo action variable err: %v", err)) + } + return to.TextResult(map[string]any{"message": "variable created", "status": resp.StatusCode}) +} + +func updateRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called updateRepoActionVariableFn") + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { + return to.ErrorResult(errors.New("owner is required")) + } + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { + return to.ErrorResult(errors.New("repo is required")) + } + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { + return to.ErrorResult(errors.New("name is required")) + } + value, err := params.GetString(req.GetArguments(), "value") + if err != nil || value == "" { + return to.ErrorResult(errors.New("value is required")) + } + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + resp, err := client.UpdateRepoActionVariable(owner, repo, name, value) + if err != nil { + return to.ErrorResult(fmt.Errorf("update repo action variable err: %v", err)) + } + return to.TextResult(map[string]any{"message": "variable updated", "status": resp.StatusCode}) +} + +func deleteRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called deleteRepoActionVariableFn") + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil || owner == "" { + return to.ErrorResult(errors.New("owner is required")) + } + repo, err := params.GetString(req.GetArguments(), "repo") + if err != nil || repo == "" { + return to.ErrorResult(errors.New("repo is required")) + } + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { + return to.ErrorResult(errors.New("name is required")) + } + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + resp, err := client.DeleteRepoActionVariable(owner, repo, name) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete repo action variable err: %v", err)) + } + return to.TextResult(map[string]any{"message": "variable deleted", "status": resp.StatusCode}) +} + +func listOrgActionVariablesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called listOrgActionVariablesFn") + org, err := params.GetString(req.GetArguments(), "org") + if err != nil || org == "" { + return to.ErrorResult(errors.New("org is required")) + } + 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: page, PageSize: pageSize}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list org action variables err: %v", err)) + } + return to.TextResult(variables) +} + +func getOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called getOrgActionVariableFn") + org, err := params.GetString(req.GetArguments(), "org") + if err != nil || org == "" { + return to.ErrorResult(errors.New("org is required")) + } + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { + return to.ErrorResult(errors.New("name is required")) + } + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + variable, _, err := client.GetOrgActionVariable(org, name) + if err != nil { + return to.ErrorResult(fmt.Errorf("get org action variable err: %v", err)) + } + return to.TextResult(variable) +} + +func createOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called createOrgActionVariableFn") + org, err := params.GetString(req.GetArguments(), "org") + if err != nil || org == "" { + return to.ErrorResult(errors.New("org is required")) + } + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { + return to.ErrorResult(errors.New("name is required")) + } + value, err := params.GetString(req.GetArguments(), "value") + if err != nil || value == "" { + return to.ErrorResult(errors.New("value is required")) + } + description, _ := req.GetArguments()["description"].(string) + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + resp, err := client.CreateOrgActionVariable(org, gitea_sdk.CreateOrgActionVariableOption{ + Name: name, + Value: value, + Description: description, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("create org action variable err: %v", err)) + } + return to.TextResult(map[string]any{"message": "variable created", "status": resp.StatusCode}) +} + +func updateOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called updateOrgActionVariableFn") + org, err := params.GetString(req.GetArguments(), "org") + if err != nil || org == "" { + return to.ErrorResult(errors.New("org is required")) + } + name, err := params.GetString(req.GetArguments(), "name") + if err != nil || name == "" { + return to.ErrorResult(errors.New("name is required")) + } + value, err := params.GetString(req.GetArguments(), "value") + if err != nil || value == "" { + return to.ErrorResult(errors.New("value is required")) + } + description, _ := req.GetArguments()["description"].(string) + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + resp, err := client.UpdateOrgActionVariable(org, name, gitea_sdk.UpdateOrgActionVariableOption{ + Value: value, + Description: description, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("update org action variable err: %v", err)) + } + return to.TextResult(map[string]any{"message": "variable updated", "status": resp.StatusCode}) +} + +func deleteOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called deleteOrgActionVariableFn") + org, err := params.GetString(req.GetArguments(), "org") + if err != nil || org == "" { + return to.ErrorResult(errors.New("org is required")) + } + 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) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete org action variable err: %v", err)) + } + return to.TextResult(map[string]any{"message": "variable deleted"}) +} diff --git a/operation/actions/logs.go b/operation/actions/logs.go deleted file mode 100644 index b9ca705..0000000 --- a/operation/actions/logs.go +++ /dev/null @@ -1,187 +0,0 @@ -package actions - -import ( - "context" - "errors" - "fmt" - "net/http" - "net/url" - "os" - "path/filepath" - - "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" - - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" -) - -const ( - GetRepoActionJobLogPreviewToolName = "get_repo_action_job_log_preview" - DownloadRepoActionJobLogToolName = "download_repo_action_job_log" -) - -var ( - GetRepoActionJobLogPreviewTool = mcp.NewTool( - GetRepoActionJobLogPreviewToolName, - mcp.WithDescription("Get a repository Actions job log preview (tail/limited for chat-friendly output)"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("job_id", mcp.Required(), mcp.Description("job ID")), - mcp.WithNumber("tail_lines", mcp.Description("number of lines from the end of the log"), mcp.DefaultNumber(200), mcp.Min(1)), - mcp.WithNumber("max_bytes", mcp.Description("max bytes to return"), mcp.DefaultNumber(65536), mcp.Min(1024)), - ) - - DownloadRepoActionJobLogTool = mcp.NewTool( - DownloadRepoActionJobLogToolName, - mcp.WithDescription("Download a repository Actions job log to a file on the MCP server filesystem"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("job_id", mcp.Required(), mcp.Description("job ID")), - mcp.WithString("output_path", mcp.Description("optional output file path; if omitted, uses ~/.gitea-mcp/artifacts/actions-logs/...")), - ) -) - -func init() { - Tool.RegisterRead(server.ServerTool{Tool: GetRepoActionJobLogPreviewTool, Handler: GetRepoActionJobLogPreviewFn}) - Tool.RegisterRead(server.ServerTool{Tool: DownloadRepoActionJobLogTool, Handler: DownloadRepoActionJobLogFn}) -} - -func logPaths(owner, repo string, jobID int64) []string { - // Primary candidate endpoints, plus a few commonly-seen variants across versions. - // We try these in order; 404/405 falls through. - return []string{ - fmt.Sprintf("repos/%s/%s/actions/jobs/%d/logs", url.PathEscape(owner), url.PathEscape(repo), jobID), - fmt.Sprintf("repos/%s/%s/actions/jobs/%d/log", url.PathEscape(owner), url.PathEscape(repo), jobID), - fmt.Sprintf("repos/%s/%s/actions/tasks/%d/log", url.PathEscape(owner), url.PathEscape(repo), jobID), - fmt.Sprintf("repos/%s/%s/actions/task/%d/log", url.PathEscape(owner), url.PathEscape(repo), jobID), - } -} - -func fetchJobLogBytes(ctx context.Context, owner, repo string, jobID int64) ([]byte, string, error) { - var lastErr error - for _, p := range logPaths(owner, repo, jobID) { - b, _, err := gitea.DoBytes(ctx, "GET", p, nil, nil, "text/plain") - if err == nil { - return b, p, nil - } - lastErr = err - var httpErr *gitea.HTTPError - if errors.As(err, &httpErr) && (httpErr.StatusCode == http.StatusNotFound || httpErr.StatusCode == http.StatusMethodNotAllowed) { - continue - } - return nil, p, err - } - return nil, "", lastErr -} - -func tailByLines(data []byte, tailLines int) []byte { - if tailLines <= 0 || len(data) == 0 { - return data - } - // Find the start index of the last N lines by scanning backwards. - lines := 0 - i := len(data) - 1 - for i >= 0 { - if data[i] == '\n' { - lines++ - if lines > tailLines { - return data[i+1:] - } - } - i-- - } - return data -} - -func limitBytes(data []byte, maxBytes int) ([]byte, bool) { - if maxBytes <= 0 { - return data, false - } - if len(data) <= maxBytes { - return data, false - } - // Keep the tail so the most recent log content is preserved. - return data[len(data)-maxBytes:], true -} - -func GetRepoActionJobLogPreviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called GetRepoActionJobLogPreviewFn") - owner, err := params.GetString(req.GetArguments(), "owner") - if err != nil { - return to.ErrorResult(err) - } - 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 { - return to.ErrorResult(err) - } - tailLines := int(params.GetOptionalInt(req.GetArguments(), "tail_lines", 200)) - maxBytes := int(params.GetOptionalInt(req.GetArguments(), "max_bytes", 65536)) - raw, usedPath, err := fetchJobLogBytes(ctx, owner, repo, jobID) - if err != nil { - return to.ErrorResult(fmt.Errorf("get job log err: %v", err)) - } - - tailed := tailByLines(raw, tailLines) - limited, truncated := limitBytes(tailed, maxBytes) - - return to.TextResult(map[string]any{ - "endpoint": usedPath, - "job_id": jobID, - "bytes": len(raw), - "tail_lines": tailLines, - "max_bytes": maxBytes, - "truncated": truncated, - "log": string(limited), - }) -} - -func DownloadRepoActionJobLogFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called DownloadRepoActionJobLogFn") - owner, err := params.GetString(req.GetArguments(), "owner") - if err != nil { - return to.ErrorResult(err) - } - 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 { - return to.ErrorResult(err) - } - outputPath, _ := req.GetArguments()["output_path"].(string) - - raw, usedPath, err := fetchJobLogBytes(ctx, owner, repo, jobID) - if err != nil { - return to.ErrorResult(fmt.Errorf("download job log err: %v", err)) - } - - if outputPath == "" { - home, _ := os.UserHomeDir() - if home == "" { - home = os.TempDir() - } - outputPath = filepath.Join(home, ".gitea-mcp", "artifacts", "actions-logs", owner, repo, fmt.Sprintf("%d.log", jobID)) - } - - if err := os.MkdirAll(filepath.Dir(outputPath), 0o700); err != nil { - return to.ErrorResult(fmt.Errorf("create output dir err: %v", err)) - } - if err := os.WriteFile(outputPath, raw, 0o600); err != nil { - return to.ErrorResult(fmt.Errorf("write log file err: %v", err)) - } - - return to.TextResult(map[string]any{ - "endpoint": usedPath, - "job_id": jobID, - "path": outputPath, - "bytes": len(raw), - }) -} diff --git a/operation/actions/runs.go b/operation/actions/runs.go index da9d915..9f20fa9 100644 --- a/operation/actions/runs.go +++ b/operation/actions/runs.go @@ -4,9 +4,10 @@ import ( "context" "errors" "fmt" - "maps" "net/http" "net/url" + "os" + "path/filepath" "strconv" "gitea.com/gitea/gitea-mcp/pkg/gitea" @@ -19,114 +20,88 @@ import ( ) const ( - ListRepoActionWorkflowsToolName = "list_repo_action_workflows" - GetRepoActionWorkflowToolName = "get_repo_action_workflow" - DispatchRepoActionWorkflowToolName = "dispatch_repo_action_workflow" - - ListRepoActionRunsToolName = "list_repo_action_runs" - GetRepoActionRunToolName = "get_repo_action_run" - CancelRepoActionRunToolName = "cancel_repo_action_run" - RerunRepoActionRunToolName = "rerun_repo_action_run" - - ListRepoActionJobsToolName = "list_repo_action_jobs" - ListRepoActionRunJobsToolName = "list_repo_action_run_jobs" + ActionsRunReadToolName = "actions_run_read" + ActionsRunWriteToolName = "actions_run_write" ) var ( - ListRepoActionWorkflowsTool = mcp.NewTool( - ListRepoActionWorkflowsToolName, - mcp.WithDescription("List repository Actions workflows"), + ActionsRunReadTool = mcp.NewTool( + ActionsRunReadToolName, + mcp.WithDescription("Read Actions workflow, run, and job data. Use method 'list_workflows'/'get_workflow' for workflows, 'list_runs'/'get_run' for runs, 'list_jobs'/'list_run_jobs' for jobs, 'get_job_log_preview'/'download_job_log' for logs."), + mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list_workflows", "get_workflow", "list_runs", "get_run", "list_jobs", "list_run_jobs", "get_job_log_preview", "download_job_log")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("workflow_id", mcp.Description("workflow ID or filename (required for 'get_workflow')")), + mcp.WithNumber("run_id", mcp.Description("run ID (required for 'get_run', 'list_run_jobs')")), + mcp.WithNumber("job_id", mcp.Description("job ID (required for 'get_job_log_preview', 'download_job_log')")), + mcp.WithString("status", mcp.Description("optional status filter (for 'list_runs', 'list_jobs')")), + mcp.WithNumber("tail_lines", mcp.Description("number of lines from end of log (for 'get_job_log_preview')"), mcp.DefaultNumber(200), mcp.Min(1)), + mcp.WithNumber("max_bytes", mcp.Description("max bytes to return (for 'get_job_log_preview')"), mcp.DefaultNumber(65536), mcp.Min(1024)), + mcp.WithString("output_path", mcp.Description("output file path (for 'download_job_log')")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30), mcp.Min(1)), + mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30), mcp.Min(1)), ) - GetRepoActionWorkflowTool = mcp.NewTool( - GetRepoActionWorkflowToolName, - mcp.WithDescription("Get a repository Actions workflow by ID"), + ActionsRunWriteTool = mcp.NewTool( + ActionsRunWriteToolName, + mcp.WithDescription("Trigger, cancel, or rerun Actions workflows."), + mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("dispatch_workflow", "cancel_run", "rerun_run")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("workflow_id", mcp.Required(), mcp.Description("workflow ID or filename (e.g. 'my-workflow.yml')")), - ) - - DispatchRepoActionWorkflowTool = mcp.NewTool( - DispatchRepoActionWorkflowToolName, - mcp.WithDescription("Trigger (dispatch) a repository Actions workflow"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("workflow_id", mcp.Required(), mcp.Description("workflow ID or filename (e.g. 'my-workflow.yml')")), - mcp.WithString("ref", mcp.Required(), mcp.Description("git ref (branch or tag)")), - mcp.WithObject("inputs", mcp.Description("workflow inputs object")), - ) - - ListRepoActionRunsTool = mcp.NewTool( - ListRepoActionRunsToolName, - mcp.WithDescription("List repository Actions workflow runs"), - 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(30), mcp.Min(1)), - mcp.WithString("status", mcp.Description("optional status filter")), - ) - - GetRepoActionRunTool = mcp.NewTool( - GetRepoActionRunToolName, - mcp.WithDescription("Get a repository Actions run by ID"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("run_id", mcp.Required(), mcp.Description("run ID")), - ) - - CancelRepoActionRunTool = mcp.NewTool( - CancelRepoActionRunToolName, - mcp.WithDescription("Cancel a repository Actions run"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("run_id", mcp.Required(), mcp.Description("run ID")), - ) - - RerunRepoActionRunTool = mcp.NewTool( - RerunRepoActionRunToolName, - mcp.WithDescription("Rerun a repository Actions run"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("run_id", mcp.Required(), mcp.Description("run ID")), - ) - - ListRepoActionJobsTool = mcp.NewTool( - ListRepoActionJobsToolName, - mcp.WithDescription("List repository Actions jobs"), - 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(30), mcp.Min(1)), - mcp.WithString("status", mcp.Description("optional status filter")), - ) - - ListRepoActionRunJobsTool = mcp.NewTool( - ListRepoActionRunJobsToolName, - mcp.WithDescription("List Actions jobs for a specific run"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - 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(30), mcp.Min(1)), + mcp.WithString("workflow_id", mcp.Description("workflow ID or filename (required for 'dispatch_workflow')")), + mcp.WithString("ref", mcp.Description("git ref branch or tag (required for 'dispatch_workflow')")), + mcp.WithObject("inputs", mcp.Description("workflow inputs object (for 'dispatch_workflow')")), + mcp.WithNumber("run_id", mcp.Description("run ID (required for 'cancel_run', 'rerun_run')")), ) ) func init() { - Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionWorkflowsTool, Handler: ListRepoActionWorkflowsFn}) - Tool.RegisterRead(server.ServerTool{Tool: GetRepoActionWorkflowTool, Handler: GetRepoActionWorkflowFn}) - Tool.RegisterWrite(server.ServerTool{Tool: DispatchRepoActionWorkflowTool, Handler: DispatchRepoActionWorkflowFn}) + Tool.RegisterRead(server.ServerTool{Tool: ActionsRunReadTool, Handler: runReadFn}) + Tool.RegisterWrite(server.ServerTool{Tool: ActionsRunWriteTool, Handler: runWriteFn}) +} - Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionRunsTool, Handler: ListRepoActionRunsFn}) - Tool.RegisterRead(server.ServerTool{Tool: GetRepoActionRunTool, Handler: GetRepoActionRunFn}) - Tool.RegisterWrite(server.ServerTool{Tool: CancelRepoActionRunTool, Handler: CancelRepoActionRunFn}) - Tool.RegisterWrite(server.ServerTool{Tool: RerunRepoActionRunTool, Handler: RerunRepoActionRunFn}) +func runReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + method, err := params.GetString(req.GetArguments(), "method") + if err != nil { + return to.ErrorResult(err) + } + switch method { + case "list_workflows": + return listRepoActionWorkflowsFn(ctx, req) + case "get_workflow": + return getRepoActionWorkflowFn(ctx, req) + case "list_runs": + return listRepoActionRunsFn(ctx, req) + case "get_run": + return getRepoActionRunFn(ctx, req) + case "list_jobs": + return listRepoActionJobsFn(ctx, req) + case "list_run_jobs": + return listRepoActionRunJobsFn(ctx, req) + case "get_job_log_preview": + return getRepoActionJobLogPreviewFn(ctx, req) + case "download_job_log": + return downloadRepoActionJobLogFn(ctx, req) + default: + return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) + } +} - Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionJobsTool, Handler: ListRepoActionJobsFn}) - Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionRunJobsTool, Handler: ListRepoActionRunJobsFn}) +func runWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + method, err := params.GetString(req.GetArguments(), "method") + if err != nil { + return to.ErrorResult(err) + } + switch method { + case "dispatch_workflow": + return dispatchRepoActionWorkflowFn(ctx, req) + case "cancel_run": + return cancelRepoActionRunFn(ctx, req) + case "rerun_run": + return rerunRepoActionRunFn(ctx, req) + default: + return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) + } } func doJSONWithFallback(ctx context.Context, method string, paths []string, query url.Values, body, respOut any) error { @@ -146,8 +121,8 @@ func doJSONWithFallback(ctx context.Context, method string, paths []string, quer return lastErr } -func ListRepoActionWorkflowsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called ListRepoActionWorkflowsFn") +func listRepoActionWorkflowsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called listRepoActionWorkflowsFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) @@ -174,8 +149,8 @@ func ListRepoActionWorkflowsFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.TextResult(slimActionWorkflows(result)) } -func GetRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called GetRepoActionWorkflowFn") +func getRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called getRepoActionWorkflowFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) @@ -202,8 +177,8 @@ func GetRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp return to.TextResult(slimActionWorkflow(result)) } -func DispatchRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called DispatchRepoActionWorkflowFn") +func dispatchRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called dispatchRepoActionWorkflowFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) @@ -225,9 +200,6 @@ func DispatchRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) if raw, exists := req.GetArguments()["inputs"]; exists { if m, ok := raw.(map[string]any); ok { inputs = m - } else if m, ok := raw.(map[string]any); ok { - inputs = make(map[string]any, len(m)) - maps.Copy(inputs, m) } } @@ -255,8 +227,8 @@ func DispatchRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) return to.TextResult(map[string]any{"message": "workflow dispatched"}) } -func ListRepoActionRunsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called ListRepoActionRunsFn") +func listRepoActionRunsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called listRepoActionRunsFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) @@ -288,8 +260,8 @@ func ListRepoActionRunsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca return to.TextResult(slimActionRuns(result)) } -func GetRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called GetRepoActionRunFn") +func getRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called getRepoActionRunFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) @@ -316,8 +288,8 @@ func GetRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return to.TextResult(slimActionRun(result)) } -func CancelRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called CancelRepoActionRunFn") +func cancelRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called cancelRepoActionRunFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) @@ -343,8 +315,8 @@ func CancelRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.C return to.TextResult(map[string]any{"message": "run cancellation requested"}) } -func RerunRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called RerunRepoActionRunFn") +func rerunRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called rerunRepoActionRunFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) @@ -375,8 +347,8 @@ func RerunRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca return to.TextResult(map[string]any{"message": "run rerun requested"}) } -func ListRepoActionJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called ListRepoActionJobsFn") +func listRepoActionJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called listRepoActionJobsFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) @@ -408,8 +380,8 @@ func ListRepoActionJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca return to.TextResult(slimActionJobs(result)) } -func ListRepoActionRunJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called ListRepoActionRunJobsFn") +func listRepoActionRunJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called listRepoActionRunJobsFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil || owner == "" { return to.ErrorResult(errors.New("owner is required")) @@ -440,3 +412,138 @@ func ListRepoActionRunJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp } return to.TextResult(slimActionJobs(result)) } + +// Log functions (merged from logs.go) + +func logPaths(owner, repo string, jobID int64) []string { + return []string{ + fmt.Sprintf("repos/%s/%s/actions/jobs/%d/logs", url.PathEscape(owner), url.PathEscape(repo), jobID), + fmt.Sprintf("repos/%s/%s/actions/jobs/%d/log", url.PathEscape(owner), url.PathEscape(repo), jobID), + fmt.Sprintf("repos/%s/%s/actions/tasks/%d/log", url.PathEscape(owner), url.PathEscape(repo), jobID), + fmt.Sprintf("repos/%s/%s/actions/task/%d/log", url.PathEscape(owner), url.PathEscape(repo), jobID), + } +} + +func fetchJobLogBytes(ctx context.Context, owner, repo string, jobID int64) ([]byte, string, error) { + var lastErr error + for _, p := range logPaths(owner, repo, jobID) { + b, _, err := gitea.DoBytes(ctx, "GET", p, nil, nil, "text/plain") + if err == nil { + return b, p, nil + } + lastErr = err + var httpErr *gitea.HTTPError + if errors.As(err, &httpErr) && (httpErr.StatusCode == http.StatusNotFound || httpErr.StatusCode == http.StatusMethodNotAllowed) { + continue + } + return nil, p, err + } + return nil, "", lastErr +} + +func tailByLines(data []byte, tailLines int) []byte { + if tailLines <= 0 || len(data) == 0 { + return data + } + lines := 0 + i := len(data) - 1 + for i >= 0 { + if data[i] == '\n' { + lines++ + if lines > tailLines { + return data[i+1:] + } + } + i-- + } + return data +} + +func limitBytes(data []byte, maxBytes int) ([]byte, bool) { + if maxBytes <= 0 { + return data, false + } + if len(data) <= maxBytes { + return data, false + } + return data[len(data)-maxBytes:], true +} + +func getRepoActionJobLogPreviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called getRepoActionJobLogPreviewFn") + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) + } + 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 { + return to.ErrorResult(err) + } + tailLines := int(params.GetOptionalInt(req.GetArguments(), "tail_lines", 200)) + maxBytes := int(params.GetOptionalInt(req.GetArguments(), "max_bytes", 65536)) + raw, usedPath, err := fetchJobLogBytes(ctx, owner, repo, jobID) + if err != nil { + return to.ErrorResult(fmt.Errorf("get job log err: %v", err)) + } + + tailed := tailByLines(raw, tailLines) + limited, truncated := limitBytes(tailed, maxBytes) + + return to.TextResult(map[string]any{ + "endpoint": usedPath, + "job_id": jobID, + "bytes": len(raw), + "tail_lines": tailLines, + "max_bytes": maxBytes, + "truncated": truncated, + "log": string(limited), + }) +} + +func downloadRepoActionJobLogFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called downloadRepoActionJobLogFn") + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) + } + 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 { + return to.ErrorResult(err) + } + outputPath, _ := req.GetArguments()["output_path"].(string) + + raw, usedPath, err := fetchJobLogBytes(ctx, owner, repo, jobID) + if err != nil { + return to.ErrorResult(fmt.Errorf("download job log err: %v", err)) + } + + if outputPath == "" { + home, _ := os.UserHomeDir() + if home == "" { + home = os.TempDir() + } + outputPath = filepath.Join(home, ".gitea-mcp", "artifacts", "actions-logs", owner, repo, fmt.Sprintf("%d.log", jobID)) + } + + if err := os.MkdirAll(filepath.Dir(outputPath), 0o700); err != nil { + return to.ErrorResult(fmt.Errorf("create output dir err: %v", err)) + } + if err := os.WriteFile(outputPath, raw, 0o600); err != nil { + return to.ErrorResult(fmt.Errorf("write log file err: %v", err)) + } + + return to.TextResult(map[string]any{ + "endpoint": usedPath, + "job_id": jobID, + "path": outputPath, + "bytes": len(raw), + }) +} diff --git a/operation/actions/secrets.go b/operation/actions/secrets.go deleted file mode 100644 index 92d85b0..0000000 --- a/operation/actions/secrets.go +++ /dev/null @@ -1,280 +0,0 @@ -package actions - -import ( - "context" - "errors" - "fmt" - "net/url" - "time" - - "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" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" -) - -const ( - ListRepoActionSecretsToolName = "list_repo_action_secrets" - UpsertRepoActionSecretToolName = "upsert_repo_action_secret" - DeleteRepoActionSecretToolName = "delete_repo_action_secret" - ListOrgActionSecretsToolName = "list_org_action_secrets" - UpsertOrgActionSecretToolName = "upsert_org_action_secret" - DeleteOrgActionSecretToolName = "delete_org_action_secret" -) - -type secretMeta struct { - Name string `json:"name"` - Description string `json:"description,omitempty"` - CreatedAt time.Time `json:"created_at,omitzero"` -} - -var ( - ListRepoActionSecretsTool = mcp.NewTool( - ListRepoActionSecretsToolName, - mcp.WithDescription("List repository Actions secrets (metadata only; secret values are never returned)"), - 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(30), mcp.Min(1)), - ) - - UpsertRepoActionSecretTool = mcp.NewTool( - UpsertRepoActionSecretToolName, - mcp.WithDescription("Create or update (upsert) a repository Actions secret"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("name", mcp.Required(), mcp.Description("secret name")), - mcp.WithString("data", mcp.Required(), mcp.Description("secret value")), - mcp.WithString("description", mcp.Description("secret description")), - ) - - DeleteRepoActionSecretTool = mcp.NewTool( - DeleteRepoActionSecretToolName, - mcp.WithDescription("Delete a repository Actions secret"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("secretName", mcp.Required(), mcp.Description("secret name")), - ) - - ListOrgActionSecretsTool = mcp.NewTool( - ListOrgActionSecretsToolName, - 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(30), mcp.Min(1)), - ) - - UpsertOrgActionSecretTool = mcp.NewTool( - UpsertOrgActionSecretToolName, - mcp.WithDescription("Create or update (upsert) an organization Actions secret"), - mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), - mcp.WithString("name", mcp.Required(), mcp.Description("secret name")), - mcp.WithString("data", mcp.Required(), mcp.Description("secret value")), - mcp.WithString("description", mcp.Description("secret description")), - ) - - DeleteOrgActionSecretTool = mcp.NewTool( - DeleteOrgActionSecretToolName, - mcp.WithDescription("Delete an organization Actions secret"), - mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), - mcp.WithString("secretName", mcp.Required(), mcp.Description("secret name")), - ) -) - -func init() { - Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionSecretsTool, Handler: ListRepoActionSecretsFn}) - Tool.RegisterWrite(server.ServerTool{Tool: UpsertRepoActionSecretTool, Handler: UpsertRepoActionSecretFn}) - Tool.RegisterWrite(server.ServerTool{Tool: DeleteRepoActionSecretTool, Handler: DeleteRepoActionSecretFn}) - - Tool.RegisterRead(server.ServerTool{Tool: ListOrgActionSecretsTool, Handler: ListOrgActionSecretsFn}) - Tool.RegisterWrite(server.ServerTool{Tool: UpsertOrgActionSecretTool, Handler: UpsertOrgActionSecretFn}) - Tool.RegisterWrite(server.ServerTool{Tool: DeleteOrgActionSecretTool, Handler: DeleteOrgActionSecretFn}) -} - -func ListRepoActionSecretsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called ListRepoActionSecretsFn") - owner, err := params.GetString(req.GetArguments(), "owner") - if err != nil || owner == "" { - return to.ErrorResult(errors.New("owner is required")) - } - repo, err := params.GetString(req.GetArguments(), "repo") - if err != nil || repo == "" { - return to.ErrorResult(errors.New("repo is required")) - } - 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)) - } - - secrets, _, err := client.ListRepoActionSecret(owner, repo, gitea_sdk.ListRepoActionSecretOption{ - ListOptions: gitea_sdk.ListOptions{Page: page, PageSize: pageSize}, - }) - if err != nil { - return to.ErrorResult(fmt.Errorf("list repo action secrets err: %v", err)) - } - - metas := make([]secretMeta, 0, len(secrets)) - for _, s := range secrets { - if s == nil { - continue - } - metas = append(metas, secretMeta{ - Name: s.Name, - Description: s.Description, - CreatedAt: s.Created, - }) - } - return to.TextResult(metas) -} - -func UpsertRepoActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called UpsertRepoActionSecretFn") - owner, err := params.GetString(req.GetArguments(), "owner") - if err != nil || owner == "" { - return to.ErrorResult(errors.New("owner is required")) - } - repo, err := params.GetString(req.GetArguments(), "repo") - if err != nil || repo == "" { - return to.ErrorResult(errors.New("repo is required")) - } - name, err := params.GetString(req.GetArguments(), "name") - if err != nil || name == "" { - return to.ErrorResult(errors.New("name is required")) - } - data, err := params.GetString(req.GetArguments(), "data") - if err != nil || data == "" { - return to.ErrorResult(errors.New("data is required")) - } - description, _ := req.GetArguments()["description"].(string) - - client, err := gitea.ClientFromContext(ctx) - if err != nil { - return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) - } - resp, err := client.CreateRepoActionSecret(owner, repo, gitea_sdk.CreateSecretOption{ - Name: name, - Data: data, - Description: description, - }) - if err != nil { - return to.ErrorResult(fmt.Errorf("upsert repo action secret err: %v", err)) - } - return to.TextResult(map[string]any{"message": "secret upserted", "status": resp.StatusCode}) -} - -func DeleteRepoActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called DeleteRepoActionSecretFn") - owner, err := params.GetString(req.GetArguments(), "owner") - if err != nil || owner == "" { - return to.ErrorResult(errors.New("owner is required")) - } - repo, err := params.GetString(req.GetArguments(), "repo") - if err != nil || repo == "" { - return to.ErrorResult(errors.New("repo is required")) - } - secretName, err := params.GetString(req.GetArguments(), "secretName") - if err != nil || secretName == "" { - return to.ErrorResult(errors.New("secretName is required")) - } - - client, err := gitea.ClientFromContext(ctx) - if err != nil { - return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) - } - resp, err := client.DeleteRepoActionSecret(owner, repo, secretName) - if err != nil { - return to.ErrorResult(fmt.Errorf("delete repo action secret err: %v", err)) - } - return to.TextResult(map[string]any{"message": "secret deleted", "status": resp.StatusCode}) -} - -func ListOrgActionSecretsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called ListOrgActionSecretsFn") - org, err := params.GetString(req.GetArguments(), "org") - if err != nil || org == "" { - return to.ErrorResult(errors.New("org is required")) - } - 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)) - } - - secrets, _, err := client.ListOrgActionSecret(org, gitea_sdk.ListOrgActionSecretOption{ - ListOptions: gitea_sdk.ListOptions{Page: page, PageSize: pageSize}, - }) - if err != nil { - return to.ErrorResult(fmt.Errorf("list org action secrets err: %v", err)) - } - - metas := make([]secretMeta, 0, len(secrets)) - for _, s := range secrets { - if s == nil { - continue - } - metas = append(metas, secretMeta{ - Name: s.Name, - Description: s.Description, - CreatedAt: s.Created, - }) - } - return to.TextResult(metas) -} - -func UpsertOrgActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called UpsertOrgActionSecretFn") - org, err := params.GetString(req.GetArguments(), "org") - if err != nil || org == "" { - return to.ErrorResult(errors.New("org is required")) - } - name, err := params.GetString(req.GetArguments(), "name") - if err != nil || name == "" { - return to.ErrorResult(errors.New("name is required")) - } - data, err := params.GetString(req.GetArguments(), "data") - if err != nil || data == "" { - return to.ErrorResult(errors.New("data is required")) - } - description, _ := req.GetArguments()["description"].(string) - - client, err := gitea.ClientFromContext(ctx) - if err != nil { - return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) - } - resp, err := client.CreateOrgActionSecret(org, gitea_sdk.CreateSecretOption{ - Name: name, - Data: data, - Description: description, - }) - if err != nil { - return to.ErrorResult(fmt.Errorf("upsert org action secret err: %v", err)) - } - return to.TextResult(map[string]any{"message": "secret upserted", "status": resp.StatusCode}) -} - -func DeleteOrgActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called DeleteOrgActionSecretFn") - org, err := params.GetString(req.GetArguments(), "org") - if err != nil || org == "" { - return to.ErrorResult(errors.New("org is required")) - } - 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) - if err != nil { - return to.ErrorResult(fmt.Errorf("delete org action secret err: %v", err)) - } - return to.TextResult(map[string]any{"message": "secret deleted"}) -} diff --git a/operation/actions/variables.go b/operation/actions/variables.go deleted file mode 100644 index e2d579a..0000000 --- a/operation/actions/variables.go +++ /dev/null @@ -1,389 +0,0 @@ -package actions - -import ( - "context" - "errors" - "fmt" - "net/url" - "strconv" - - "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" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" -) - -const ( - ListRepoActionVariablesToolName = "list_repo_action_variables" - GetRepoActionVariableToolName = "get_repo_action_variable" - CreateRepoActionVariableToolName = "create_repo_action_variable" - UpdateRepoActionVariableToolName = "update_repo_action_variable" - DeleteRepoActionVariableToolName = "delete_repo_action_variable" - - ListOrgActionVariablesToolName = "list_org_action_variables" - GetOrgActionVariableToolName = "get_org_action_variable" - CreateOrgActionVariableToolName = "create_org_action_variable" - UpdateOrgActionVariableToolName = "update_org_action_variable" - DeleteOrgActionVariableToolName = "delete_org_action_variable" -) - -var ( - ListRepoActionVariablesTool = mcp.NewTool( - ListRepoActionVariablesToolName, - mcp.WithDescription("List repository Actions variables"), - 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(30), mcp.Min(1)), - ) - - GetRepoActionVariableTool = mcp.NewTool( - GetRepoActionVariableToolName, - mcp.WithDescription("Get a repository Actions variable by name"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("name", mcp.Required(), mcp.Description("variable name")), - ) - - CreateRepoActionVariableTool = mcp.NewTool( - CreateRepoActionVariableToolName, - mcp.WithDescription("Create a repository Actions variable"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("name", mcp.Required(), mcp.Description("variable name")), - mcp.WithString("value", mcp.Required(), mcp.Description("variable value")), - ) - - UpdateRepoActionVariableTool = mcp.NewTool( - UpdateRepoActionVariableToolName, - mcp.WithDescription("Update a repository Actions variable"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("name", mcp.Required(), mcp.Description("variable name")), - mcp.WithString("value", mcp.Required(), mcp.Description("new variable value")), - ) - - DeleteRepoActionVariableTool = mcp.NewTool( - DeleteRepoActionVariableToolName, - mcp.WithDescription("Delete a repository Actions variable"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("name", mcp.Required(), mcp.Description("variable name")), - ) - - ListOrgActionVariablesTool = mcp.NewTool( - ListOrgActionVariablesToolName, - 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(30), mcp.Min(1)), - ) - - GetOrgActionVariableTool = mcp.NewTool( - GetOrgActionVariableToolName, - mcp.WithDescription("Get an organization Actions variable by name"), - mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), - mcp.WithString("name", mcp.Required(), mcp.Description("variable name")), - ) - - CreateOrgActionVariableTool = mcp.NewTool( - CreateOrgActionVariableToolName, - mcp.WithDescription("Create an organization Actions variable"), - mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), - mcp.WithString("name", mcp.Required(), mcp.Description("variable name")), - mcp.WithString("value", mcp.Required(), mcp.Description("variable value")), - mcp.WithString("description", mcp.Description("variable description")), - ) - - UpdateOrgActionVariableTool = mcp.NewTool( - UpdateOrgActionVariableToolName, - mcp.WithDescription("Update an organization Actions variable"), - mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), - mcp.WithString("name", mcp.Required(), mcp.Description("variable name")), - mcp.WithString("value", mcp.Required(), mcp.Description("new variable value")), - mcp.WithString("description", mcp.Description("new variable description")), - ) - - DeleteOrgActionVariableTool = mcp.NewTool( - DeleteOrgActionVariableToolName, - mcp.WithDescription("Delete an organization Actions variable"), - mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), - mcp.WithString("name", mcp.Required(), mcp.Description("variable name")), - ) -) - -func init() { - Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionVariablesTool, Handler: ListRepoActionVariablesFn}) - Tool.RegisterRead(server.ServerTool{Tool: GetRepoActionVariableTool, Handler: GetRepoActionVariableFn}) - Tool.RegisterWrite(server.ServerTool{Tool: CreateRepoActionVariableTool, Handler: CreateRepoActionVariableFn}) - Tool.RegisterWrite(server.ServerTool{Tool: UpdateRepoActionVariableTool, Handler: UpdateRepoActionVariableFn}) - Tool.RegisterWrite(server.ServerTool{Tool: DeleteRepoActionVariableTool, Handler: DeleteRepoActionVariableFn}) - - Tool.RegisterRead(server.ServerTool{Tool: ListOrgActionVariablesTool, Handler: ListOrgActionVariablesFn}) - Tool.RegisterRead(server.ServerTool{Tool: GetOrgActionVariableTool, Handler: GetOrgActionVariableFn}) - Tool.RegisterWrite(server.ServerTool{Tool: CreateOrgActionVariableTool, Handler: CreateOrgActionVariableFn}) - Tool.RegisterWrite(server.ServerTool{Tool: UpdateOrgActionVariableTool, Handler: UpdateOrgActionVariableFn}) - Tool.RegisterWrite(server.ServerTool{Tool: DeleteOrgActionVariableTool, Handler: DeleteOrgActionVariableFn}) -} - -func ListRepoActionVariablesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called ListRepoActionVariablesFn") - owner, err := params.GetString(req.GetArguments(), "owner") - if err != nil || owner == "" { - return to.ErrorResult(errors.New("owner is required")) - } - repo, err := params.GetString(req.GetArguments(), "repo") - if err != nil || repo == "" { - return to.ErrorResult(errors.New("repo is required")) - } - page, pageSize := params.GetPagination(req.GetArguments(), 30) - - query := url.Values{} - 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) - if err != nil { - return to.ErrorResult(fmt.Errorf("list repo action variables err: %v", err)) - } - return to.TextResult(result) -} - -func GetRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called GetRepoActionVariableFn") - owner, err := params.GetString(req.GetArguments(), "owner") - if err != nil || owner == "" { - return to.ErrorResult(errors.New("owner is required")) - } - repo, err := params.GetString(req.GetArguments(), "repo") - if err != nil || repo == "" { - return to.ErrorResult(errors.New("repo is required")) - } - name, err := params.GetString(req.GetArguments(), "name") - if err != nil || name == "" { - return to.ErrorResult(errors.New("name is required")) - } - - client, err := gitea.ClientFromContext(ctx) - if err != nil { - return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) - } - variable, _, err := client.GetRepoActionVariable(owner, repo, name) - if err != nil { - return to.ErrorResult(fmt.Errorf("get repo action variable err: %v", err)) - } - return to.TextResult(variable) -} - -func CreateRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called CreateRepoActionVariableFn") - owner, err := params.GetString(req.GetArguments(), "owner") - if err != nil || owner == "" { - return to.ErrorResult(errors.New("owner is required")) - } - repo, err := params.GetString(req.GetArguments(), "repo") - if err != nil || repo == "" { - return to.ErrorResult(errors.New("repo is required")) - } - name, err := params.GetString(req.GetArguments(), "name") - if err != nil || name == "" { - return to.ErrorResult(errors.New("name is required")) - } - value, err := params.GetString(req.GetArguments(), "value") - if err != nil || value == "" { - return to.ErrorResult(errors.New("value is required")) - } - - client, err := gitea.ClientFromContext(ctx) - if err != nil { - return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) - } - resp, err := client.CreateRepoActionVariable(owner, repo, name, value) - if err != nil { - return to.ErrorResult(fmt.Errorf("create repo action variable err: %v", err)) - } - return to.TextResult(map[string]any{"message": "variable created", "status": resp.StatusCode}) -} - -func UpdateRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called UpdateRepoActionVariableFn") - owner, err := params.GetString(req.GetArguments(), "owner") - if err != nil || owner == "" { - return to.ErrorResult(errors.New("owner is required")) - } - repo, err := params.GetString(req.GetArguments(), "repo") - if err != nil || repo == "" { - return to.ErrorResult(errors.New("repo is required")) - } - name, err := params.GetString(req.GetArguments(), "name") - if err != nil || name == "" { - return to.ErrorResult(errors.New("name is required")) - } - value, err := params.GetString(req.GetArguments(), "value") - if err != nil || value == "" { - return to.ErrorResult(errors.New("value is required")) - } - - client, err := gitea.ClientFromContext(ctx) - if err != nil { - return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) - } - resp, err := client.UpdateRepoActionVariable(owner, repo, name, value) - if err != nil { - return to.ErrorResult(fmt.Errorf("update repo action variable err: %v", err)) - } - return to.TextResult(map[string]any{"message": "variable updated", "status": resp.StatusCode}) -} - -func DeleteRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called DeleteRepoActionVariableFn") - owner, err := params.GetString(req.GetArguments(), "owner") - if err != nil || owner == "" { - return to.ErrorResult(errors.New("owner is required")) - } - repo, err := params.GetString(req.GetArguments(), "repo") - if err != nil || repo == "" { - return to.ErrorResult(errors.New("repo is required")) - } - name, err := params.GetString(req.GetArguments(), "name") - if err != nil || name == "" { - return to.ErrorResult(errors.New("name is required")) - } - - client, err := gitea.ClientFromContext(ctx) - if err != nil { - return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) - } - resp, err := client.DeleteRepoActionVariable(owner, repo, name) - if err != nil { - return to.ErrorResult(fmt.Errorf("delete repo action variable err: %v", err)) - } - return to.TextResult(map[string]any{"message": "variable deleted", "status": resp.StatusCode}) -} - -func ListOrgActionVariablesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called ListOrgActionVariablesFn") - org, err := params.GetString(req.GetArguments(), "org") - if err != nil || org == "" { - return to.ErrorResult(errors.New("org is required")) - } - 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: page, PageSize: pageSize}, - }) - if err != nil { - return to.ErrorResult(fmt.Errorf("list org action variables err: %v", err)) - } - return to.TextResult(variables) -} - -func GetOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called GetOrgActionVariableFn") - org, err := params.GetString(req.GetArguments(), "org") - if err != nil || org == "" { - return to.ErrorResult(errors.New("org is required")) - } - name, err := params.GetString(req.GetArguments(), "name") - if err != nil || name == "" { - return to.ErrorResult(errors.New("name is required")) - } - - client, err := gitea.ClientFromContext(ctx) - if err != nil { - return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) - } - variable, _, err := client.GetOrgActionVariable(org, name) - if err != nil { - return to.ErrorResult(fmt.Errorf("get org action variable err: %v", err)) - } - return to.TextResult(variable) -} - -func CreateOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called CreateOrgActionVariableFn") - org, err := params.GetString(req.GetArguments(), "org") - if err != nil || org == "" { - return to.ErrorResult(errors.New("org is required")) - } - name, err := params.GetString(req.GetArguments(), "name") - if err != nil || name == "" { - return to.ErrorResult(errors.New("name is required")) - } - value, err := params.GetString(req.GetArguments(), "value") - if err != nil || value == "" { - return to.ErrorResult(errors.New("value is required")) - } - description, _ := req.GetArguments()["description"].(string) - - client, err := gitea.ClientFromContext(ctx) - if err != nil { - return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) - } - resp, err := client.CreateOrgActionVariable(org, gitea_sdk.CreateOrgActionVariableOption{ - Name: name, - Value: value, - Description: description, - }) - if err != nil { - return to.ErrorResult(fmt.Errorf("create org action variable err: %v", err)) - } - return to.TextResult(map[string]any{"message": "variable created", "status": resp.StatusCode}) -} - -func UpdateOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called UpdateOrgActionVariableFn") - org, err := params.GetString(req.GetArguments(), "org") - if err != nil || org == "" { - return to.ErrorResult(errors.New("org is required")) - } - name, err := params.GetString(req.GetArguments(), "name") - if err != nil || name == "" { - return to.ErrorResult(errors.New("name is required")) - } - value, err := params.GetString(req.GetArguments(), "value") - if err != nil || value == "" { - return to.ErrorResult(errors.New("value is required")) - } - description, _ := req.GetArguments()["description"].(string) - - client, err := gitea.ClientFromContext(ctx) - if err != nil { - return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) - } - resp, err := client.UpdateOrgActionVariable(org, name, gitea_sdk.UpdateOrgActionVariableOption{ - Value: value, - Description: description, - }) - if err != nil { - return to.ErrorResult(fmt.Errorf("update org action variable err: %v", err)) - } - return to.TextResult(map[string]any{"message": "variable updated", "status": resp.StatusCode}) -} - -func DeleteOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called DeleteOrgActionVariableFn") - org, err := params.GetString(req.GetArguments(), "org") - if err != nil || org == "" { - return to.ErrorResult(errors.New("org is required")) - } - 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) - if err != nil { - return to.ErrorResult(fmt.Errorf("delete org action variable err: %v", err)) - } - return to.TextResult(map[string]any{"message": "variable deleted"}) -} diff --git a/operation/issue/issue.go b/operation/issue/issue.go index 6b9deb4..5158150 100644 --- a/operation/issue/issue.go +++ b/operation/issue/issue.go @@ -18,24 +18,12 @@ import ( var Tool = tool.New() const ( - GetIssueByIndexToolName = "get_issue_by_index" - ListRepoIssuesToolName = "list_repo_issues" - CreateIssueToolName = "create_issue" - CreateIssueCommentToolName = "create_issue_comment" - EditIssueToolName = "edit_issue" - EditIssueCommentToolName = "edit_issue_comment" - GetIssueCommentsByIndexToolName = "get_issue_comments_by_index" + ListRepoIssuesToolName = "list_issues" + IssueReadToolName = "issue_read" + IssueWriteToolName = "issue_write" ) var ( - GetIssueByIndexTool = mcp.NewTool( - GetIssueByIndexToolName, - mcp.WithDescription("get issue by index"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("repository issue index")), - ) - ListRepoIssuesTool = mcp.NewTool( ListRepoIssuesToolName, mcp.WithDescription("List repository issues"), @@ -43,91 +31,99 @@ 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(30)), + mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)), ) - CreateIssueTool = mcp.NewTool( - CreateIssueToolName, - mcp.WithDescription("create issue"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("title", mcp.Required(), mcp.Description("issue title")), - mcp.WithString("body", mcp.Required(), mcp.Description("issue body")), - ) - - CreateIssueCommentTool = mcp.NewTool( - CreateIssueCommentToolName, - mcp.WithDescription("create issue comment"), + IssueReadTool = mcp.NewTool( + IssueReadToolName, + mcp.WithDescription("Get information about a specific issue. Use method 'get' for issue details, 'get_comments' for issue comments, 'get_labels' for issue labels."), + mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("get", "get_comments", "get_labels")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithNumber("index", mcp.Required(), mcp.Description("repository issue index")), - mcp.WithString("body", mcp.Required(), mcp.Description("issue comment body")), ) - EditIssueTool = mcp.NewTool( - EditIssueToolName, - mcp.WithDescription("edit issue"), + IssueWriteTool = mcp.NewTool( + IssueWriteToolName, + mcp.WithDescription("Create or update issues and comments, manage labels. Use method 'create' to create an issue, 'update' to edit, 'add_comment'/'edit_comment' for comments, 'add_labels'/'remove_label'/'replace_labels'/'clear_labels' for label management."), + mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create", "update", "add_comment", "edit_comment", "add_labels", "remove_label", "replace_labels", "clear_labels")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("repository issue index")), - mcp.WithString("title", mcp.Description("issue title"), mcp.DefaultString("")), - mcp.WithString("body", mcp.Description("issue body content")), - mcp.WithArray("assignees", mcp.Description("usernames to assign to this issue"), mcp.Items(map[string]any{"type": "string"})), - mcp.WithNumber("milestone", mcp.Description("milestone number")), - mcp.WithString("state", mcp.Description("issue state, one of open, closed, all")), - ) - - EditIssueCommentTool = mcp.NewTool( - EditIssueCommentToolName, - mcp.WithDescription("edit issue comment"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("commentID", mcp.Required(), mcp.Description("id of issue comment")), - mcp.WithString("body", mcp.Required(), mcp.Description("issue comment body")), - ) - - GetIssueCommentsByIndexTool = mcp.NewTool( - GetIssueCommentsByIndexToolName, - mcp.WithDescription("get issue comment by index"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("repository issue index")), + mcp.WithNumber("index", mcp.Description("issue index (required for all methods except 'create')")), + mcp.WithString("title", mcp.Description("issue title (required for 'create')")), + mcp.WithString("body", mcp.Description("issue/comment body (required for 'create', 'add_comment', 'edit_comment')")), + mcp.WithArray("assignees", mcp.Description("usernames to assign (for 'create', 'update')"), mcp.Items(map[string]any{"type": "string"})), + mcp.WithNumber("milestone", mcp.Description("milestone number (for 'create', 'update')")), + mcp.WithString("state", mcp.Description("issue state, one of open, closed, all (for 'update')")), + mcp.WithNumber("commentID", mcp.Description("id of issue comment (required for 'edit_comment')")), + mcp.WithArray("labels", mcp.Description("array of label IDs (for 'add_labels', 'replace_labels')"), mcp.Items(map[string]any{"type": "number"})), + mcp.WithNumber("label_id", mcp.Description("label ID to remove (required for 'remove_label')")), ) ) func init() { - Tool.RegisterRead(server.ServerTool{ - Tool: GetIssueByIndexTool, - Handler: GetIssueByIndexFn, - }) Tool.RegisterRead(server.ServerTool{ Tool: ListRepoIssuesTool, - Handler: ListRepoIssuesFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: CreateIssueTool, - Handler: CreateIssueFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: CreateIssueCommentTool, - Handler: CreateIssueCommentFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: EditIssueTool, - Handler: EditIssueFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: EditIssueCommentTool, - Handler: EditIssueCommentFn, + Handler: listRepoIssuesFn, }) Tool.RegisterRead(server.ServerTool{ - Tool: GetIssueCommentsByIndexTool, - Handler: GetIssueCommentsByIndexFn, + Tool: IssueReadTool, + Handler: issueReadFn, + }) + Tool.RegisterWrite(server.ServerTool{ + Tool: IssueWriteTool, + Handler: issueWriteFn, }) } -func GetIssueByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called GetIssueByIndexFn") +func issueReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + args := req.GetArguments() + method, err := params.GetString(args, "method") + if err != nil { + return to.ErrorResult(err) + } + switch method { + case "get": + return getIssueByIndexFn(ctx, req) + case "get_comments": + return getIssueCommentsByIndexFn(ctx, req) + case "get_labels": + return getIssueLabelsFn(ctx, req) + default: + return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) + } +} + +func issueWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + args := req.GetArguments() + method, err := params.GetString(args, "method") + if err != nil { + return to.ErrorResult(err) + } + switch method { + case "create": + return createIssueFn(ctx, req) + case "update": + return editIssueFn(ctx, req) + case "add_comment": + return createIssueCommentFn(ctx, req) + case "edit_comment": + return editIssueCommentFn(ctx, req) + case "add_labels": + return addIssueLabelsFn(ctx, req) + case "remove_label": + return removeIssueLabelFn(ctx, req) + case "replace_labels": + return replaceIssueLabelsFn(ctx, req) + case "clear_labels": + return clearIssueLabelsFn(ctx, req) + default: + return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) + } +} + +func getIssueByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called getIssueByIndexFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -152,7 +148,7 @@ func GetIssueByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT return to.TextResult(slimIssue(issue)) } -func ListRepoIssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func listRepoIssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListIssuesFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { @@ -185,8 +181,8 @@ func ListRepoIssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(slimIssues(issues)) } -func CreateIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called CreateIssueFn") +func createIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called createIssueFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -207,10 +203,17 @@ func CreateIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } - issue, _, err := client.CreateIssue(owner, repo, gitea_sdk.CreateIssueOption{ + opt := gitea_sdk.CreateIssueOption{ Title: title, Body: body, - }) + } + opt.Assignees = params.GetStringSlice(req.GetArguments(), "assignees") + if val, exists := req.GetArguments()["milestone"]; exists { + if milestone, ok := params.ToInt64(val); ok { + opt.Milestone = milestone + } + } + issue, _, err := client.CreateIssue(owner, repo, opt) if err != nil { return to.ErrorResult(fmt.Errorf("create %v/%v/issue err: %v", owner, repo, err)) } @@ -218,8 +221,8 @@ func CreateIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR return to.TextResult(slimIssue(issue)) } -func CreateIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called CreateIssueCommentFn") +func createIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called createIssueCommentFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -251,8 +254,8 @@ func CreateIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca return to.TextResult(slimComment(issueComment)) } -func EditIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called EditIssueFn") +func editIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called editIssueFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -299,8 +302,8 @@ func EditIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes return to.TextResult(slimIssue(issue)) } -func EditIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called EditIssueCommentFn") +func editIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called editIssueCommentFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -332,8 +335,8 @@ func EditIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return to.TextResult(slimComment(issueComment)) } -func GetIssueCommentsByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called GetIssueCommentsByIndexFn") +func getIssueCommentsByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called getIssueCommentsByIndexFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -358,3 +361,147 @@ func GetIssueCommentsByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.TextResult(slimComments(issue)) } + +func getIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called getIssueLabelsFn") + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) + } + 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) + } + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + labels, _, err := client.GetIssueLabels(owner, repo, index, gitea_sdk.ListLabelsOptions{}) + if err != nil { + return to.ErrorResult(fmt.Errorf("get %v/%v/issues/%v/labels err: %v", owner, repo, index, err)) + } + return to.TextResult(slimLabels(labels)) +} + +// Issue label operations (moved from label package) + +func addIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called addIssueLabelsFn") + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) + } + 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) + } + labels, err := params.GetInt64Slice(req.GetArguments(), "labels") + if err != nil { + return to.ErrorResult(err) + } + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + issueLabels, _, err := client.AddIssueLabels(owner, repo, index, gitea_sdk.IssueLabelsOption{Labels: labels}) + if err != nil { + return to.ErrorResult(fmt.Errorf("add labels to %v/%v/issue/%v err: %v", owner, repo, index, err)) + } + return to.TextResult(slimLabels(issueLabels)) +} + +func replaceIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called replaceIssueLabelsFn") + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) + } + 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) + } + labels, err := params.GetInt64Slice(req.GetArguments(), "labels") + if err != nil { + return to.ErrorResult(err) + } + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + issueLabels, _, err := client.ReplaceIssueLabels(owner, repo, index, gitea_sdk.IssueLabelsOption{Labels: labels}) + if err != nil { + return to.ErrorResult(fmt.Errorf("replace labels on %v/%v/issue/%v err: %v", owner, repo, index, err)) + } + return to.TextResult(slimLabels(issueLabels)) +} + +func clearIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called clearIssueLabelsFn") + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) + } + 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) + } + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.ClearIssueLabels(owner, repo, index) + if err != nil { + return to.ErrorResult(fmt.Errorf("clear labels on %v/%v/issue/%v err: %v", owner, repo, index, err)) + } + return to.TextResult("Labels cleared successfully") +} + +func removeIssueLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called removeIssueLabelFn") + owner, err := params.GetString(req.GetArguments(), "owner") + if err != nil { + return to.ErrorResult(err) + } + 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) + } + labelID, err := params.GetIndex(req.GetArguments(), "label_id") + if err != nil { + return to.ErrorResult(err) + } + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteIssueLabel(owner, repo, index, labelID) + if err != nil { + return to.ErrorResult(fmt.Errorf("remove label %v from %v/%v/issue/%v err: %v", labelID, owner, repo, index, err)) + } + return to.TextResult("Label removed successfully") +} diff --git a/operation/issue/slim.go b/operation/issue/slim.go index 6916703..511a849 100644 --- a/operation/issue/slim.go +++ b/operation/issue/slim.go @@ -114,3 +114,20 @@ func slimComments(comments []*gitea_sdk.Comment) []map[string]any { } return out } + +func slimLabels(labels []*gitea_sdk.Label) []map[string]any { + out := make([]map[string]any, 0, len(labels)) + for _, l := range labels { + if l == nil { + continue + } + out = append(out, map[string]any{ + "id": l.ID, + "name": l.Name, + "color": l.Color, + "description": l.Description, + "exclusive": l.Exclusive, + }) + } + return out +} diff --git a/operation/label/label.go b/operation/label/label.go index 67a8581..f9b4f1d 100644 --- a/operation/label/label.go +++ b/operation/label/label.go @@ -2,7 +2,6 @@ package label import ( "context" - "errors" "fmt" "gitea.com/gitea/gitea-mcp/pkg/gitea" @@ -19,197 +18,93 @@ import ( var Tool = tool.New() const ( - ListRepoLabelsToolName = "list_repo_labels" - GetRepoLabelToolName = "get_repo_label" - CreateRepoLabelToolName = "create_repo_label" - EditRepoLabelToolName = "edit_repo_label" - DeleteRepoLabelToolName = "delete_repo_label" - AddIssueLabelsToolName = "add_issue_labels" - ReplaceIssueLabelsToolName = "replace_issue_labels" - ClearIssueLabelsToolName = "clear_issue_labels" - RemoveIssueLabelToolName = "remove_issue_label" - ListOrgLabelsToolName = "list_org_labels" - CreateOrgLabelToolName = "create_org_label" - EditOrgLabelToolName = "edit_org_label" - DeleteOrgLabelToolName = "delete_org_label" + LabelReadToolName = "label_read" + LabelWriteToolName = "label_write" ) var ( - ListRepoLabelsTool = mcp.NewTool( - ListRepoLabelsToolName, - mcp.WithDescription("Lists all labels for a given repository"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + LabelReadTool = mcp.NewTool( + LabelReadToolName, + mcp.WithDescription("Read label information. Use method 'list_repo_labels' to list repository labels, 'get_repo_label' to get a specific repo label, 'list_org_labels' to list organization labels."), + mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list_repo_labels", "get_repo_label", "list_org_labels")), + mcp.WithString("owner", mcp.Description("repository owner (required for repo methods)")), + mcp.WithString("repo", mcp.Description("repository name (required for repo methods)")), + mcp.WithString("org", mcp.Description("organization name (required for 'list_org')")), + mcp.WithNumber("id", mcp.Description("label ID (required for 'get_repo')")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30)), + mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)), ) - GetRepoLabelTool = mcp.NewTool( - GetRepoLabelToolName, - mcp.WithDescription("Gets a single label by its ID for a repository"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")), - ) - - CreateRepoLabelTool = mcp.NewTool( - CreateRepoLabelToolName, - mcp.WithDescription("Creates a new label for a repository"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("name", mcp.Required(), mcp.Description("label name")), - mcp.WithString("color", mcp.Required(), mcp.Description("label color (hex code, e.g., #RRGGBB)")), + LabelWriteTool = mcp.NewTool( + LabelWriteToolName, + mcp.WithDescription("Create, edit, or delete labels for repositories or organizations."), + mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create_repo_label", "edit_repo_label", "delete_repo_label", "create_org_label", "edit_org_label", "delete_org_label")), + mcp.WithString("owner", mcp.Description("repository owner (required for repo methods)")), + mcp.WithString("repo", mcp.Description("repository name (required for repo methods)")), + mcp.WithString("org", mcp.Description("organization name (required for org methods)")), + mcp.WithNumber("id", mcp.Description("label ID (required for edit/delete methods)")), + mcp.WithString("name", mcp.Description("label name (required for create, optional for edit)")), + mcp.WithString("color", mcp.Description("label color hex code e.g. #RRGGBB (required for create, optional for edit)")), mcp.WithString("description", mcp.Description("label description")), - ) - - EditRepoLabelTool = mcp.NewTool( - EditRepoLabelToolName, - mcp.WithDescription("Edits an existing label in a repository"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")), - mcp.WithString("name", mcp.Description("new label name")), - mcp.WithString("color", mcp.Description("new label color (hex code, e.g., #RRGGBB)")), - mcp.WithString("description", mcp.Description("new label description")), - ) - - DeleteRepoLabelTool = mcp.NewTool( - DeleteRepoLabelToolName, - mcp.WithDescription("Deletes a label from a repository"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")), - ) - - AddIssueLabelsTool = mcp.NewTool( - AddIssueLabelsToolName, - mcp.WithDescription("Adds one or more labels to an issue"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), - mcp.WithArray("labels", mcp.Required(), mcp.Description("array of label IDs to add"), mcp.Items(map[string]any{"type": "number"})), - ) - - ReplaceIssueLabelsTool = mcp.NewTool( - ReplaceIssueLabelsToolName, - mcp.WithDescription("Replaces all labels on an issue"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), - mcp.WithArray("labels", mcp.Required(), mcp.Description("array of label IDs to replace with"), mcp.Items(map[string]any{"type": "number"})), - ) - - ClearIssueLabelsTool = mcp.NewTool( - ClearIssueLabelsToolName, - mcp.WithDescription("Removes all labels from an issue"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), - ) - - RemoveIssueLabelTool = mcp.NewTool( - RemoveIssueLabelToolName, - mcp.WithDescription("Removes a single label from an issue"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), - mcp.WithNumber("label_id", mcp.Required(), mcp.Description("label ID to remove")), - ) - - ListOrgLabelsTool = mcp.NewTool( - ListOrgLabelsToolName, - 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(30)), - ) - - CreateOrgLabelTool = mcp.NewTool( - CreateOrgLabelToolName, - mcp.WithDescription("Creates a new label for an organization"), - mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), - mcp.WithString("name", mcp.Required(), mcp.Description("label name")), - mcp.WithString("color", mcp.Required(), mcp.Description("label color (hex code, e.g., #RRGGBB)")), - mcp.WithString("description", mcp.Description("label description")), - mcp.WithBoolean("exclusive", mcp.Description("whether the label is exclusive"), mcp.DefaultBool(false)), - ) - - EditOrgLabelTool = mcp.NewTool( - EditOrgLabelToolName, - mcp.WithDescription("Edits an existing organization label"), - mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), - mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")), - mcp.WithString("name", mcp.Description("new label name")), - mcp.WithString("color", mcp.Description("new label color (hex code, e.g., #RRGGBB)")), - mcp.WithString("description", mcp.Description("new label description")), - mcp.WithBoolean("exclusive", mcp.Description("whether the label is exclusive")), - ) - - DeleteOrgLabelTool = mcp.NewTool( - DeleteOrgLabelToolName, - mcp.WithDescription("Deletes an organization label by ID"), - mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), - mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")), + mcp.WithBoolean("exclusive", mcp.Description("whether the label is exclusive (org labels only)")), ) ) func init() { Tool.RegisterRead(server.ServerTool{ - Tool: ListRepoLabelsTool, - Handler: ListRepoLabelsFn, - }) - Tool.RegisterRead(server.ServerTool{ - Tool: GetRepoLabelTool, - Handler: GetRepoLabelFn, + Tool: LabelReadTool, + Handler: labelReadFn, }) Tool.RegisterWrite(server.ServerTool{ - Tool: CreateRepoLabelTool, - Handler: CreateRepoLabelFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: EditRepoLabelTool, - Handler: EditRepoLabelFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: DeleteRepoLabelTool, - Handler: DeleteRepoLabelFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: AddIssueLabelsTool, - Handler: AddIssueLabelsFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: ReplaceIssueLabelsTool, - Handler: ReplaceIssueLabelsFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: ClearIssueLabelsTool, - Handler: ClearIssueLabelsFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: RemoveIssueLabelTool, - Handler: RemoveIssueLabelFn, - }) - Tool.RegisterRead(server.ServerTool{ - Tool: ListOrgLabelsTool, - Handler: ListOrgLabelsFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: CreateOrgLabelTool, - Handler: CreateOrgLabelFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: EditOrgLabelTool, - Handler: EditOrgLabelFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: DeleteOrgLabelTool, - Handler: DeleteOrgLabelFn, + Tool: LabelWriteTool, + Handler: labelWriteFn, }) } -func ListRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called ListRepoLabelsFn") +func labelReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + args := req.GetArguments() + method, err := params.GetString(args, "method") + if err != nil { + return to.ErrorResult(err) + } + switch method { + case "list_repo_labels": + return listRepoLabelsFn(ctx, req) + case "get_repo_label": + return getRepoLabelFn(ctx, req) + case "list_org_labels": + return listOrgLabelsFn(ctx, req) + default: + return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) + } +} + +func labelWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + args := req.GetArguments() + method, err := params.GetString(args, "method") + if err != nil { + return to.ErrorResult(err) + } + switch method { + case "create_repo_label": + return createRepoLabelFn(ctx, req) + case "edit_repo_label": + return editRepoLabelFn(ctx, req) + case "delete_repo_label": + return deleteRepoLabelFn(ctx, req) + case "create_org_label": + return createOrgLabelFn(ctx, req) + case "edit_org_label": + return editOrgLabelFn(ctx, req) + case "delete_org_label": + return deleteOrgLabelFn(ctx, req) + default: + return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) + } +} + +func listRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called listRepoLabelsFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -237,8 +132,8 @@ func ListRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(slimLabels(labels)) } -func GetRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called GetRepoLabelFn") +func getRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called getRepoLabelFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -263,8 +158,8 @@ func GetRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool return to.TextResult(slimLabel(label)) } -func CreateRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called CreateRepoLabelFn") +func createRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called createRepoLabelFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -300,8 +195,8 @@ func CreateRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT return to.TextResult(slimLabel(label)) } -func EditRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called EditRepoLabelFn") +func editRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called editRepoLabelFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -337,8 +232,8 @@ func EditRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo return to.TextResult(slimLabel(label)) } -func DeleteRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called DeleteRepoLabelFn") +func deleteRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called deleteRepoLabelFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -363,148 +258,8 @@ func DeleteRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT return to.TextResult("Label deleted successfully") } -func AddIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called AddIssueLabelsFn") - owner, err := params.GetString(req.GetArguments(), "owner") - if err != nil { - return to.ErrorResult(err) - } - 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) - } - labelsRaw, ok := req.GetArguments()["labels"].([]any) - if !ok { - return to.ErrorResult(errors.New("labels (array of IDs) is required")) - } - var labels []int64 - for _, l := range labelsRaw { - if labelID, ok := params.ToInt64(l); ok { - labels = append(labels, labelID) - } else { - return to.ErrorResult(errors.New("invalid label ID in labels array")) - } - } - - opt := gitea_sdk.IssueLabelsOption{ - Labels: labels, - } - - client, err := gitea.ClientFromContext(ctx) - if err != nil { - return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) - } - issueLabels, _, err := client.AddIssueLabels(owner, repo, index, opt) - if err != nil { - return to.ErrorResult(fmt.Errorf("add labels to %v/%v/issue/%v err: %v", owner, repo, index, err)) - } - return to.TextResult(slimLabels(issueLabels)) -} - -func ReplaceIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called ReplaceIssueLabelsFn") - owner, err := params.GetString(req.GetArguments(), "owner") - if err != nil { - return to.ErrorResult(err) - } - 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) - } - labelsRaw, ok := req.GetArguments()["labels"].([]any) - if !ok { - return to.ErrorResult(errors.New("labels (array of IDs) is required")) - } - var labels []int64 - for _, l := range labelsRaw { - if labelID, ok := params.ToInt64(l); ok { - labels = append(labels, labelID) - } else { - return to.ErrorResult(errors.New("invalid label ID in labels array")) - } - } - - opt := gitea_sdk.IssueLabelsOption{ - Labels: labels, - } - - client, err := gitea.ClientFromContext(ctx) - if err != nil { - return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) - } - issueLabels, _, err := client.ReplaceIssueLabels(owner, repo, index, opt) - if err != nil { - return to.ErrorResult(fmt.Errorf("replace labels on %v/%v/issue/%v err: %v", owner, repo, index, err)) - } - return to.TextResult(slimLabels(issueLabels)) -} - -func ClearIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called ClearIssueLabelsFn") - owner, err := params.GetString(req.GetArguments(), "owner") - if err != nil { - return to.ErrorResult(err) - } - 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) - } - - client, err := gitea.ClientFromContext(ctx) - if err != nil { - return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) - } - _, err = client.ClearIssueLabels(owner, repo, index) - if err != nil { - return to.ErrorResult(fmt.Errorf("clear labels on %v/%v/issue/%v err: %v", owner, repo, index, err)) - } - return to.TextResult("Labels cleared successfully") -} - -func RemoveIssueLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called RemoveIssueLabelFn") - owner, err := params.GetString(req.GetArguments(), "owner") - if err != nil { - return to.ErrorResult(err) - } - 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) - } - labelID, err := params.GetIndex(req.GetArguments(), "label_id") - if err != nil { - return to.ErrorResult(err) - } - - client, err := gitea.ClientFromContext(ctx) - if err != nil { - return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) - } - _, err = client.DeleteIssueLabel(owner, repo, index, labelID) - if err != nil { - return to.ErrorResult(fmt.Errorf("remove label %v from %v/%v/issue/%v err: %v", labelID, owner, repo, index, err)) - } - return to.TextResult("Label removed successfully") -} - -func ListOrgLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called ListOrgLabelsFn") +func listOrgLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called listOrgLabelsFn") org, err := params.GetString(req.GetArguments(), "org") if err != nil { return to.ErrorResult(err) @@ -528,8 +283,8 @@ func ListOrgLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo return to.TextResult(slimLabels(labels)) } -func CreateOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called CreateOrgLabelFn") +func createOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called createOrgLabelFn") org, err := params.GetString(req.GetArguments(), "org") if err != nil { return to.ErrorResult(err) @@ -563,8 +318,8 @@ func CreateOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(slimLabel(label)) } -func EditOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called EditOrgLabelFn") +func editOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called editOrgLabelFn") org, err := params.GetString(req.GetArguments(), "org") if err != nil { return to.ErrorResult(err) @@ -599,8 +354,8 @@ func EditOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool return to.TextResult(slimLabel(label)) } -func DeleteOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called DeleteOrgLabelFn") +func deleteOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called deleteOrgLabelFn") org, err := params.GetString(req.GetArguments(), "org") if err != nil { return to.ErrorResult(err) diff --git a/operation/milestone/milestone.go b/operation/milestone/milestone.go index 40d49a9..1d6ca9a 100644 --- a/operation/milestone/milestone.go +++ b/operation/milestone/milestone.go @@ -18,89 +18,83 @@ import ( var Tool = tool.New() const ( - GetMilestoneToolName = "get_milestone" - ListMilestonesToolName = "list_milestones" - CreateMilestoneToolName = "create_milestone" - EditMilestoneToolName = "edit_milestone" - DeleteMilestoneToolName = "delete_milestone" + MilestoneReadToolName = "milestone_read" + MilestoneWriteToolName = "milestone_write" ) var ( - GetMilestoneTool = mcp.NewTool( - GetMilestoneToolName, - mcp.WithDescription("get milestone by id"), + MilestoneReadTool = mcp.NewTool( + MilestoneReadToolName, + mcp.WithDescription("Read milestone information. Use method 'get' to get a specific milestone, 'list' to list milestones."), + mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("get", "list")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("id", mcp.Required(), mcp.Description("milestone id")), - ) - - ListMilestonesTool = mcp.NewTool( - ListMilestonesToolName, - mcp.WithDescription("List milestones"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("state", mcp.Description("milestone state"), mcp.DefaultString("all")), - mcp.WithString("name", mcp.Description("milestone name")), + mcp.WithNumber("id", mcp.Description("milestone id (required for 'get')")), + mcp.WithString("state", mcp.Description("milestone state (for 'list')"), mcp.DefaultString("all")), + mcp.WithString("name", mcp.Description("milestone name filter (for 'list')")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30)), + mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)), ) - CreateMilestoneTool = mcp.NewTool( - CreateMilestoneToolName, - mcp.WithDescription("create milestone"), + MilestoneWriteTool = mcp.NewTool( + MilestoneWriteToolName, + mcp.WithDescription("Create, edit, or delete milestones."), + mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create", "edit", "delete")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("title", mcp.Required(), mcp.Description("milestone title")), + mcp.WithNumber("id", mcp.Description("milestone id (required for 'edit', 'delete')")), + mcp.WithString("title", mcp.Description("milestone title (required for 'create')")), mcp.WithString("description", mcp.Description("milestone description")), mcp.WithString("due_on", mcp.Description("due date")), - ) - - EditMilestoneTool = mcp.NewTool( - EditMilestoneToolName, - mcp.WithDescription("edit milestone"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("id", mcp.Required(), mcp.Description("milestone id")), - mcp.WithString("title", mcp.Description("milestone title")), - mcp.WithString("description", mcp.Description("milestone description")), - mcp.WithString("due_on", mcp.Description("due date")), - mcp.WithString("state", mcp.Description("milestone state, one of open, closed")), - ) - - DeleteMilestoneTool = mcp.NewTool( - DeleteMilestoneToolName, - mcp.WithDescription("delete milestone"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("id", mcp.Required(), mcp.Description("milestone id")), + mcp.WithString("state", mcp.Description("milestone state, one of open, closed (for 'edit')")), ) ) func init() { Tool.RegisterRead(server.ServerTool{ - Tool: GetMilestoneTool, - Handler: GetMilestoneFn, - }) - Tool.RegisterRead(server.ServerTool{ - Tool: ListMilestonesTool, - Handler: ListMilestonesFn, + Tool: MilestoneReadTool, + Handler: milestoneReadFn, }) Tool.RegisterWrite(server.ServerTool{ - Tool: CreateMilestoneTool, - Handler: CreateMilestoneFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: EditMilestoneTool, - Handler: EditMilestoneFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: DeleteMilestoneTool, - Handler: DeleteMilestoneFn, + Tool: MilestoneWriteTool, + Handler: milestoneWriteFn, }) } -func GetMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called GetMilestoneFn") +func milestoneReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + method, err := params.GetString(req.GetArguments(), "method") + if err != nil { + return to.ErrorResult(err) + } + switch method { + case "get": + return getMilestoneFn(ctx, req) + case "list": + return listMilestonesFn(ctx, req) + default: + return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) + } +} + +func milestoneWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + method, err := params.GetString(req.GetArguments(), "method") + if err != nil { + return to.ErrorResult(err) + } + switch method { + case "create": + return createMilestoneFn(ctx, req) + case "edit": + return editMilestoneFn(ctx, req) + case "delete": + return deleteMilestoneFn(ctx, req) + default: + return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) + } +} + +func getMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called getMilestoneFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -125,8 +119,8 @@ func GetMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool return to.TextResult(slimMilestone(milestone)) } -func ListMilestonesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called ListMilestonesFn") +func listMilestonesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called listMilestonesFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -157,8 +151,8 @@ func ListMilestonesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(slimMilestones(milestones)) } -func CreateMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called CreateMilestoneFn") +func createMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called createMilestoneFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -193,8 +187,8 @@ func CreateMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT return to.TextResult(slimMilestone(milestone)) } -func EditMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called EditMilestoneFn") +func editMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called editMilestoneFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -235,8 +229,8 @@ func EditMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo return to.TextResult(slimMilestone(milestone)) } -func DeleteMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called DeleteMilestoneFn") +func deleteMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called deleteMilestoneFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) diff --git a/operation/pull/pull.go b/operation/pull/pull.go index 8e3b46a..9ceedca 100644 --- a/operation/pull/pull.go +++ b/operation/pull/pull.go @@ -18,41 +18,13 @@ import ( var Tool = tool.New() const ( - GetPullRequestByIndexToolName = "get_pull_request_by_index" - GetPullRequestDiffToolName = "get_pull_request_diff" - ListRepoPullRequestsToolName = "list_repo_pull_requests" - CreatePullRequestToolName = "create_pull_request" - CreatePullRequestReviewerToolName = "create_pull_request_reviewer" - DeletePullRequestReviewerToolName = "delete_pull_request_reviewer" - ListPullRequestReviewsToolName = "list_pull_request_reviews" - GetPullRequestReviewToolName = "get_pull_request_review" - ListPullRequestReviewCommentsToolName = "list_pull_request_review_comments" - CreatePullRequestReviewToolName = "create_pull_request_review" - SubmitPullRequestReviewToolName = "submit_pull_request_review" - DeletePullRequestReviewToolName = "delete_pull_request_review" - DismissPullRequestReviewToolName = "dismiss_pull_request_review" - MergePullRequestToolName = "merge_pull_request" - EditPullRequestToolName = "edit_pull_request" + ListRepoPullRequestsToolName = "list_pull_requests" + PullRequestReadToolName = "pull_request_read" + PullRequestWriteToolName = "pull_request_write" + PullRequestReviewWriteToolName = "pull_request_review_write" ) var ( - GetPullRequestByIndexTool = mcp.NewTool( - GetPullRequestByIndexToolName, - mcp.WithDescription("get pull request by index"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("repository pull request index")), - ) - - GetPullRequestDiffTool = mcp.NewTool( - GetPullRequestDiffToolName, - mcp.WithDescription("get pull request diff"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("repository pull request index")), - mcp.WithBoolean("binary", mcp.Description("whether to include binary file changes")), - ) - ListRepoPullRequestsTool = mcp.NewTool( ListRepoPullRequestsToolName, mcp.WithDescription("List repository pull requests"), @@ -62,78 +34,58 @@ 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(30)), + mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)), ) - CreatePullRequestTool = mcp.NewTool( - CreatePullRequestToolName, - mcp.WithDescription("create pull request"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("title", mcp.Required(), mcp.Description("pull request title")), - mcp.WithString("body", mcp.Required(), mcp.Description("pull request body")), - mcp.WithString("head", mcp.Required(), mcp.Description("pull request head")), - mcp.WithString("base", mcp.Required(), mcp.Description("pull request base")), - ) - - CreatePullRequestReviewerTool = mcp.NewTool( - CreatePullRequestReviewerToolName, - mcp.WithDescription("create pull request reviewer"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")), - mcp.WithArray("reviewers", mcp.Description("list of reviewer usernames"), mcp.Items(map[string]any{"type": "string"})), - mcp.WithArray("team_reviewers", mcp.Description("list of team reviewer names"), mcp.Items(map[string]any{"type": "string"})), - ) - - DeletePullRequestReviewerTool = mcp.NewTool( - DeletePullRequestReviewerToolName, - mcp.WithDescription("remove reviewer requests from a pull request"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")), - mcp.WithArray("reviewers", mcp.Description("list of reviewer usernames to remove"), mcp.Items(map[string]any{"type": "string"})), - mcp.WithArray("team_reviewers", mcp.Description("list of team reviewer names to remove"), mcp.Items(map[string]any{"type": "string"})), - ) - - ListPullRequestReviewsTool = mcp.NewTool( - ListPullRequestReviewsToolName, - mcp.WithDescription("list all reviews for a pull request"), + PullRequestReadTool = mcp.NewTool( + PullRequestReadToolName, + mcp.WithDescription("Get pull request information. Use method 'get' for PR details, 'get_diff' for diff, 'get_reviews'/'get_review'/'get_review_comments' for review data."), + mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("get", "get_diff", "get_reviews", "get_review", "get_review_comments")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")), + mcp.WithNumber("review_id", mcp.Description("review ID (required for 'get_review', 'get_review_comments')")), + mcp.WithBoolean("binary", mcp.Description("whether to include binary file changes (for 'get_diff')")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30)), + mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)), ) - GetPullRequestReviewTool = mcp.NewTool( - GetPullRequestReviewToolName, - mcp.WithDescription("get a specific review for a pull request"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")), - mcp.WithNumber("review_id", mcp.Required(), mcp.Description("review ID")), - ) - - ListPullRequestReviewCommentsTool = mcp.NewTool( - ListPullRequestReviewCommentsToolName, - mcp.WithDescription("list all comments for a specific pull request review"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")), - mcp.WithNumber("review_id", mcp.Required(), mcp.Description("review ID")), - ) - - CreatePullRequestReviewTool = mcp.NewTool( - CreatePullRequestReviewToolName, - mcp.WithDescription("create a review for a pull request"), + PullRequestWriteTool = mcp.NewTool( + PullRequestWriteToolName, + mcp.WithDescription("Create, update, or merge pull requests, manage reviewers."), + mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create", "update", "merge", "add_reviewers", "remove_reviewers")), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("index", mcp.Description("pull request index (required for all methods except 'create')")), + mcp.WithString("title", mcp.Description("PR title (required for 'create', optional for 'update', 'merge')")), + mcp.WithString("body", mcp.Description("PR body (required for 'create', optional for 'update')")), + mcp.WithString("head", mcp.Description("PR head branch (required for 'create')")), + mcp.WithString("base", mcp.Description("PR base branch (required for 'create', optional for 'update')")), + mcp.WithString("assignee", mcp.Description("username to assign (for 'update')")), + mcp.WithArray("assignees", mcp.Description("usernames to assign (for 'update')"), mcp.Items(map[string]any{"type": "string"})), + mcp.WithNumber("milestone", mcp.Description("milestone number (for 'update')")), + mcp.WithString("state", mcp.Description("PR state (for 'update')"), mcp.Enum("open", "closed")), + mcp.WithBoolean("allow_maintainer_edit", mcp.Description("allow maintainer to edit (for 'update')")), + mcp.WithString("merge_style", mcp.Description("merge style (for 'merge')"), mcp.Enum("merge", "rebase", "rebase-merge", "squash", "fast-forward-only"), mcp.DefaultString("merge")), + mcp.WithString("message", mcp.Description("merge commit message (for 'merge') or dismissal reason")), + mcp.WithBoolean("delete_branch", mcp.Description("delete branch after merge (for 'merge')")), + mcp.WithArray("reviewers", mcp.Description("reviewer usernames (for 'add_reviewers', 'remove_reviewers')"), mcp.Items(map[string]any{"type": "string"})), + mcp.WithArray("team_reviewers", mcp.Description("team reviewer names (for 'add_reviewers', 'remove_reviewers')"), mcp.Items(map[string]any{"type": "string"})), + ) + + PullRequestReviewWriteTool = mcp.NewTool( + PullRequestReviewWriteToolName, + mcp.WithDescription("Manage pull request reviews: create, submit, delete, or dismiss."), + mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create", "submit", "delete", "dismiss")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")), + mcp.WithNumber("review_id", mcp.Description("review ID (required for 'submit', 'delete', 'dismiss')")), mcp.WithString("state", mcp.Description("review state"), mcp.Enum("APPROVED", "REQUEST_CHANGES", "COMMENT", "PENDING")), mcp.WithString("body", mcp.Description("review body/comment")), - mcp.WithString("commit_id", mcp.Description("commit SHA to review")), - mcp.WithArray("comments", mcp.Description("inline review comments (objects with path, body, old_line_num, new_line_num)"), mcp.Items(map[string]any{ + mcp.WithString("commit_id", mcp.Description("commit SHA to review (for 'create')")), + mcp.WithString("message", mcp.Description("dismissal reason (for 'dismiss')")), + mcp.WithArray("comments", mcp.Description("inline review comments (for 'create')"), mcp.Items(map[string]any{ "type": "object", "properties": map[string]any{ "path": map[string]any{"type": "string", "description": "file path to comment on"}, @@ -143,131 +95,90 @@ var ( }, })), ) - - SubmitPullRequestReviewTool = mcp.NewTool( - SubmitPullRequestReviewToolName, - mcp.WithDescription("submit a pending pull request review"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")), - mcp.WithNumber("review_id", mcp.Required(), mcp.Description("review ID")), - mcp.WithString("state", mcp.Required(), mcp.Description("final review state"), mcp.Enum("APPROVED", "REQUEST_CHANGES", "COMMENT")), - mcp.WithString("body", mcp.Description("submission message")), - ) - - DeletePullRequestReviewTool = mcp.NewTool( - DeletePullRequestReviewToolName, - mcp.WithDescription("delete a pull request review"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")), - mcp.WithNumber("review_id", mcp.Required(), mcp.Description("review ID")), - ) - - DismissPullRequestReviewTool = mcp.NewTool( - DismissPullRequestReviewToolName, - mcp.WithDescription("dismiss a pull request review"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")), - mcp.WithNumber("review_id", mcp.Required(), mcp.Description("review ID")), - mcp.WithString("message", mcp.Description("dismissal reason")), - ) - - MergePullRequestTool = mcp.NewTool( - MergePullRequestToolName, - mcp.WithDescription("merge a pull request"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")), - mcp.WithString("merge_style", mcp.Description("merge style: merge, rebase, rebase-merge, squash, fast-forward-only"), mcp.Enum("merge", "rebase", "rebase-merge", "squash", "fast-forward-only"), mcp.DefaultString("merge")), - mcp.WithString("title", mcp.Description("custom merge commit title")), - mcp.WithString("message", mcp.Description("custom merge commit message")), - mcp.WithBoolean("delete_branch", mcp.Description("delete the branch after merge"), mcp.DefaultBool(false)), - ) - - EditPullRequestTool = mcp.NewTool( - EditPullRequestToolName, - mcp.WithDescription("edit a pull request"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")), - mcp.WithString("title", mcp.Description("pull request title")), - mcp.WithString("body", mcp.Description("pull request body content")), - mcp.WithString("base", mcp.Description("pull request base branch")), - mcp.WithString("assignee", mcp.Description("username to assign")), - mcp.WithArray("assignees", mcp.Description("usernames to assign"), mcp.Items(map[string]any{"type": "string"})), - mcp.WithNumber("milestone", mcp.Description("milestone number")), - mcp.WithString("state", mcp.Description("pull request state"), mcp.Enum("open", "closed")), - mcp.WithBoolean("allow_maintainer_edit", mcp.Description("allow maintainer to edit the pull request")), - ) ) func init() { - Tool.RegisterRead(server.ServerTool{ - Tool: GetPullRequestByIndexTool, - Handler: GetPullRequestByIndexFn, - }) - Tool.RegisterRead(server.ServerTool{ - Tool: GetPullRequestDiffTool, - Handler: GetPullRequestDiffFn, - }) Tool.RegisterRead(server.ServerTool{ Tool: ListRepoPullRequestsTool, - Handler: ListRepoPullRequestsFn, + Handler: listRepoPullRequestsFn, }) Tool.RegisterRead(server.ServerTool{ - Tool: ListPullRequestReviewsTool, - Handler: ListPullRequestReviewsFn, - }) - Tool.RegisterRead(server.ServerTool{ - Tool: GetPullRequestReviewTool, - Handler: GetPullRequestReviewFn, - }) - Tool.RegisterRead(server.ServerTool{ - Tool: ListPullRequestReviewCommentsTool, - Handler: ListPullRequestReviewCommentsFn, + Tool: PullRequestReadTool, + Handler: pullRequestReadFn, }) Tool.RegisterWrite(server.ServerTool{ - Tool: CreatePullRequestTool, - Handler: CreatePullRequestFn, + Tool: PullRequestWriteTool, + Handler: pullRequestWriteFn, }) Tool.RegisterWrite(server.ServerTool{ - Tool: CreatePullRequestReviewerTool, - Handler: CreatePullRequestReviewerFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: DeletePullRequestReviewerTool, - Handler: DeletePullRequestReviewerFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: CreatePullRequestReviewTool, - Handler: CreatePullRequestReviewFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: SubmitPullRequestReviewTool, - Handler: SubmitPullRequestReviewFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: DeletePullRequestReviewTool, - Handler: DeletePullRequestReviewFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: DismissPullRequestReviewTool, - Handler: DismissPullRequestReviewFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: MergePullRequestTool, - Handler: MergePullRequestFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: EditPullRequestTool, - Handler: EditPullRequestFn, + Tool: PullRequestReviewWriteTool, + Handler: pullRequestReviewWriteFn, }) } -func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called GetPullRequestByIndexFn") +func pullRequestReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + method, err := params.GetString(req.GetArguments(), "method") + if err != nil { + return to.ErrorResult(err) + } + switch method { + case "get": + return getPullRequestByIndexFn(ctx, req) + case "get_diff": + return getPullRequestDiffFn(ctx, req) + case "get_reviews": + return listPullRequestReviewsFn(ctx, req) + case "get_review": + return getPullRequestReviewFn(ctx, req) + case "get_review_comments": + return listPullRequestReviewCommentsFn(ctx, req) + default: + return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) + } +} + +func pullRequestWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + method, err := params.GetString(req.GetArguments(), "method") + if err != nil { + return to.ErrorResult(err) + } + switch method { + case "create": + return createPullRequestFn(ctx, req) + case "update": + return editPullRequestFn(ctx, req) + case "merge": + return mergePullRequestFn(ctx, req) + case "add_reviewers": + return createPullRequestReviewerFn(ctx, req) + case "remove_reviewers": + return deletePullRequestReviewerFn(ctx, req) + default: + return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) + } +} + +func pullRequestReviewWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + method, err := params.GetString(req.GetArguments(), "method") + if err != nil { + return to.ErrorResult(err) + } + switch method { + case "create": + return createPullRequestReviewFn(ctx, req) + case "submit": + return submitPullRequestReviewFn(ctx, req) + case "delete": + return deletePullRequestReviewFn(ctx, req) + case "dismiss": + return dismissPullRequestReviewFn(ctx, req) + default: + return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) + } +} + +func getPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called getPullRequestByIndexFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -293,8 +204,8 @@ func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp return to.TextResult(slimPullRequest(pr)) } -func GetPullRequestDiffFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called GetPullRequestDiffFn") +func getPullRequestDiffFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called getPullRequestDiffFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -324,7 +235,7 @@ func GetPullRequestDiffFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca return to.TextResult(string(diffBytes)) } -func ListRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { +func listRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListRepoPullRequests") args := req.GetArguments() owner, err := params.GetString(args, "owner") @@ -360,8 +271,8 @@ func ListRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp. return to.TextResult(slimPullRequests(pullRequests)) } -func CreatePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called CreatePullRequestFn") +func createPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called createPullRequestFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -404,8 +315,8 @@ func CreatePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Cal return to.TextResult(slimPullRequest(pr)) } -func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called CreatePullRequestReviewerFn") +func createPullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called createPullRequestReviewerFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -436,7 +347,6 @@ func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) ( return to.ErrorResult(fmt.Errorf("create review requests for %v/%v/pr/%v err: %v", owner, repo, index, err)) } - // Return a success message instead of the Response object which contains non-serializable functions successMsg := map[string]any{ "message": "Successfully created review requests", "reviewers": reviewers, @@ -448,8 +358,8 @@ func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) ( return to.TextResult(successMsg) } -func DeletePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called DeletePullRequestReviewerFn") +func deletePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called deletePullRequestReviewerFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -491,8 +401,8 @@ func DeletePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) ( return to.TextResult(successMsg) } -func ListPullRequestReviewsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called ListPullRequestReviewsFn") +func listPullRequestReviewsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called listPullRequestReviewsFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -526,8 +436,8 @@ func ListPullRequestReviewsFn(ctx context.Context, req mcp.CallToolRequest) (*mc return to.TextResult(slimReviews(reviews)) } -func GetPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called GetPullRequestReviewFn") +func getPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called getPullRequestReviewFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -559,8 +469,8 @@ func GetPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp. return to.TextResult(slimReview(review)) } -func ListPullRequestReviewCommentsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called ListPullRequestReviewCommentsFn") +func listPullRequestReviewCommentsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called listPullRequestReviewCommentsFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -592,8 +502,8 @@ func ListPullRequestReviewCommentsFn(ctx context.Context, req mcp.CallToolReques return to.TextResult(slimReviewComments(comments)) } -func CreatePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called CreatePullRequestReviewFn") +func createPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called createPullRequestReviewFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -657,8 +567,8 @@ func CreatePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.TextResult(slimReview(review)) } -func SubmitPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called SubmitPullRequestReviewFn") +func submitPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called submitPullRequestReviewFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -701,8 +611,8 @@ func SubmitPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.TextResult(slimReview(review)) } -func DeletePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called DeletePullRequestReviewFn") +func deletePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called deletePullRequestReviewFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -741,8 +651,8 @@ func DeletePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.TextResult(successMsg) } -func DismissPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called DismissPullRequestReviewFn") +func dismissPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called dismissPullRequestReviewFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -786,8 +696,8 @@ func DismissPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (* return to.TextResult(successMsg) } -func MergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called MergePullRequestFn") +func mergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called mergePullRequestFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -843,8 +753,8 @@ func MergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return to.TextResult(successMsg) } -func EditPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called EditPullRequestFn") +func editPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called editPullRequestFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { diff --git a/operation/pull/pull_test.go b/operation/pull/pull_test.go index ab123bf..cdcb081 100644 --- a/operation/pull/pull_test.go +++ b/operation/pull/pull_test.go @@ -13,7 +13,7 @@ import ( "github.com/mark3labs/mcp-go/mcp" ) -func TestEditPullRequestFn(t *testing.T) { +func Test_editPullRequestFn(t *testing.T) { const ( owner = "octo" repo = "demo" @@ -87,9 +87,9 @@ func TestEditPullRequestFn(t *testing.T) { }, } - result, err := EditPullRequestFn(context.Background(), req) + result, err := editPullRequestFn(context.Background(), req) if err != nil { - t.Fatalf("EditPullRequestFn() error = %v", err) + t.Fatalf("editPullRequestFn() error = %v", err) } mu.Lock() @@ -127,7 +127,7 @@ func TestEditPullRequestFn(t *testing.T) { } } -func TestMergePullRequestFn(t *testing.T) { +func Test_mergePullRequestFn(t *testing.T) { const ( owner = "octo" repo = "demo" @@ -202,9 +202,9 @@ func TestMergePullRequestFn(t *testing.T) { }, } - result, err := MergePullRequestFn(context.Background(), req) + result, err := mergePullRequestFn(context.Background(), req) if err != nil { - t.Fatalf("MergePullRequestFn() error = %v", err) + t.Fatalf("mergePullRequestFn() error = %v", err) } mu.Lock() @@ -254,7 +254,7 @@ func TestMergePullRequestFn(t *testing.T) { } } -func TestGetPullRequestDiffFn(t *testing.T) { +func Test_getPullRequestDiffFn(t *testing.T) { const ( owner = "octo" repo = "demo" @@ -334,9 +334,9 @@ func TestGetPullRequestDiffFn(t *testing.T) { }, } - result, err := GetPullRequestDiffFn(context.Background(), req) + result, err := getPullRequestDiffFn(context.Background(), req) if err != nil { - t.Fatalf("GetPullRequestDiffFn() error = %v", err) + t.Fatalf("getPullRequestDiffFn() error = %v", err) } select { diff --git a/operation/repo/commit.go b/operation/repo/commit.go index b372870..1169307 100644 --- a/operation/repo/commit.go +++ b/operation/repo/commit.go @@ -15,7 +15,7 @@ import ( ) const ( - ListRepoCommitsToolName = "list_repo_commits" + ListRepoCommitsToolName = "list_commits" ) var ListRepoCommitsTool = mcp.NewTool( @@ -26,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(30), mcp.Min(1)), + mcp.WithNumber("perPage", mcp.Required(), mcp.Description("results per page"), mcp.DefaultNumber(30), mcp.Min(1)), ) func init() { @@ -51,7 +51,7 @@ func ListRepoCommitsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT if err != nil { return to.ErrorResult(err) } - pageSize, err := params.GetIndex(args, "page_size") + pageSize, err := params.GetIndex(args, "perPage") if err != nil { return to.ErrorResult(err) } diff --git a/operation/repo/file.go b/operation/repo/file.go index f6e10e1..a133c40 100644 --- a/operation/repo/file.go +++ b/operation/repo/file.go @@ -19,11 +19,10 @@ import ( ) const ( - GetFileToolName = "get_file_content" - GetDirToolName = "get_dir_content" - CreateFileToolName = "create_file" - UpdateFileToolName = "update_file" - DeleteFileToolName = "delete_file" + GetFileToolName = "get_file_contents" + GetDirToolName = "get_dir_contents" + CreateOrUpdateFileToolName = "create_or_update_file" + DeleteFileToolName = "delete_file" ) var ( @@ -46,28 +45,17 @@ var ( mcp.WithString("filePath", mcp.Required(), mcp.Description("directory path")), ) - CreateFileTool = mcp.NewTool( - CreateFileToolName, - mcp.WithDescription("Create file"), + CreateOrUpdateFileTool = mcp.NewTool( + CreateOrUpdateFileToolName, + mcp.WithDescription("Create or update a file. If sha is provided, updates the existing file; otherwise creates a new file."), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("filePath", mcp.Required(), mcp.Description("file path")), mcp.WithString("content", mcp.Required(), mcp.Description("file content")), mcp.WithString("message", mcp.Required(), mcp.Description("commit message")), mcp.WithString("branch_name", mcp.Required(), mcp.Description("branch name")), - mcp.WithString("new_branch_name", mcp.Description("new branch name")), - ) - - UpdateFileTool = mcp.NewTool( - UpdateFileToolName, - mcp.WithDescription("Update file"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("filePath", mcp.Required(), mcp.Description("file path")), - mcp.WithString("sha", mcp.Required(), mcp.Description("sha is the SHA for the file that already exists")), - mcp.WithString("content", mcp.Required(), mcp.Description("file content")), - mcp.WithString("message", mcp.Required(), mcp.Description("commit message")), - mcp.WithString("branch_name", mcp.Required(), mcp.Description("branch name")), + mcp.WithString("sha", mcp.Description("SHA of the existing file (required for update, omit for create)")), + mcp.WithString("new_branch_name", mcp.Description("new branch name (for create only)")), ) DeleteFileTool = mcp.NewTool( @@ -78,7 +66,7 @@ var ( mcp.WithString("filePath", mcp.Required(), mcp.Description("file path")), mcp.WithString("message", mcp.Required(), mcp.Description("commit message")), mcp.WithString("branch_name", mcp.Required(), mcp.Description("branch name")), - mcp.WithString("sha", mcp.Description("sha")), + mcp.WithString("sha", mcp.Required(), mcp.Description("sha")), ) ) @@ -92,12 +80,8 @@ func init() { Handler: GetDirContentFn, }) Tool.RegisterWrite(server.ServerTool{ - Tool: CreateFileTool, - Handler: CreateFileFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: UpdateFileTool, - Handler: UpdateFileFn, + Tool: CreateOrUpdateFileTool, + Handler: CreateOrUpdateFileFn, }) Tool.RegisterWrite(server.ServerTool{ Tool: DeleteFileTool, @@ -160,7 +144,7 @@ func GetFileContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo // remove the last blank line if exists // git does not consider the last line as a new line - if contentLines[len(contentLines)-1].Content == "" { + if len(contentLines) > 0 && contentLines[len(contentLines)-1].Content == "" { contentLines = contentLines[:len(contentLines)-1] } @@ -201,8 +185,8 @@ func GetDirContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo return to.TextResult(slimDirEntries(content)) } -func CreateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called CreateFileFn") +func CreateOrUpdateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CreateOrUpdateFileFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -219,6 +203,31 @@ func CreateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe content, _ := args["content"].(string) message, _ := args["message"].(string) branchName, _ := args["branch_name"].(string) + sha, _ := args["sha"].(string) + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + + if sha != "" { + // Update existing file + opt := gitea_sdk.UpdateFileOptions{ + SHA: sha, + Content: base64.StdEncoding.EncodeToString([]byte(content)), + FileOptions: gitea_sdk.FileOptions{ + Message: message, + BranchName: branchName, + }, + } + _, _, err = client.UpdateFile(owner, repo, filePath, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("update file err: %v", err)) + } + return to.TextResult("Update file success") + } + + // Create new file opt := gitea_sdk.CreateFileOptions{ Content: base64.StdEncoding.EncodeToString([]byte(content)), FileOptions: gitea_sdk.FileOptions{ @@ -226,10 +235,8 @@ func CreateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe BranchName: branchName, }, } - - client, err := gitea.ClientFromContext(ctx) - if err != nil { - return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + if newBranch, ok := args["new_branch_name"].(string); ok && newBranch != "" { + opt.NewBranchName = newBranch } _, _, err = client.CreateFile(owner, repo, filePath, opt) if err != nil { @@ -238,48 +245,6 @@ func CreateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe return to.TextResult("Create file success") } -func UpdateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called UpdateFileFn") - args := req.GetArguments() - owner, err := params.GetString(args, "owner") - if err != nil { - return to.ErrorResult(err) - } - repo, err := params.GetString(args, "repo") - if err != nil { - return to.ErrorResult(err) - } - filePath, err := params.GetString(args, "filePath") - if err != nil { - return to.ErrorResult(err) - } - sha, err := params.GetString(args, "sha") - if err != nil { - return to.ErrorResult(err) - } - content, _ := args["content"].(string) - message, _ := args["message"].(string) - branchName, _ := args["branch_name"].(string) - - opt := gitea_sdk.UpdateFileOptions{ - SHA: sha, - Content: base64.StdEncoding.EncodeToString([]byte(content)), - FileOptions: gitea_sdk.FileOptions{ - Message: message, - BranchName: branchName, - }, - } - client, err := gitea.ClientFromContext(ctx) - if err != nil { - return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) - } - _, _, err = client.UpdateFile(owner, repo, filePath, opt) - if err != nil { - return to.ErrorResult(fmt.Errorf("update file err: %v", err)) - } - return to.TextResult("Update file success") -} - func DeleteFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called DeleteFileFn") args := req.GetArguments() diff --git a/operation/repo/release.go b/operation/repo/release.go index c70189e..44928a7 100644 --- a/operation/repo/release.go +++ b/operation/repo/release.go @@ -67,7 +67,7 @@ var ( mcp.WithBoolean("is_draft", mcp.Description("Whether the release is draft"), mcp.DefaultBool(false)), mcp.WithBoolean("is_pre_release", mcp.Description("Whether the release is pre-release"), mcp.DefaultBool(false)), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(20), mcp.Min(1)), + mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(20), mcp.Min(1)), ) ) @@ -242,7 +242,7 @@ func ListReleasesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool pIsPreRelease = new(isPreRelease) } page := params.GetOptionalInt(args, "page", 1) - pageSize := params.GetOptionalInt(args, "pageSize", 20) + pageSize := params.GetOptionalInt(args, "perPage", 20) client, err := gitea.ClientFromContext(ctx) if err != nil { diff --git a/operation/repo/repo.go b/operation/repo/repo.go index dde7099..a52781e 100644 --- a/operation/repo/repo.go +++ b/operation/repo/repo.go @@ -53,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(30), mcp.Min(1)), + mcp.WithNumber("perPage", mcp.Required(), mcp.Description("results per page"), mcp.DefaultNumber(30), mcp.Min(1)), ) ) @@ -72,39 +72,6 @@ func init() { }) } -func RegisterTool(s *server.MCPServer) { - s.AddTool(CreateRepoTool, CreateRepoFn) - s.AddTool(ForkRepoTool, ForkRepoFn) - s.AddTool(ListMyReposTool, ListMyReposFn) - - // File - s.AddTool(GetFileContentTool, GetFileContentFn) - s.AddTool(CreateFileTool, CreateFileFn) - s.AddTool(UpdateFileTool, UpdateFileFn) - s.AddTool(DeleteFileTool, DeleteFileFn) - - // Branch - s.AddTool(CreateBranchTool, CreateBranchFn) - s.AddTool(DeleteBranchTool, DeleteBranchFn) - s.AddTool(ListBranchesTool, ListBranchesFn) - - // Release - s.AddTool(CreateReleaseTool, CreateReleaseFn) - s.AddTool(DeleteReleaseTool, DeleteReleaseFn) - s.AddTool(GetReleaseTool, GetReleaseFn) - s.AddTool(GetLatestReleaseTool, GetLatestReleaseFn) - s.AddTool(ListReleasesTool, ListReleasesFn) - - // Tag - s.AddTool(CreateTagTool, CreateTagFn) - s.AddTool(DeleteTagTool, DeleteTagFn) - s.AddTool(GetTagTool, GetTagFn) - s.AddTool(ListTagsTool, ListTagsFn) - - // Commit - s.AddTool(ListRepoCommitsTool, ListRepoCommitsFn) -} - func CreateRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CreateRepoFn") args := req.GetArguments() diff --git a/operation/repo/tag.go b/operation/repo/tag.go index 6dcf494..ee97f38 100644 --- a/operation/repo/tag.go +++ b/operation/repo/tag.go @@ -54,7 +54,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(20), mcp.Min(1)), + mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(20), mcp.Min(1)), ) ) @@ -179,7 +179,7 @@ func ListTagsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResu return to.ErrorResult(err) } page := params.GetOptionalInt(args, "page", 1) - pageSize := params.GetOptionalInt(args, "pageSize", 20) + pageSize := params.GetOptionalInt(args, "perPage", 20) client, err := gitea.ClientFromContext(ctx) if err != nil { diff --git a/operation/search/search.go b/operation/search/search.go index 1472591..b9c1427 100644 --- a/operation/search/search.go +++ b/operation/search/search.go @@ -29,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(30)), + mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)), ) SearOrgTeamsTool = mcp.NewTool( @@ -39,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(30)), + mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)), ) SearchReposTool = mcp.NewTool( @@ -54,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(30)), + mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)), ) ) diff --git a/operation/timetracking/timetracking.go b/operation/timetracking/timetracking.go index fb601fc..e22fbf2 100644 --- a/operation/timetracking/timetracking.go +++ b/operation/timetracking/timetracking.go @@ -19,114 +19,83 @@ import ( var Tool = tool.New() const ( - // Stopwatch tools - StartStopwatchToolName = "start_stopwatch" - StopStopwatchToolName = "stop_stopwatch" - DeleteStopwatchToolName = "delete_stopwatch" - GetMyStopwatchesToolName = "get_my_stopwatches" - - // Tracked time tools - ListTrackedTimesToolName = "list_tracked_times" - AddTrackedTimeToolName = "add_tracked_time" - DeleteTrackedTimeToolName = "delete_tracked_time" - ListRepoTimesToolName = "list_repo_times" - GetMyTimesToolName = "get_my_times" + TimetrackingReadToolName = "timetracking_read" + TimetrackingWriteToolName = "timetracking_write" ) var ( - // Stopwatch tools - StartStopwatchTool = mcp.NewTool( - StartStopwatchToolName, - mcp.WithDescription("Start a stopwatch on an issue to track time spent"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), - ) - - StopStopwatchTool = mcp.NewTool( - StopStopwatchToolName, - mcp.WithDescription("Stop a running stopwatch on an issue and record the tracked time"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), - ) - - DeleteStopwatchTool = mcp.NewTool( - DeleteStopwatchToolName, - mcp.WithDescription("Delete/cancel a running stopwatch without recording time"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), - ) - - GetMyStopwatchesTool = mcp.NewTool( - GetMyStopwatchesToolName, - mcp.WithDescription("Get all currently running stopwatches for the authenticated user"), - ) - - // Tracked time tools - ListTrackedTimesTool = mcp.NewTool( - ListTrackedTimesToolName, - mcp.WithDescription("List tracked times for a specific issue"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), + TimetrackingReadTool = mcp.NewTool( + TimetrackingReadToolName, + mcp.WithDescription("Read time tracking data. Use method 'list_issue_times' for issue times, 'list_repo_times' for repository times, 'get_my_stopwatches' for active stopwatches, 'get_my_times' for all your tracked times."), + mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list_issue_times", "list_repo_times", "get_my_stopwatches", "get_my_times")), + mcp.WithString("owner", mcp.Description("repository owner (required for 'list_issue_times', 'list_repo_times')")), + mcp.WithString("repo", mcp.Description("repository name (required for 'list_issue_times', 'list_repo_times')")), + mcp.WithNumber("index", mcp.Description("issue index (required for 'list_issue_times')")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30)), + mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)), ) - AddTrackedTimeTool = mcp.NewTool( - AddTrackedTimeToolName, - mcp.WithDescription("Manually add tracked time to an issue"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), - mcp.WithNumber("time", mcp.Required(), mcp.Description("time to add in seconds")), - ) - - DeleteTrackedTimeTool = mcp.NewTool( - DeleteTrackedTimeToolName, - mcp.WithDescription("Delete a tracked time entry from an issue"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), - mcp.WithNumber("id", mcp.Required(), mcp.Description("tracked time entry ID")), - ) - - ListRepoTimesTool = mcp.NewTool( - ListRepoTimesToolName, - mcp.WithDescription("List all tracked times for a repository"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(30)), - ) - - GetMyTimesTool = mcp.NewTool( - GetMyTimesToolName, - mcp.WithDescription("Get all tracked times for the authenticated user"), + TimetrackingWriteTool = mcp.NewTool( + TimetrackingWriteToolName, + mcp.WithDescription("Manage time tracking: stopwatches and tracked time entries."), + mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("start_stopwatch", "stop_stopwatch", "delete_stopwatch", "add_time", "delete_time")), + mcp.WithString("owner", mcp.Description("repository owner (required for all methods)")), + mcp.WithString("repo", mcp.Description("repository name (required for all methods)")), + mcp.WithNumber("index", mcp.Description("issue index (required for all methods)")), + mcp.WithNumber("time", mcp.Description("time to add in seconds (required for 'add_time')")), + mcp.WithNumber("id", mcp.Description("tracked time entry ID (required for 'delete_time')")), ) ) func init() { - // Stopwatch tools - Tool.RegisterWrite(server.ServerTool{Tool: StartStopwatchTool, Handler: StartStopwatchFn}) - Tool.RegisterWrite(server.ServerTool{Tool: StopStopwatchTool, Handler: StopStopwatchFn}) - Tool.RegisterWrite(server.ServerTool{Tool: DeleteStopwatchTool, Handler: DeleteStopwatchFn}) - Tool.RegisterRead(server.ServerTool{Tool: GetMyStopwatchesTool, Handler: GetMyStopwatchesFn}) + Tool.RegisterRead(server.ServerTool{Tool: TimetrackingReadTool, Handler: readFn}) + Tool.RegisterWrite(server.ServerTool{Tool: TimetrackingWriteTool, Handler: writeFn}) +} - // Tracked time tools - Tool.RegisterRead(server.ServerTool{Tool: ListTrackedTimesTool, Handler: ListTrackedTimesFn}) - Tool.RegisterWrite(server.ServerTool{Tool: AddTrackedTimeTool, Handler: AddTrackedTimeFn}) - Tool.RegisterWrite(server.ServerTool{Tool: DeleteTrackedTimeTool, Handler: DeleteTrackedTimeFn}) - Tool.RegisterRead(server.ServerTool{Tool: ListRepoTimesTool, Handler: ListRepoTimesFn}) - Tool.RegisterRead(server.ServerTool{Tool: GetMyTimesTool, Handler: GetMyTimesFn}) +func readFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + method, err := params.GetString(req.GetArguments(), "method") + if err != nil { + return to.ErrorResult(err) + } + switch method { + case "list_issue_times": + return listTrackedTimesFn(ctx, req) + case "list_repo_times": + return listRepoTimesFn(ctx, req) + case "get_my_stopwatches": + return getMyStopwatchesFn(ctx, req) + case "get_my_times": + return getMyTimesFn(ctx, req) + default: + return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) + } +} + +func writeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + method, err := params.GetString(req.GetArguments(), "method") + if err != nil { + return to.ErrorResult(err) + } + switch method { + case "start_stopwatch": + return startStopwatchFn(ctx, req) + case "stop_stopwatch": + return stopStopwatchFn(ctx, req) + case "delete_stopwatch": + return deleteStopwatchFn(ctx, req) + case "add_time": + return addTrackedTimeFn(ctx, req) + case "delete_time": + return deleteTrackedTimeFn(ctx, req) + default: + return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) + } } // Stopwatch handler functions -func StartStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called StartStopwatchFn") +func startStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called startStopwatchFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -150,8 +119,8 @@ func StartStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(fmt.Sprintf("Stopwatch started on issue %s/%s#%d", owner, repo, index)) } -func StopStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called StopStopwatchFn") +func stopStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called stopStopwatchFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -175,8 +144,8 @@ func StopStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo return to.TextResult(fmt.Sprintf("Stopwatch stopped on issue %s/%s#%d - time recorded", owner, repo, index)) } -func DeleteStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called DeleteStopwatchFn") +func deleteStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called deleteStopwatchFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -200,8 +169,8 @@ func DeleteStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT return to.TextResult(fmt.Sprintf("Stopwatch deleted/cancelled on issue %s/%s#%d", owner, repo, index)) } -func GetMyStopwatchesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called GetMyStopwatchesFn") +func getMyStopwatchesFn(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called getMyStopwatchesFn") client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) @@ -218,8 +187,8 @@ func GetMyStopwatchesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call // Tracked time handler functions -func ListTrackedTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called ListTrackedTimesFn") +func listTrackedTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called listTrackedTimesFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -253,8 +222,8 @@ func ListTrackedTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return to.TextResult(slimTrackedTimes(times)) } -func AddTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called AddTrackedTimeFn") +func addTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called addTrackedTimeFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -285,8 +254,8 @@ func AddTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(slimTrackedTime(trackedTime)) } -func DeleteTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called DeleteTrackedTimeFn") +func deleteTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called deleteTrackedTimeFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -315,8 +284,8 @@ func DeleteTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Cal return to.TextResult(fmt.Sprintf("Tracked time entry %d deleted from issue %s/%s#%d", id, owner, repo, index)) } -func ListRepoTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called ListRepoTimesFn") +func listRepoTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called listRepoTimesFn") owner, err := params.GetString(req.GetArguments(), "owner") if err != nil { return to.ErrorResult(err) @@ -346,8 +315,8 @@ func ListRepoTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo return to.TextResult(slimTrackedTimes(times)) } -func GetMyTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called GetMyTimesFn") +func getMyTimesFn(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called getMyTimesFn") client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) diff --git a/operation/user/user.go b/operation/user/user.go index b093dd0..c0c6e02 100644 --- a/operation/user/user.go +++ b/operation/user/user.go @@ -16,8 +16,8 @@ import ( ) const ( - // GetMyUserInfoToolName is the unique tool name used for MCP registration and lookup of the get_my_user_info command. - GetMyUserInfoToolName = "get_my_user_info" + // GetMyUserInfoToolName is the unique tool name used for MCP registration and lookup of the get_me command. + GetMyUserInfoToolName = "get_me" // GetUserOrgsToolName is the unique tool name used for MCP registration and lookup of the get_user_orgs command. GetUserOrgsToolName = "get_user_orgs" @@ -39,12 +39,12 @@ var ( ) // GetUserOrgsTool is the MCP tool for listing organizations for the authenticated user. - // It supports pagination via "page" and "pageSize" arguments with default values specified above. + // It supports pagination via "page" and "perPage" arguments with default values specified above. GetUserOrgsTool = mcp.NewTool( GetUserOrgsToolName, mcp.WithDescription("Get organizations associated with the authenticated user"), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(defaultPage)), - mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(defaultPageSize)), + mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(defaultPageSize)), ) ) @@ -66,7 +66,7 @@ func registerTools() { } } -// GetUserInfoFn is the handler for "get_my_user_info" MCP tool requests. +// GetUserInfoFn is the handler for "get_me" 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) { log.Debugf("[User] Called GetUserInfoFn") diff --git a/operation/wiki/wiki.go b/operation/wiki/wiki.go index 9ed42b6..1e4373b 100644 --- a/operation/wiki/wiki.go +++ b/operation/wiki/wiki.go @@ -18,97 +18,80 @@ import ( var Tool = tool.New() const ( - ListWikiPagesToolName = "list_wiki_pages" - GetWikiPageToolName = "get_wiki_page" - GetWikiRevisionsToolName = "get_wiki_revisions" - CreateWikiPageToolName = "create_wiki_page" - UpdateWikiPageToolName = "update_wiki_page" - DeleteWikiPageToolName = "delete_wiki_page" + WikiReadToolName = "wiki_read" + WikiWriteToolName = "wiki_write" ) var ( - ListWikiPagesTool = mcp.NewTool( - ListWikiPagesToolName, - mcp.WithDescription("List all wiki pages in a repository"), + WikiReadTool = mcp.NewTool( + WikiReadToolName, + mcp.WithDescription("Read wiki page information. Use method 'list' to list pages, 'get' to get page content, 'get_revisions' for revision history."), + mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list", "get", "get_revisions")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("pageName", mcp.Description("wiki page name (required for 'get', 'get_revisions')")), ) - GetWikiPageTool = mcp.NewTool( - GetWikiPageToolName, - mcp.WithDescription("Get a wiki page content and metadata"), + WikiWriteTool = mcp.NewTool( + WikiWriteToolName, + mcp.WithDescription("Create, update, or delete wiki pages."), + mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create", "update", "delete")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("pageName", mcp.Required(), mcp.Description("wiki page name")), - ) - - GetWikiRevisionsTool = mcp.NewTool( - GetWikiRevisionsToolName, - mcp.WithDescription("Get revisions history of a wiki page"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("pageName", mcp.Required(), mcp.Description("wiki page name")), - ) - - CreateWikiPageTool = mcp.NewTool( - CreateWikiPageToolName, - mcp.WithDescription("Create a new wiki page"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("title", mcp.Required(), mcp.Description("wiki page title")), - mcp.WithString("content_base64", mcp.Required(), mcp.Description("page content, base64 encoded")), - mcp.WithString("message", mcp.Description("commit message (optional)")), - ) - - UpdateWikiPageTool = mcp.NewTool( - UpdateWikiPageToolName, - mcp.WithDescription("Update an existing wiki page"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("pageName", mcp.Required(), mcp.Description("current wiki page name")), - mcp.WithString("title", mcp.Description("new page title (optional)")), - mcp.WithString("content_base64", mcp.Required(), mcp.Description("page content, base64 encoded")), - mcp.WithString("message", mcp.Description("commit message (optional)")), - ) - - DeleteWikiPageTool = mcp.NewTool( - DeleteWikiPageToolName, - mcp.WithDescription("Delete a wiki page"), - mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), - mcp.WithString("pageName", mcp.Required(), mcp.Description("wiki page name to delete")), + mcp.WithString("pageName", mcp.Description("wiki page name (required for 'update', 'delete')")), + mcp.WithString("title", mcp.Description("wiki page title (required for 'create', optional for 'update')")), + mcp.WithString("content_base64", mcp.Description("page content, base64 encoded (required for 'create', 'update')")), + mcp.WithString("message", mcp.Description("commit message")), ) ) func init() { Tool.RegisterRead(server.ServerTool{ - Tool: ListWikiPagesTool, - Handler: ListWikiPagesFn, - }) - Tool.RegisterRead(server.ServerTool{ - Tool: GetWikiPageTool, - Handler: GetWikiPageFn, - }) - Tool.RegisterRead(server.ServerTool{ - Tool: GetWikiRevisionsTool, - Handler: GetWikiRevisionsFn, + Tool: WikiReadTool, + Handler: wikiReadFn, }) Tool.RegisterWrite(server.ServerTool{ - Tool: CreateWikiPageTool, - Handler: CreateWikiPageFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: UpdateWikiPageTool, - Handler: UpdateWikiPageFn, - }) - Tool.RegisterWrite(server.ServerTool{ - Tool: DeleteWikiPageTool, - Handler: DeleteWikiPageFn, + Tool: WikiWriteTool, + Handler: wikiWriteFn, }) } -func ListWikiPagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called ListWikiPagesFn") +func wikiReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + method, err := params.GetString(req.GetArguments(), "method") + if err != nil { + return to.ErrorResult(err) + } + switch method { + case "list": + return listWikiPagesFn(ctx, req) + case "get": + return getWikiPageFn(ctx, req) + case "get_revisions": + return getWikiRevisionsFn(ctx, req) + default: + return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) + } +} + +func wikiWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + method, err := params.GetString(req.GetArguments(), "method") + if err != nil { + return to.ErrorResult(err) + } + switch method { + case "create": + return createWikiPageFn(ctx, req) + case "update": + return updateWikiPageFn(ctx, req) + case "delete": + return deleteWikiPageFn(ctx, req) + default: + return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) + } +} + +func listWikiPagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called listWikiPagesFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -119,9 +102,8 @@ func ListWikiPagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo 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.PathEscape(owner), url.PathEscape(repo)), nil, nil, &result) if err != nil { return to.ErrorResult(fmt.Errorf("list wiki pages err: %v", err)) } @@ -129,8 +111,8 @@ func ListWikiPagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo return to.TextResult(result) } -func GetWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called GetWikiPageFn") +func getWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called getWikiPageFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -146,7 +128,7 @@ func GetWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR } 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.PathEscape(owner), url.PathEscape(repo), url.PathEscape(pageName)), nil, nil, &result) if err != nil { return to.ErrorResult(fmt.Errorf("get wiki page err: %v", err)) } @@ -154,8 +136,8 @@ func GetWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR return to.TextResult(result) } -func GetWikiRevisionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called GetWikiRevisionsFn") +func getWikiRevisionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called getWikiRevisionsFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -171,7 +153,7 @@ func GetWikiRevisionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call } 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.PathEscape(owner), url.PathEscape(repo), url.PathEscape(pageName)), nil, nil, &result) if err != nil { return to.ErrorResult(fmt.Errorf("get wiki revisions err: %v", err)) } @@ -179,8 +161,8 @@ func GetWikiRevisionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return to.TextResult(result) } -func CreateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called CreateWikiPageFn") +func createWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called createWikiPageFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -211,7 +193,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.PathEscape(owner), url.PathEscape(repo)), nil, requestBody, &result) if err != nil { return to.ErrorResult(fmt.Errorf("create wiki page err: %v", err)) } @@ -219,8 +201,8 @@ func CreateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(result) } -func UpdateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called UpdateWikiPageFn") +func updateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called updateWikiPageFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -247,7 +229,6 @@ func UpdateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo if title, ok := args["title"].(string); ok && title != "" { requestBody["title"] = title } else { - // Utiliser pageName comme fallback pour éviter "unnamed" requestBody["title"] = pageName } @@ -258,7 +239,7 @@ func UpdateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo } 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.PathEscape(owner), url.PathEscape(repo), url.PathEscape(pageName)), nil, requestBody, &result) if err != nil { return to.ErrorResult(fmt.Errorf("update wiki page err: %v", err)) } @@ -266,8 +247,8 @@ func UpdateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo return to.TextResult(result) } -func DeleteWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - log.Debugf("Called DeleteWikiPageFn") +func deleteWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called deleteWikiPageFn") args := req.GetArguments() owner, err := params.GetString(args, "owner") if err != nil { @@ -282,7 +263,7 @@ func DeleteWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo 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.PathEscape(owner), url.PathEscape(repo), url.PathEscape(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 f12abf6..d96e47c 100644 --- a/pkg/params/params.go +++ b/pkg/params/params.go @@ -41,9 +41,9 @@ func GetStringSlice(args map[string]any, key string) []string { return out } -// GetPagination extracts page and pageSize parameters, returning them as ints. +// GetPagination extracts page and perPage 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)) + return int(GetOptionalInt(args, "page", 1)), int(GetOptionalInt(args, "perPage", defaultPageSize)) } // ToInt64 converts a value to int64, accepting both float64 (JSON number) and @@ -84,6 +84,23 @@ func GetIndex(args map[string]any, key string) (int64, error) { return 0, fmt.Errorf("%s must be a number or numeric string", key) } +// GetInt64Slice extracts a required int64 slice parameter from MCP tool arguments. +func GetInt64Slice(args map[string]any, key string) ([]int64, error) { + raw, ok := args[key].([]any) + if !ok { + return nil, fmt.Errorf("%s (array of IDs) is required", key) + } + out := make([]int64, 0, len(raw)) + for _, v := range raw { + id, ok := ToInt64(v) + if !ok { + return nil, fmt.Errorf("invalid ID in %s array", key) + } + out = append(out, id) + } + return out, nil +} + // GetOptionalInt extracts an optional integer parameter from MCP tool arguments. // Returns defaultVal if the key is missing or the value cannot be parsed. // Accepts both float64 (JSON number) and string representations.