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 <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
This commit is contained in:
silverwind
2026-03-06 19:12:15 +00:00
committed by silverwind
parent c3db4fb65f
commit bba612d238
22 changed files with 1574 additions and 2039 deletions

View File

@@ -47,17 +47,24 @@ This is a **Gitea MCP (Model Context Protocol) Server** written in Go that provi
## Available Tools ## 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 - **User**: get_me, get_user_orgs
- **Repository**: create_repo, fork_repo, list_my_repos, search_repos - **Search**: search_users, search_repos, search_org_teams
- **Branches/Tags**: create_branch, delete_branch, list_branches, create_tag, list_tags - **Repository**: create_repo, fork_repo, list_my_repos
- **Files**: get_file_content, create_file, update_file, delete_file, get_dir_content - **Branches**: list_branches, create_branch, delete_branch
- **Issues**: create_issue, list_repo_issues, create_issue_comment, edit_issue - **Tags**: list_tags, get_tag, create_tag, delete_tag
- **Pull Requests**: create_pull_request, list_repo_pull_requests, get_pull_request_by_index - **Files**: get_file_contents, get_dir_contents, create_or_update_file, delete_file
- **Releases**: create_release, list_releases, get_latest_release - **Commits**: list_commits
- **Wiki**: create_wiki_page, update_wiki_page, list_wiki_pages - **Issues**: list_issues, issue_read, issue_write
- **Search**: search_repos, search_users, search_org_teams - **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 - **Version**: get_gitea_mcp_server_version
## Common Development Patterns ## Common Development Patterns

555
operation/actions/config.go Normal file
View File

@@ -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"})
}

View File

@@ -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),
})
}

View File

@@ -4,9 +4,10 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"maps"
"net/http" "net/http"
"net/url" "net/url"
"os"
"path/filepath"
"strconv" "strconv"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
@@ -19,114 +20,88 @@ import (
) )
const ( const (
ListRepoActionWorkflowsToolName = "list_repo_action_workflows" ActionsRunReadToolName = "actions_run_read"
GetRepoActionWorkflowToolName = "get_repo_action_workflow" ActionsRunWriteToolName = "actions_run_write"
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"
) )
var ( var (
ListRepoActionWorkflowsTool = mcp.NewTool( ActionsRunReadTool = mcp.NewTool(
ListRepoActionWorkflowsToolName, ActionsRunReadToolName,
mcp.WithDescription("List repository Actions workflows"), 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("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), 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("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( ActionsRunWriteTool = mcp.NewTool(
GetRepoActionWorkflowToolName, ActionsRunWriteToolName,
mcp.WithDescription("Get a repository Actions workflow by ID"), 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("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), 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("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')")),
DispatchRepoActionWorkflowTool = mcp.NewTool( mcp.WithNumber("run_id", mcp.Description("run ID (required for 'cancel_run', 'rerun_run')")),
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)),
) )
) )
func init() { func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionWorkflowsTool, Handler: ListRepoActionWorkflowsFn}) Tool.RegisterRead(server.ServerTool{Tool: ActionsRunReadTool, Handler: runReadFn})
Tool.RegisterRead(server.ServerTool{Tool: GetRepoActionWorkflowTool, Handler: GetRepoActionWorkflowFn}) Tool.RegisterWrite(server.ServerTool{Tool: ActionsRunWriteTool, Handler: runWriteFn})
Tool.RegisterWrite(server.ServerTool{Tool: DispatchRepoActionWorkflowTool, Handler: DispatchRepoActionWorkflowFn}) }
Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionRunsTool, Handler: ListRepoActionRunsFn}) func runReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
Tool.RegisterRead(server.ServerTool{Tool: GetRepoActionRunTool, Handler: GetRepoActionRunFn}) method, err := params.GetString(req.GetArguments(), "method")
Tool.RegisterWrite(server.ServerTool{Tool: CancelRepoActionRunTool, Handler: CancelRepoActionRunFn}) if err != nil {
Tool.RegisterWrite(server.ServerTool{Tool: RerunRepoActionRunTool, Handler: RerunRepoActionRunFn}) 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}) func runWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionRunJobsTool, Handler: ListRepoActionRunJobsFn}) 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 { 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 return lastErr
} }
func ListRepoActionWorkflowsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func listRepoActionWorkflowsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoActionWorkflowsFn") log.Debugf("Called listRepoActionWorkflowsFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil || owner == "" { if err != nil || owner == "" {
return to.ErrorResult(errors.New("owner is required")) 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)) return to.TextResult(slimActionWorkflows(result))
} }
func GetRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func getRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetRepoActionWorkflowFn") log.Debugf("Called getRepoActionWorkflowFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil || owner == "" { if err != nil || owner == "" {
return to.ErrorResult(errors.New("owner is required")) 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)) return to.TextResult(slimActionWorkflow(result))
} }
func DispatchRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func dispatchRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DispatchRepoActionWorkflowFn") log.Debugf("Called dispatchRepoActionWorkflowFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil || owner == "" { if err != nil || owner == "" {
return to.ErrorResult(errors.New("owner is required")) 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 raw, exists := req.GetArguments()["inputs"]; exists {
if m, ok := raw.(map[string]any); ok { if m, ok := raw.(map[string]any); ok {
inputs = m 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"}) return to.TextResult(map[string]any{"message": "workflow dispatched"})
} }
func ListRepoActionRunsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func listRepoActionRunsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoActionRunsFn") log.Debugf("Called listRepoActionRunsFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil || owner == "" { if err != nil || owner == "" {
return to.ErrorResult(errors.New("owner is required")) 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)) return to.TextResult(slimActionRuns(result))
} }
func GetRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func getRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetRepoActionRunFn") log.Debugf("Called getRepoActionRunFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil || owner == "" { if err != nil || owner == "" {
return to.ErrorResult(errors.New("owner is required")) 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)) return to.TextResult(slimActionRun(result))
} }
func CancelRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func cancelRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CancelRepoActionRunFn") log.Debugf("Called cancelRepoActionRunFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil || owner == "" { if err != nil || owner == "" {
return to.ErrorResult(errors.New("owner is required")) 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"}) return to.TextResult(map[string]any{"message": "run cancellation requested"})
} }
func RerunRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func rerunRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called RerunRepoActionRunFn") log.Debugf("Called rerunRepoActionRunFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil || owner == "" { if err != nil || owner == "" {
return to.ErrorResult(errors.New("owner is required")) 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"}) return to.TextResult(map[string]any{"message": "run rerun requested"})
} }
func ListRepoActionJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func listRepoActionJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoActionJobsFn") log.Debugf("Called listRepoActionJobsFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil || owner == "" { if err != nil || owner == "" {
return to.ErrorResult(errors.New("owner is required")) 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)) return to.TextResult(slimActionJobs(result))
} }
func ListRepoActionRunJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func listRepoActionRunJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoActionRunJobsFn") log.Debugf("Called listRepoActionRunJobsFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil || owner == "" { if err != nil || owner == "" {
return to.ErrorResult(errors.New("owner is required")) 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)) 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),
})
}

