diff --git a/operation/actions/config.go b/operation/actions/config.go index b8020b5..d707f85 100644 --- a/operation/actions/config.go +++ b/operation/actions/config.go @@ -8,6 +8,7 @@ import ( "strconv" "time" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" @@ -48,6 +49,7 @@ var ( ActionsConfigReadTool = mcp.NewTool( ActionsConfigReadToolName, mcp.WithDescription("Read Actions secrets and variables configuration."), + mcp.WithToolAnnotation(annotation.ReadOnly("Read Actions secrets and variables")), 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)")), @@ -60,6 +62,7 @@ var ( ActionsConfigWriteTool = mcp.NewTool( ActionsConfigWriteToolName, mcp.WithDescription("Manage Actions secrets and variables: create, update, or delete."), + mcp.WithToolAnnotation(annotation.Destructive("Manage Actions secrets and variables")), 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)")), diff --git a/operation/actions/runs.go b/operation/actions/runs.go index 021ec3d..6e9f9f8 100644 --- a/operation/actions/runs.go +++ b/operation/actions/runs.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strconv" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" @@ -28,6 +29,7 @@ var ( ActionsRunReadTool = mcp.NewTool( ActionsRunReadToolName, mcp.WithDescription("Read Actions workflow, run, and job data. Use method 'list_workflows'/'get_workflow' for workflows, 'list_runs'/'get_run' for runs, 'list_jobs'/'list_run_jobs' for jobs, 'get_job_log_preview'/'download_job_log' for logs."), + mcp.WithToolAnnotation(annotation.ReadOnly("Read Actions workflow, run, and job data")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list_workflows", "get_workflow", "list_runs", "get_run", "list_jobs", "list_run_jobs", "get_job_log_preview", "download_job_log")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), @@ -45,6 +47,7 @@ var ( ActionsRunWriteTool = mcp.NewTool( ActionsRunWriteToolName, mcp.WithDescription("Trigger, cancel, or rerun Actions workflows."), + mcp.WithToolAnnotation(annotation.Write("Trigger, cancel, or rerun Actions workflows")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("dispatch_workflow", "cancel_run", "rerun_run")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), diff --git a/operation/issue/issue.go b/operation/issue/issue.go index 0920808..6e5c38b 100644 --- a/operation/issue/issue.go +++ b/operation/issue/issue.go @@ -5,6 +5,7 @@ import ( "fmt" "net/url" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" @@ -40,6 +41,7 @@ var ( ListRepoIssuesTool = mcp.NewTool( ListRepoIssuesToolName, mcp.WithDescription("List repository issues"), + mcp.WithToolAnnotation(annotation.ReadOnly("List repository issues")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("state", mcp.Description("issue state"), mcp.DefaultString("all")), @@ -53,6 +55,7 @@ var ( IssueReadTool = mcp.NewTool( IssueReadToolName, mcp.WithDescription("Get information about a specific issue. Use method 'get' for issue details, 'get_comments' for issue comments, 'get_labels' for issue labels."), + mcp.WithToolAnnotation(annotation.ReadOnly("Read issue details")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("get", "get_comments", "get_labels")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), @@ -62,6 +65,7 @@ var ( IssueWriteTool = mcp.NewTool( IssueWriteToolName, mcp.WithDescription("Create or update issues and comments, manage labels. Use method 'create' to create an issue, 'update' to edit, 'add_comment'/'edit_comment' for comments, 'add_labels'/'remove_label'/'replace_labels'/'clear_labels' for label management."), + mcp.WithToolAnnotation(annotation.Write("Create or update issues, comments, and labels")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create", "update", "add_comment", "edit_comment", "add_labels", "remove_label", "replace_labels", "clear_labels")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), diff --git a/operation/label/label.go b/operation/label/label.go index c92f6b1..ebae338 100644 --- a/operation/label/label.go +++ b/operation/label/label.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" @@ -26,6 +27,7 @@ var ( LabelReadTool = mcp.NewTool( LabelReadToolName, mcp.WithDescription("Read label information. Use method 'list_repo_labels' to list repository labels, 'get_repo_label' to get a specific repo label, 'list_org_labels' to list organization labels."), + mcp.WithToolAnnotation(annotation.ReadOnly("Read labels")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list_repo_labels", "get_repo_label", "list_org_labels")), mcp.WithString("owner", mcp.Description("repository owner (required for repo methods)")), mcp.WithString("repo", mcp.Description("repository name (required for repo methods)")), @@ -38,6 +40,7 @@ var ( LabelWriteTool = mcp.NewTool( LabelWriteToolName, mcp.WithDescription("Create, edit, or delete labels for repositories or organizations."), + mcp.WithToolAnnotation(annotation.Destructive("Create, update, or delete labels")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create_repo_label", "edit_repo_label", "delete_repo_label", "create_org_label", "edit_org_label", "delete_org_label")), mcp.WithString("owner", mcp.Description("repository owner (required for repo methods)")), mcp.WithString("repo", mcp.Description("repository name (required for repo methods)")), diff --git a/operation/milestone/milestone.go b/operation/milestone/milestone.go index af7fbe0..b86d701 100644 --- a/operation/milestone/milestone.go +++ b/operation/milestone/milestone.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" @@ -26,6 +27,7 @@ var ( MilestoneReadTool = mcp.NewTool( MilestoneReadToolName, mcp.WithDescription("Read milestone information. Use method 'get' to get a specific milestone, 'list' to list milestones."), + mcp.WithToolAnnotation(annotation.ReadOnly("Read milestones")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("get", "list")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), @@ -39,6 +41,7 @@ var ( MilestoneWriteTool = mcp.NewTool( MilestoneWriteToolName, mcp.WithDescription("Create, edit, or delete milestones."), + mcp.WithToolAnnotation(annotation.Destructive("Create, update, or delete milestones")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create", "edit", "delete")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), diff --git a/operation/notification/notification.go b/operation/notification/notification.go index 138c67d..ef0383d 100644 --- a/operation/notification/notification.go +++ b/operation/notification/notification.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" @@ -27,6 +28,7 @@ var ( NotificationReadTool = mcp.NewTool( NotificationReadToolName, mcp.WithDescription("Get notifications. Use method 'list' to list notifications (optionally scoped to a repo), 'get' to get a single notification thread by ID."), + mcp.WithToolAnnotation(annotation.ReadOnly("Read notifications")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list", "get")), mcp.WithString("owner", mcp.Description("repository owner (for 'list' to scope to a repo)")), mcp.WithString("repo", mcp.Description("repository name (for 'list' to scope to a repo)")), @@ -42,6 +44,7 @@ var ( NotificationWriteTool = mcp.NewTool( NotificationWriteToolName, mcp.WithDescription("Manage notifications. Use method 'mark_read' to mark a single notification as read, 'mark_all_read' to mark all notifications as read (optionally scoped to a repo)."), + mcp.WithToolAnnotation(annotation.Write("Manage notifications")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("mark_read", "mark_all_read")), mcp.WithNumber("id", mcp.Description("notification thread ID (required for 'mark_read')")), mcp.WithString("owner", mcp.Description("repository owner (for 'mark_all_read' to scope to a repo)")), diff --git a/operation/pull/pull.go b/operation/pull/pull.go index 50e58ca..949e376 100644 --- a/operation/pull/pull.go +++ b/operation/pull/pull.go @@ -6,6 +6,7 @@ import ( "net/url" "strings" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" @@ -30,6 +31,7 @@ var ( ListRepoPullRequestsTool = mcp.NewTool( ListRepoPullRequestsToolName, mcp.WithDescription("List repository pull requests"), + mcp.WithToolAnnotation(annotation.ReadOnly("List pull requests")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("state", mcp.Description("state"), mcp.Enum("open", "closed", "all"), mcp.DefaultString("all")), @@ -42,6 +44,7 @@ var ( PullRequestReadTool = mcp.NewTool( PullRequestReadToolName, mcp.WithDescription("Get pull request information. Use method 'get' for PR details, 'get_diff' for diff, 'get_reviews'/'get_review'/'get_review_comments' for review data."), + mcp.WithToolAnnotation(annotation.ReadOnly("Read pull request details")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("get", "get_diff", "get_reviews", "get_review", "get_review_comments")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), @@ -54,8 +57,9 @@ var ( PullRequestWriteTool = mcp.NewTool( PullRequestWriteToolName, - mcp.WithDescription("Create, update, or merge pull requests, manage reviewers."), - mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create", "update", "merge", "add_reviewers", "remove_reviewers")), + mcp.WithDescription("Create, update, close, reopen, or merge pull requests, manage reviewers."), + mcp.WithToolAnnotation(annotation.Write("Create, update, close, reopen, or merge pull requests")), + mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create", "update", "close", "reopen", "merge", "add_reviewers", "remove_reviewers")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithNumber("index", mcp.Description("pull request index (required for all methods except 'create')")), @@ -85,6 +89,7 @@ var ( PullRequestReviewWriteTool = mcp.NewTool( PullRequestReviewWriteToolName, mcp.WithDescription("Manage pull request reviews: create, submit, delete, or dismiss."), + mcp.WithToolAnnotation(annotation.Write("Submit a pull request review")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create", "submit", "delete", "dismiss")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), @@ -156,6 +161,10 @@ func pullRequestWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return createPullRequestFn(ctx, req) case "update": return editPullRequestFn(ctx, req) + case "close": + return closePullRequestFn(ctx, req) + case "reopen": + return reopenPullRequestFn(ctx, req) case "merge": return mergePullRequestFn(ctx, req) case "add_reviewers": @@ -167,6 +176,66 @@ func pullRequestWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call } } +func closePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + 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)) + } + + state := gitea_sdk.StateClosed + pr, _, err := client.EditPullRequest(owner, repo, index, gitea_sdk.EditPullRequestOption{ + State: &state, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("close %v/%v/pr/%v err: %v", owner, repo, index, err)) + } + + return to.TextResult(slimPullRequest(pr)) +} + +func reopenPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + 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)) + } + + state := gitea_sdk.StateOpen + pr, _, err := client.EditPullRequest(owner, repo, index, gitea_sdk.EditPullRequestOption{ + State: &state, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("reopen %v/%v/pr/%v err: %v", owner, repo, index, err)) + } + + return to.TextResult(slimPullRequest(pr)) +} + func pullRequestReviewWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { method, err := params.GetString(req.GetArguments(), "method") if err != nil { diff --git a/operation/pull/pull_test.go b/operation/pull/pull_test.go index 4195de0..7da727a 100644 --- a/operation/pull/pull_test.go +++ b/operation/pull/pull_test.go @@ -911,3 +911,131 @@ func Test_getPullRequestByIndexFn_assetsFailureNonFatal(t *testing.T) { t.Fatalf("expected PR body preserved when assets fail, got: %s", body) } } + +func Test_closePullRequestFn(t *testing.T) { + const ( + owner = "octo" + repo = "demo" + index = 7 + ) + + var gotBody map[string]any + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v1/version": + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"version":"1.12.0"}`)) + case fmt.Sprintf("/api/v1/repos/%s/%s", owner, repo): + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"private":false}`)) + case fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner, repo, index): + if r.Method != http.MethodPatch { + t.Errorf("expected PATCH method, got %s", r.Method) + } + _ = json.NewDecoder(r.Body).Decode(&gotBody) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(fmt.Appendf(nil, `{"index":%d,"title":"Fix bug","state":"closed","head":{"ref":"fix-branch"},"base":{"ref":"main"}}`, index)) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusNotFound) + } + }) + + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + + origHost := flag.Host + origToken := flag.Token + flag.Host = server.URL + flag.Token = "test-token" + t.Cleanup(func() { flag.Host = origHost; flag.Token = origToken }) + + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Arguments: map[string]any{ + "method": "close", + "owner": owner, + "repo": repo, + "index": float64(index), + }, + }, + } + + result, err := closePullRequestFn(context.Background(), req) + if err != nil { + t.Fatalf("closePullRequestFn() error = %v", err) + } + + if gotBody["state"] != "closed" { + t.Errorf("expected state=closed, got %v", gotBody["state"]) + } + + if len(result.Content) == 0 { + t.Fatalf("expected content in result") + } +} + +func Test_reopenPullRequestFn(t *testing.T) { + const ( + owner = "octo" + repo = "demo" + index = 7 + ) + + var gotBody map[string]any + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v1/version": + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"version":"1.12.0"}`)) + case fmt.Sprintf("/api/v1/repos/%s/%s", owner, repo): + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"private":false}`)) + case fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner, repo, index): + if r.Method != http.MethodPatch { + t.Errorf("expected PATCH method, got %s", r.Method) + } + _ = json.NewDecoder(r.Body).Decode(&gotBody) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(fmt.Appendf(nil, `{"index":%d,"title":"Fix bug","state":"open","head":{"ref":"fix-branch"},"base":{"ref":"main"}}`, index)) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusNotFound) + } + }) + + server := httptest.NewServer(handler) + t.Cleanup(server.Close) + + origHost := flag.Host + origToken := flag.Token + flag.Host = server.URL + flag.Token = "test-token" + t.Cleanup(func() { flag.Host = origHost; flag.Token = origToken }) + + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Arguments: map[string]any{ + "method": "reopen", + "owner": owner, + "repo": repo, + "index": float64(index), + }, + }, + } + + result, err := reopenPullRequestFn(context.Background(), req) + if err != nil { + t.Fatalf("reopenPullRequestFn() error = %v", err) + } + + if gotBody["state"] != "open" { + t.Errorf("expected state=open, got %v", gotBody["state"]) + } + + if len(result.Content) == 0 { + t.Fatalf("expected content in result") + } +} diff --git a/operation/repo/branch.go b/operation/repo/branch.go index b702d7a..664b13f 100644 --- a/operation/repo/branch.go +++ b/operation/repo/branch.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" @@ -24,6 +25,7 @@ var ( CreateBranchTool = mcp.NewTool( CreateBranchToolName, mcp.WithDescription("Create branch"), + mcp.WithToolAnnotation(annotation.Write("Create a new branch")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("branch", mcp.Required(), mcp.Description("Name of the branch to create")), @@ -33,6 +35,7 @@ var ( DeleteBranchTool = mcp.NewTool( DeleteBranchToolName, mcp.WithDescription("Delete branch"), + mcp.WithToolAnnotation(annotation.Destructive("Delete a branch")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("branch", mcp.Required(), mcp.Description("Name of the branch to delete")), @@ -41,6 +44,7 @@ var ( ListBranchesTool = mcp.NewTool( ListBranchesToolName, mcp.WithDescription("List branches"), + mcp.WithToolAnnotation(annotation.ReadOnly("List repository branches")), 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)), diff --git a/operation/repo/commit.go b/operation/repo/commit.go index 8929b3d..da313ab 100644 --- a/operation/repo/commit.go +++ b/operation/repo/commit.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" @@ -23,6 +24,7 @@ var ( ListRepoCommitsTool = mcp.NewTool( ListRepoCommitsToolName, mcp.WithDescription("List repository commits"), + mcp.WithToolAnnotation(annotation.ReadOnly("List repository commits")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("sha", mcp.Description("SHA or branch to start listing commits from")), @@ -34,6 +36,7 @@ var ( GetCommitTool = mcp.NewTool( GetCommitToolName, mcp.WithDescription("Get details of a specific commit"), + mcp.WithToolAnnotation(annotation.ReadOnly("Get commit details")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("sha", mcp.Required(), mcp.Description("commit SHA")), diff --git a/operation/repo/file.go b/operation/repo/file.go index a133c40..6193d36 100644 --- a/operation/repo/file.go +++ b/operation/repo/file.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" @@ -29,6 +30,7 @@ var ( GetFileContentTool = mcp.NewTool( GetFileToolName, mcp.WithDescription("Get file Content and Metadata"), + mcp.WithToolAnnotation(annotation.ReadOnly("Get file content")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("ref", mcp.Required(), mcp.Description("ref can be branch/tag/commit")), @@ -39,6 +41,7 @@ var ( GetDirContentTool = mcp.NewTool( GetDirToolName, mcp.WithDescription("Get a list of entries in a directory"), + mcp.WithToolAnnotation(annotation.ReadOnly("Get directory contents")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("ref", mcp.Required(), mcp.Description("ref can be branch/tag/commit")), @@ -48,6 +51,7 @@ var ( CreateOrUpdateFileTool = mcp.NewTool( CreateOrUpdateFileToolName, mcp.WithDescription("Create or update a file. If sha is provided, updates the existing file; otherwise creates a new file."), + mcp.WithToolAnnotation(annotation.Write("Create or update a 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")), @@ -61,6 +65,7 @@ var ( DeleteFileTool = mcp.NewTool( DeleteFileToolName, mcp.WithDescription("Delete file"), + mcp.WithToolAnnotation(annotation.Destructive("Delete a 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")), diff --git a/operation/repo/release.go b/operation/repo/release.go index e93dcc8..432e531 100644 --- a/operation/repo/release.go +++ b/operation/repo/release.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" @@ -26,6 +27,7 @@ var ( CreateReleaseTool = mcp.NewTool( CreateReleaseToolName, mcp.WithDescription("Create release"), + mcp.WithToolAnnotation(annotation.Write("Create a release")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("tag_name", mcp.Required(), mcp.Description("tag name")), @@ -39,6 +41,7 @@ var ( DeleteReleaseTool = mcp.NewTool( DeleteReleaseToolName, mcp.WithDescription("Delete release"), + mcp.WithToolAnnotation(annotation.Destructive("Delete a release")), 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("release id")), @@ -47,6 +50,7 @@ var ( GetReleaseTool = mcp.NewTool( GetReleaseToolName, mcp.WithDescription("Get release"), + mcp.WithToolAnnotation(annotation.ReadOnly("Get release details")), 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("release id")), @@ -55,6 +59,7 @@ var ( GetLatestReleaseTool = mcp.NewTool( GetLatestReleaseToolName, mcp.WithDescription("Get latest release"), + mcp.WithToolAnnotation(annotation.ReadOnly("Get latest release")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), ) @@ -62,6 +67,7 @@ var ( ListReleasesTool = mcp.NewTool( ListReleasesToolName, mcp.WithDescription("List releases"), + mcp.WithToolAnnotation(annotation.ReadOnly("List releases")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithBoolean("is_draft", mcp.Description("Whether the release is draft"), mcp.DefaultBool(false)), diff --git a/operation/repo/repo.go b/operation/repo/repo.go index 9140803..c22c960 100644 --- a/operation/repo/repo.go +++ b/operation/repo/repo.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" @@ -29,6 +30,7 @@ var ( CreateRepoTool = mcp.NewTool( CreateRepoToolName, mcp.WithDescription("Create repository in personal account or organization"), + mcp.WithToolAnnotation(annotation.Write("Create a new repository")), mcp.WithString("name", mcp.Required(), mcp.Description("Name of the repository to create")), mcp.WithString("description", mcp.Description("Description of the repository to create")), mcp.WithBoolean("private", mcp.Description("Whether the repository is private")), @@ -47,6 +49,7 @@ var ( ForkRepoTool = mcp.NewTool( ForkRepoToolName, mcp.WithDescription("Fork repository"), + mcp.WithToolAnnotation(annotation.Write("Fork a repository")), mcp.WithString("user", mcp.Required(), mcp.Description("User name of the repository to fork")), mcp.WithString("repo", mcp.Required(), mcp.Description("Repository name to fork")), mcp.WithString("organization", mcp.Description("Organization name to fork")), @@ -56,6 +59,7 @@ var ( ListMyReposTool = mcp.NewTool( ListMyReposToolName, mcp.WithDescription("List my repositories"), + mcp.WithToolAnnotation(annotation.ReadOnly("List my repositories")), mcp.WithNumber("page", mcp.Required(), mcp.Description("Page number"), mcp.DefaultNumber(1), mcp.Min(1)), mcp.WithNumber("perPage", mcp.Required(), mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(30), mcp.Min(1)), ) @@ -63,6 +67,7 @@ var ( ListOrgReposTool = mcp.NewTool( ListOrgReposToolName, mcp.WithDescription("List repositories of an organization"), + mcp.WithToolAnnotation(annotation.ReadOnly("List organization repositories")), mcp.WithString("org", mcp.Required(), mcp.Description("Organization name")), mcp.WithNumber("page", mcp.Required(), mcp.Description("Page number"), mcp.DefaultNumber(1), mcp.Min(1)), mcp.WithNumber("pageSize", mcp.Required(), mcp.Description("Page size number"), mcp.DefaultNumber(100), mcp.Min(1)), diff --git a/operation/repo/tag.go b/operation/repo/tag.go index 6338fbd..67f6b7f 100644 --- a/operation/repo/tag.go +++ b/operation/repo/tag.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" @@ -25,6 +26,7 @@ var ( CreateTagTool = mcp.NewTool( CreateTagToolName, mcp.WithDescription("Create tag"), + mcp.WithToolAnnotation(annotation.Write("Create a tag")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("tag_name", mcp.Required(), mcp.Description("tag name")), @@ -35,6 +37,7 @@ var ( DeleteTagTool = mcp.NewTool( DeleteTagToolName, mcp.WithDescription("Delete tag"), + mcp.WithToolAnnotation(annotation.Destructive("Delete a tag")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("tag_name", mcp.Required(), mcp.Description("tag name")), @@ -43,6 +46,7 @@ var ( GetTagTool = mcp.NewTool( GetTagToolName, mcp.WithDescription("Get tag"), + mcp.WithToolAnnotation(annotation.ReadOnly("Get tag details")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("tag_name", mcp.Required(), mcp.Description("tag name")), @@ -51,6 +55,7 @@ var ( ListTagsTool = mcp.NewTool( ListTagsToolName, mcp.WithDescription("List tags"), + mcp.WithToolAnnotation(annotation.ReadOnly("List tags")), 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)), diff --git a/operation/repo/tree.go b/operation/repo/tree.go index 6b6e536..4c76360 100644 --- a/operation/repo/tree.go +++ b/operation/repo/tree.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" @@ -21,6 +22,7 @@ const ( var GetRepoTreeTool = mcp.NewTool( GetRepoTreeToolName, mcp.WithDescription("Get the file tree of a repository"), + mcp.WithToolAnnotation(annotation.ReadOnly("Get repository file tree")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("tree_sha", mcp.Required(), mcp.Description("SHA, branch name, or tag name")), diff --git a/operation/search/search.go b/operation/search/search.go index bdc8f88..c405861 100644 --- a/operation/search/search.go +++ b/operation/search/search.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" @@ -29,6 +30,7 @@ var ( SearchUsersTool = mcp.NewTool( SearchUsersToolName, mcp.WithDescription("search users"), + mcp.WithToolAnnotation(annotation.ReadOnly("Search users")), mcp.WithString("keyword", mcp.Required(), mcp.Description("Keyword")), mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)), mcp.WithNumber("perPage", mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(30)), @@ -37,6 +39,7 @@ var ( SearOrgTeamsTool = mcp.NewTool( SearchOrgTeamsToolName, mcp.WithDescription("search organization teams"), + mcp.WithToolAnnotation(annotation.ReadOnly("Search organization teams")), mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), mcp.WithString("query", mcp.Required(), mcp.Description("search organization teams")), mcp.WithBoolean("includeDescription", mcp.Description("include description?")), @@ -47,6 +50,7 @@ var ( SearchReposTool = mcp.NewTool( SearchReposToolName, mcp.WithDescription("search repos"), + mcp.WithToolAnnotation(annotation.ReadOnly("Search repositories")), mcp.WithString("keyword", mcp.Required(), mcp.Description("Keyword")), mcp.WithBoolean("keywordIsTopic", mcp.Description("KeywordIsTopic")), mcp.WithBoolean("keywordInDescription", mcp.Description("KeywordInDescription")), @@ -62,6 +66,7 @@ var ( SearchIssuesTool = mcp.NewTool( SearchIssuesToolName, mcp.WithDescription("Search for issues and pull requests across all accessible repositories"), + mcp.WithToolAnnotation(annotation.ReadOnly("Search issues")), mcp.WithString("query", mcp.Required(), mcp.Description("search keyword")), mcp.WithString("state", mcp.Description("filter by state: open, closed, all"), mcp.Enum("open", "closed", "all")), mcp.WithString("type", mcp.Description("filter by type: issues, pulls"), mcp.Enum("issues", "pulls")), diff --git a/operation/timetracking/timetracking.go b/operation/timetracking/timetracking.go index 68bbb97..9fca68f 100644 --- a/operation/timetracking/timetracking.go +++ b/operation/timetracking/timetracking.go @@ -5,6 +5,7 @@ import ( "context" "fmt" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" @@ -27,6 +28,7 @@ var ( TimetrackingReadTool = mcp.NewTool( TimetrackingReadToolName, mcp.WithDescription("Read time tracking data. Use method 'list_issue_times' for issue times, 'list_repo_times' for repository times, 'get_my_stopwatches' for active stopwatches, 'get_my_times' for all your tracked times."), + mcp.WithToolAnnotation(annotation.ReadOnly("Read tracked time")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list_issue_times", "list_repo_times", "get_my_stopwatches", "get_my_times")), mcp.WithString("owner", mcp.Description("repository owner (required for 'list_issue_times', 'list_repo_times')")), mcp.WithString("repo", mcp.Description("repository name (required for 'list_issue_times', 'list_repo_times')")), @@ -38,6 +40,7 @@ var ( TimetrackingWriteTool = mcp.NewTool( TimetrackingWriteToolName, mcp.WithDescription("Manage time tracking: stopwatches and tracked time entries."), + mcp.WithToolAnnotation(annotation.Write("Add or manage tracked time")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("start_stopwatch", "stop_stopwatch", "delete_stopwatch", "add_time", "delete_time")), mcp.WithString("owner", mcp.Description("repository owner (required for all methods)")), mcp.WithString("repo", mcp.Description("repository name (required for all methods)")), diff --git a/operation/user/user.go b/operation/user/user.go index 0271ece..b79b6dc 100644 --- a/operation/user/user.go +++ b/operation/user/user.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" @@ -36,6 +37,7 @@ var ( GetMyUserInfoTool = mcp.NewTool( GetMyUserInfoToolName, mcp.WithDescription("Get my user info"), + mcp.WithToolAnnotation(annotation.ReadOnly("Get current user information")), ) // GetUserOrgsTool is the MCP tool for listing organizations for the authenticated user. @@ -43,6 +45,7 @@ var ( GetUserOrgsTool = mcp.NewTool( GetUserOrgsToolName, mcp.WithDescription("Get organizations associated with the authenticated user"), + mcp.WithToolAnnotation(annotation.ReadOnly("Get user organizations")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(defaultPage)), mcp.WithNumber("perPage", mcp.Description("results per page (may be capped by the server's MAX_RESPONSE_ITEMS setting, default 50)"), mcp.DefaultNumber(defaultPageSize)), ) diff --git a/operation/version/version.go b/operation/version/version.go index 72907b5..7e8cbbb 100644 --- a/operation/version/version.go +++ b/operation/version/version.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/flag" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/to" @@ -22,6 +23,7 @@ const ( var GetGiteaMCPServerVersionTool = mcp.NewTool( GetGiteaMCPServerVersion, mcp.WithDescription("Get Gitea MCP Server Version"), + mcp.WithToolAnnotation(annotation.ReadOnly("Get server version")), ) func init() { diff --git a/operation/wiki/wiki.go b/operation/wiki/wiki.go index b019724..c28cf1a 100644 --- a/operation/wiki/wiki.go +++ b/operation/wiki/wiki.go @@ -6,6 +6,7 @@ import ( "fmt" "net/url" + "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/params" @@ -27,6 +28,7 @@ var ( WikiReadTool = mcp.NewTool( WikiReadToolName, mcp.WithDescription("Read wiki page information. Use method 'list' to list pages, 'get' to get page content, 'get_revisions' for revision history."), + mcp.WithToolAnnotation(annotation.ReadOnly("Read wiki pages")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list", "get", "get_revisions")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), @@ -36,6 +38,7 @@ var ( WikiWriteTool = mcp.NewTool( WikiWriteToolName, mcp.WithDescription("Create, update, or delete wiki pages."), + mcp.WithToolAnnotation(annotation.Destructive("Create, update, or delete wiki pages")), mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create", "update", "delete")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), diff --git a/pkg/annotation/annotation.go b/pkg/annotation/annotation.go new file mode 100644 index 0000000..fd9596d --- /dev/null +++ b/pkg/annotation/annotation.go @@ -0,0 +1,32 @@ +// Package annotation provides shared MCP tool annotation helpers. +package annotation + +import "github.com/mark3labs/mcp-go/mcp" + +// ReadOnly returns a ToolAnnotation for read-only tools. +func ReadOnly(title string) mcp.ToolAnnotation { + t := true + return mcp.ToolAnnotation{ + Title: title, + ReadOnlyHint: &t, + } +} + +// Write returns a ToolAnnotation for write tools. +func Write(title string) mcp.ToolAnnotation { + f := false + return mcp.ToolAnnotation{ + Title: title, + ReadOnlyHint: &f, + } +} + +// Destructive returns a ToolAnnotation for destructive write tools. +func Destructive(title string) mcp.ToolAnnotation { + f, t := false, true + return mcp.ToolAnnotation{ + Title: title, + ReadOnlyHint: &f, + DestructiveHint: &t, + } +}