diff --git a/README.md b/README.md
index a6e740e66..2679d761d 100644
--- a/README.md
+++ b/README.md
@@ -401,18 +401,15 @@ The following sets of tools are available (all are on by default):
Code Security
-- **get_code_scanning_alert** - Get code scanning alert
- - `alertNumber`: The number of the alert. (number, required)
- - `owner`: The owner of the repository. (string, required)
- - `repo`: The name of the repository. (string, required)
-
-- **list_code_scanning_alerts** - List code scanning alerts
- - `owner`: The owner of the repository. (string, required)
- - `ref`: The Git reference for the results you want to list. (string, optional)
- - `repo`: The name of the repository. (string, required)
- - `severity`: Filter code scanning alerts by severity (string, optional)
- - `state`: Filter code scanning alerts by state. Defaults to open (string, optional)
- - `tool_name`: The name of the tool used for code scanning. (string, optional)
+- **manage_code_scanning_alerts** - Manage Code Scanning Alerts
+ - `alertNumber`: The number of the alert (required for 'get' operation) (number, optional)
+ - `operation`: Operation to perform: 'list', 'get' (string, required)
+ - `owner`: The owner of the repository (string, required)
+ - `ref`: The Git reference for the results you want to list (used for 'list' operation) (string, optional)
+ - `repo`: The name of the repository (string, required)
+ - `severity`: Filter code scanning alerts by severity (used for 'list' operation) (string, optional)
+ - `state`: Filter code scanning alerts by state (used for 'list' operation) (string, optional)
+ - `tool_name`: The name of the tool used for code scanning (used for 'list' operation) (string, optional)
@@ -484,23 +481,17 @@ The following sets of tools are available (all are on by default):
Gists
-- **create_gist** - Create Gist
- - `content`: Content for simple single-file gist creation (string, required)
- - `description`: Description of the gist (string, optional)
- - `filename`: Filename for simple single-file gist creation (string, required)
- - `public`: Whether the gist is public (boolean, optional)
-
-- **list_gists** - List Gists
+- **manage_gist** - Manage Gist
+ - `content`: Content for gist file (required for 'create' and 'update' operations) (string, optional)
+ - `description`: Description of the gist (used for 'create' and 'update' operations) (string, optional)
+ - `filename`: Filename for gist file (required for 'create' and 'update' operations) (string, optional)
+ - `gist_id`: ID of the gist (required for 'update' and 'get' operations) (string, optional)
+ - `operation`: Operation to perform: 'list', 'create', 'update', 'get' (string, required)
- `page`: Page number for pagination (min 1) (number, optional)
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
- - `since`: Only gists updated after this time (ISO 8601 timestamp) (string, optional)
- - `username`: GitHub username (omit for authenticated user's gists) (string, optional)
-
-- **update_gist** - Update Gist
- - `content`: Content for the file (string, required)
- - `description`: Updated description of the gist (string, optional)
- - `filename`: Filename to update or create (string, required)
- - `gist_id`: ID of the gist to update (string, required)
+ - `public`: Whether the gist is public (used for 'create' operation) (boolean, optional)
+ - `since`: Only gists updated after this time (ISO 8601 timestamp, used for 'list' operation) (string, optional)
+ - `username`: GitHub username (omit for authenticated user's gists, used for 'list' and 'get' operations) (string, optional)
@@ -508,12 +499,6 @@ The following sets of tools are available (all are on by default):
Issues
-- **add_issue_comment** - Add comment to issue
- - `body`: Comment content (string, required)
- - `issue_number`: Issue number to comment on (number, required)
- - `owner`: Repository owner (string, required)
- - `repo`: Repository name (string, required)
-
- **add_sub_issue** - Add sub-issue
- `issue_number`: The number of the parent issue (number, required)
- `owner`: Repository owner (string, required)
@@ -526,21 +511,6 @@ The following sets of tools are available (all are on by default):
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
-- **create_issue** - Open new issue
- - `assignees`: Usernames to assign to this issue (string[], optional)
- - `body`: Issue body content (string, optional)
- - `labels`: Labels to apply to this issue (string[], optional)
- - `milestone`: Milestone number (number, optional)
- - `owner`: Repository owner (string, required)
- - `repo`: Repository name (string, required)
- - `title`: Issue title (string, required)
- - `type`: Type of this issue (string, optional)
-
-- **get_issue** - Get issue details
- - `issue_number`: The number of the issue (number, required)
- - `owner`: The owner of the repository (string, required)
- - `repo`: The name of the repository (string, required)
-
- **get_issue_comments** - Get issue comments
- `issue_number`: Issue number (number, required)
- `owner`: Repository owner (string, required)
@@ -551,17 +521,6 @@ The following sets of tools are available (all are on by default):
- **list_issue_types** - List available issue types
- `owner`: The organization owner of the repository (string, required)
-- **list_issues** - List issues
- - `after`: Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs. (string, optional)
- - `direction`: Order direction. If provided, the 'orderBy' also needs to be provided. (string, optional)
- - `labels`: Filter by labels (string[], optional)
- - `orderBy`: Order issues by field. If provided, the 'direction' also needs to be provided. (string, optional)
- - `owner`: Repository owner (string, required)
- - `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
- - `repo`: Repository name (string, required)
- - `since`: Filter by date (ISO 8601 timestamp) (string, optional)
- - `state`: Filter by state, by default both open and closed issues are returned when not provided (string, optional)
-
- **list_sub_issues** - List sub-issues
- `issue_number`: Issue number (number, required)
- `owner`: Repository owner (string, required)
@@ -569,6 +528,26 @@ The following sets of tools are available (all are on by default):
- `per_page`: Number of results per page (max 100, default: 30) (number, optional)
- `repo`: Repository name (string, required)
+- **manage_issue** - Manage Issue
+ - `assignees`: Usernames to assign to this issue (used for 'create', 'update' operations) (string[], optional)
+ - `body`: Issue body content (used for 'create', 'update', 'add_comment' operations) (string, optional)
+ - `direction`: Order direction for list operation: 'ASC', 'DESC' (string, optional)
+ - `issue_number`: Issue number (required for 'get', 'update', 'add_comment' operations) (number, optional)
+ - `labels`: Labels to apply to this issue (used for 'create', 'update' operations) (string[], optional)
+ - `list_labels`: Filter by labels for list operation (string[], optional)
+ - `list_state`: Filter by state for list operation: 'OPEN', 'CLOSED' (string, optional)
+ - `milestone`: Milestone number (used for 'create', 'update' operations) (number, optional)
+ - `operation`: Operation to perform: 'list', 'get', 'create', 'update', 'add_comment' (string, required)
+ - `orderBy`: Order issues by field for list operation: 'CREATED_AT', 'UPDATED_AT', 'COMMENTS' (string, optional)
+ - `owner`: Repository owner (string, required)
+ - `page`: Page number for pagination (min 1) (number, optional)
+ - `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
+ - `repo`: Repository name (string, required)
+ - `since`: Filter by date (ISO 8601 timestamp, used for 'list' operation) (string, optional)
+ - `state`: Issue state (used for 'update' operation): 'open' or 'closed' (string, optional)
+ - `title`: Issue title (required for 'create' operation) (string, optional)
+ - `type`: Type of this issue (used for 'create' operation) (string, optional)
+
- **remove_sub_issue** - Remove sub-issue
- `issue_number`: The number of the parent issue (number, required)
- `owner`: Repository owner (string, required)
@@ -592,18 +571,6 @@ The following sets of tools are available (all are on by default):
- `repo`: Optional repository name. If provided with owner, only issues for this repository are listed. (string, optional)
- `sort`: Sort field by number of matches of categories, defaults to best match (string, optional)
-- **update_issue** - Edit issue
- - `assignees`: New assignees (string[], optional)
- - `body`: New description (string, optional)
- - `issue_number`: Issue number to update (number, required)
- - `labels`: New labels (string[], optional)
- - `milestone`: New milestone number (number, optional)
- - `owner`: Repository owner (string, required)
- - `repo`: Repository name (string, required)
- - `state`: New state (string, optional)
- - `title`: New title (string, optional)
- - `type`: New issue type (string, optional)
-
@@ -685,26 +652,11 @@ The following sets of tools are available (all are on by default):
- `pullNumber`: Pull request number (number, required)
- `repo`: Repository name (string, required)
-- **create_pull_request** - Open new pull request
- - `base`: Branch to merge into (string, required)
- - `body`: PR description (string, optional)
- - `draft`: Create as draft PR (boolean, optional)
- - `head`: Branch containing changes (string, required)
- - `maintainer_can_modify`: Allow maintainer edits (boolean, optional)
- - `owner`: Repository owner (string, required)
- - `repo`: Repository name (string, required)
- - `title`: PR title (string, required)
-
- **delete_pending_pull_request_review** - Delete the requester's latest pending pull request review
- `owner`: Repository owner (string, required)
- `pullNumber`: Pull request number (number, required)
- `repo`: Repository name (string, required)
-- **get_pull_request** - Get pull request details
- - `owner`: Repository owner (string, required)
- - `pullNumber`: Pull request number (number, required)
- - `repo`: Repository name (string, required)
-
- **get_pull_request_comments** - Get pull request comments
- `owner`: Repository owner (string, required)
- `pullNumber`: Pull request number (number, required)
@@ -732,24 +684,28 @@ The following sets of tools are available (all are on by default):
- `pullNumber`: Pull request number (number, required)
- `repo`: Repository name (string, required)
-- **list_pull_requests** - List pull requests
- - `base`: Filter by base branch (string, optional)
- - `direction`: Sort direction (string, optional)
- - `head`: Filter by head user/org and branch (string, optional)
+- **manage_pull_request** - Manage Pull Request
+ - `base`: The name of the branch you want the changes pulled into (required for 'create' operation) (string, optional)
+ - `body`: Pull request body content (used for 'create', 'update' operations) (string, optional)
+ - `commit_message`: Extra detail to append to merge commit message (used for 'merge' operation) (string, optional)
+ - `commit_title`: Title for the merge commit (used for 'merge' operation) (string, optional)
+ - `direction`: Sort direction for list operation: 'asc', 'desc' (string, optional)
+ - `draft`: Whether to create as draft (used for 'create' operation) (boolean, optional)
+ - `head`: The name of the branch where your changes are implemented (required for 'create' operation) (string, optional)
+ - `list_base`: Filter by base branch for list operation (string, optional)
+ - `list_head`: Filter by head user/org and branch for list operation (string, optional)
+ - `list_state`: Filter by state for list operation: 'open', 'closed', 'all' (string, optional)
+ - `maintainer_can_modify`: Whether maintainers can modify the pull request (used for 'create' operation) (boolean, optional)
+ - `merge_method`: Merge method to use (used for 'merge' operation): 'merge', 'squash', 'rebase' (string, optional)
+ - `operation`: Operation to perform: 'list', 'get', 'create', 'update', 'merge' (string, required)
- `owner`: Repository owner (string, required)
- `page`: Page number for pagination (min 1) (number, optional)
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
+ - `pullNumber`: Pull request number (required for 'get', 'update', 'merge' operations) (number, optional)
- `repo`: Repository name (string, required)
- - `sort`: Sort by (string, optional)
- - `state`: Filter by state (string, optional)
-
-- **merge_pull_request** - Merge pull request
- - `commit_message`: Extra detail for merge commit (string, optional)
- - `commit_title`: Title for merge commit (string, optional)
- - `merge_method`: Merge method (string, optional)
- - `owner`: Repository owner (string, required)
- - `pullNumber`: Pull request number (number, required)
- - `repo`: Repository name (string, required)
+ - `sort`: Sort by for list operation: 'created', 'updated', 'popularity', 'long-running' (string, optional)
+ - `state`: State of the pull request (used for 'update' operation): 'open' or 'closed' (string, optional)
+ - `title`: Pull request title (required for 'create' operation) (string, optional)
- **request_copilot_review** - Request Copilot review
- `owner`: Repository owner (string, required)
@@ -772,18 +728,6 @@ The following sets of tools are available (all are on by default):
- `pullNumber`: Pull request number (number, required)
- `repo`: Repository name (string, required)
-- **update_pull_request** - Edit pull request
- - `base`: New base branch name (string, optional)
- - `body`: New description (string, optional)
- - `draft`: Mark pull request as draft (true) or ready for review (false) (boolean, optional)
- - `maintainer_can_modify`: Allow maintainer edits (boolean, optional)
- - `owner`: Repository owner (string, required)
- - `pullNumber`: Pull request number to update (number, required)
- - `repo`: Repository name (string, required)
- - `reviewers`: GitHub usernames to request reviews from (string[], optional)
- - `state`: New state (string, optional)
- - `title`: New title (string, optional)
-
- **update_pull_request_branch** - Update pull request branch
- `expectedHeadSha`: The expected SHA of the pull request's HEAD ref (string, optional)
- `owner`: Repository owner (string, required)
@@ -811,12 +755,6 @@ The following sets of tools are available (all are on by default):
- `repo`: Repository name (string, required)
- `sha`: Required if updating an existing file. The blob SHA of the file being replaced. (string, optional)
-- **create_repository** - Create repository
- - `autoInit`: Initialize with README (boolean, optional)
- - `description`: Repository description (string, optional)
- - `name`: Repository name (string, required)
- - `private`: Whether repo should be private (boolean, optional)
-
- **delete_file** - Delete file
- `branch`: Branch to delete the file from (string, required)
- `message`: Commit message (string, required)
@@ -824,11 +762,6 @@ The following sets of tools are available (all are on by default):
- `path`: Path to the file to delete (string, required)
- `repo`: Repository name (string, required)
-- **fork_repository** - Fork repository
- - `organization`: Organization to fork to (string, optional)
- - `owner`: Repository owner (string, required)
- - `repo`: Repository name (string, required)
-
- **get_commit** - Get commit details
- `owner`: Repository owner (string, required)
- `page`: Page number for pagination (min 1) (number, optional)
@@ -836,13 +769,6 @@ The following sets of tools are available (all are on by default):
- `repo`: Repository name (string, required)
- `sha`: Commit SHA, branch name, or tag name (string, required)
-- **get_file_contents** - Get file or directory contents
- - `owner`: Repository owner (username or organization) (string, required)
- - `path`: Path to file/directory (directories must end with a slash '/') (string, optional)
- - `ref`: Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head` (string, optional)
- - `repo`: Repository name (string, required)
- - `sha`: Accepts optional commit SHA. If specified, it will be used instead of ref (string, optional)
-
- **get_latest_release** - Get latest release
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
@@ -883,6 +809,20 @@ The following sets of tools are available (all are on by default):
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
- `repo`: Repository name (string, required)
+- **manage_repository** - Manage Repository
+ - `autoInit`: Initialize with README (used for 'create' operation) (boolean, optional)
+ - `default_branch_only`: Fork only default branch (used for 'fork' operation) (string, optional)
+ - `description`: Repository description (used for 'create' operation) (string, optional)
+ - `name`: Repository name (required for 'create' operation) (string, optional)
+ - `operation`: Operation to perform: 'create', 'fork', 'get_file_contents' (string, required)
+ - `organization`: Organization to fork to (used for 'fork' operation) (string, optional)
+ - `owner`: Repository owner (required for 'fork', 'get_file_contents' operations) (string, optional)
+ - `path`: Path to file/directory (required for 'get_file_contents' operation) (string, optional)
+ - `private`: Whether repo should be private (used for 'create' operation) (boolean, optional)
+ - `ref`: Git reference such as branch, tag, or commit SHA (used for 'get_file_contents' operation) (string, optional)
+ - `repo`: Repository name (required for 'fork', 'get_file_contents' operations) (string, optional)
+ - `sha`: Commit SHA (used for 'get_file_contents' operation) (string, optional)
+
- **push_files** - Push files to repository
- `branch`: Branch to push to (string, required)
- `files`: Array of file objects to push, each object with path (string) and content (string) (object[], required)
@@ -908,17 +848,14 @@ The following sets of tools are available (all are on by default):
Secret Protection
-- **get_secret_scanning_alert** - Get secret scanning alert
- - `alertNumber`: The number of the alert. (number, required)
- - `owner`: The owner of the repository. (string, required)
- - `repo`: The name of the repository. (string, required)
-
-- **list_secret_scanning_alerts** - List secret scanning alerts
- - `owner`: The owner of the repository. (string, required)
- - `repo`: The name of the repository. (string, required)
- - `resolution`: Filter by resolution (string, optional)
- - `secret_type`: A comma-separated list of secret types to return. All default secret patterns are returned. To return generic patterns, pass the token name(s) in the parameter. (string, optional)
- - `state`: Filter by state (string, optional)
+- **manage_secret_scanning_alerts** - Manage Secret Scanning Alerts
+ - `alertNumber`: The number of the alert (required for 'get' operation) (number, optional)
+ - `operation`: Operation to perform: 'list', 'get' (string, required)
+ - `owner`: The owner of the repository (string, required)
+ - `repo`: The name of the repository (string, required)
+ - `resolution`: Filter by resolution (used for 'list' operation) (string, optional)
+ - `secret_type`: A comma-separated list of secret types to return (used for 'list' operation) (string, optional)
+ - `state`: Filter by state (used for 'list' operation) (string, optional)
diff --git a/github-mcp-server b/github-mcp-server
index 864242c24..ea1b191b2 100755
Binary files a/github-mcp-server and b/github-mcp-server differ
diff --git a/pkg/github/code_scanning.go b/pkg/github/code_scanning.go
index 47eaa4be0..61960c52e 100644
--- a/pkg/github/code_scanning.go
+++ b/pkg/github/code_scanning.go
@@ -14,6 +14,166 @@ import (
"github.com/mark3labs/mcp-go/server"
)
+// ManageCodeScanningAlerts creates a consolidated tool to perform operations on code scanning alerts
+func ManageCodeScanningAlerts(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+ return mcp.NewTool("manage_code_scanning_alerts",
+ mcp.WithDescription(t("TOOL_MANAGE_CODE_SCANNING_ALERTS_DESCRIPTION", "Manage code scanning alerts with various operations: list, get")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_MANAGE_CODE_SCANNING_ALERTS", "Manage Code Scanning Alerts"),
+ ReadOnlyHint: ToBoolPtr(true),
+ }),
+ mcp.WithString("operation",
+ mcp.Required(),
+ mcp.Description("Operation to perform: 'list', 'get'"),
+ mcp.Enum("list", "get"),
+ ),
+ mcp.WithString("owner",
+ mcp.Required(),
+ mcp.Description("The owner of the repository"),
+ ),
+ mcp.WithString("repo",
+ mcp.Required(),
+ mcp.Description("The name of the repository"),
+ ),
+ // Parameters for get operation
+ mcp.WithNumber("alertNumber",
+ mcp.Description("The number of the alert (required for 'get' operation)"),
+ ),
+ // Parameters for list operation
+ mcp.WithString("state",
+ mcp.Description("Filter code scanning alerts by state (used for 'list' operation)"),
+ mcp.DefaultString("open"),
+ mcp.Enum("open", "closed", "dismissed", "fixed"),
+ ),
+ mcp.WithString("ref",
+ mcp.Description("The Git reference for the results you want to list (used for 'list' operation)"),
+ ),
+ mcp.WithString("severity",
+ mcp.Description("Filter code scanning alerts by severity (used for 'list' operation)"),
+ mcp.Enum("critical", "high", "medium", "low", "warning", "note", "error"),
+ ),
+ mcp.WithString("tool_name",
+ mcp.Description("The name of the tool used for code scanning (used for 'list' operation)"),
+ ),
+ ),
+ func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ operation, err := RequiredParam[string](request, "operation")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ switch operation {
+ case "list":
+ return handleListCodeScanningAlerts(ctx, getClient, request)
+ case "get":
+ return handleGetCodeScanningAlert(ctx, getClient, request)
+ default:
+ return mcp.NewToolResultError(fmt.Sprintf("unsupported operation: %s", operation)), nil
+ }
+ }
+}
+
+func handleGetCodeScanningAlert(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ owner, err := RequiredParam[string](request, "owner")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ repo, err := RequiredParam[string](request, "repo")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ alertNumber, err := RequiredInt(request, "alertNumber")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ alert, resp, err := client.CodeScanning.GetAlert(ctx, owner, repo, int64(alertNumber))
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx,
+ "failed to get alert",
+ resp,
+ err,
+ ), nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != http.StatusOK {
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response body: %w", err)
+ }
+ return mcp.NewToolResultError(fmt.Sprintf("failed to get alert: %s", string(body))), nil
+ }
+
+ r, err := json.Marshal(alert)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal alert: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
+func handleListCodeScanningAlerts(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ owner, err := RequiredParam[string](request, "owner")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ repo, err := RequiredParam[string](request, "repo")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ ref, err := OptionalParam[string](request, "ref")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ state, err := OptionalParam[string](request, "state")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ severity, err := OptionalParam[string](request, "severity")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ toolName, err := OptionalParam[string](request, "tool_name")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+ alerts, resp, err := client.CodeScanning.ListAlertsForRepo(ctx, owner, repo, &github.AlertListOptions{Ref: ref, State: state, Severity: severity, ToolName: toolName})
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx,
+ "failed to list alerts",
+ resp,
+ err,
+ ), nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != http.StatusOK {
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response body: %w", err)
+ }
+ return mcp.NewToolResultError(fmt.Sprintf("failed to list alerts: %s", string(body))), nil
+ }
+
+ r, err := json.Marshal(alerts)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal alerts: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
func GetCodeScanningAlert(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_code_scanning_alert",
mcp.WithDescription(t("TOOL_GET_CODE_SCANNING_ALERT_DESCRIPTION", "Get details of a specific code scanning alert in a GitHub repository.")),
diff --git a/pkg/github/code_scanning_test.go b/pkg/github/code_scanning_test.go
index 5d4cc732d..db8b59b8e 100644
--- a/pkg/github/code_scanning_test.go
+++ b/pkg/github/code_scanning_test.go
@@ -247,3 +247,70 @@ func Test_ListCodeScanningAlerts(t *testing.T) {
})
}
}
+
+func Test_ManageCodeScanningAlerts(t *testing.T) {
+// Verify tool definition
+mockClient := github.NewClient(nil)
+tool, _ := ManageCodeScanningAlerts(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+
+assert.Equal(t, "manage_code_scanning_alerts", tool.Name)
+assert.NotEmpty(t, tool.Description)
+assert.Contains(t, tool.InputSchema.Properties, "operation")
+assert.Contains(t, tool.InputSchema.Required, "operation")
+
+t.Run("list operation", func(t *testing.T) {
+// Setup mock alerts for success case
+mockAlerts := []*github.Alert{
+{
+Number: github.Ptr(1),
+Rule: &github.Rule{
+ID: github.Ptr("rule1"),
+Severity: github.Ptr("error"),
+Description: github.Ptr("Test rule"),
+},
+State: github.Ptr("open"),
+},
+}
+
+mockedHTTPClient := mock.NewMockedHTTPClient(
+mock.WithRequestMatch(mock.GetReposCodeScanningAlertsByOwnerByRepo, mockAlerts),
+)
+client := github.NewClient(mockedHTTPClient)
+_, handler := ManageCodeScanningAlerts(stubGetClientFn(client), translations.NullTranslationHelper)
+
+request := createMCPRequest(map[string]interface{}{
+"operation": "list",
+"owner": "testowner",
+"repo": "testrepo",
+})
+
+result, err := handler(context.Background(), request)
+require.NoError(t, err)
+assert.False(t, result.IsError)
+
+var alerts []*github.Alert
+textContent := getTextResult(t, result)
+err = json.Unmarshal([]byte(textContent.Text), &alerts)
+require.NoError(t, err)
+assert.Len(t, alerts, 1)
+assert.Equal(t, 1, *alerts[0].Number)
+})
+
+t.Run("unsupported operation", func(t *testing.T) {
+mockedHTTPClient := mock.NewMockedHTTPClient()
+client := github.NewClient(mockedHTTPClient)
+_, handler := ManageCodeScanningAlerts(stubGetClientFn(client), translations.NullTranslationHelper)
+
+request := createMCPRequest(map[string]interface{}{
+"operation": "delete",
+"owner": "testowner",
+"repo": "testrepo",
+})
+
+result, err := handler(context.Background(), request)
+require.NoError(t, err)
+assert.True(t, result.IsError)
+textContent := getTextResult(t, result)
+assert.Contains(t, textContent.Text, "unsupported operation: delete")
+})
+}
diff --git a/pkg/github/gists.go b/pkg/github/gists.go
index fce34f6a8..1068ea463 100644
--- a/pkg/github/gists.go
+++ b/pkg/github/gists.go
@@ -13,6 +13,277 @@ import (
"github.com/mark3labs/mcp-go/server"
)
+// ManageGist creates a consolidated tool to perform CRUD operations on gists
+func ManageGist(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+ return mcp.NewTool("manage_gist",
+ mcp.WithDescription(t("TOOL_MANAGE_GIST_DESCRIPTION", "Manage gists with various operations: list, create, update, get")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_MANAGE_GIST", "Manage Gist"),
+ ReadOnlyHint: ToBoolPtr(false),
+ }),
+ mcp.WithString("operation",
+ mcp.Required(),
+ mcp.Description("Operation to perform: 'list', 'create', 'update', 'get'"),
+ mcp.Enum("list", "create", "update", "get"),
+ ),
+ // Parameters for list operation
+ mcp.WithString("username",
+ mcp.Description("GitHub username (omit for authenticated user's gists, used for 'list' and 'get' operations)"),
+ ),
+ mcp.WithString("since",
+ mcp.Description("Only gists updated after this time (ISO 8601 timestamp, used for 'list' operation)"),
+ ),
+ // Parameters for create/update operations
+ mcp.WithString("gist_id",
+ mcp.Description("ID of the gist (required for 'update' and 'get' operations)"),
+ ),
+ mcp.WithString("description",
+ mcp.Description("Description of the gist (used for 'create' and 'update' operations)"),
+ ),
+ mcp.WithString("filename",
+ mcp.Description("Filename for gist file (required for 'create' and 'update' operations)"),
+ ),
+ mcp.WithString("content",
+ mcp.Description("Content for gist file (required for 'create' and 'update' operations)"),
+ ),
+ mcp.WithBoolean("public",
+ mcp.Description("Whether the gist is public (used for 'create' operation)"),
+ mcp.DefaultBool(false),
+ ),
+ WithPagination(),
+ ),
+ func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ operation, err := RequiredParam[string](request, "operation")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ switch operation {
+ case "list":
+ return handleListGists(ctx, getClient, request)
+ case "create":
+ return handleCreateGist(ctx, getClient, request)
+ case "update":
+ return handleUpdateGist(ctx, getClient, request)
+ case "get":
+ return handleGetGist(ctx, getClient, request)
+ default:
+ return mcp.NewToolResultError(fmt.Sprintf("unsupported operation: %s", operation)), nil
+ }
+ }
+}
+
+func handleListGists(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ username, err := OptionalParam[string](request, "username")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ since, err := OptionalParam[string](request, "since")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ pagination, err := OptionalPaginationParams(request)
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ opts := &github.GistListOptions{
+ ListOptions: github.ListOptions{
+ Page: pagination.Page,
+ PerPage: pagination.PerPage,
+ },
+ }
+
+ // Parse since timestamp if provided
+ if since != "" {
+ sinceTime, err := parseISOTimestamp(since)
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("invalid since timestamp: %v", err)), nil
+ }
+ opts.Since = sinceTime
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ gists, resp, err := client.Gists.List(ctx, username, opts)
+ if err != nil {
+ return nil, fmt.Errorf("failed to list gists: %w", err)
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != http.StatusOK {
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response body: %w", err)
+ }
+ return mcp.NewToolResultError(fmt.Sprintf("failed to list gists: %s", string(body))), nil
+ }
+
+ r, err := json.Marshal(gists)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
+func handleCreateGist(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ description, err := OptionalParam[string](request, "description")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ filename, err := RequiredParam[string](request, "filename")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ content, err := RequiredParam[string](request, "content")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ public, err := OptionalParam[bool](request, "public")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ files := make(map[github.GistFilename]github.GistFile)
+ files[github.GistFilename(filename)] = github.GistFile{
+ Filename: github.Ptr(filename),
+ Content: github.Ptr(content),
+ }
+
+ gist := &github.Gist{
+ Files: files,
+ Public: github.Ptr(public),
+ Description: github.Ptr(description),
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ createdGist, resp, err := client.Gists.Create(ctx, gist)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create gist: %w", err)
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != http.StatusCreated {
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response body: %w", err)
+ }
+ return mcp.NewToolResultError(fmt.Sprintf("failed to create gist: %s", string(body))), nil
+ }
+
+ r, err := json.Marshal(createdGist)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
+func handleUpdateGist(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ gistID, err := RequiredParam[string](request, "gist_id")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ description, err := OptionalParam[string](request, "description")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ filename, err := RequiredParam[string](request, "filename")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ content, err := RequiredParam[string](request, "content")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ files := make(map[github.GistFilename]github.GistFile)
+ files[github.GistFilename(filename)] = github.GistFile{
+ Filename: github.Ptr(filename),
+ Content: github.Ptr(content),
+ }
+
+ gist := &github.Gist{
+ Files: files,
+ Description: github.Ptr(description),
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ updatedGist, resp, err := client.Gists.Edit(ctx, gistID, gist)
+ if err != nil {
+ return nil, fmt.Errorf("failed to update gist: %w", err)
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != http.StatusOK {
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response body: %w", err)
+ }
+ return mcp.NewToolResultError(fmt.Sprintf("failed to update gist: %s", string(body))), nil
+ }
+
+ r, err := json.Marshal(updatedGist)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
+func handleGetGist(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ gistID, err := RequiredParam[string](request, "gist_id")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ gist, resp, err := client.Gists.Get(ctx, gistID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get gist: %w", err)
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != http.StatusOK {
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response body: %w", err)
+ }
+ return mcp.NewToolResultError(fmt.Sprintf("failed to get gist: %s", string(body))), nil
+ }
+
+ r, err := json.Marshal(gist)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
// ListGists creates a tool to list gists for a user
func ListGists(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("list_gists",
diff --git a/pkg/github/gists_test.go b/pkg/github/gists_test.go
index 49d63a252..31aa78aa3 100644
--- a/pkg/github/gists_test.go
+++ b/pkg/github/gists_test.go
@@ -505,3 +505,71 @@ func Test_UpdateGist(t *testing.T) {
})
}
}
+
+func Test_ManageGist(t *testing.T) {
+// Verify tool definition
+mockClient := github.NewClient(nil)
+tool, _ := ManageGist(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+
+assert.Equal(t, "manage_gist", tool.Name)
+assert.NotEmpty(t, tool.Description)
+assert.Contains(t, tool.InputSchema.Properties, "operation")
+assert.Contains(t, tool.InputSchema.Required, "operation")
+
+t.Run("list operation", func(t *testing.T) {
+// Setup mock gists for success case
+mockGists := []*github.Gist{
+{
+ID: github.Ptr("gist1"),
+Description: github.Ptr("First Gist"),
+HTMLURL: github.Ptr("https://gist.github.com/user/gist1"),
+Public: github.Ptr(true),
+CreatedAt: &github.Timestamp{Time: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)},
+Owner: &github.User{Login: github.Ptr("user")},
+Files: map[github.GistFilename]github.GistFile{
+"file1.txt": {
+Filename: github.Ptr("file1.txt"),
+Content: github.Ptr("content of file 1"),
+},
+},
+},
+}
+
+mockedHTTPClient := mock.NewMockedHTTPClient(
+mock.WithRequestMatchPages(mock.GetGists, mockGists, 1),
+)
+client := github.NewClient(mockedHTTPClient)
+_, handler := ManageGist(stubGetClientFn(client), translations.NullTranslationHelper)
+
+request := createMCPRequest(map[string]interface{}{
+"operation": "list",
+})
+
+result, err := handler(context.Background(), request)
+require.NoError(t, err)
+assert.False(t, result.IsError)
+
+var gists []*github.Gist
+textContent := getTextResult(t, result)
+err = json.Unmarshal([]byte(textContent.Text), &gists)
+require.NoError(t, err)
+assert.Len(t, gists, 1)
+assert.Equal(t, "gist1", *gists[0].ID)
+})
+
+t.Run("unsupported operation", func(t *testing.T) {
+mockedHTTPClient := mock.NewMockedHTTPClient()
+client := github.NewClient(mockedHTTPClient)
+_, handler := ManageGist(stubGetClientFn(client), translations.NullTranslationHelper)
+
+request := createMCPRequest(map[string]interface{}{
+"operation": "delete",
+})
+
+result, err := handler(context.Background(), request)
+require.NoError(t, err)
+assert.True(t, result.IsError)
+textContent := getTextResult(t, result)
+assert.Contains(t, textContent.Text, "unsupported operation: delete")
+})
+}
diff --git a/pkg/github/issues.go b/pkg/github/issues.go
index 89375ae90..97343427d 100644
--- a/pkg/github/issues.go
+++ b/pkg/github/issues.go
@@ -18,6 +18,494 @@ import (
"github.com/shurcooL/githubv4"
)
+// ManageIssue creates a consolidated tool to perform CRUD operations on issues
+func ManageIssue(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+ return mcp.NewTool("manage_issue",
+ mcp.WithDescription(t("TOOL_MANAGE_ISSUE_DESCRIPTION", "Manage issues with various operations: list, get, create, update, add_comment")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_MANAGE_ISSUE", "Manage Issue"),
+ ReadOnlyHint: ToBoolPtr(false),
+ }),
+ mcp.WithString("operation",
+ mcp.Required(),
+ mcp.Description("Operation to perform: 'list', 'get', 'create', 'update', 'add_comment'"),
+ mcp.Enum("list", "get", "create", "update", "add_comment"),
+ ),
+ mcp.WithString("owner",
+ mcp.Required(),
+ mcp.Description("Repository owner"),
+ ),
+ mcp.WithString("repo",
+ mcp.Required(),
+ mcp.Description("Repository name"),
+ ),
+ // Parameters for get, update, add_comment operations
+ mcp.WithNumber("issue_number",
+ mcp.Description("Issue number (required for 'get', 'update', 'add_comment' operations)"),
+ ),
+ // Parameters for create operation
+ mcp.WithString("title",
+ mcp.Description("Issue title (required for 'create' operation)"),
+ ),
+ mcp.WithString("body",
+ mcp.Description("Issue body content (used for 'create', 'update', 'add_comment' operations)"),
+ ),
+ mcp.WithArray("assignees",
+ mcp.Description("Usernames to assign to this issue (used for 'create', 'update' operations)"),
+ mcp.Items(
+ map[string]any{
+ "type": "string",
+ },
+ ),
+ ),
+ mcp.WithArray("labels",
+ mcp.Description("Labels to apply to this issue (used for 'create', 'update' operations)"),
+ mcp.Items(
+ map[string]any{
+ "type": "string",
+ },
+ ),
+ ),
+ mcp.WithNumber("milestone",
+ mcp.Description("Milestone number (used for 'create', 'update' operations)"),
+ ),
+ mcp.WithString("type",
+ mcp.Description("Type of this issue (used for 'create' operation)"),
+ ),
+ mcp.WithString("state",
+ mcp.Description("Issue state (used for 'update' operation): 'open' or 'closed'"),
+ mcp.Enum("open", "closed"),
+ ),
+ // Parameters for list operation
+ mcp.WithString("since",
+ mcp.Description("Filter by date (ISO 8601 timestamp, used for 'list' operation)"),
+ ),
+ mcp.WithString("list_state",
+ mcp.Description("Filter by state for list operation: 'OPEN', 'CLOSED'"),
+ mcp.Enum("OPEN", "CLOSED"),
+ ),
+ mcp.WithArray("list_labels",
+ mcp.Description("Filter by labels for list operation"),
+ mcp.Items(
+ map[string]any{
+ "type": "string",
+ },
+ ),
+ ),
+ mcp.WithString("direction",
+ mcp.Description("Order direction for list operation: 'ASC', 'DESC'"),
+ mcp.Enum("ASC", "DESC"),
+ ),
+ mcp.WithString("orderBy",
+ mcp.Description("Order issues by field for list operation: 'CREATED_AT', 'UPDATED_AT', 'COMMENTS'"),
+ mcp.Enum("CREATED_AT", "UPDATED_AT", "COMMENTS"),
+ ),
+ WithPagination(),
+ ),
+ func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ operation, err := RequiredParam[string](request, "operation")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ switch operation {
+ case "list":
+ return handleListIssues(ctx, getGQLClient, request)
+ case "get":
+ return handleGetIssue(ctx, getClient, request)
+ case "create":
+ return handleCreateIssue(ctx, getClient, request)
+ case "update":
+ return handleUpdateIssue(ctx, getClient, request)
+ case "add_comment":
+ return handleAddIssueComment(ctx, getClient, request)
+ default:
+ return mcp.NewToolResultError(fmt.Sprintf("unsupported operation: %s", operation)), nil
+ }
+ }
+}
+
+func handleListIssues(ctx context.Context, getGQLClient GetGQLClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ owner, err := RequiredParam[string](request, "owner")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ repo, err := RequiredParam[string](request, "repo")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ since, err := OptionalParam[string](request, "since")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ state, err := OptionalParam[string](request, "list_state")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ labels, err := OptionalStringArrayParam(request, "list_labels")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ direction, err := OptionalParam[string](request, "direction")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ orderBy, err := OptionalParam[string](request, "orderBy")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ pagination, err := OptionalPaginationParams(request)
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ client, err := getGQLClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub GraphQL client: %w", err)
+ }
+
+ // Set defaults
+ first := 30
+ if pagination.PerPage > 0 {
+ first = pagination.PerPage
+ }
+
+ // Set up variables for GraphQL query
+ variables := map[string]interface{}{
+ "owner": githubv4.String(owner),
+ "repo": githubv4.String(repo),
+ "first": githubv4.Int(first),
+ }
+
+ // Handle pagination cursor
+ if pagination.After != "" {
+ variables["after"] = githubv4.String(pagination.After)
+ }
+
+ // Handle state filtering
+ if state != "" {
+ variables["states"] = []githubv4.IssueState{githubv4.IssueState(state)}
+ }
+
+ // Handle ordering
+ if direction != "" && orderBy != "" {
+ variables["direction"] = githubv4.OrderDirection(direction)
+ variables["orderBy"] = githubv4.IssueOrderField(orderBy)
+ }
+
+ // Handle since filtering
+ var sinceTime *githubv4.DateTime
+ if since != "" {
+ parsedTime, err := parseISOTimestamp(since)
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("invalid since timestamp: %v", err)), nil
+ }
+ dt := githubv4.DateTime{Time: parsedTime}
+ sinceTime = &dt
+ variables["since"] = dt
+ }
+
+ // Choose the right query based on whether we have labels and/or since filtering
+ hasLabels := len(labels) > 0
+ hasSince := sinceTime != nil
+
+ if hasLabels {
+ variables["labels"] = labels
+ }
+
+ // Execute the appropriate query
+ var result IssueQueryResult
+ switch {
+ case hasLabels && hasSince:
+ result = &ListIssuesQueryTypeWithLabelsWithSince{}
+ case hasLabels && !hasSince:
+ result = &ListIssuesQueryTypeWithLabels{}
+ case !hasLabels && hasSince:
+ result = &ListIssuesQueryWithSince{}
+ default:
+ result = &ListIssuesQuery{}
+ }
+
+ err = client.Query(ctx, result, variables)
+ if err != nil {
+ return nil, fmt.Errorf("failed to query issues: %w", err)
+ }
+
+ fragment := result.GetIssueFragment()
+
+ // Convert GraphQL fragments to GitHub issue structs
+ issues := make([]*github.Issue, len(fragment.Nodes))
+ for i, node := range fragment.Nodes {
+ issues[i] = fragmentToIssue(node)
+ }
+
+ // Prepare response with pagination info
+ response := map[string]interface{}{
+ "issues": issues,
+ "pageInfo": map[string]interface{}{
+ "hasNextPage": bool(fragment.PageInfo.HasNextPage),
+ "hasPreviousPage": bool(fragment.PageInfo.HasPreviousPage),
+ "startCursor": string(fragment.PageInfo.StartCursor),
+ "endCursor": string(fragment.PageInfo.EndCursor),
+ },
+ "totalCount": fragment.TotalCount,
+ }
+
+ r, err := json.Marshal(response)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
+func handleGetIssue(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ owner, err := RequiredParam[string](request, "owner")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ repo, err := RequiredParam[string](request, "repo")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ issueNumber, err := RequiredInt(request, "issue_number")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ issue, resp, err := client.Issues.Get(ctx, owner, repo, issueNumber)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get issue: %w", err)
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ r, err := json.Marshal(issue)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
+func handleCreateIssue(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ owner, err := RequiredParam[string](request, "owner")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ repo, err := RequiredParam[string](request, "repo")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ title, err := RequiredParam[string](request, "title")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ // Optional parameters
+ body, err := OptionalParam[string](request, "body")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ assignees, err := OptionalStringArrayParam(request, "assignees")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ labels, err := OptionalStringArrayParam(request, "labels")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ milestone, err := OptionalIntParam(request, "milestone")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ var milestoneNum *int
+ if milestone != 0 {
+ milestoneNum = &milestone
+ }
+
+ issueType, err := OptionalParam[string](request, "type")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ // Create the issue request
+ issueRequest := &github.IssueRequest{
+ Title: github.Ptr(title),
+ Body: github.Ptr(body),
+ Assignees: &assignees,
+ Labels: &labels,
+ Milestone: milestoneNum,
+ }
+
+ // Add custom type field if specified
+ if issueType != "" {
+ if issueRequest.Body == nil {
+ issueRequest.Body = github.Ptr("")
+ }
+ *issueRequest.Body = fmt.Sprintf("\n%s", issueType, *issueRequest.Body)
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ issue, resp, err := client.Issues.Create(ctx, owner, repo, issueRequest)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create issue: %w", err)
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ r, err := json.Marshal(issue)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
+func handleUpdateIssue(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ owner, err := RequiredParam[string](request, "owner")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ repo, err := RequiredParam[string](request, "repo")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ issueNumber, err := RequiredInt(request, "issue_number")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ // Optional parameters
+ title, err := OptionalParam[string](request, "title")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ body, err := OptionalParam[string](request, "body")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ state, err := OptionalParam[string](request, "state")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ assignees, err := OptionalStringArrayParam(request, "assignees")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ labels, err := OptionalStringArrayParam(request, "labels")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ milestone, err := OptionalIntParam(request, "milestone")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ // Create the issue request
+ issueRequest := &github.IssueRequest{}
+
+ if title != "" {
+ issueRequest.Title = github.Ptr(title)
+ }
+ if body != "" {
+ issueRequest.Body = github.Ptr(body)
+ }
+ if state != "" {
+ issueRequest.State = github.Ptr(state)
+ }
+ if len(assignees) > 0 {
+ issueRequest.Assignees = &assignees
+ }
+ if len(labels) > 0 {
+ issueRequest.Labels = &labels
+ }
+ if milestone != 0 {
+ issueRequest.Milestone = &milestone
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ issue, resp, err := client.Issues.Edit(ctx, owner, repo, issueNumber, issueRequest)
+ if err != nil {
+ return nil, fmt.Errorf("failed to update issue: %w", err)
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ r, err := json.Marshal(issue)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
+func handleAddIssueComment(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ owner, err := RequiredParam[string](request, "owner")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ repo, err := RequiredParam[string](request, "repo")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ issueNumber, err := RequiredInt(request, "issue_number")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ body, err := RequiredParam[string](request, "body")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ comment := &github.IssueComment{
+ Body: github.Ptr(body),
+ }
+
+ createdComment, resp, err := client.Issues.CreateComment(ctx, owner, repo, issueNumber, comment)
+ if err != nil {
+ return nil, fmt.Errorf("failed to add comment: %w", err)
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ r, err := json.Marshal(createdComment)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
// IssueFragment represents a fragment of an issue node in the GraphQL API.
type IssueFragment struct {
Number githubv4.Int
diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go
index 63c5594d3..348579d17 100644
--- a/pkg/github/pullrequests.go
+++ b/pkg/github/pullrequests.go
@@ -17,6 +17,437 @@ import (
"github.com/github/github-mcp-server/pkg/translations"
)
+// ManagePullRequest creates a consolidated tool to perform CRUD operations on pull requests
+func ManagePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+ return mcp.NewTool("manage_pull_request",
+ mcp.WithDescription(t("TOOL_MANAGE_PULL_REQUEST_DESCRIPTION", "Manage pull requests with various operations: list, get, create, update, merge")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_MANAGE_PULL_REQUEST", "Manage Pull Request"),
+ ReadOnlyHint: ToBoolPtr(false),
+ }),
+ mcp.WithString("operation",
+ mcp.Required(),
+ mcp.Description("Operation to perform: 'list', 'get', 'create', 'update', 'merge'"),
+ mcp.Enum("list", "get", "create", "update", "merge"),
+ ),
+ mcp.WithString("owner",
+ mcp.Required(),
+ mcp.Description("Repository owner"),
+ ),
+ mcp.WithString("repo",
+ mcp.Required(),
+ mcp.Description("Repository name"),
+ ),
+ // Parameters for get, update, merge operations
+ mcp.WithNumber("pullNumber",
+ mcp.Description("Pull request number (required for 'get', 'update', 'merge' operations)"),
+ ),
+ // Parameters for create operation
+ mcp.WithString("title",
+ mcp.Description("Pull request title (required for 'create' operation)"),
+ ),
+ mcp.WithString("body",
+ mcp.Description("Pull request body content (used for 'create', 'update' operations)"),
+ ),
+ mcp.WithString("head",
+ mcp.Description("The name of the branch where your changes are implemented (required for 'create' operation)"),
+ ),
+ mcp.WithString("base",
+ mcp.Description("The name of the branch you want the changes pulled into (required for 'create' operation)"),
+ ),
+ mcp.WithBoolean("draft",
+ mcp.Description("Whether to create as draft (used for 'create' operation)"),
+ mcp.DefaultBool(false),
+ ),
+ mcp.WithBoolean("maintainer_can_modify",
+ mcp.Description("Whether maintainers can modify the pull request (used for 'create' operation)"),
+ mcp.DefaultBool(true),
+ ),
+ // Parameters for update operation
+ mcp.WithString("state",
+ mcp.Description("State of the pull request (used for 'update' operation): 'open' or 'closed'"),
+ mcp.Enum("open", "closed"),
+ ),
+ // Parameters for merge operation
+ mcp.WithString("commit_title",
+ mcp.Description("Title for the merge commit (used for 'merge' operation)"),
+ ),
+ mcp.WithString("commit_message",
+ mcp.Description("Extra detail to append to merge commit message (used for 'merge' operation)"),
+ ),
+ mcp.WithString("merge_method",
+ mcp.Description("Merge method to use (used for 'merge' operation): 'merge', 'squash', 'rebase'"),
+ mcp.Enum("merge", "squash", "rebase"),
+ mcp.DefaultString("merge"),
+ ),
+ // Parameters for list operation
+ mcp.WithString("list_state",
+ mcp.Description("Filter by state for list operation: 'open', 'closed', 'all'"),
+ mcp.Enum("open", "closed", "all"),
+ mcp.DefaultString("open"),
+ ),
+ mcp.WithString("list_head",
+ mcp.Description("Filter by head user/org and branch for list operation"),
+ ),
+ mcp.WithString("list_base",
+ mcp.Description("Filter by base branch for list operation"),
+ ),
+ mcp.WithString("sort",
+ mcp.Description("Sort by for list operation: 'created', 'updated', 'popularity', 'long-running'"),
+ mcp.Enum("created", "updated", "popularity", "long-running"),
+ mcp.DefaultString("created"),
+ ),
+ mcp.WithString("direction",
+ mcp.Description("Sort direction for list operation: 'asc', 'desc'"),
+ mcp.Enum("asc", "desc"),
+ mcp.DefaultString("desc"),
+ ),
+ WithPagination(),
+ ),
+ func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ operation, err := RequiredParam[string](request, "operation")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ switch operation {
+ case "list":
+ return handleListPullRequests(ctx, getClient, request)
+ case "get":
+ return handleGetPullRequest(ctx, getClient, request)
+ case "create":
+ return handleCreatePullRequest(ctx, getClient, request)
+ case "update":
+ return handleUpdatePullRequest(ctx, getClient, getGQLClient, request)
+ case "merge":
+ return handleMergePullRequest(ctx, getClient, request)
+ default:
+ return mcp.NewToolResultError(fmt.Sprintf("unsupported operation: %s", operation)), nil
+ }
+ }
+}
+
+func handleListPullRequests(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ owner, err := RequiredParam[string](request, "owner")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ repo, err := RequiredParam[string](request, "repo")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ state, err := OptionalParam[string](request, "list_state")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ if state == "" {
+ state = "open"
+ }
+
+ head, err := OptionalParam[string](request, "list_head")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ base, err := OptionalParam[string](request, "list_base")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ sort, err := OptionalParam[string](request, "sort")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ if sort == "" {
+ sort = "created"
+ }
+
+ direction, err := OptionalParam[string](request, "direction")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ if direction == "" {
+ direction = "desc"
+ }
+
+ pagination, err := OptionalPaginationParams(request)
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ opts := &github.PullRequestListOptions{
+ State: state,
+ Head: head,
+ Base: base,
+ Sort: sort,
+ Direction: direction,
+ ListOptions: github.ListOptions{
+ Page: pagination.Page,
+ PerPage: pagination.PerPage,
+ },
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ prs, resp, err := client.PullRequests.List(ctx, owner, repo, opts)
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx,
+ "failed to list pull requests",
+ resp,
+ err,
+ ), nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ r, err := json.Marshal(prs)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
+func handleGetPullRequest(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ owner, err := RequiredParam[string](request, "owner")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ repo, err := RequiredParam[string](request, "repo")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ pullNumber, err := RequiredInt(request, "pullNumber")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+ pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber)
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx,
+ "failed to get pull request",
+ resp,
+ err,
+ ), nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != http.StatusOK {
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response body: %w", err)
+ }
+ return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil
+ }
+
+ r, err := json.Marshal(pr)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
+func handleCreatePullRequest(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ owner, err := RequiredParam[string](request, "owner")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ repo, err := RequiredParam[string](request, "repo")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ title, err := RequiredParam[string](request, "title")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ head, err := RequiredParam[string](request, "head")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ base, err := RequiredParam[string](request, "base")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ body, err := OptionalParam[string](request, "body")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ draft, err := OptionalParam[bool](request, "draft")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ maintainerCanModify, err := OptionalParam[bool](request, "maintainer_can_modify")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ // Create the pull request
+ newPR := &github.NewPullRequest{
+ Title: github.Ptr(title),
+ Head: github.Ptr(head),
+ Base: github.Ptr(base),
+ Body: github.Ptr(body),
+ Draft: github.Ptr(draft),
+ MaintainerCanModify: github.Ptr(maintainerCanModify),
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ pr, resp, err := client.PullRequests.Create(ctx, owner, repo, newPR)
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx,
+ "failed to create pull request",
+ resp,
+ err,
+ ), nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ r, err := json.Marshal(pr)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
+func handleUpdatePullRequest(ctx context.Context, getClient GetClientFn, getGQLClient GetGQLClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ owner, err := RequiredParam[string](request, "owner")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ repo, err := RequiredParam[string](request, "repo")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ pullNumber, err := RequiredInt(request, "pullNumber")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ title, err := OptionalParam[string](request, "title")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ body, err := OptionalParam[string](request, "body")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ state, err := OptionalParam[string](request, "state")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ pull := &github.PullRequest{}
+ if title != "" {
+ pull.Title = github.Ptr(title)
+ }
+ if body != "" {
+ pull.Body = github.Ptr(body)
+ }
+ if state != "" {
+ pull.State = github.Ptr(state)
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ updatedPR, resp, err := client.PullRequests.Edit(ctx, owner, repo, pullNumber, pull)
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx,
+ "failed to update pull request",
+ resp,
+ err,
+ ), nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ r, err := json.Marshal(updatedPR)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
+func handleMergePullRequest(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ owner, err := RequiredParam[string](request, "owner")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ repo, err := RequiredParam[string](request, "repo")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ pullNumber, err := RequiredInt(request, "pullNumber")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ commitTitle, err := OptionalParam[string](request, "commit_title")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ commitMessage, err := OptionalParam[string](request, "commit_message")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ mergeMethod, err := OptionalParam[string](request, "merge_method")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ if mergeMethod == "" {
+ mergeMethod = "merge"
+ }
+
+ options := &github.PullRequestOptions{
+ CommitTitle: commitTitle,
+ MergeMethod: mergeMethod,
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ result, resp, err := client.PullRequests.Merge(ctx, owner, repo, pullNumber, commitMessage, options)
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx,
+ "failed to merge pull request",
+ resp,
+ err,
+ ), nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ r, err := json.Marshal(result)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
// GetPullRequest creates a tool to get details of a specific pull request.
func GetPullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
return mcp.NewTool("get_pull_request",
diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go
index de2c6d01f..9ddc818db 100644
--- a/pkg/github/repositories.go
+++ b/pkg/github/repositories.go
@@ -18,6 +18,299 @@ import (
"github.com/mark3labs/mcp-go/server"
)
+// ManageRepository creates a consolidated tool to perform operations on repositories
+func ManageRepository(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+ return mcp.NewTool("manage_repository",
+ mcp.WithDescription(t("TOOL_MANAGE_REPOSITORY_DESCRIPTION", "Manage repositories with various operations: create, fork, get_file_contents")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_MANAGE_REPOSITORY", "Manage Repository"),
+ ReadOnlyHint: ToBoolPtr(false),
+ }),
+ mcp.WithString("operation",
+ mcp.Required(),
+ mcp.Description("Operation to perform: 'create', 'fork', 'get_file_contents'"),
+ mcp.Enum("create", "fork", "get_file_contents"),
+ ),
+ // Parameters for create operation
+ mcp.WithString("name",
+ mcp.Description("Repository name (required for 'create' operation)"),
+ ),
+ mcp.WithString("description",
+ mcp.Description("Repository description (used for 'create' operation)"),
+ ),
+ mcp.WithBoolean("private",
+ mcp.Description("Whether repo should be private (used for 'create' operation)"),
+ ),
+ mcp.WithBoolean("autoInit",
+ mcp.Description("Initialize with README (used for 'create' operation)"),
+ ),
+ // Parameters for fork operation
+ mcp.WithString("owner",
+ mcp.Description("Repository owner (required for 'fork', 'get_file_contents' operations)"),
+ ),
+ mcp.WithString("repo",
+ mcp.Description("Repository name (required for 'fork', 'get_file_contents' operations)"),
+ ),
+ mcp.WithString("organization",
+ mcp.Description("Organization to fork to (used for 'fork' operation)"),
+ ),
+ mcp.WithString("default_branch_only",
+ mcp.Description("Fork only default branch (used for 'fork' operation)"),
+ ),
+ // Parameters for get_file_contents operation
+ mcp.WithString("path",
+ mcp.Description("Path to file/directory (required for 'get_file_contents' operation)"),
+ mcp.DefaultString("/"),
+ ),
+ mcp.WithString("ref",
+ mcp.Description("Git reference such as branch, tag, or commit SHA (used for 'get_file_contents' operation)"),
+ ),
+ mcp.WithString("sha",
+ mcp.Description("Commit SHA (used for 'get_file_contents' operation)"),
+ ),
+ ),
+ func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ operation, err := RequiredParam[string](request, "operation")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ switch operation {
+ case "create":
+ return handleCreateRepository(ctx, getClient, request)
+ case "fork":
+ return handleForkRepository(ctx, getClient, request)
+ case "get_file_contents":
+ return handleGetFileContents(ctx, getClient, getRawClient, request)
+ default:
+ return mcp.NewToolResultError(fmt.Sprintf("unsupported operation: %s", operation)), nil
+ }
+ }
+}
+
+func handleCreateRepository(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ name, err := RequiredParam[string](request, "name")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ description, err := OptionalParam[string](request, "description")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ private, err := OptionalParam[bool](request, "private")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ autoInit, err := OptionalParam[bool](request, "autoInit")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ repo := &github.Repository{
+ Name: github.Ptr(name),
+ Description: github.Ptr(description),
+ Private: github.Ptr(private),
+ AutoInit: github.Ptr(autoInit),
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+ createdRepo, resp, err := client.Repositories.Create(ctx, "", repo)
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx,
+ "failed to create repository",
+ resp,
+ err,
+ ), nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ r, err := json.Marshal(createdRepo)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
+func handleForkRepository(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ owner, err := RequiredParam[string](request, "owner")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ repo, err := RequiredParam[string](request, "repo")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ organization, err := OptionalParam[string](request, "organization")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ defaultBranchOnly, err := OptionalParam[bool](request, "default_branch_only")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ opts := &github.RepositoryCreateForkOptions{
+ Organization: organization,
+ DefaultBranchOnly: defaultBranchOnly,
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ fork, resp, err := client.Repositories.CreateFork(ctx, owner, repo, opts)
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx,
+ "failed to fork repository",
+ resp,
+ err,
+ ), nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ r, err := json.Marshal(fork)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
+func handleGetFileContents(ctx context.Context, getClient GetClientFn, getRawClient raw.GetRawClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ owner, err := RequiredParam[string](request, "owner")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ repo, err := RequiredParam[string](request, "repo")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ path, err := OptionalParam[string](request, "path")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ if path == "" {
+ path = "/"
+ }
+ ref, err := OptionalParam[string](request, "ref")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ sha, err := OptionalParam[string](request, "sha")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ // Use the existing GetFileContents logic
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ opts := &github.RepositoryContentGetOptions{}
+ if ref != "" {
+ opts.Ref = ref
+ }
+ if sha != "" {
+ opts.Ref = sha
+ }
+
+ // Handle directory listing
+ if strings.HasSuffix(path, "/") {
+ _, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, strings.TrimSuffix(path, "/"), opts)
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx,
+ "failed to get directory contents",
+ resp,
+ err,
+ ), nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ r, err := json.Marshal(dirContent)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+ }
+
+ // Handle file content
+ fileContent, _, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts)
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx,
+ "failed to get file contents",
+ resp,
+ err,
+ ), nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if fileContent == nil {
+ return mcp.NewToolResultError("file not found"), nil
+ }
+
+ // For non-text files, return metadata only
+ if fileContent.Encoding != nil && *fileContent.Encoding == "base64" {
+ // Try to decode and check if it's text
+ if fileContent.Content != nil {
+ decoded, err := base64.StdEncoding.DecodeString(*fileContent.Content)
+ if err == nil && isTextFile(decoded) {
+ response := map[string]interface{}{
+ "name": fileContent.GetName(),
+ "path": fileContent.GetPath(),
+ "sha": fileContent.GetSHA(),
+ "size": fileContent.GetSize(),
+ "url": fileContent.GetURL(),
+ "html_url": fileContent.GetHTMLURL(),
+ "type": fileContent.GetType(),
+ "content": string(decoded),
+ "encoding": "text",
+ }
+
+ r, err := json.Marshal(response)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+ return mcp.NewToolResultText(string(r)), nil
+ }
+ }
+ }
+
+ // Return metadata for binary files or when content can't be decoded
+ r, err := json.Marshal(fileContent)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
+// isTextFile checks if the content is likely to be text
+func isTextFile(content []byte) bool {
+ // Check for null bytes which indicate binary content
+ for _, b := range content[:min(len(content), 1024)] {
+ if b == 0 {
+ return false
+ }
+ }
+ return true
+}
+
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
+
func GetCommit(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_commit",
mcp.WithDescription(t("TOOL_GET_COMMITS_DESCRIPTION", "Get details for a commit from a GitHub repository")),
diff --git a/pkg/github/secret_scanning.go b/pkg/github/secret_scanning.go
index c140c34ad..9e0fe982b 100644
--- a/pkg/github/secret_scanning.go
+++ b/pkg/github/secret_scanning.go
@@ -14,6 +14,158 @@ import (
"github.com/mark3labs/mcp-go/server"
)
+// ManageSecretScanningAlerts creates a consolidated tool to perform operations on secret scanning alerts
+func ManageSecretScanningAlerts(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+ return mcp.NewTool("manage_secret_scanning_alerts",
+ mcp.WithDescription(t("TOOL_MANAGE_SECRET_SCANNING_ALERTS_DESCRIPTION", "Manage secret scanning alerts with various operations: list, get")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_MANAGE_SECRET_SCANNING_ALERTS", "Manage Secret Scanning Alerts"),
+ ReadOnlyHint: ToBoolPtr(true),
+ }),
+ mcp.WithString("operation",
+ mcp.Required(),
+ mcp.Description("Operation to perform: 'list', 'get'"),
+ mcp.Enum("list", "get"),
+ ),
+ mcp.WithString("owner",
+ mcp.Required(),
+ mcp.Description("The owner of the repository"),
+ ),
+ mcp.WithString("repo",
+ mcp.Required(),
+ mcp.Description("The name of the repository"),
+ ),
+ // Parameters for get operation
+ mcp.WithNumber("alertNumber",
+ mcp.Description("The number of the alert (required for 'get' operation)"),
+ ),
+ // Parameters for list operation
+ mcp.WithString("state",
+ mcp.Description("Filter by state (used for 'list' operation)"),
+ mcp.Enum("open", "resolved"),
+ ),
+ mcp.WithString("secret_type",
+ mcp.Description("A comma-separated list of secret types to return (used for 'list' operation)"),
+ ),
+ mcp.WithString("resolution",
+ mcp.Description("Filter by resolution (used for 'list' operation)"),
+ mcp.Enum("false_positive", "wont_fix", "revoked", "pattern_edited", "pattern_deleted", "used_in_tests"),
+ ),
+ ),
+ func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ operation, err := RequiredParam[string](request, "operation")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ switch operation {
+ case "list":
+ return handleListSecretScanningAlerts(ctx, getClient, request)
+ case "get":
+ return handleGetSecretScanningAlert(ctx, getClient, request)
+ default:
+ return mcp.NewToolResultError(fmt.Sprintf("unsupported operation: %s", operation)), nil
+ }
+ }
+}
+
+func handleGetSecretScanningAlert(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ owner, err := RequiredParam[string](request, "owner")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ repo, err := RequiredParam[string](request, "repo")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ alertNumber, err := RequiredInt(request, "alertNumber")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+
+ alert, resp, err := client.SecretScanning.GetAlert(ctx, owner, repo, int64(alertNumber))
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx,
+ fmt.Sprintf("failed to get alert with number '%d'", alertNumber),
+ resp,
+ err,
+ ), nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != http.StatusOK {
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response body: %w", err)
+ }
+ return mcp.NewToolResultError(fmt.Sprintf("failed to get alert: %s", string(body))), nil
+ }
+
+ r, err := json.Marshal(alert)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal alert: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
+func handleListSecretScanningAlerts(ctx context.Context, getClient GetClientFn, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ owner, err := RequiredParam[string](request, "owner")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ repo, err := RequiredParam[string](request, "repo")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ state, err := OptionalParam[string](request, "state")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ secretType, err := OptionalParam[string](request, "secret_type")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ resolution, err := OptionalParam[string](request, "resolution")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+ }
+ alerts, resp, err := client.SecretScanning.ListAlertsForRepo(ctx, owner, repo, &github.SecretScanningAlertListOptions{State: state, SecretType: secretType, Resolution: resolution})
+ if err != nil {
+ return ghErrors.NewGitHubAPIErrorResponse(ctx,
+ fmt.Sprintf("failed to list alerts for repository '%s/%s'", owner, repo),
+ resp,
+ err,
+ ), nil
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != http.StatusOK {
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response body: %w", err)
+ }
+ return mcp.NewToolResultError(fmt.Sprintf("failed to list alerts: %s", string(body))), nil
+ }
+
+ r, err := json.Marshal(alerts)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal alerts: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
+
func GetSecretScanningAlert(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool(
"get_secret_scanning_alert",
diff --git a/pkg/github/secret_scanning_test.go b/pkg/github/secret_scanning_test.go
index ce33fe318..37c36d951 100644
--- a/pkg/github/secret_scanning_test.go
+++ b/pkg/github/secret_scanning_test.go
@@ -247,3 +247,67 @@ func Test_ListSecretScanningAlerts(t *testing.T) {
})
}
}
+
+
+func Test_ManageSecretScanningAlerts(t *testing.T) {
+// Verify tool definition
+mockClient := github.NewClient(nil)
+tool, _ := ManageSecretScanningAlerts(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+
+assert.Equal(t, "manage_secret_scanning_alerts", tool.Name)
+assert.NotEmpty(t, tool.Description)
+assert.Contains(t, tool.InputSchema.Properties, "operation")
+assert.Contains(t, tool.InputSchema.Required, "operation")
+
+t.Run("list operation", func(t *testing.T) {
+// Setup mock alerts for success case
+mockAlerts := []*github.SecretScanningAlert{
+{
+Number: github.Ptr(1),
+State: github.Ptr("open"),
+HTMLURL: github.Ptr("https://github.com/owner/repo/security/secret-scanning/1"),
+},
+}
+
+mockedHTTPClient := mock.NewMockedHTTPClient(
+mock.WithRequestMatch(mock.GetReposSecretScanningAlertsByOwnerByRepo, mockAlerts),
+)
+client := github.NewClient(mockedHTTPClient)
+_, handler := ManageSecretScanningAlerts(stubGetClientFn(client), translations.NullTranslationHelper)
+
+request := createMCPRequest(map[string]interface{}{
+"operation": "list",
+"owner": "testowner",
+"repo": "testrepo",
+})
+
+result, err := handler(context.Background(), request)
+require.NoError(t, err)
+assert.False(t, result.IsError)
+
+var alerts []*github.SecretScanningAlert
+textContent := getTextResult(t, result)
+err = json.Unmarshal([]byte(textContent.Text), &alerts)
+require.NoError(t, err)
+assert.Len(t, alerts, 1)
+assert.Equal(t, 1, *alerts[0].Number)
+})
+
+t.Run("unsupported operation", func(t *testing.T) {
+mockedHTTPClient := mock.NewMockedHTTPClient()
+client := github.NewClient(mockedHTTPClient)
+_, handler := ManageSecretScanningAlerts(stubGetClientFn(client), translations.NullTranslationHelper)
+
+request := createMCPRequest(map[string]interface{}{
+"operation": "delete",
+"owner": "testowner",
+"repo": "testrepo",
+})
+
+result, err := handler(context.Background(), request)
+require.NoError(t, err)
+assert.True(t, result.IsError)
+textContent := getTextResult(t, result)
+assert.Contains(t, textContent.Text, "unsupported operation: delete")
+})
+}
diff --git a/pkg/github/tools.go b/pkg/github/tools.go
index 728d78097..78ce41fbd 100644
--- a/pkg/github/tools.go
+++ b/pkg/github/tools.go
@@ -24,7 +24,6 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
repos := toolsets.NewToolset("repos", "GitHub Repository related tools").
AddReadTools(
toolsets.NewServerTool(SearchRepositories(getClient, t)),
- toolsets.NewServerTool(GetFileContents(getClient, getRawClient, t)),
toolsets.NewServerTool(ListCommits(getClient, t)),
toolsets.NewServerTool(SearchCode(getClient, t)),
toolsets.NewServerTool(GetCommit(getClient, t)),
@@ -36,9 +35,8 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
toolsets.NewServerTool(GetReleaseByTag(getClient, t)),
).
AddWriteTools(
+ toolsets.NewServerTool(ManageRepository(getClient, getRawClient, t)),
toolsets.NewServerTool(CreateOrUpdateFile(getClient, t)),
- toolsets.NewServerTool(CreateRepository(getClient, t)),
- toolsets.NewServerTool(ForkRepository(getClient, t)),
toolsets.NewServerTool(CreateBranch(getClient, t)),
toolsets.NewServerTool(PushFiles(getClient, t)),
toolsets.NewServerTool(DeleteFile(getClient, t)),
@@ -52,17 +50,13 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
)
issues := toolsets.NewToolset("issues", "GitHub Issues related tools").
AddReadTools(
- toolsets.NewServerTool(GetIssue(getClient, t)),
toolsets.NewServerTool(SearchIssues(getClient, t)),
- toolsets.NewServerTool(ListIssues(getGQLClient, t)),
toolsets.NewServerTool(GetIssueComments(getClient, t)),
toolsets.NewServerTool(ListIssueTypes(getClient, t)),
toolsets.NewServerTool(ListSubIssues(getClient, t)),
).
AddWriteTools(
- toolsets.NewServerTool(CreateIssue(getClient, t)),
- toolsets.NewServerTool(AddIssueComment(getClient, t)),
- toolsets.NewServerTool(UpdateIssue(getClient, t)),
+ toolsets.NewServerTool(ManageIssue(getClient, getGQLClient, t)),
toolsets.NewServerTool(AssignCopilotToIssue(getGQLClient, t)),
toolsets.NewServerTool(AddSubIssue(getClient, t)),
toolsets.NewServerTool(RemoveSubIssue(getClient, t)),
@@ -81,8 +75,6 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
)
pullRequests := toolsets.NewToolset("pull_requests", "GitHub Pull Request related tools").
AddReadTools(
- toolsets.NewServerTool(GetPullRequest(getClient, t)),
- toolsets.NewServerTool(ListPullRequests(getClient, t)),
toolsets.NewServerTool(GetPullRequestFiles(getClient, t)),
toolsets.NewServerTool(SearchPullRequests(getClient, t)),
toolsets.NewServerTool(GetPullRequestStatus(getClient, t)),
@@ -91,10 +83,8 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
toolsets.NewServerTool(GetPullRequestDiff(getClient, t)),
).
AddWriteTools(
- toolsets.NewServerTool(MergePullRequest(getClient, t)),
+ toolsets.NewServerTool(ManagePullRequest(getClient, getGQLClient, t)),
toolsets.NewServerTool(UpdatePullRequestBranch(getClient, t)),
- toolsets.NewServerTool(CreatePullRequest(getClient, t)),
- toolsets.NewServerTool(UpdatePullRequest(getClient, getGQLClient, t)),
toolsets.NewServerTool(RequestCopilotReview(getClient, t)),
// Reviews
@@ -106,13 +96,11 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
)
codeSecurity := toolsets.NewToolset("code_security", "Code security related tools, such as GitHub Code Scanning").
AddReadTools(
- toolsets.NewServerTool(GetCodeScanningAlert(getClient, t)),
- toolsets.NewServerTool(ListCodeScanningAlerts(getClient, t)),
+ toolsets.NewServerTool(ManageCodeScanningAlerts(getClient, t)),
)
secretProtection := toolsets.NewToolset("secret_protection", "Secret protection related tools, such as GitHub Secret Scanning").
AddReadTools(
- toolsets.NewServerTool(GetSecretScanningAlert(getClient, t)),
- toolsets.NewServerTool(ListSecretScanningAlerts(getClient, t)),
+ toolsets.NewServerTool(ManageSecretScanningAlerts(getClient, t)),
)
dependabot := toolsets.NewToolset("dependabot", "Dependabot tools").
AddReadTools(
@@ -179,12 +167,8 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
)
gists := toolsets.NewToolset("gists", "GitHub Gist related tools").
- AddReadTools(
- toolsets.NewServerTool(ListGists(getClient, t)),
- ).
AddWriteTools(
- toolsets.NewServerTool(CreateGist(getClient, t)),
- toolsets.NewServerTool(UpdateGist(getClient, t)),
+ toolsets.NewServerTool(ManageGist(getClient, t)),
)
// Add toolsets to the group