View File

@@ -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"})
}

View File

@@ -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"})
}

View File

@@ -18,24 +18,12 @@ import (
var Tool = tool.New() var Tool = tool.New()
const ( const (
GetIssueByIndexToolName = "get_issue_by_index" ListRepoIssuesToolName = "list_issues"
ListRepoIssuesToolName = "list_repo_issues" IssueReadToolName = "issue_read"
CreateIssueToolName = "create_issue" IssueWriteToolName = "issue_write"
CreateIssueCommentToolName = "create_issue_comment"
EditIssueToolName = "edit_issue"
EditIssueCommentToolName = "edit_issue_comment"
GetIssueCommentsByIndexToolName = "get_issue_comments_by_index"
) )
var ( 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( ListRepoIssuesTool = mcp.NewTool(
ListRepoIssuesToolName, ListRepoIssuesToolName,
mcp.WithDescription("List repository issues"), mcp.WithDescription("List repository issues"),
@@ -43,91 +31,99 @@ var (
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("state", mcp.Description("issue state"), mcp.DefaultString("all")), mcp.WithString("state", mcp.Description("issue state"), mcp.DefaultString("all")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), 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( IssueReadTool = mcp.NewTool(
CreateIssueToolName, IssueReadToolName,
mcp.WithDescription("create issue"), 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("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("get", "get_comments", "get_labels")),
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"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("repository issue index")), mcp.WithNumber("index", mcp.Required(), mcp.Description("repository issue index")),
mcp.WithString("body", mcp.Required(), mcp.Description("issue comment body")),
) )
EditIssueTool = mcp.NewTool( IssueWriteTool = mcp.NewTool(
EditIssueToolName, IssueWriteToolName,
mcp.WithDescription("edit issue"), 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("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), 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"), mcp.DefaultString("")), mcp.WithString("title", mcp.Description("issue title (required for 'create')")),
mcp.WithString("body", mcp.Description("issue body content")), mcp.WithString("body", mcp.Description("issue/comment body (required for 'create', 'add_comment', 'edit_comment')")),
mcp.WithArray("assignees", mcp.Description("usernames to assign to this issue"), mcp.Items(map[string]any{"type": "string"})), 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")), mcp.WithNumber("milestone", mcp.Description("milestone number (for 'create', 'update')")),
mcp.WithString("state", mcp.Description("issue state, one of open, closed, all")), 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"})),
EditIssueCommentTool = mcp.NewTool( mcp.WithNumber("label_id", mcp.Description("label ID to remove (required for 'remove_label')")),
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")),
) )
) )
func init() { func init() {
Tool.RegisterRead(server.ServerTool{
Tool: GetIssueByIndexTool,
Handler: GetIssueByIndexFn,
})
Tool.RegisterRead(server.ServerTool{ Tool.RegisterRead(server.ServerTool{
Tool: ListRepoIssuesTool, Tool: ListRepoIssuesTool,
Handler: ListRepoIssuesFn, 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,
}) })
Tool.RegisterRead(server.ServerTool{ Tool.RegisterRead(server.ServerTool{
Tool: GetIssueCommentsByIndexTool, Tool: IssueReadTool,
Handler: GetIssueCommentsByIndexFn, Handler: issueReadFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: IssueWriteTool,
Handler: issueWriteFn,
}) })
} }
func GetIssueByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func issueReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetIssueByIndexFn") 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") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -152,7 +148,7 @@ func GetIssueByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
return to.TextResult(slimIssue(issue)) 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") log.Debugf("Called ListIssuesFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
@@ -185,8 +181,8 @@ func ListRepoIssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
return to.TextResult(slimIssues(issues)) return to.TextResult(slimIssues(issues))
} }
func CreateIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func createIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateIssueFn") log.Debugf("Called createIssueFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -207,10 +203,17 @@ func CreateIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) 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, Title: title,
Body: body, 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 { if err != nil {
return to.ErrorResult(fmt.Errorf("create %v/%v/issue err: %v", owner, repo, err)) 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)) return to.TextResult(slimIssue(issue))
} }
func CreateIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func createIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateIssueCommentFn") log.Debugf("Called createIssueCommentFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -251,8 +254,8 @@ func CreateIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
return to.TextResult(slimComment(issueComment)) return to.TextResult(slimComment(issueComment))
} }
func EditIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func editIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditIssueFn") log.Debugf("Called editIssueFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -299,8 +302,8 @@ func EditIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes
return to.TextResult(slimIssue(issue)) return to.TextResult(slimIssue(issue))
} }
func EditIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func editIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditIssueCommentFn") log.Debugf("Called editIssueCommentFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -332,8 +335,8 @@ func EditIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
return to.TextResult(slimComment(issueComment)) return to.TextResult(slimComment(issueComment))
} }
func GetIssueCommentsByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func getIssueCommentsByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetIssueCommentsByIndexFn") log.Debugf("Called getIssueCommentsByIndexFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -358,3 +361,147 @@ func GetIssueCommentsByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*m
return to.TextResult(slimComments(issue)) 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")
}

View File

@@ -114,3 +114,20 @@ func slimComments(comments []*gitea_sdk.Comment) []map[string]any {
} }
return out 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
}

View File

@@ -2,7 +2,6 @@ package label
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
@@ -19,197 +18,93 @@ import (
var Tool = tool.New() var Tool = tool.New()
const ( const (
ListRepoLabelsToolName = "list_repo_labels" LabelReadToolName = "label_read"
GetRepoLabelToolName = "get_repo_label" LabelWriteToolName = "label_write"
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"
) )
var ( var (
ListRepoLabelsTool = mcp.NewTool( LabelReadTool = mcp.NewTool(
ListRepoLabelsToolName, LabelReadToolName,
mcp.WithDescription("Lists all labels for a given repository"), 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("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list_repo_labels", "get_repo_label", "list_org_labels")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), 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("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( LabelWriteTool = mcp.NewTool(
GetRepoLabelToolName, LabelWriteToolName,
mcp.WithDescription("Gets a single label by its ID for a repository"), mcp.WithDescription("Create, edit, or delete labels for repositories or organizations."),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), 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("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("owner", mcp.Description("repository owner (required for repo methods)")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")), 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)")),
CreateRepoLabelTool = mcp.NewTool( mcp.WithString("name", mcp.Description("label name (required for create, optional for edit)")),
CreateRepoLabelToolName, mcp.WithString("color", mcp.Description("label color hex code e.g. #RRGGBB (required for create, optional for edit)")),
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)")),
mcp.WithString("description", mcp.Description("label description")), mcp.WithString("description", mcp.Description("label description")),
) mcp.WithBoolean("exclusive", mcp.Description("whether the label is exclusive (org labels only)")),
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")),
) )
) )
func init() { func init() {
Tool.RegisterRead(server.ServerTool{ Tool.RegisterRead(server.ServerTool{
Tool: ListRepoLabelsTool, Tool: LabelReadTool,
Handler: ListRepoLabelsFn, Handler: labelReadFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: GetRepoLabelTool,
Handler: GetRepoLabelFn,
}) })
Tool.RegisterWrite(server.ServerTool{ Tool.RegisterWrite(server.ServerTool{
Tool: CreateRepoLabelTool, Tool: LabelWriteTool,
Handler: CreateRepoLabelFn, Handler: labelWriteFn,
})
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,
}) })
} }
func ListRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func labelReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoLabelsFn") 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") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -237,8 +132,8 @@ func ListRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
return to.TextResult(slimLabels(labels)) return to.TextResult(slimLabels(labels))
} }
func GetRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func getRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetRepoLabelFn") log.Debugf("Called getRepoLabelFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -263,8 +158,8 @@ func GetRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
return to.TextResult(slimLabel(label)) return to.TextResult(slimLabel(label))
} }
func CreateRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func createRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateRepoLabelFn") log.Debugf("Called createRepoLabelFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -300,8 +195,8 @@ func CreateRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
return to.TextResult(slimLabel(label)) return to.TextResult(slimLabel(label))
} }
func EditRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func editRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditRepoLabelFn") log.Debugf("Called editRepoLabelFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -337,8 +232,8 @@ func EditRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
return to.TextResult(slimLabel(label)) return to.TextResult(slimLabel(label))
} }
func DeleteRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func deleteRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteRepoLabelFn") log.Debugf("Called deleteRepoLabelFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -363,148 +258,8 @@ func DeleteRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
return to.TextResult("Label deleted successfully") return to.TextResult("Label deleted successfully")
} }
func AddIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func listOrgLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AddIssueLabelsFn") log.Debugf("Called listOrgLabelsFn")
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")
org, err := params.GetString(req.GetArguments(), "org") org, err := params.GetString(req.GetArguments(), "org")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -528,8 +283,8 @@ func ListOrgLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
return to.TextResult(slimLabels(labels)) return to.TextResult(slimLabels(labels))
} }
func CreateOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func createOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateOrgLabelFn") log.Debugf("Called createOrgLabelFn")
org, err := params.GetString(req.GetArguments(), "org") org, err := params.GetString(req.GetArguments(), "org")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -563,8 +318,8 @@ func CreateOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
return to.TextResult(slimLabel(label)) return to.TextResult(slimLabel(label))
} }
func EditOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func editOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditOrgLabelFn") log.Debugf("Called editOrgLabelFn")
org, err := params.GetString(req.GetArguments(), "org") org, err := params.GetString(req.GetArguments(), "org")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -599,8 +354,8 @@ func EditOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
return to.TextResult(slimLabel(label)) return to.TextResult(slimLabel(label))
} }
func DeleteOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func deleteOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteOrgLabelFn") log.Debugf("Called deleteOrgLabelFn")
org, err := params.GetString(req.GetArguments(), "org") org, err := params.GetString(req.GetArguments(), "org")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)

View File

@@ -18,89 +18,83 @@ import (
var Tool = tool.New() var Tool = tool.New()
const ( const (
GetMilestoneToolName = "get_milestone" MilestoneReadToolName = "milestone_read"
ListMilestonesToolName = "list_milestones" MilestoneWriteToolName = "milestone_write"
CreateMilestoneToolName = "create_milestone"
EditMilestoneToolName = "edit_milestone"
DeleteMilestoneToolName = "delete_milestone"
) )
var ( var (
GetMilestoneTool = mcp.NewTool( MilestoneReadTool = mcp.NewTool(
GetMilestoneToolName, MilestoneReadToolName,
mcp.WithDescription("get milestone by id"), 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("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("milestone id")), 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')")),
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("page", mcp.Description("page number"), mcp.DefaultNumber(1)), 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( MilestoneWriteTool = mcp.NewTool(
CreateMilestoneToolName, MilestoneWriteToolName,
mcp.WithDescription("create milestone"), 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("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), 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("description", mcp.Description("milestone description")),
mcp.WithString("due_on", mcp.Description("due date")), mcp.WithString("due_on", mcp.Description("due date")),
) mcp.WithString("state", mcp.Description("milestone state, one of open, closed (for 'edit')")),
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")),
) )
) )
func init() { func init() {
Tool.RegisterRead(server.ServerTool{ Tool.RegisterRead(server.ServerTool{
Tool: GetMilestoneTool, Tool: MilestoneReadTool,
Handler: GetMilestoneFn, Handler: milestoneReadFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: ListMilestonesTool,
Handler: ListMilestonesFn,
}) })
Tool.RegisterWrite(server.ServerTool{ Tool.RegisterWrite(server.ServerTool{
Tool: CreateMilestoneTool, Tool: MilestoneWriteTool,
Handler: CreateMilestoneFn, Handler: milestoneWriteFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: EditMilestoneTool,
Handler: EditMilestoneFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: DeleteMilestoneTool,
Handler: DeleteMilestoneFn,
}) })
} }
func GetMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func milestoneReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetMilestoneFn") 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") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -125,8 +119,8 @@ func GetMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
return to.TextResult(slimMilestone(milestone)) return to.TextResult(slimMilestone(milestone))
} }
func ListMilestonesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func listMilestonesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListMilestonesFn") log.Debugf("Called listMilestonesFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -157,8 +151,8 @@ func ListMilestonesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
return to.TextResult(slimMilestones(milestones)) return to.TextResult(slimMilestones(milestones))
} }
func CreateMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func createMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateMilestoneFn") log.Debugf("Called createMilestoneFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -193,8 +187,8 @@ func CreateMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
return to.TextResult(slimMilestone(milestone)) return to.TextResult(slimMilestone(milestone))
} }
func EditMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func editMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditMilestoneFn") log.Debugf("Called editMilestoneFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -235,8 +229,8 @@ func EditMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
return to.TextResult(slimMilestone(milestone)) return to.TextResult(slimMilestone(milestone))
} }
func DeleteMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func deleteMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteMilestoneFn") log.Debugf("Called deleteMilestoneFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)

View File

@@ -18,41 +18,13 @@ import (
var Tool = tool.New() var Tool = tool.New()
const ( const (
GetPullRequestByIndexToolName = "get_pull_request_by_index" ListRepoPullRequestsToolName = "list_pull_requests"
GetPullRequestDiffToolName = "get_pull_request_diff" PullRequestReadToolName = "pull_request_read"
ListRepoPullRequestsToolName = "list_repo_pull_requests" PullRequestWriteToolName = "pull_request_write"
CreatePullRequestToolName = "create_pull_request" PullRequestReviewWriteToolName = "pull_request_review_write"
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"
) )
var ( 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( ListRepoPullRequestsTool = mcp.NewTool(
ListRepoPullRequestsToolName, ListRepoPullRequestsToolName,
mcp.WithDescription("List repository pull requests"), 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.WithString("sort", mcp.Description("sort"), mcp.Enum("oldest", "recentupdate", "leastupdate", "mostcomment", "leastcomment", "priority"), mcp.DefaultString("recentupdate")),
mcp.WithNumber("milestone", mcp.Description("milestone")), mcp.WithNumber("milestone", mcp.Description("milestone")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), 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( PullRequestReadTool = mcp.NewTool(
CreatePullRequestToolName, PullRequestReadToolName,
mcp.WithDescription("create pull request"), 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("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("get", "get_diff", "get_reviews", "get_review", "get_review_comments")),
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"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")), 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("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( PullRequestWriteTool = mcp.NewTool(
GetPullRequestReviewToolName, PullRequestWriteToolName,
mcp.WithDescription("get a specific review for a pull request"), mcp.WithDescription("Create, update, or merge pull requests, manage reviewers."),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create", "update", "merge", "add_reviewers", "remove_reviewers")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("review_id", mcp.Required(), mcp.Description("review ID")), 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')")),
ListPullRequestReviewCommentsTool = mcp.NewTool( mcp.WithString("head", mcp.Description("PR head branch (required for 'create')")),
ListPullRequestReviewCommentsToolName, mcp.WithString("base", mcp.Description("PR base branch (required for 'create', optional for 'update')")),
mcp.WithDescription("list all comments for a specific pull request review"), mcp.WithString("assignee", mcp.Description("username to assign (for 'update')")),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithArray("assignees", mcp.Description("usernames to assign (for 'update')"), mcp.Items(map[string]any{"type": "string"})),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithNumber("milestone", mcp.Description("milestone number (for 'update')")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")), mcp.WithString("state", mcp.Description("PR state (for 'update')"), mcp.Enum("open", "closed")),
mcp.WithNumber("review_id", mcp.Required(), mcp.Description("review ID")), 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")),
CreatePullRequestReviewTool = mcp.NewTool( mcp.WithBoolean("delete_branch", mcp.Description("delete branch after merge (for 'merge')")),
CreatePullRequestReviewToolName, mcp.WithArray("reviewers", mcp.Description("reviewer usernames (for 'add_reviewers', 'remove_reviewers')"), mcp.Items(map[string]any{"type": "string"})),
mcp.WithDescription("create a review for a pull request"), 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("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")), 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("state", mcp.Description("review state"), mcp.Enum("APPROVED", "REQUEST_CHANGES", "COMMENT", "PENDING")),
mcp.WithString("body", mcp.Description("review body/comment")), mcp.WithString("body", mcp.Description("review body/comment")),
mcp.WithString("commit_id", mcp.Description("commit SHA to review")), mcp.WithString("commit_id", mcp.Description("commit SHA to review (for 'create')")),
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("message", mcp.Description("dismissal reason (for 'dismiss')")),
mcp.WithArray("comments", mcp.Description("inline review comments (for 'create')"), mcp.Items(map[string]any{
"type": "object", "type": "object",
"properties": map[string]any{ "properties": map[string]any{
"path": map[string]any{"type": "string", "description": "file path to comment on"}, "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() { func init() {
Tool.RegisterRead(server.ServerTool{
Tool: GetPullRequestByIndexTool,
Handler: GetPullRequestByIndexFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: GetPullRequestDiffTool,
Handler: GetPullRequestDiffFn,
})
Tool.RegisterRead(server.ServerTool{ Tool.RegisterRead(server.ServerTool{
Tool: ListRepoPullRequestsTool, Tool: ListRepoPullRequestsTool,
Handler: ListRepoPullRequestsFn, Handler: listRepoPullRequestsFn,
}) })
Tool.RegisterRead(server.ServerTool{ Tool.RegisterRead(server.ServerTool{
Tool: ListPullRequestReviewsTool, Tool: PullRequestReadTool,
Handler: ListPullRequestReviewsFn, Handler: pullRequestReadFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: GetPullRequestReviewTool,
Handler: GetPullRequestReviewFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: ListPullRequestReviewCommentsTool,
Handler: ListPullRequestReviewCommentsFn,
}) })
Tool.RegisterWrite(server.ServerTool{ Tool.RegisterWrite(server.ServerTool{
Tool: CreatePullRequestTool, Tool: PullRequestWriteTool,
Handler: CreatePullRequestFn, Handler: pullRequestWriteFn,
}) })
Tool.RegisterWrite(server.ServerTool{ Tool.RegisterWrite(server.ServerTool{
Tool: CreatePullRequestReviewerTool, Tool: PullRequestReviewWriteTool,
Handler: CreatePullRequestReviewerFn, Handler: pullRequestReviewWriteFn,
})
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,
}) })
} }
func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func pullRequestReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetPullRequestByIndexFn") 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() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {
@@ -293,8 +204,8 @@ func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp
return to.TextResult(slimPullRequest(pr)) return to.TextResult(slimPullRequest(pr))
} }
func GetPullRequestDiffFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func getPullRequestDiffFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetPullRequestDiffFn") log.Debugf("Called getPullRequestDiffFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {
@@ -324,7 +235,7 @@ func GetPullRequestDiffFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
return to.TextResult(string(diffBytes)) 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") log.Debugf("Called ListRepoPullRequests")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
@@ -360,8 +271,8 @@ func ListRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.
return to.TextResult(slimPullRequests(pullRequests)) return to.TextResult(slimPullRequests(pullRequests))
} }
func CreatePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func createPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreatePullRequestFn") log.Debugf("Called createPullRequestFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {
@@ -404,8 +315,8 @@ func CreatePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Cal
return to.TextResult(slimPullRequest(pr)) return to.TextResult(slimPullRequest(pr))
} }
func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func createPullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreatePullRequestReviewerFn") log.Debugf("Called createPullRequestReviewerFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { 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 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{ successMsg := map[string]any{
"message": "Successfully created review requests", "message": "Successfully created review requests",
"reviewers": reviewers, "reviewers": reviewers,
@@ -448,8 +358,8 @@ func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (
return to.TextResult(successMsg) return to.TextResult(successMsg)
} }
func DeletePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func deletePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeletePullRequestReviewerFn") log.Debugf("Called deletePullRequestReviewerFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {
@@ -491,8 +401,8 @@ func DeletePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (
return to.TextResult(successMsg) return to.TextResult(successMsg)
} }
func ListPullRequestReviewsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func listPullRequestReviewsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListPullRequestReviewsFn") log.Debugf("Called listPullRequestReviewsFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {
@@ -526,8 +436,8 @@ func ListPullRequestReviewsFn(ctx context.Context, req mcp.CallToolRequest) (*mc
return to.TextResult(slimReviews(reviews)) return to.TextResult(slimReviews(reviews))
} }
func GetPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func getPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetPullRequestReviewFn") log.Debugf("Called getPullRequestReviewFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {
@@ -559,8 +469,8 @@ func GetPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.
return to.TextResult(slimReview(review)) return to.TextResult(slimReview(review))
} }
func ListPullRequestReviewCommentsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func listPullRequestReviewCommentsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListPullRequestReviewCommentsFn") log.Debugf("Called listPullRequestReviewCommentsFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {
@@ -592,8 +502,8 @@ func ListPullRequestReviewCommentsFn(ctx context.Context, req mcp.CallToolReques
return to.TextResult(slimReviewComments(comments)) return to.TextResult(slimReviewComments(comments))
} }
func CreatePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func createPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreatePullRequestReviewFn") log.Debugf("Called createPullRequestReviewFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {
@@ -657,8 +567,8 @@ func CreatePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m
return to.TextResult(slimReview(review)) return to.TextResult(slimReview(review))
} }
func SubmitPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func submitPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called SubmitPullRequestReviewFn") log.Debugf("Called submitPullRequestReviewFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {
@@ -701,8 +611,8 @@ func SubmitPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m
return to.TextResult(slimReview(review)) return to.TextResult(slimReview(review))
} }
func DeletePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func deletePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeletePullRequestReviewFn") log.Debugf("Called deletePullRequestReviewFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {
@@ -741,8 +651,8 @@ func DeletePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m
return to.TextResult(successMsg) return to.TextResult(successMsg)
} }
func DismissPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func dismissPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DismissPullRequestReviewFn") log.Debugf("Called dismissPullRequestReviewFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {
@@ -786,8 +696,8 @@ func DismissPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*
return to.TextResult(successMsg) return to.TextResult(successMsg)
} }
func MergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func mergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called MergePullRequestFn") log.Debugf("Called mergePullRequestFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {
@@ -843,8 +753,8 @@ func MergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
return to.TextResult(successMsg) return to.TextResult(successMsg)
} }
func EditPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func editPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditPullRequestFn") log.Debugf("Called editPullRequestFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {

View File

@@ -13,7 +13,7 @@ import (
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
) )
func TestEditPullRequestFn(t *testing.T) { func Test_editPullRequestFn(t *testing.T) {
const ( const (
owner = "octo" owner = "octo"
repo = "demo" 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 { if err != nil {
t.Fatalf("EditPullRequestFn() error = %v", err) t.Fatalf("editPullRequestFn() error = %v", err)
} }
mu.Lock() mu.Lock()
@@ -127,7 +127,7 @@ func TestEditPullRequestFn(t *testing.T) {
} }
} }
func TestMergePullRequestFn(t *testing.T) { func Test_mergePullRequestFn(t *testing.T) {
const ( const (
owner = "octo" owner = "octo"
repo = "demo" 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 { if err != nil {
t.Fatalf("MergePullRequestFn() error = %v", err) t.Fatalf("mergePullRequestFn() error = %v", err)
} }
mu.Lock() mu.Lock()
@@ -254,7 +254,7 @@ func TestMergePullRequestFn(t *testing.T) {
} }
} }
func TestGetPullRequestDiffFn(t *testing.T) { func Test_getPullRequestDiffFn(t *testing.T) {
const ( const (
owner = "octo" owner = "octo"
repo = "demo" 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 { if err != nil {
t.Fatalf("GetPullRequestDiffFn() error = %v", err) t.Fatalf("getPullRequestDiffFn() error = %v", err)
} }
select { select {

View File

@@ -15,7 +15,7 @@ import (
) )
const ( const (
ListRepoCommitsToolName = "list_repo_commits" ListRepoCommitsToolName = "list_commits"
) )
var ListRepoCommitsTool = mcp.NewTool( 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("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.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", 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() { func init() {
@@ -51,7 +51,7 @@ func ListRepoCommitsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
} }
pageSize, err := params.GetIndex(args, "page_size") pageSize, err := params.GetIndex(args, "perPage")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
} }

View File

@@ -19,11 +19,10 @@ import (
) )
const ( const (
GetFileToolName = "get_file_content" GetFileToolName = "get_file_contents"
GetDirToolName = "get_dir_content" GetDirToolName = "get_dir_contents"
CreateFileToolName = "create_file" CreateOrUpdateFileToolName = "create_or_update_file"
UpdateFileToolName = "update_file" DeleteFileToolName = "delete_file"
DeleteFileToolName = "delete_file"
) )
var ( var (
@@ -46,28 +45,17 @@ var (
mcp.WithString("filePath", mcp.Required(), mcp.Description("directory path")), mcp.WithString("filePath", mcp.Required(), mcp.Description("directory path")),
) )
CreateFileTool = mcp.NewTool( CreateOrUpdateFileTool = mcp.NewTool(
CreateFileToolName, CreateOrUpdateFileToolName,
mcp.WithDescription("Create file"), 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("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("filePath", mcp.Required(), mcp.Description("file path")), mcp.WithString("filePath", mcp.Required(), mcp.Description("file path")),
mcp.WithString("content", mcp.Required(), mcp.Description("file content")), mcp.WithString("content", mcp.Required(), mcp.Description("file content")),
mcp.WithString("message", mcp.Required(), mcp.Description("commit message")), mcp.WithString("message", mcp.Required(), mcp.Description("commit message")),
mcp.WithString("branch_name", mcp.Required(), mcp.Description("branch name")), mcp.WithString("branch_name", mcp.Required(), mcp.Description("branch name")),
mcp.WithString("new_branch_name", mcp.Description("new 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)")),
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")),
) )
DeleteFileTool = mcp.NewTool( DeleteFileTool = mcp.NewTool(
@@ -78,7 +66,7 @@ var (
mcp.WithString("filePath", mcp.Required(), mcp.Description("file path")), mcp.WithString("filePath", mcp.Required(), mcp.Description("file path")),
mcp.WithString("message", mcp.Required(), mcp.Description("commit message")), mcp.WithString("message", mcp.Required(), mcp.Description("commit message")),
mcp.WithString("branch_name", mcp.Required(), mcp.Description("branch name")), 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, Handler: GetDirContentFn,
}) })
Tool.RegisterWrite(server.ServerTool{ Tool.RegisterWrite(server.ServerTool{
Tool: CreateFileTool, Tool: CreateOrUpdateFileTool,
Handler: CreateFileFn, Handler: CreateOrUpdateFileFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: UpdateFileTool,
Handler: UpdateFileFn,
}) })
Tool.RegisterWrite(server.ServerTool{ Tool.RegisterWrite(server.ServerTool{
Tool: DeleteFileTool, Tool: DeleteFileTool,
@@ -160,7 +144,7 @@ func GetFileContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
// remove the last blank line if exists // remove the last blank line if exists
// git does not consider the last line as a new line // 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] contentLines = contentLines[:len(contentLines)-1]
} }
@@ -201,8 +185,8 @@ func GetDirContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
return to.TextResult(slimDirEntries(content)) return to.TextResult(slimDirEntries(content))
} }
func CreateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func CreateOrUpdateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateFileFn") log.Debugf("Called CreateOrUpdateFileFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {
@@ -219,6 +203,31 @@ func CreateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
content, _ := args["content"].(string) content, _ := args["content"].(string)
message, _ := args["message"].(string) message, _ := args["message"].(string)
branchName, _ := args["branch_name"].(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{ opt := gitea_sdk.CreateFileOptions{
Content: base64.StdEncoding.EncodeToString([]byte(content)), Content: base64.StdEncoding.EncodeToString([]byte(content)),
FileOptions: gitea_sdk.FileOptions{ FileOptions: gitea_sdk.FileOptions{
@@ -226,10 +235,8 @@ func CreateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
BranchName: branchName, BranchName: branchName,
}, },
} }
if newBranch, ok := args["new_branch_name"].(string); ok && newBranch != "" {
client, err := gitea.ClientFromContext(ctx) opt.NewBranchName = newBranch
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
_, _, err = client.CreateFile(owner, repo, filePath, opt) _, _, err = client.CreateFile(owner, repo, filePath, opt)
if err != nil { if err != nil {
@@ -238,48 +245,6 @@ func CreateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
return to.TextResult("Create file success") 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) { func DeleteFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteFileFn") log.Debugf("Called DeleteFileFn")
args := req.GetArguments() args := req.GetArguments()

View File

@@ -67,7 +67,7 @@ var (
mcp.WithBoolean("is_draft", mcp.Description("Whether the release is draft"), mcp.DefaultBool(false)), 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.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("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) pIsPreRelease = new(isPreRelease)
} }
page := params.GetOptionalInt(args, "page", 1) page := params.GetOptionalInt(args, "page", 1)
pageSize := params.GetOptionalInt(args, "pageSize", 20) pageSize := params.GetOptionalInt(args, "perPage", 20)
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {

View File

@@ -53,7 +53,7 @@ var (
ListMyReposToolName, ListMyReposToolName,
mcp.WithDescription("List my repositories"), mcp.WithDescription("List my repositories"),
mcp.WithNumber("page", mcp.Required(), mcp.Description("Page number"), mcp.DefaultNumber(1), mcp.Min(1)), 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) { func CreateRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateRepoFn") log.Debugf("Called CreateRepoFn")
args := req.GetArguments() args := req.GetArguments()

View File

@@ -54,7 +54,7 @@ var (
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)), 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) return to.ErrorResult(err)
} }
page := params.GetOptionalInt(args, "page", 1) page := params.GetOptionalInt(args, "page", 1)
pageSize := params.GetOptionalInt(args, "pageSize", 20) pageSize := params.GetOptionalInt(args, "perPage", 20)
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {

View File

@@ -29,7 +29,7 @@ var (
mcp.WithDescription("search users"), mcp.WithDescription("search users"),
mcp.WithString("keyword", mcp.Required(), mcp.Description("Keyword")), mcp.WithString("keyword", mcp.Required(), mcp.Description("Keyword")),
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)), 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( SearOrgTeamsTool = mcp.NewTool(
@@ -39,7 +39,7 @@ var (
mcp.WithString("query", mcp.Required(), mcp.Description("search organization teams")), mcp.WithString("query", mcp.Required(), mcp.Description("search organization teams")),
mcp.WithBoolean("includeDescription", mcp.Description("include description?")), mcp.WithBoolean("includeDescription", mcp.Description("include description?")),
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)), 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( SearchReposTool = mcp.NewTool(
@@ -54,7 +54,7 @@ var (
mcp.WithString("sort", mcp.Description("Sort")), mcp.WithString("sort", mcp.Description("Sort")),
mcp.WithString("order", mcp.Description("Order")), mcp.WithString("order", mcp.Description("Order")),
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)), 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)),
) )
) )

View File

@@ -19,114 +19,83 @@ import (
var Tool = tool.New() var Tool = tool.New()
const ( const (
// Stopwatch tools TimetrackingReadToolName = "timetracking_read"
StartStopwatchToolName = "start_stopwatch" TimetrackingWriteToolName = "timetracking_write"
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"
) )
var ( var (
// Stopwatch tools TimetrackingReadTool = mcp.NewTool(
StartStopwatchTool = mcp.NewTool( TimetrackingReadToolName,
StartStopwatchToolName, 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.WithDescription("Start a stopwatch on an issue to track time spent"), 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.Required(), mcp.Description("repository owner")), mcp.WithString("owner", mcp.Description("repository owner (required for 'list_issue_times', 'list_repo_times')")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("repo", mcp.Description("repository name (required for 'list_issue_times', 'list_repo_times')")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), mcp.WithNumber("index", mcp.Description("issue index (required for 'list_issue_times')")),
)
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")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), 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( TimetrackingWriteTool = mcp.NewTool(
AddTrackedTimeToolName, TimetrackingWriteToolName,
mcp.WithDescription("Manually add tracked time to an issue"), mcp.WithDescription("Manage time tracking: stopwatches and tracked time entries."),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("start_stopwatch", "stop_stopwatch", "delete_stopwatch", "add_time", "delete_time")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("owner", mcp.Description("repository owner (required for all methods)")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), mcp.WithString("repo", mcp.Description("repository name (required for all methods)")),
mcp.WithNumber("time", mcp.Required(), mcp.Description("time to add in seconds")), 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')")),
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"),
) )
) )
func init() { func init() {
// Stopwatch tools Tool.RegisterRead(server.ServerTool{Tool: TimetrackingReadTool, Handler: readFn})
Tool.RegisterWrite(server.ServerTool{Tool: StartStopwatchTool, Handler: StartStopwatchFn}) Tool.RegisterWrite(server.ServerTool{Tool: TimetrackingWriteTool, Handler: writeFn})
Tool.RegisterWrite(server.ServerTool{Tool: StopStopwatchTool, Handler: StopStopwatchFn}) }
Tool.RegisterWrite(server.ServerTool{Tool: DeleteStopwatchTool, Handler: DeleteStopwatchFn})
Tool.RegisterRead(server.ServerTool{Tool: GetMyStopwatchesTool, Handler: GetMyStopwatchesFn})
// Tracked time tools func readFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
Tool.RegisterRead(server.ServerTool{Tool: ListTrackedTimesTool, Handler: ListTrackedTimesFn}) method, err := params.GetString(req.GetArguments(), "method")
Tool.RegisterWrite(server.ServerTool{Tool: AddTrackedTimeTool, Handler: AddTrackedTimeFn}) if err != nil {
Tool.RegisterWrite(server.ServerTool{Tool: DeleteTrackedTimeTool, Handler: DeleteTrackedTimeFn}) return to.ErrorResult(err)
Tool.RegisterRead(server.ServerTool{Tool: ListRepoTimesTool, Handler: ListRepoTimesFn}) }
Tool.RegisterRead(server.ServerTool{Tool: GetMyTimesTool, Handler: GetMyTimesFn}) 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 // Stopwatch handler functions
func StartStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func startStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called StartStopwatchFn") log.Debugf("Called startStopwatchFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) 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)) 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) { func stopStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called StopStopwatchFn") log.Debugf("Called stopStopwatchFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) 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)) 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) { func deleteStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteStopwatchFn") log.Debugf("Called deleteStopwatchFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) 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)) 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) { func getMyStopwatchesFn(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetMyStopwatchesFn") log.Debugf("Called getMyStopwatchesFn")
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) 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 // Tracked time handler functions
func ListTrackedTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func listTrackedTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListTrackedTimesFn") log.Debugf("Called listTrackedTimesFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -253,8 +222,8 @@ func ListTrackedTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
return to.TextResult(slimTrackedTimes(times)) return to.TextResult(slimTrackedTimes(times))
} }
func AddTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func addTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AddTrackedTimeFn") log.Debugf("Called addTrackedTimeFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -285,8 +254,8 @@ func AddTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
return to.TextResult(slimTrackedTime(trackedTime)) return to.TextResult(slimTrackedTime(trackedTime))
} }
func DeleteTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func deleteTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteTrackedTimeFn") log.Debugf("Called deleteTrackedTimeFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) 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)) 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) { func listRepoTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoTimesFn") log.Debugf("Called listRepoTimesFn")
owner, err := params.GetString(req.GetArguments(), "owner") owner, err := params.GetString(req.GetArguments(), "owner")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
@@ -346,8 +315,8 @@ func ListRepoTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
return to.TextResult(slimTrackedTimes(times)) return to.TextResult(slimTrackedTimes(times))
} }
func GetMyTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func getMyTimesFn(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetMyTimesFn") log.Debugf("Called getMyTimesFn")
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))

View File

@@ -16,8 +16,8 @@ import (
) )
const ( const (
// GetMyUserInfoToolName is the unique tool name used for MCP registration and lookup of the get_my_user_info command. // GetMyUserInfoToolName is the unique tool name used for MCP registration and lookup of the get_me command.
GetMyUserInfoToolName = "get_my_user_info" GetMyUserInfoToolName = "get_me"
// GetUserOrgsToolName is the unique tool name used for MCP registration and lookup of the get_user_orgs command. // GetUserOrgsToolName is the unique tool name used for MCP registration and lookup of the get_user_orgs command.
GetUserOrgsToolName = "get_user_orgs" GetUserOrgsToolName = "get_user_orgs"
@@ -39,12 +39,12 @@ var (
) )
// GetUserOrgsTool is the MCP tool for listing organizations for the authenticated user. // 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( GetUserOrgsTool = mcp.NewTool(
GetUserOrgsToolName, GetUserOrgsToolName,
mcp.WithDescription("Get organizations associated with the authenticated user"), mcp.WithDescription("Get organizations associated with the authenticated user"),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(defaultPage)), 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. // Logs invocation, fetches current user info from gitea, wraps result for MCP.
func GetUserInfoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func GetUserInfoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("[User] Called GetUserInfoFn") log.Debugf("[User] Called GetUserInfoFn")

View File

@@ -18,97 +18,80 @@ import (
var Tool = tool.New() var Tool = tool.New()
const ( const (
ListWikiPagesToolName = "list_wiki_pages" WikiReadToolName = "wiki_read"
GetWikiPageToolName = "get_wiki_page" WikiWriteToolName = "wiki_write"
GetWikiRevisionsToolName = "get_wiki_revisions"
CreateWikiPageToolName = "create_wiki_page"
UpdateWikiPageToolName = "update_wiki_page"
DeleteWikiPageToolName = "delete_wiki_page"
) )
var ( var (
ListWikiPagesTool = mcp.NewTool( WikiReadTool = mcp.NewTool(
ListWikiPagesToolName, WikiReadToolName,
mcp.WithDescription("List all wiki pages in a repository"), 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("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), 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( WikiWriteTool = mcp.NewTool(
GetWikiPageToolName, WikiWriteToolName,
mcp.WithDescription("Get a wiki page content and metadata"), 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("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("pageName", mcp.Required(), mcp.Description("wiki page name")), 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')")),
GetWikiRevisionsTool = mcp.NewTool( mcp.WithString("message", mcp.Description("commit message")),
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")),
) )
) )
func init() { func init() {
Tool.RegisterRead(server.ServerTool{ Tool.RegisterRead(server.ServerTool{
Tool: ListWikiPagesTool, Tool: WikiReadTool,
Handler: ListWikiPagesFn, Handler: wikiReadFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: GetWikiPageTool,
Handler: GetWikiPageFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: GetWikiRevisionsTool,
Handler: GetWikiRevisionsFn,
}) })
Tool.RegisterWrite(server.ServerTool{ Tool.RegisterWrite(server.ServerTool{
Tool: CreateWikiPageTool, Tool: WikiWriteTool,
Handler: CreateWikiPageFn, Handler: wikiWriteFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: UpdateWikiPageTool,
Handler: UpdateWikiPageFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: DeleteWikiPageTool,
Handler: DeleteWikiPageFn,
}) })
} }
func ListWikiPagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func wikiReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListWikiPagesFn") 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() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {
@@ -119,9 +102,8 @@ func ListWikiPagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
return to.ErrorResult(err) return to.ErrorResult(err)
} }
// Use direct HTTP request because SDK does not support yet wikis
var result any 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 { if err != nil {
return to.ErrorResult(fmt.Errorf("list wiki pages err: %v", err)) 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) return to.TextResult(result)
} }
func GetWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func getWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetWikiPageFn") log.Debugf("Called getWikiPageFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {
@@ -146,7 +128,7 @@ func GetWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR
} }
var result any 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 { if err != nil {
return to.ErrorResult(fmt.Errorf("get wiki page err: %v", err)) 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) return to.TextResult(result)
} }
func GetWikiRevisionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func getWikiRevisionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetWikiRevisionsFn") log.Debugf("Called getWikiRevisionsFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {
@@ -171,7 +153,7 @@ func GetWikiRevisionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
} }
var result any 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 { if err != nil {
return to.ErrorResult(fmt.Errorf("get wiki revisions err: %v", err)) 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) return to.TextResult(result)
} }
func CreateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func createWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateWikiPageFn") log.Debugf("Called createWikiPageFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {
@@ -211,7 +193,7 @@ func CreateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
} }
var result any 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 { if err != nil {
return to.ErrorResult(fmt.Errorf("create wiki page err: %v", err)) 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) return to.TextResult(result)
} }
func UpdateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func updateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called UpdateWikiPageFn") log.Debugf("Called updateWikiPageFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { 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 != "" { if title, ok := args["title"].(string); ok && title != "" {
requestBody["title"] = title requestBody["title"] = title
} else { } else {
// Utiliser pageName comme fallback pour éviter "unnamed"
requestBody["title"] = pageName requestBody["title"] = pageName
} }
@@ -258,7 +239,7 @@ func UpdateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
} }
var result any 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 { if err != nil {
return to.ErrorResult(fmt.Errorf("update wiki page err: %v", err)) 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) return to.TextResult(result)
} }
func DeleteWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func deleteWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteWikiPageFn") log.Debugf("Called deleteWikiPageFn")
args := req.GetArguments() args := req.GetArguments()
owner, err := params.GetString(args, "owner") owner, err := params.GetString(args, "owner")
if err != nil { if err != nil {
@@ -282,7 +263,7 @@ func DeleteWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
return to.ErrorResult(err) 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 { if err != nil {
return to.ErrorResult(fmt.Errorf("delete wiki page err: %v", err)) return to.ErrorResult(fmt.Errorf("delete wiki page err: %v", err))
} }

View File

@@ -41,9 +41,9 @@ func GetStringSlice(args map[string]any, key string) []string {
return out 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) { 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 // 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) 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. // GetOptionalInt extracts an optional integer parameter from MCP tool arguments.
// Returns defaultVal if the key is missing or the value cannot be parsed. // Returns defaultVal if the key is missing or the value cannot be parsed.
// Accepts both float64 (JSON number) and string representations. // Accepts both float64 (JSON number) and string representations.