From 9a40fd8e62cc533837d4945b71bd399320413544 Mon Sep 17 00:00:00 2001 From: Sangmin Lee Date: Tue, 8 Apr 2025 02:31:50 +0900 Subject: [PATCH 01/41] fix: correct spelling of 'license' --- script/licenses | 2 +- script/licenses-check | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/script/licenses b/script/licenses index f231a458..c7f8ed4c 100755 --- a/script/licenses +++ b/script/licenses @@ -10,7 +10,7 @@ trap "rm -fr ${TEMPDIR}" EXIT for goos in linux darwin windows ; do # Note: we ignore warnings because we want the command to succeed, however the output should be checked - # for any new warnings, and potentially we may need to add licence information. + # for any new warnings, and potentially we may need to add license information. # # Normally these warnings are packages containing non go code, which may or may not require explicit attribution, # depending on the license. diff --git a/script/licenses-check b/script/licenses-check index 369277ca..5ad93027 100755 --- a/script/licenses-check +++ b/script/licenses-check @@ -4,7 +4,7 @@ go install github.com/google/go-licenses@latest for goos in linux darwin windows ; do # Note: we ignore warnings because we want the command to succeed, however the output should be checked - # for any new warnings, and potentially we may need to add licence information. + # for any new warnings, and potentially we may need to add license information. # # Normally these warnings are packages containing non go code, which may or may not require explicit attribution, # depending on the license. From 973fb5d5c87fb36f8f9f532df1ba685807806ef6 Mon Sep 17 00:00:00 2001 From: Yuki Ito Date: Tue, 8 Apr 2025 14:23:11 +0900 Subject: [PATCH 02/41] chore: Remove unnecessary trailing periods from descriptions (#170) --- cmd/mcpcurl/README.md | 4 ++-- pkg/github/issues.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/mcpcurl/README.md b/cmd/mcpcurl/README.md index 95e1339a..0104a1b3 100644 --- a/cmd/mcpcurl/README.md +++ b/cmd/mcpcurl/README.md @@ -49,7 +49,7 @@ Available Commands: create_repository Create a new GitHub repository in your account fork_repository Fork a GitHub repository to your account or specified organization get_file_contents Get the contents of a file or directory from a GitHub repository - get_issue Get details of a specific issue in a GitHub repository. + get_issue Get details of a specific issue in a GitHub repository get_issue_comments Get comments for a GitHub issue list_commits Get list of commits of a branch in a GitHub repository list_issues List issues in a GitHub repository with filtering options @@ -74,7 +74,7 @@ Get help for a specific tool: ```bash % ./mcpcurl --stdio-server-cmd "docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN mcp/github" tools get_issue --help -Get details of a specific issue in a GitHub repository. +Get details of a specific issue in a GitHub repository Usage: mcpcurl tools get_issue [flags] diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 1632e9e8..5367622d 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -17,18 +17,18 @@ import ( // getIssue creates a tool to get details of a specific issue in a GitHub repository. func getIssue(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_issue", - mcp.WithDescription(t("TOOL_GET_ISSUE_DESCRIPTION", "Get details of a specific issue in a GitHub repository.")), + mcp.WithDescription(t("TOOL_GET_ISSUE_DESCRIPTION", "Get details of a specific issue in a GitHub repository")), mcp.WithString("owner", mcp.Required(), - mcp.Description("The owner of the repository."), + mcp.Description("The owner of the repository"), ), mcp.WithString("repo", mcp.Required(), - mcp.Description("The name of the repository."), + mcp.Description("The name of the repository"), ), mcp.WithNumber("issue_number", mcp.Required(), - mcp.Description("The number of the issue."), + mcp.Description("The number of the issue"), ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { From 9a5fe115d837eca576e38b323ec1c3d824807acd Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Mon, 7 Apr 2025 23:07:56 +0200 Subject: [PATCH 03/41] chore: export code scanning funcs --- pkg/github/code_scanning.go | 4 ++-- pkg/github/code_scanning_test.go | 8 ++++---- pkg/github/server.go | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/github/code_scanning.go b/pkg/github/code_scanning.go index 81ee2c31..1b9cd876 100644 --- a/pkg/github/code_scanning.go +++ b/pkg/github/code_scanning.go @@ -13,7 +13,7 @@ import ( "github.com/mark3labs/mcp-go/server" ) -func getCodeScanningAlert(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func GetCodeScanningAlert(client *github.Client, 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.")), mcp.WithString("owner", @@ -66,7 +66,7 @@ func getCodeScanningAlert(client *github.Client, t translations.TranslationHelpe } } -func listCodeScanningAlerts(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func ListCodeScanningAlerts(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_code_scanning_alerts", mcp.WithDescription(t("TOOL_LIST_CODE_SCANNING_ALERTS_DESCRIPTION", "List code scanning alerts in a GitHub repository.")), mcp.WithString("owner", diff --git a/pkg/github/code_scanning_test.go b/pkg/github/code_scanning_test.go index ec4d671e..f1f3a1de 100644 --- a/pkg/github/code_scanning_test.go +++ b/pkg/github/code_scanning_test.go @@ -16,7 +16,7 @@ import ( func Test_GetCodeScanningAlert(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := getCodeScanningAlert(mockClient, translations.NullTranslationHelper) + tool, _ := GetCodeScanningAlert(mockClient, translations.NullTranslationHelper) assert.Equal(t, "get_code_scanning_alert", tool.Name) assert.NotEmpty(t, tool.Description) @@ -82,7 +82,7 @@ func Test_GetCodeScanningAlert(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := getCodeScanningAlert(client, translations.NullTranslationHelper) + _, handler := GetCodeScanningAlert(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -118,7 +118,7 @@ func Test_GetCodeScanningAlert(t *testing.T) { func Test_ListCodeScanningAlerts(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := listCodeScanningAlerts(mockClient, translations.NullTranslationHelper) + tool, _ := ListCodeScanningAlerts(mockClient, translations.NullTranslationHelper) assert.Equal(t, "list_code_scanning_alerts", tool.Name) assert.NotEmpty(t, tool.Description) @@ -201,7 +201,7 @@ func Test_ListCodeScanningAlerts(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := listCodeScanningAlerts(client, translations.NullTranslationHelper) + _, handler := ListCodeScanningAlerts(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) diff --git a/pkg/github/server.go b/pkg/github/server.go index bf3583b9..ec485a74 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -75,8 +75,8 @@ func NewServer(client *github.Client, version string, readOnly bool, t translati s.AddTool(getMe(client, t)) // Add GitHub tools - Code Scanning - s.AddTool(getCodeScanningAlert(client, t)) - s.AddTool(listCodeScanningAlerts(client, t)) + s.AddTool(GetCodeScanningAlert(client, t)) + s.AddTool(ListCodeScanningAlerts(client, t)) return s } From f4770fa8a25ad6a71a1882f3587bd68de801666e Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Mon, 7 Apr 2025 23:22:56 +0200 Subject: [PATCH 04/41] chore: export issues funcs --- pkg/github/issues.go | 28 ++++++++++++++-------------- pkg/github/issues_test.go | 28 ++++++++++++++-------------- pkg/github/server.go | 14 +++++++------- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 5367622d..5836dedd 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -14,8 +14,8 @@ import ( "github.com/mark3labs/mcp-go/server" ) -// getIssue creates a tool to get details of a specific issue in a GitHub repository. -func getIssue(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// GetIssue creates a tool to get details of a specific issue in a GitHub repository. +func GetIssue(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_issue", mcp.WithDescription(t("TOOL_GET_ISSUE_DESCRIPTION", "Get details of a specific issue in a GitHub repository")), mcp.WithString("owner", @@ -68,8 +68,8 @@ func getIssue(client *github.Client, t translations.TranslationHelperFunc) (tool } } -// addIssueComment creates a tool to add a comment to an issue. -func addIssueComment(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// AddIssueComment creates a tool to add a comment to an issue. +func AddIssueComment(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("add_issue_comment", mcp.WithDescription(t("TOOL_ADD_ISSUE_COMMENT_DESCRIPTION", "Add a comment to an existing issue")), mcp.WithString("owner", @@ -134,8 +134,8 @@ func addIssueComment(client *github.Client, t translations.TranslationHelperFunc } } -// searchIssues creates a tool to search for issues and pull requests. -func searchIssues(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// SearchIssues creates a tool to search for issues and pull requests. +func SearchIssues(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("search_issues", mcp.WithDescription(t("TOOL_SEARCH_ISSUES_DESCRIPTION", "Search for issues and pull requests across GitHub repositories")), mcp.WithString("q", @@ -214,8 +214,8 @@ func searchIssues(client *github.Client, t translations.TranslationHelperFunc) ( } } -// createIssue creates a tool to create a new issue in a GitHub repository. -func createIssue(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// CreateIssue creates a tool to create a new issue in a GitHub repository. +func CreateIssue(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("create_issue", mcp.WithDescription(t("TOOL_CREATE_ISSUE_DESCRIPTION", "Create a new issue in a GitHub repository")), mcp.WithString("owner", @@ -328,8 +328,8 @@ func createIssue(client *github.Client, t translations.TranslationHelperFunc) (t } } -// listIssues creates a tool to list and filter repository issues -func listIssues(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// ListIssues creates a tool to list and filter repository issues +func ListIssues(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_issues", mcp.WithDescription(t("TOOL_LIST_ISSUES_DESCRIPTION", "List issues in a GitHub repository with filtering options")), mcp.WithString("owner", @@ -442,8 +442,8 @@ func listIssues(client *github.Client, t translations.TranslationHelperFunc) (to } } -// updateIssue creates a tool to update an existing issue in a GitHub repository. -func updateIssue(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// UpdateIssue creates a tool to update an existing issue in a GitHub repository. +func UpdateIssue(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("update_issue", mcp.WithDescription(t("TOOL_UPDATE_ISSUE_DESCRIPTION", "Update an existing issue in a GitHub repository")), mcp.WithString("owner", @@ -580,8 +580,8 @@ func updateIssue(client *github.Client, t translations.TranslationHelperFunc) (t } } -// getIssueComments creates a tool to get comments for a GitHub issue. -func getIssueComments(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// GetIssueComments creates a tool to get comments for a GitHub issue. +func GetIssueComments(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_issue_comments", mcp.WithDescription(t("TOOL_GET_ISSUE_COMMENTS_DESCRIPTION", "Get comments for a GitHub issue")), mcp.WithString("owner", diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 485169fd..04a2ae19 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -18,7 +18,7 @@ import ( func Test_GetIssue(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := getIssue(mockClient, translations.NullTranslationHelper) + tool, _ := GetIssue(mockClient, translations.NullTranslationHelper) assert.Equal(t, "get_issue", tool.Name) assert.NotEmpty(t, tool.Description) @@ -82,7 +82,7 @@ func Test_GetIssue(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := getIssue(client, translations.NullTranslationHelper) + _, handler := GetIssue(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -114,7 +114,7 @@ func Test_GetIssue(t *testing.T) { func Test_AddIssueComment(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := addIssueComment(mockClient, translations.NullTranslationHelper) + tool, _ := AddIssueComment(mockClient, translations.NullTranslationHelper) assert.Equal(t, "add_issue_comment", tool.Name) assert.NotEmpty(t, tool.Description) @@ -185,7 +185,7 @@ func Test_AddIssueComment(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := addIssueComment(client, translations.NullTranslationHelper) + _, handler := AddIssueComment(client, translations.NullTranslationHelper) // Create call request request := mcp.CallToolRequest{ @@ -237,7 +237,7 @@ func Test_AddIssueComment(t *testing.T) { func Test_SearchIssues(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := searchIssues(mockClient, translations.NullTranslationHelper) + tool, _ := SearchIssues(mockClient, translations.NullTranslationHelper) assert.Equal(t, "search_issues", tool.Name) assert.NotEmpty(t, tool.Description) @@ -352,7 +352,7 @@ func Test_SearchIssues(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := searchIssues(client, translations.NullTranslationHelper) + _, handler := SearchIssues(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -393,7 +393,7 @@ func Test_SearchIssues(t *testing.T) { func Test_CreateIssue(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := createIssue(mockClient, translations.NullTranslationHelper) + tool, _ := CreateIssue(mockClient, translations.NullTranslationHelper) assert.Equal(t, "create_issue", tool.Name) assert.NotEmpty(t, tool.Description) @@ -505,7 +505,7 @@ func Test_CreateIssue(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := createIssue(client, translations.NullTranslationHelper) + _, handler := CreateIssue(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -566,7 +566,7 @@ func Test_CreateIssue(t *testing.T) { func Test_ListIssues(t *testing.T) { // Verify tool definition mockClient := github.NewClient(nil) - tool, _ := listIssues(mockClient, translations.NullTranslationHelper) + tool, _ := ListIssues(mockClient, translations.NullTranslationHelper) assert.Equal(t, "list_issues", tool.Name) assert.NotEmpty(t, tool.Description) @@ -697,7 +697,7 @@ func Test_ListIssues(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := listIssues(client, translations.NullTranslationHelper) + _, handler := ListIssues(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -742,7 +742,7 @@ func Test_ListIssues(t *testing.T) { func Test_UpdateIssue(t *testing.T) { // Verify tool definition mockClient := github.NewClient(nil) - tool, _ := updateIssue(mockClient, translations.NullTranslationHelper) + tool, _ := UpdateIssue(mockClient, translations.NullTranslationHelper) assert.Equal(t, "update_issue", tool.Name) assert.NotEmpty(t, tool.Description) @@ -881,7 +881,7 @@ func Test_UpdateIssue(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := updateIssue(client, translations.NullTranslationHelper) + _, handler := UpdateIssue(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -999,7 +999,7 @@ func Test_ParseISOTimestamp(t *testing.T) { func Test_GetIssueComments(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := getIssueComments(mockClient, translations.NullTranslationHelper) + tool, _ := GetIssueComments(mockClient, translations.NullTranslationHelper) assert.Equal(t, "get_issue_comments", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1099,7 +1099,7 @@ func Test_GetIssueComments(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := getIssueComments(client, translations.NullTranslationHelper) + _, handler := GetIssueComments(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) diff --git a/pkg/github/server.go b/pkg/github/server.go index ec485a74..7ece3553 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -31,14 +31,14 @@ func NewServer(client *github.Client, version string, readOnly bool, t translati s.AddResourceTemplate(getRepositoryResourcePrContent(client, t)) // Add GitHub tools - Issues - s.AddTool(getIssue(client, t)) - s.AddTool(searchIssues(client, t)) - s.AddTool(listIssues(client, t)) - s.AddTool(getIssueComments(client, t)) + s.AddTool(GetIssue(client, t)) + s.AddTool(SearchIssues(client, t)) + s.AddTool(ListIssues(client, t)) + s.AddTool(GetIssueComments(client, t)) if !readOnly { - s.AddTool(createIssue(client, t)) - s.AddTool(addIssueComment(client, t)) - s.AddTool(updateIssue(client, t)) + s.AddTool(CreateIssue(client, t)) + s.AddTool(AddIssueComment(client, t)) + s.AddTool(UpdateIssue(client, t)) } // Add GitHub tools - Pull Requests From c85dd07e5edf43964b268fda2a621aa2a0188b58 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Mon, 7 Apr 2025 23:45:12 +0200 Subject: [PATCH 05/41] chore: export pr funcs --- pkg/github/pullrequests.go | 40 ++++++++++++++++----------------- pkg/github/pullrequests_test.go | 40 ++++++++++++++++----------------- pkg/github/server.go | 20 ++++++++--------- 3 files changed, 50 insertions(+), 50 deletions(-) diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index 25090cb7..a5cd86b4 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -13,8 +13,8 @@ import ( "github.com/mark3labs/mcp-go/server" ) -// getPullRequest creates a tool to get details of a specific pull request. -func getPullRequest(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// GetPullRequest creates a tool to get details of a specific pull request. +func GetPullRequest(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_pull_request", mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_DESCRIPTION", "Get details of a specific pull request")), mcp.WithString("owner", @@ -67,8 +67,8 @@ func getPullRequest(client *github.Client, t translations.TranslationHelperFunc) } } -// listPullRequests creates a tool to list and filter repository pull requests. -func listPullRequests(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// ListPullRequests creates a tool to list and filter repository pull requests. +func ListPullRequests(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_pull_requests", mcp.WithDescription(t("TOOL_LIST_PULL_REQUESTS_DESCRIPTION", "List and filter repository pull requests")), mcp.WithString("owner", @@ -165,8 +165,8 @@ func listPullRequests(client *github.Client, t translations.TranslationHelperFun } } -// mergePullRequest creates a tool to merge a pull request. -func mergePullRequest(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// MergePullRequest creates a tool to merge a pull request. +func MergePullRequest(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("merge_pull_request", mcp.WithDescription(t("TOOL_MERGE_PULL_REQUEST_DESCRIPTION", "Merge a pull request")), mcp.WithString("owner", @@ -245,8 +245,8 @@ func mergePullRequest(client *github.Client, t translations.TranslationHelperFun } } -// getPullRequestFiles creates a tool to get the list of files changed in a pull request. -func getPullRequestFiles(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// GetPullRequestFiles creates a tool to get the list of files changed in a pull request. +func GetPullRequestFiles(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_pull_request_files", mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_FILES_DESCRIPTION", "Get the list of files changed in a pull request")), mcp.WithString("owner", @@ -300,8 +300,8 @@ func getPullRequestFiles(client *github.Client, t translations.TranslationHelper } } -// getPullRequestStatus creates a tool to get the combined status of all status checks for a pull request. -func getPullRequestStatus(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// GetPullRequestStatus creates a tool to get the combined status of all status checks for a pull request. +func GetPullRequestStatus(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_pull_request_status", mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_STATUS_DESCRIPTION", "Get the combined status of all status checks for a pull request")), mcp.WithString("owner", @@ -369,8 +369,8 @@ func getPullRequestStatus(client *github.Client, t translations.TranslationHelpe } } -// updatePullRequestBranch creates a tool to update a pull request branch with the latest changes from the base branch. -func updatePullRequestBranch(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// UpdatePullRequestBranch creates a tool to update a pull request branch with the latest changes from the base branch. +func UpdatePullRequestBranch(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("update_pull_request_branch", mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_BRANCH_DESCRIPTION", "Update a pull request branch with the latest changes from the base branch")), mcp.WithString("owner", @@ -439,8 +439,8 @@ func updatePullRequestBranch(client *github.Client, t translations.TranslationHe } } -// getPullRequestComments creates a tool to get the review comments on a pull request. -func getPullRequestComments(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// GetPullRequestComments creates a tool to get the review comments on a pull request. +func GetPullRequestComments(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_pull_request_comments", mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_COMMENTS_DESCRIPTION", "Get the review comments on a pull request")), mcp.WithString("owner", @@ -499,8 +499,8 @@ func getPullRequestComments(client *github.Client, t translations.TranslationHel } } -// getPullRequestReviews creates a tool to get the reviews on a pull request. -func getPullRequestReviews(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// GetPullRequestReviews creates a tool to get the reviews on a pull request. +func GetPullRequestReviews(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_pull_request_reviews", mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_REVIEWS_DESCRIPTION", "Get the reviews on a pull request")), mcp.WithString("owner", @@ -553,8 +553,8 @@ func getPullRequestReviews(client *github.Client, t translations.TranslationHelp } } -// createPullRequestReview creates a tool to submit a review on a pull request. -func createPullRequestReview(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// CreatePullRequestReview creates a tool to submit a review on a pull request. +func CreatePullRequestReview(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("create_pull_request_review", mcp.WithDescription(t("TOOL_CREATE_PULL_REQUEST_REVIEW_DESCRIPTION", "Create a review on a pull request")), mcp.WithString("owner", @@ -745,8 +745,8 @@ func createPullRequestReview(client *github.Client, t translations.TranslationHe } } -// createPullRequest creates a tool to create a new pull request. -func createPullRequest(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// CreatePullRequest creates a tool to create a new pull request. +func CreatePullRequest(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("create_pull_request", mcp.WithDescription(t("TOOL_CREATE_PULL_REQUEST_DESCRIPTION", "Create a new pull request in a GitHub repository")), mcp.WithString("owner", diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go index cf1afcdc..34c41cc7 100644 --- a/pkg/github/pullrequests_test.go +++ b/pkg/github/pullrequests_test.go @@ -17,7 +17,7 @@ import ( func Test_GetPullRequest(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := getPullRequest(mockClient, translations.NullTranslationHelper) + tool, _ := GetPullRequest(mockClient, translations.NullTranslationHelper) assert.Equal(t, "get_pull_request", tool.Name) assert.NotEmpty(t, tool.Description) @@ -94,7 +94,7 @@ func Test_GetPullRequest(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := getPullRequest(client, translations.NullTranslationHelper) + _, handler := GetPullRequest(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -129,7 +129,7 @@ func Test_GetPullRequest(t *testing.T) { func Test_ListPullRequests(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := listPullRequests(mockClient, translations.NullTranslationHelper) + tool, _ := ListPullRequests(mockClient, translations.NullTranslationHelper) assert.Equal(t, "list_pull_requests", tool.Name) assert.NotEmpty(t, tool.Description) @@ -221,7 +221,7 @@ func Test_ListPullRequests(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := listPullRequests(client, translations.NullTranslationHelper) + _, handler := ListPullRequests(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -259,7 +259,7 @@ func Test_ListPullRequests(t *testing.T) { func Test_MergePullRequest(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := mergePullRequest(mockClient, translations.NullTranslationHelper) + tool, _ := MergePullRequest(mockClient, translations.NullTranslationHelper) assert.Equal(t, "merge_pull_request", tool.Name) assert.NotEmpty(t, tool.Description) @@ -336,7 +336,7 @@ func Test_MergePullRequest(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := mergePullRequest(client, translations.NullTranslationHelper) + _, handler := MergePullRequest(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -370,7 +370,7 @@ func Test_MergePullRequest(t *testing.T) { func Test_GetPullRequestFiles(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := getPullRequestFiles(mockClient, translations.NullTranslationHelper) + tool, _ := GetPullRequestFiles(mockClient, translations.NullTranslationHelper) assert.Equal(t, "get_pull_request_files", tool.Name) assert.NotEmpty(t, tool.Description) @@ -448,7 +448,7 @@ func Test_GetPullRequestFiles(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := getPullRequestFiles(client, translations.NullTranslationHelper) + _, handler := GetPullRequestFiles(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -486,7 +486,7 @@ func Test_GetPullRequestFiles(t *testing.T) { func Test_GetPullRequestStatus(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := getPullRequestStatus(mockClient, translations.NullTranslationHelper) + tool, _ := GetPullRequestStatus(mockClient, translations.NullTranslationHelper) assert.Equal(t, "get_pull_request_status", tool.Name) assert.NotEmpty(t, tool.Description) @@ -608,7 +608,7 @@ func Test_GetPullRequestStatus(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := getPullRequestStatus(client, translations.NullTranslationHelper) + _, handler := GetPullRequestStatus(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -647,7 +647,7 @@ func Test_GetPullRequestStatus(t *testing.T) { func Test_UpdatePullRequestBranch(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := updatePullRequestBranch(mockClient, translations.NullTranslationHelper) + tool, _ := UpdatePullRequestBranch(mockClient, translations.NullTranslationHelper) assert.Equal(t, "update_pull_request_branch", tool.Name) assert.NotEmpty(t, tool.Description) @@ -735,7 +735,7 @@ func Test_UpdatePullRequestBranch(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := updatePullRequestBranch(client, translations.NullTranslationHelper) + _, handler := UpdatePullRequestBranch(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -763,7 +763,7 @@ func Test_UpdatePullRequestBranch(t *testing.T) { func Test_GetPullRequestComments(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := getPullRequestComments(mockClient, translations.NullTranslationHelper) + tool, _ := GetPullRequestComments(mockClient, translations.NullTranslationHelper) assert.Equal(t, "get_pull_request_comments", tool.Name) assert.NotEmpty(t, tool.Description) @@ -851,7 +851,7 @@ func Test_GetPullRequestComments(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := getPullRequestComments(client, translations.NullTranslationHelper) + _, handler := GetPullRequestComments(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -890,7 +890,7 @@ func Test_GetPullRequestComments(t *testing.T) { func Test_GetPullRequestReviews(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := getPullRequestReviews(mockClient, translations.NullTranslationHelper) + tool, _ := GetPullRequestReviews(mockClient, translations.NullTranslationHelper) assert.Equal(t, "get_pull_request_reviews", tool.Name) assert.NotEmpty(t, tool.Description) @@ -974,7 +974,7 @@ func Test_GetPullRequestReviews(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := getPullRequestReviews(client, translations.NullTranslationHelper) + _, handler := GetPullRequestReviews(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -1013,7 +1013,7 @@ func Test_GetPullRequestReviews(t *testing.T) { func Test_CreatePullRequestReview(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := createPullRequestReview(mockClient, translations.NullTranslationHelper) + tool, _ := CreatePullRequestReview(mockClient, translations.NullTranslationHelper) assert.Equal(t, "create_pull_request_review", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1341,7 +1341,7 @@ func Test_CreatePullRequestReview(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := createPullRequestReview(client, translations.NullTranslationHelper) + _, handler := CreatePullRequestReview(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -1384,7 +1384,7 @@ func Test_CreatePullRequestReview(t *testing.T) { func Test_CreatePullRequest(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := createPullRequest(mockClient, translations.NullTranslationHelper) + tool, _ := CreatePullRequest(mockClient, translations.NullTranslationHelper) assert.Equal(t, "create_pull_request", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1496,7 +1496,7 @@ func Test_CreatePullRequest(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := createPullRequest(client, translations.NullTranslationHelper) + _, handler := CreatePullRequest(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) diff --git a/pkg/github/server.go b/pkg/github/server.go index 7ece3553..e5fdac2f 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -42,17 +42,17 @@ func NewServer(client *github.Client, version string, readOnly bool, t translati } // Add GitHub tools - Pull Requests - s.AddTool(getPullRequest(client, t)) - s.AddTool(listPullRequests(client, t)) - s.AddTool(getPullRequestFiles(client, t)) - s.AddTool(getPullRequestStatus(client, t)) - s.AddTool(getPullRequestComments(client, t)) - s.AddTool(getPullRequestReviews(client, t)) + s.AddTool(GetPullRequest(client, t)) + s.AddTool(ListPullRequests(client, t)) + s.AddTool(GetPullRequestFiles(client, t)) + s.AddTool(GetPullRequestStatus(client, t)) + s.AddTool(GetPullRequestComments(client, t)) + s.AddTool(GetPullRequestReviews(client, t)) if !readOnly { - s.AddTool(mergePullRequest(client, t)) - s.AddTool(updatePullRequestBranch(client, t)) - s.AddTool(createPullRequestReview(client, t)) - s.AddTool(createPullRequest(client, t)) + s.AddTool(MergePullRequest(client, t)) + s.AddTool(UpdatePullRequestBranch(client, t)) + s.AddTool(CreatePullRequestReview(client, t)) + s.AddTool(CreatePullRequest(client, t)) } // Add GitHub tools - Repositories From c1bdd6a0f0ae45be545589450f934d2f203d8c8c Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Tue, 8 Apr 2025 00:28:20 +0200 Subject: [PATCH 06/41] chore: export repository funcs --- pkg/github/repositories.go | 28 +++++++++++----------- pkg/github/repositories_test.go | 28 +++++++++++----------- pkg/github/repository_resource.go | 33 +++++++++++++------------- pkg/github/repository_resource_test.go | 22 ++++++++--------- pkg/github/server.go | 24 +++++++++---------- 5 files changed, 68 insertions(+), 67 deletions(-) diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 5b8725d1..7725438b 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -13,8 +13,8 @@ import ( "github.com/mark3labs/mcp-go/server" ) -// listCommits creates a tool to get commits of a branch in a repository. -func listCommits(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// ListCommits creates a tool to get commits of a branch in a repository. +func ListCommits(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_commits", mcp.WithDescription(t("TOOL_LIST_COMMITS_DESCRIPTION", "Get list of commits of a branch in a GitHub repository")), mcp.WithString("owner", @@ -79,8 +79,8 @@ func listCommits(client *github.Client, t translations.TranslationHelperFunc) (t } } -// createOrUpdateFile creates a tool to create or update a file in a GitHub repository. -func createOrUpdateFile(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// CreateOrUpdateFile creates a tool to create or update a file in a GitHub repository. +func CreateOrUpdateFile(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("create_or_update_file", mcp.WithDescription(t("TOOL_CREATE_OR_UPDATE_FILE_DESCRIPTION", "Create or update a single file in a GitHub repository")), mcp.WithString("owner", @@ -180,8 +180,8 @@ func createOrUpdateFile(client *github.Client, t translations.TranslationHelperF } } -// createRepository creates a tool to create a new GitHub repository. -func createRepository(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// CreateRepository creates a tool to create a new GitHub repository. +func CreateRepository(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("create_repository", mcp.WithDescription(t("TOOL_CREATE_REPOSITORY_DESCRIPTION", "Create a new GitHub repository in your account")), mcp.WithString("name", @@ -246,8 +246,8 @@ func createRepository(client *github.Client, t translations.TranslationHelperFun } } -// getFileContents creates a tool to get the contents of a file or directory from a GitHub repository. -func getFileContents(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// GetFileContents creates a tool to get the contents of a file or directory from a GitHub repository. +func GetFileContents(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_file_contents", mcp.WithDescription(t("TOOL_GET_FILE_CONTENTS_DESCRIPTION", "Get the contents of a file or directory from a GitHub repository")), mcp.WithString("owner", @@ -315,8 +315,8 @@ func getFileContents(client *github.Client, t translations.TranslationHelperFunc } } -// forkRepository creates a tool to fork a repository. -func forkRepository(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// ForkRepository creates a tool to fork a repository. +func ForkRepository(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("fork_repository", mcp.WithDescription(t("TOOL_FORK_REPOSITORY_DESCRIPTION", "Fork a GitHub repository to your account or specified organization")), mcp.WithString("owner", @@ -378,8 +378,8 @@ func forkRepository(client *github.Client, t translations.TranslationHelperFunc) } } -// createBranch creates a tool to create a new branch. -func createBranch(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// CreateBranch creates a tool to create a new branch. +func CreateBranch(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("create_branch", mcp.WithDescription(t("TOOL_CREATE_BRANCH_DESCRIPTION", "Create a new branch in a GitHub repository")), mcp.WithString("owner", @@ -458,8 +458,8 @@ func createBranch(client *github.Client, t translations.TranslationHelperFunc) ( } } -// pushFiles creates a tool to push multiple files in a single commit to a GitHub repository. -func pushFiles(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// PushFiles creates a tool to push multiple files in a single commit to a GitHub repository. +func PushFiles(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("push_files", mcp.WithDescription(t("TOOL_PUSH_FILES_DESCRIPTION", "Push multiple files to a GitHub repository in a single commit")), mcp.WithString("owner", diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index f7ed8e71..5c47183d 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -18,7 +18,7 @@ import ( func Test_GetFileContents(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := getFileContents(mockClient, translations.NullTranslationHelper) + tool, _ := GetFileContents(mockClient, translations.NullTranslationHelper) assert.Equal(t, "get_file_contents", tool.Name) assert.NotEmpty(t, tool.Description) @@ -132,7 +132,7 @@ func Test_GetFileContents(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := getFileContents(client, translations.NullTranslationHelper) + _, handler := GetFileContents(client, translations.NullTranslationHelper) // Create call request request := mcp.CallToolRequest{ @@ -189,7 +189,7 @@ func Test_GetFileContents(t *testing.T) { func Test_ForkRepository(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := forkRepository(mockClient, translations.NullTranslationHelper) + tool, _ := ForkRepository(mockClient, translations.NullTranslationHelper) assert.Equal(t, "fork_repository", tool.Name) assert.NotEmpty(t, tool.Description) @@ -259,7 +259,7 @@ func Test_ForkRepository(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := forkRepository(client, translations.NullTranslationHelper) + _, handler := ForkRepository(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -287,7 +287,7 @@ func Test_ForkRepository(t *testing.T) { func Test_CreateBranch(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := createBranch(mockClient, translations.NullTranslationHelper) + tool, _ := CreateBranch(mockClient, translations.NullTranslationHelper) assert.Equal(t, "create_branch", tool.Name) assert.NotEmpty(t, tool.Description) @@ -445,7 +445,7 @@ func Test_CreateBranch(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := createBranch(client, translations.NullTranslationHelper) + _, handler := CreateBranch(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -478,7 +478,7 @@ func Test_CreateBranch(t *testing.T) { func Test_ListCommits(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := listCommits(mockClient, translations.NullTranslationHelper) + tool, _ := ListCommits(mockClient, translations.NullTranslationHelper) assert.Equal(t, "list_commits", tool.Name) assert.NotEmpty(t, tool.Description) @@ -614,7 +614,7 @@ func Test_ListCommits(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := listCommits(client, translations.NullTranslationHelper) + _, handler := ListCommits(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -652,7 +652,7 @@ func Test_ListCommits(t *testing.T) { func Test_CreateOrUpdateFile(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := createOrUpdateFile(mockClient, translations.NullTranslationHelper) + tool, _ := CreateOrUpdateFile(mockClient, translations.NullTranslationHelper) assert.Equal(t, "create_or_update_file", tool.Name) assert.NotEmpty(t, tool.Description) @@ -775,7 +775,7 @@ func Test_CreateOrUpdateFile(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := createOrUpdateFile(client, translations.NullTranslationHelper) + _, handler := CreateOrUpdateFile(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -815,7 +815,7 @@ func Test_CreateOrUpdateFile(t *testing.T) { func Test_CreateRepository(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := createRepository(mockClient, translations.NullTranslationHelper) + tool, _ := CreateRepository(mockClient, translations.NullTranslationHelper) assert.Equal(t, "create_repository", tool.Name) assert.NotEmpty(t, tool.Description) @@ -923,7 +923,7 @@ func Test_CreateRepository(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := createRepository(client, translations.NullTranslationHelper) + _, handler := CreateRepository(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -961,7 +961,7 @@ func Test_CreateRepository(t *testing.T) { func Test_PushFiles(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := pushFiles(mockClient, translations.NullTranslationHelper) + tool, _ := PushFiles(mockClient, translations.NullTranslationHelper) assert.Equal(t, "push_files", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1256,7 +1256,7 @@ func Test_PushFiles(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := pushFiles(client, translations.NullTranslationHelper) + _, handler := PushFiles(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index 8b2ba7a7..47cb8bf6 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -17,52 +17,53 @@ import ( "github.com/mark3labs/mcp-go/server" ) -// getRepositoryResourceContent defines the resource template and handler for getting repository content. -func getRepositoryResourceContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +// GetRepositoryResourceContent defines the resource template and handler for getting repository content. +func GetRepositoryResourceContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { return mcp.NewResourceTemplate( "repo://{owner}/{repo}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_DESCRIPTION", "Repository Content"), ), - repositoryResourceContentsHandler(client) + RepositoryResourceContentsHandler(client) } -// getRepositoryContent defines the resource template and handler for getting repository content for a branch. -func getRepositoryResourceBranchContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +// GetRepositoryResourceBranchContent defines the resource template and handler for getting repository content for a branch. +func GetRepositoryResourceBranchContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { return mcp.NewResourceTemplate( "repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_BRANCH_DESCRIPTION", "Repository Content for specific branch"), ), - repositoryResourceContentsHandler(client) + RepositoryResourceContentsHandler(client) } -// getRepositoryResourceCommitContent defines the resource template and handler for getting repository content for a commit. -func getRepositoryResourceCommitContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +// GetRepositoryResourceCommitContent defines the resource template and handler for getting repository content for a commit. +func GetRepositoryResourceCommitContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { return mcp.NewResourceTemplate( "repo://{owner}/{repo}/sha/{sha}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_COMMIT_DESCRIPTION", "Repository Content for specific commit"), ), - repositoryResourceContentsHandler(client) + RepositoryResourceContentsHandler(client) } -// getRepositoryResourceTagContent defines the resource template and handler for getting repository content for a tag. -func getRepositoryResourceTagContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +// GetRepositoryResourceTagContent defines the resource template and handler for getting repository content for a tag. +func GetRepositoryResourceTagContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { return mcp.NewResourceTemplate( "repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_TAG_DESCRIPTION", "Repository Content for specific tag"), ), - repositoryResourceContentsHandler(client) + RepositoryResourceContentsHandler(client) } -// getRepositoryResourcePrContent defines the resource template and handler for getting repository content for a pull request. -func getRepositoryResourcePrContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +// GetRepositoryResourcePrContent defines the resource template and handler for getting repository content for a pull request. +func GetRepositoryResourcePrContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { return mcp.NewResourceTemplate( "repo://{owner}/{repo}/refs/pull/{prNumber}/head/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_PR_DESCRIPTION", "Repository Content for specific pull request"), ), - repositoryResourceContentsHandler(client) + RepositoryResourceContentsHandler(client) } -func repositoryResourceContentsHandler(client *github.Client) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { +// RepositoryResourceContentsHandler returns a handler function for repository content requests. +func RepositoryResourceContentsHandler(client *github.Client) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { return func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { // the matcher will give []string with one element // https://github.com/mark3labs/mcp-go/pull/54 diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go index adad8744..c274d1b5 100644 --- a/pkg/github/repository_resource_test.go +++ b/pkg/github/repository_resource_test.go @@ -234,7 +234,7 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := github.NewClient(tc.mockedClient) - handler := repositoryResourceContentsHandler(client) + handler := RepositoryResourceContentsHandler(client) request := mcp.ReadResourceRequest{ Params: struct { @@ -258,26 +258,26 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { } } -func Test_getRepositoryResourceContent(t *testing.T) { - tmpl, _ := getRepositoryResourceContent(nil, translations.NullTranslationHelper) +func Test_GetRepositoryResourceContent(t *testing.T) { + tmpl, _ := GetRepositoryResourceContent(nil, translations.NullTranslationHelper) require.Equal(t, "repo://{owner}/{repo}/contents{/path*}", tmpl.URITemplate.Raw()) } -func Test_getRepositoryResourceBranchContent(t *testing.T) { - tmpl, _ := getRepositoryResourceBranchContent(nil, translations.NullTranslationHelper) +func Test_GetRepositoryResourceBranchContent(t *testing.T) { + tmpl, _ := GetRepositoryResourceBranchContent(nil, translations.NullTranslationHelper) require.Equal(t, "repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}", tmpl.URITemplate.Raw()) } -func Test_getRepositoryResourceCommitContent(t *testing.T) { - tmpl, _ := getRepositoryResourceCommitContent(nil, translations.NullTranslationHelper) +func Test_GetRepositoryResourceCommitContent(t *testing.T) { + tmpl, _ := GetRepositoryResourceCommitContent(nil, translations.NullTranslationHelper) require.Equal(t, "repo://{owner}/{repo}/sha/{sha}/contents{/path*}", tmpl.URITemplate.Raw()) } -func Test_getRepositoryResourceTagContent(t *testing.T) { - tmpl, _ := getRepositoryResourceTagContent(nil, translations.NullTranslationHelper) +func Test_GetRepositoryResourceTagContent(t *testing.T) { + tmpl, _ := GetRepositoryResourceTagContent(nil, translations.NullTranslationHelper) require.Equal(t, "repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}", tmpl.URITemplate.Raw()) } -func Test_getRepositoryResourcePrContent(t *testing.T) { - tmpl, _ := getRepositoryResourcePrContent(nil, translations.NullTranslationHelper) +func Test_GetRepositoryResourcePrContent(t *testing.T) { + tmpl, _ := GetRepositoryResourcePrContent(nil, translations.NullTranslationHelper) require.Equal(t, "repo://{owner}/{repo}/refs/pull/{prNumber}/head/contents{/path*}", tmpl.URITemplate.Raw()) } diff --git a/pkg/github/server.go b/pkg/github/server.go index e5fdac2f..352d9697 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -24,11 +24,11 @@ func NewServer(client *github.Client, version string, readOnly bool, t translati server.WithLogging()) // Add GitHub Resources - s.AddResourceTemplate(getRepositoryResourceContent(client, t)) - s.AddResourceTemplate(getRepositoryResourceBranchContent(client, t)) - s.AddResourceTemplate(getRepositoryResourceCommitContent(client, t)) - s.AddResourceTemplate(getRepositoryResourceTagContent(client, t)) - s.AddResourceTemplate(getRepositoryResourcePrContent(client, t)) + s.AddResourceTemplate(GetRepositoryResourceContent(client, t)) + s.AddResourceTemplate(GetRepositoryResourceBranchContent(client, t)) + s.AddResourceTemplate(GetRepositoryResourceCommitContent(client, t)) + s.AddResourceTemplate(GetRepositoryResourceTagContent(client, t)) + s.AddResourceTemplate(GetRepositoryResourcePrContent(client, t)) // Add GitHub tools - Issues s.AddTool(GetIssue(client, t)) @@ -57,14 +57,14 @@ func NewServer(client *github.Client, version string, readOnly bool, t translati // Add GitHub tools - Repositories s.AddTool(searchRepositories(client, t)) - s.AddTool(getFileContents(client, t)) - s.AddTool(listCommits(client, t)) + s.AddTool(GetFileContents(client, t)) + s.AddTool(ListCommits(client, t)) if !readOnly { - s.AddTool(createOrUpdateFile(client, t)) - s.AddTool(createRepository(client, t)) - s.AddTool(forkRepository(client, t)) - s.AddTool(createBranch(client, t)) - s.AddTool(pushFiles(client, t)) + s.AddTool(CreateOrUpdateFile(client, t)) + s.AddTool(CreateRepository(client, t)) + s.AddTool(ForkRepository(client, t)) + s.AddTool(CreateBranch(client, t)) + s.AddTool(PushFiles(client, t)) } // Add GitHub tools - Search From 3dae8ab029a9052fb876336379f6b4e757c599e1 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Tue, 8 Apr 2025 00:32:14 +0200 Subject: [PATCH 07/41] chore: export search funcs --- pkg/github/search.go | 12 ++++++------ pkg/github/search_test.go | 12 ++++++------ pkg/github/server.go | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pkg/github/search.go b/pkg/github/search.go index 117e8298..46fed599 100644 --- a/pkg/github/search.go +++ b/pkg/github/search.go @@ -12,8 +12,8 @@ import ( "github.com/mark3labs/mcp-go/server" ) -// searchRepositories creates a tool to search for GitHub repositories. -func searchRepositories(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// SearchRepositories creates a tool to search for GitHub repositories. +func SearchRepositories(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("search_repositories", mcp.WithDescription(t("TOOL_SEARCH_REPOSITORIES_DESCRIPTION", "Search for GitHub repositories")), mcp.WithString("query", @@ -62,8 +62,8 @@ func searchRepositories(client *github.Client, t translations.TranslationHelperF } } -// searchCode creates a tool to search for code across GitHub repositories. -func searchCode(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// SearchCode creates a tool to search for code across GitHub repositories. +func SearchCode(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("search_code", mcp.WithDescription(t("TOOL_SEARCH_CODE_DESCRIPTION", "Search for code across GitHub repositories")), mcp.WithString("q", @@ -129,8 +129,8 @@ func searchCode(client *github.Client, t translations.TranslationHelperFunc) (to } } -// searchUsers creates a tool to search for GitHub users. -func searchUsers(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// SearchUsers creates a tool to search for GitHub users. +func SearchUsers(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("search_users", mcp.WithDescription(t("TOOL_SEARCH_USERS_DESCRIPTION", "Search for GitHub users")), mcp.WithString("q", diff --git a/pkg/github/search_test.go b/pkg/github/search_test.go index bf1bff45..b000a0bf 100644 --- a/pkg/github/search_test.go +++ b/pkg/github/search_test.go @@ -16,7 +16,7 @@ import ( func Test_SearchRepositories(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := searchRepositories(mockClient, translations.NullTranslationHelper) + tool, _ := SearchRepositories(mockClient, translations.NullTranslationHelper) assert.Equal(t, "search_repositories", tool.Name) assert.NotEmpty(t, tool.Description) @@ -122,7 +122,7 @@ func Test_SearchRepositories(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := searchRepositories(client, translations.NullTranslationHelper) + _, handler := SearchRepositories(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -163,7 +163,7 @@ func Test_SearchRepositories(t *testing.T) { func Test_SearchCode(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := searchCode(mockClient, translations.NullTranslationHelper) + tool, _ := SearchCode(mockClient, translations.NullTranslationHelper) assert.Equal(t, "search_code", tool.Name) assert.NotEmpty(t, tool.Description) @@ -273,7 +273,7 @@ func Test_SearchCode(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := searchCode(client, translations.NullTranslationHelper) + _, handler := SearchCode(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -314,7 +314,7 @@ func Test_SearchCode(t *testing.T) { func Test_SearchUsers(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := searchUsers(mockClient, translations.NullTranslationHelper) + tool, _ := SearchUsers(mockClient, translations.NullTranslationHelper) assert.Equal(t, "search_users", tool.Name) assert.NotEmpty(t, tool.Description) @@ -428,7 +428,7 @@ func Test_SearchUsers(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := searchUsers(client, translations.NullTranslationHelper) + _, handler := SearchUsers(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) diff --git a/pkg/github/server.go b/pkg/github/server.go index 352d9697..d8a9b230 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -56,7 +56,7 @@ func NewServer(client *github.Client, version string, readOnly bool, t translati } // Add GitHub tools - Repositories - s.AddTool(searchRepositories(client, t)) + s.AddTool(SearchRepositories(client, t)) s.AddTool(GetFileContents(client, t)) s.AddTool(ListCommits(client, t)) if !readOnly { @@ -68,8 +68,8 @@ func NewServer(client *github.Client, version string, readOnly bool, t translati } // Add GitHub tools - Search - s.AddTool(searchCode(client, t)) - s.AddTool(searchUsers(client, t)) + s.AddTool(SearchCode(client, t)) + s.AddTool(SearchUsers(client, t)) // Add GitHub tools - Users s.AddTool(getMe(client, t)) From 519ed9ebc782c4c29c1ef33a9015b027cd049863 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Tue, 8 Apr 2025 00:38:10 +0200 Subject: [PATCH 08/41] chore: export remaining search + helpers --- pkg/github/code_scanning.go | 8 +++--- pkg/github/issues.go | 52 ++++++++++++++++++------------------- pkg/github/pullrequests.go | 48 +++++++++++++++++----------------- pkg/github/repositories.go | 20 +++++++------- pkg/github/search.go | 20 +++++++------- pkg/github/server.go | 50 +++++++++++++++++------------------ pkg/github/server_test.go | 32 +++++++++++------------ 7 files changed, 115 insertions(+), 115 deletions(-) diff --git a/pkg/github/code_scanning.go b/pkg/github/code_scanning.go index 1b9cd876..dc48bdb3 100644 --- a/pkg/github/code_scanning.go +++ b/pkg/github/code_scanning.go @@ -38,7 +38,7 @@ func GetCodeScanningAlert(client *github.Client, t translations.TranslationHelpe if err != nil { return mcp.NewToolResultError(err.Error()), nil } - alertNumber, err := requiredInt(request, "alertNumber") + alertNumber, err := RequiredInt(request, "alertNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -97,15 +97,15 @@ func ListCodeScanningAlerts(client *github.Client, t translations.TranslationHel if err != nil { return mcp.NewToolResultError(err.Error()), nil } - ref, err := optionalParam[string](request, "ref") + ref, err := OptionalParam[string](request, "ref") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - state, err := optionalParam[string](request, "state") + state, err := OptionalParam[string](request, "state") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - severity, err := optionalParam[string](request, "severity") + severity, err := OptionalParam[string](request, "severity") if err != nil { return mcp.NewToolResultError(err.Error()), nil } diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 5836dedd..c983fa26 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -40,7 +40,7 @@ func GetIssue(client *github.Client, t translations.TranslationHelperFunc) (tool if err != nil { return mcp.NewToolResultError(err.Error()), nil } - issueNumber, err := requiredInt(request, "issue_number") + issueNumber, err := RequiredInt(request, "issue_number") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -98,7 +98,7 @@ func AddIssueComment(client *github.Client, t translations.TranslationHelperFunc if err != nil { return mcp.NewToolResultError(err.Error()), nil } - issueNumber, err := requiredInt(request, "issue_number") + issueNumber, err := RequiredInt(request, "issue_number") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -162,22 +162,22 @@ func SearchIssues(client *github.Client, t translations.TranslationHelperFunc) ( mcp.Description("Sort order ('asc' or 'desc')"), mcp.Enum("asc", "desc"), ), - withPagination(), + WithPagination(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { query, err := requiredParam[string](request, "q") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - sort, err := optionalParam[string](request, "sort") + sort, err := OptionalParam[string](request, "sort") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - order, err := optionalParam[string](request, "order") + order, err := OptionalParam[string](request, "order") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - pagination, err := optionalPaginationParams(request) + pagination, err := OptionalPaginationParams(request) if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -268,25 +268,25 @@ func CreateIssue(client *github.Client, t translations.TranslationHelperFunc) (t } // Optional parameters - body, err := optionalParam[string](request, "body") + body, err := OptionalParam[string](request, "body") if err != nil { return mcp.NewToolResultError(err.Error()), nil } // Get assignees - assignees, err := optionalStringArrayParam(request, "assignees") + assignees, err := OptionalStringArrayParam(request, "assignees") if err != nil { return mcp.NewToolResultError(err.Error()), nil } // Get labels - labels, err := optionalStringArrayParam(request, "labels") + labels, err := OptionalStringArrayParam(request, "labels") if err != nil { return mcp.NewToolResultError(err.Error()), nil } // Get optional milestone - milestone, err := optionalIntParam(request, "milestone") + milestone, err := OptionalIntParam(request, "milestone") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -363,7 +363,7 @@ func ListIssues(client *github.Client, t translations.TranslationHelperFunc) (to mcp.WithString("since", mcp.Description("Filter by date (ISO 8601 timestamp)"), ), - withPagination(), + WithPagination(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") @@ -378,28 +378,28 @@ func ListIssues(client *github.Client, t translations.TranslationHelperFunc) (to opts := &github.IssueListByRepoOptions{} // Set optional parameters if provided - opts.State, err = optionalParam[string](request, "state") + opts.State, err = OptionalParam[string](request, "state") if err != nil { return mcp.NewToolResultError(err.Error()), nil } // Get labels - opts.Labels, err = optionalStringArrayParam(request, "labels") + opts.Labels, err = OptionalStringArrayParam(request, "labels") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - opts.Sort, err = optionalParam[string](request, "sort") + opts.Sort, err = OptionalParam[string](request, "sort") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - opts.Direction, err = optionalParam[string](request, "direction") + opts.Direction, err = OptionalParam[string](request, "direction") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - since, err := optionalParam[string](request, "since") + since, err := OptionalParam[string](request, "since") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -497,7 +497,7 @@ func UpdateIssue(client *github.Client, t translations.TranslationHelperFunc) (t if err != nil { return mcp.NewToolResultError(err.Error()), nil } - issueNumber, err := requiredInt(request, "issue_number") + issueNumber, err := RequiredInt(request, "issue_number") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -506,7 +506,7 @@ func UpdateIssue(client *github.Client, t translations.TranslationHelperFunc) (t issueRequest := &github.IssueRequest{} // Set optional parameters if provided - title, err := optionalParam[string](request, "title") + title, err := OptionalParam[string](request, "title") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -514,7 +514,7 @@ func UpdateIssue(client *github.Client, t translations.TranslationHelperFunc) (t issueRequest.Title = github.Ptr(title) } - body, err := optionalParam[string](request, "body") + body, err := OptionalParam[string](request, "body") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -522,7 +522,7 @@ func UpdateIssue(client *github.Client, t translations.TranslationHelperFunc) (t issueRequest.Body = github.Ptr(body) } - state, err := optionalParam[string](request, "state") + state, err := OptionalParam[string](request, "state") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -531,7 +531,7 @@ func UpdateIssue(client *github.Client, t translations.TranslationHelperFunc) (t } // Get labels - labels, err := optionalStringArrayParam(request, "labels") + labels, err := OptionalStringArrayParam(request, "labels") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -540,7 +540,7 @@ func UpdateIssue(client *github.Client, t translations.TranslationHelperFunc) (t } // Get assignees - assignees, err := optionalStringArrayParam(request, "assignees") + assignees, err := OptionalStringArrayParam(request, "assignees") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -548,7 +548,7 @@ func UpdateIssue(client *github.Client, t translations.TranslationHelperFunc) (t issueRequest.Assignees = &assignees } - milestone, err := optionalIntParam(request, "milestone") + milestone, err := OptionalIntParam(request, "milestone") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -612,15 +612,15 @@ func GetIssueComments(client *github.Client, t translations.TranslationHelperFun if err != nil { return mcp.NewToolResultError(err.Error()), nil } - issueNumber, err := requiredInt(request, "issue_number") + issueNumber, err := RequiredInt(request, "issue_number") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - page, err := optionalIntParamWithDefault(request, "page", 1) + page, err := OptionalIntParamWithDefault(request, "page", 1) if err != nil { return mcp.NewToolResultError(err.Error()), nil } - perPage, err := optionalIntParamWithDefault(request, "per_page", 30) + perPage, err := OptionalIntParamWithDefault(request, "per_page", 30) if err != nil { return mcp.NewToolResultError(err.Error()), nil } diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index a5cd86b4..65b87154 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -39,7 +39,7 @@ func GetPullRequest(client *github.Client, t translations.TranslationHelperFunc) if err != nil { return mcp.NewToolResultError(err.Error()), nil } - pullNumber, err := requiredInt(request, "pullNumber") + pullNumber, err := RequiredInt(request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -94,7 +94,7 @@ func ListPullRequests(client *github.Client, t translations.TranslationHelperFun mcp.WithString("direction", mcp.Description("Sort direction ('asc', 'desc')"), ), - withPagination(), + WithPagination(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") @@ -105,27 +105,27 @@ func ListPullRequests(client *github.Client, t translations.TranslationHelperFun if err != nil { return mcp.NewToolResultError(err.Error()), nil } - state, err := optionalParam[string](request, "state") + state, err := OptionalParam[string](request, "state") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - head, err := optionalParam[string](request, "head") + head, err := OptionalParam[string](request, "head") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - base, err := optionalParam[string](request, "base") + base, err := OptionalParam[string](request, "base") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - sort, err := optionalParam[string](request, "sort") + sort, err := OptionalParam[string](request, "sort") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - direction, err := optionalParam[string](request, "direction") + direction, err := OptionalParam[string](request, "direction") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - pagination, err := optionalPaginationParams(request) + pagination, err := OptionalPaginationParams(request) if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -200,19 +200,19 @@ func MergePullRequest(client *github.Client, t translations.TranslationHelperFun if err != nil { return mcp.NewToolResultError(err.Error()), nil } - pullNumber, err := requiredInt(request, "pullNumber") + pullNumber, err := RequiredInt(request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - commitTitle, err := optionalParam[string](request, "commit_title") + commitTitle, err := OptionalParam[string](request, "commit_title") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - commitMessage, err := optionalParam[string](request, "commit_message") + commitMessage, err := OptionalParam[string](request, "commit_message") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - mergeMethod, err := optionalParam[string](request, "merge_method") + mergeMethod, err := OptionalParam[string](request, "merge_method") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -271,7 +271,7 @@ func GetPullRequestFiles(client *github.Client, t translations.TranslationHelper if err != nil { return mcp.NewToolResultError(err.Error()), nil } - pullNumber, err := requiredInt(request, "pullNumber") + pullNumber, err := RequiredInt(request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -326,7 +326,7 @@ func GetPullRequestStatus(client *github.Client, t translations.TranslationHelpe if err != nil { return mcp.NewToolResultError(err.Error()), nil } - pullNumber, err := requiredInt(request, "pullNumber") + pullNumber, err := RequiredInt(request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -398,11 +398,11 @@ func UpdatePullRequestBranch(client *github.Client, t translations.TranslationHe if err != nil { return mcp.NewToolResultError(err.Error()), nil } - pullNumber, err := requiredInt(request, "pullNumber") + pullNumber, err := RequiredInt(request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - expectedHeadSHA, err := optionalParam[string](request, "expectedHeadSha") + expectedHeadSHA, err := OptionalParam[string](request, "expectedHeadSha") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -465,7 +465,7 @@ func GetPullRequestComments(client *github.Client, t translations.TranslationHel if err != nil { return mcp.NewToolResultError(err.Error()), nil } - pullNumber, err := requiredInt(request, "pullNumber") + pullNumber, err := RequiredInt(request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -525,7 +525,7 @@ func GetPullRequestReviews(client *github.Client, t translations.TranslationHelp if err != nil { return mcp.NewToolResultError(err.Error()), nil } - pullNumber, err := requiredInt(request, "pullNumber") + pullNumber, err := RequiredInt(request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -629,7 +629,7 @@ func CreatePullRequestReview(client *github.Client, t translations.TranslationHe if err != nil { return mcp.NewToolResultError(err.Error()), nil } - pullNumber, err := requiredInt(request, "pullNumber") + pullNumber, err := RequiredInt(request, "pullNumber") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -644,7 +644,7 @@ func CreatePullRequestReview(client *github.Client, t translations.TranslationHe } // Add body if provided - body, err := optionalParam[string](request, "body") + body, err := OptionalParam[string](request, "body") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -653,7 +653,7 @@ func CreatePullRequestReview(client *github.Client, t translations.TranslationHe } // Add commit ID if provided - commitID, err := optionalParam[string](request, "commitId") + commitID, err := OptionalParam[string](request, "commitId") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -801,17 +801,17 @@ func CreatePullRequest(client *github.Client, t translations.TranslationHelperFu return mcp.NewToolResultError(err.Error()), nil } - body, err := optionalParam[string](request, "body") + body, err := OptionalParam[string](request, "body") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - draft, err := optionalParam[bool](request, "draft") + draft, err := OptionalParam[bool](request, "draft") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - maintainerCanModify, err := optionalParam[bool](request, "maintainer_can_modify") + maintainerCanModify, err := OptionalParam[bool](request, "maintainer_can_modify") if err != nil { return mcp.NewToolResultError(err.Error()), nil } diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 7725438b..2dafd4ce 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -28,7 +28,7 @@ func ListCommits(client *github.Client, t translations.TranslationHelperFunc) (t mcp.WithString("sha", mcp.Description("Branch name"), ), - withPagination(), + WithPagination(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") @@ -39,11 +39,11 @@ func ListCommits(client *github.Client, t translations.TranslationHelperFunc) (t if err != nil { return mcp.NewToolResultError(err.Error()), nil } - sha, err := optionalParam[string](request, "sha") + sha, err := OptionalParam[string](request, "sha") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - pagination, err := optionalPaginationParams(request) + pagination, err := OptionalPaginationParams(request) if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -148,7 +148,7 @@ func CreateOrUpdateFile(client *github.Client, t translations.TranslationHelperF } // If SHA is provided, set it (for updates) - sha, err := optionalParam[string](request, "sha") + sha, err := OptionalParam[string](request, "sha") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -203,15 +203,15 @@ func CreateRepository(client *github.Client, t translations.TranslationHelperFun if err != nil { return mcp.NewToolResultError(err.Error()), nil } - description, err := optionalParam[string](request, "description") + description, err := OptionalParam[string](request, "description") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - private, err := optionalParam[bool](request, "private") + private, err := OptionalParam[bool](request, "private") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - autoInit, err := optionalParam[bool](request, "autoInit") + autoInit, err := OptionalParam[bool](request, "autoInit") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -279,7 +279,7 @@ func GetFileContents(client *github.Client, t translations.TranslationHelperFunc if err != nil { return mcp.NewToolResultError(err.Error()), nil } - branch, err := optionalParam[string](request, "branch") + branch, err := OptionalParam[string](request, "branch") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -340,7 +340,7 @@ func ForkRepository(client *github.Client, t translations.TranslationHelperFunc) if err != nil { return mcp.NewToolResultError(err.Error()), nil } - org, err := optionalParam[string](request, "organization") + org, err := OptionalParam[string](request, "organization") if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -411,7 +411,7 @@ func CreateBranch(client *github.Client, t translations.TranslationHelperFunc) ( if err != nil { return mcp.NewToolResultError(err.Error()), nil } - fromBranch, err := optionalParam[string](request, "from_branch") + fromBranch, err := OptionalParam[string](request, "from_branch") if err != nil { return mcp.NewToolResultError(err.Error()), nil } diff --git a/pkg/github/search.go b/pkg/github/search.go index 46fed599..cd2ab434 100644 --- a/pkg/github/search.go +++ b/pkg/github/search.go @@ -20,14 +20,14 @@ func SearchRepositories(client *github.Client, t translations.TranslationHelperF mcp.Required(), mcp.Description("Search query"), ), - withPagination(), + WithPagination(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { query, err := requiredParam[string](request, "query") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - pagination, err := optionalPaginationParams(request) + pagination, err := OptionalPaginationParams(request) if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -77,22 +77,22 @@ func SearchCode(client *github.Client, t translations.TranslationHelperFunc) (to mcp.Description("Sort order ('asc' or 'desc')"), mcp.Enum("asc", "desc"), ), - withPagination(), + WithPagination(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { query, err := requiredParam[string](request, "q") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - sort, err := optionalParam[string](request, "sort") + sort, err := OptionalParam[string](request, "sort") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - order, err := optionalParam[string](request, "order") + order, err := OptionalParam[string](request, "order") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - pagination, err := optionalPaginationParams(request) + pagination, err := OptionalPaginationParams(request) if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -145,22 +145,22 @@ func SearchUsers(client *github.Client, t translations.TranslationHelperFunc) (t mcp.Description("Sort order ('asc' or 'desc')"), mcp.Enum("asc", "desc"), ), - withPagination(), + WithPagination(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { query, err := requiredParam[string](request, "q") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - sort, err := optionalParam[string](request, "sort") + sort, err := OptionalParam[string](request, "sort") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - order, err := optionalParam[string](request, "order") + order, err := OptionalParam[string](request, "order") if err != nil { return mcp.NewToolResultError(err.Error()), nil } - pagination, err := optionalPaginationParams(request) + pagination, err := OptionalPaginationParams(request) if err != nil { return mcp.NewToolResultError(err.Error()), nil } diff --git a/pkg/github/server.go b/pkg/github/server.go index d8a9b230..5852d581 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -72,7 +72,7 @@ func NewServer(client *github.Client, version string, readOnly bool, t translati s.AddTool(SearchUsers(client, t)) // Add GitHub tools - Users - s.AddTool(getMe(client, t)) + s.AddTool(GetMe(client, t)) // Add GitHub tools - Code Scanning s.AddTool(GetCodeScanningAlert(client, t)) @@ -80,8 +80,8 @@ func NewServer(client *github.Client, version string, readOnly bool, t translati return s } -// getMe creates a tool to get details of the authenticated user. -func getMe(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// GetMe creates a tool to get details of the authenticated user. +func GetMe(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_me", mcp.WithDescription(t("TOOL_GET_ME_DESCRIPTION", "Get details of the authenticated GitHub user. Use this when a request include \"me\", \"my\"...")), mcp.WithString("reason", @@ -144,12 +144,12 @@ func requiredParam[T comparable](r mcp.CallToolRequest, p string) (T, error) { return r.Params.Arguments[p].(T), nil } -// requiredInt is a helper function that can be used to fetch a requested parameter from the request. +// RequiredInt is a helper function that can be used to fetch a requested parameter from the request. // It does the following checks: // 1. Checks if the parameter is present in the request. // 2. Checks if the parameter is of the expected type. // 3. Checks if the parameter is not empty, i.e: non-zero value -func requiredInt(r mcp.CallToolRequest, p string) (int, error) { +func RequiredInt(r mcp.CallToolRequest, p string) (int, error) { v, err := requiredParam[float64](r, p) if err != nil { return 0, err @@ -157,11 +157,11 @@ func requiredInt(r mcp.CallToolRequest, p string) (int, error) { return int(v), nil } -// optionalParam is a helper function that can be used to fetch a requested parameter from the request. +// OptionalParam is a helper function that can be used to fetch a requested parameter from the request. // It does the following checks: // 1. Checks if the parameter is present in the request, if not, it returns its zero-value // 2. If it is present, it checks if the parameter is of the expected type and returns it -func optionalParam[T any](r mcp.CallToolRequest, p string) (T, error) { +func OptionalParam[T any](r mcp.CallToolRequest, p string) (T, error) { var zero T // Check if the parameter is present in the request @@ -177,22 +177,22 @@ func optionalParam[T any](r mcp.CallToolRequest, p string) (T, error) { return r.Params.Arguments[p].(T), nil } -// optionalIntParam is a helper function that can be used to fetch a requested parameter from the request. +// OptionalIntParam is a helper function that can be used to fetch a requested parameter from the request. // It does the following checks: // 1. Checks if the parameter is present in the request, if not, it returns its zero-value // 2. If it is present, it checks if the parameter is of the expected type and returns it -func optionalIntParam(r mcp.CallToolRequest, p string) (int, error) { - v, err := optionalParam[float64](r, p) +func OptionalIntParam(r mcp.CallToolRequest, p string) (int, error) { + v, err := OptionalParam[float64](r, p) if err != nil { return 0, err } return int(v), nil } -// optionalIntParamWithDefault is a helper function that can be used to fetch a requested parameter from the request +// OptionalIntParamWithDefault is a helper function that can be used to fetch a requested parameter from the request // similar to optionalIntParam, but it also takes a default value. -func optionalIntParamWithDefault(r mcp.CallToolRequest, p string, d int) (int, error) { - v, err := optionalIntParam(r, p) +func OptionalIntParamWithDefault(r mcp.CallToolRequest, p string, d int) (int, error) { + v, err := OptionalIntParam(r, p) if err != nil { return 0, err } @@ -202,11 +202,11 @@ func optionalIntParamWithDefault(r mcp.CallToolRequest, p string, d int) (int, e return v, nil } -// optionalStringArrayParam is a helper function that can be used to fetch a requested parameter from the request. +// OptionalStringArrayParam is a helper function that can be used to fetch a requested parameter from the request. // It does the following checks: // 1. Checks if the parameter is present in the request, if not, it returns its zero-value // 2. If it is present, iterates the elements and checks each is a string -func optionalStringArrayParam(r mcp.CallToolRequest, p string) ([]string, error) { +func OptionalStringArrayParam(r mcp.CallToolRequest, p string) ([]string, error) { // Check if the parameter is present in the request if _, ok := r.Params.Arguments[p]; !ok { return []string{}, nil @@ -230,9 +230,9 @@ func optionalStringArrayParam(r mcp.CallToolRequest, p string) ([]string, error) } } -// withPagination returns a ToolOption that adds "page" and "perPage" parameters to the tool. +// WithPagination returns a ToolOption that adds "page" and "perPage" parameters to the tool. // The "page" parameter is optional, min 1. The "perPage" parameter is optional, min 1, max 100. -func withPagination() mcp.ToolOption { +func WithPagination() mcp.ToolOption { return func(tool *mcp.Tool) { mcp.WithNumber("page", mcp.Description("Page number for pagination (min 1)"), @@ -247,26 +247,26 @@ func withPagination() mcp.ToolOption { } } -type paginationParams struct { +type PaginationParams struct { page int perPage int } -// optionalPaginationParams returns the "page" and "perPage" parameters from the request, +// OptionalPaginationParams returns the "page" and "perPage" parameters from the request, // or their default values if not present, "page" default is 1, "perPage" default is 30. // In future, we may want to make the default values configurable, or even have this // function returned from `withPagination`, where the defaults are provided alongside // the min/max values. -func optionalPaginationParams(r mcp.CallToolRequest) (paginationParams, error) { - page, err := optionalIntParamWithDefault(r, "page", 1) +func OptionalPaginationParams(r mcp.CallToolRequest) (PaginationParams, error) { + page, err := OptionalIntParamWithDefault(r, "page", 1) if err != nil { - return paginationParams{}, err + return PaginationParams{}, err } - perPage, err := optionalIntParamWithDefault(r, "perPage", 30) + perPage, err := OptionalIntParamWithDefault(r, "perPage", 30) if err != nil { - return paginationParams{}, err + return PaginationParams{}, err } - return paginationParams{ + return PaginationParams{ page: page, perPage: perPage, }, nil diff --git a/pkg/github/server_test.go b/pkg/github/server_test.go index 149fb77a..979046fc 100644 --- a/pkg/github/server_test.go +++ b/pkg/github/server_test.go @@ -18,7 +18,7 @@ import ( func Test_GetMe(t *testing.T) { // Verify tool definition mockClient := github.NewClient(nil) - tool, _ := getMe(mockClient, translations.NullTranslationHelper) + tool, _ := GetMe(mockClient, translations.NullTranslationHelper) assert.Equal(t, "get_me", tool.Name) assert.NotEmpty(t, tool.Description) @@ -96,7 +96,7 @@ func Test_GetMe(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := getMe(client, translations.NullTranslationHelper) + _, handler := GetMe(client, translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -262,7 +262,7 @@ func Test_OptionalStringParam(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { request := createMCPRequest(tc.params) - result, err := optionalParam[string](request, tc.paramName) + result, err := OptionalParam[string](request, tc.paramName) if tc.expectError { assert.Error(t, err) @@ -308,7 +308,7 @@ func Test_RequiredNumberParam(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { request := createMCPRequest(tc.params) - result, err := requiredInt(request, tc.paramName) + result, err := RequiredInt(request, tc.paramName) if tc.expectError { assert.Error(t, err) @@ -361,7 +361,7 @@ func Test_OptionalNumberParam(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { request := createMCPRequest(tc.params) - result, err := optionalIntParam(request, tc.paramName) + result, err := OptionalIntParam(request, tc.paramName) if tc.expectError { assert.Error(t, err) @@ -419,7 +419,7 @@ func Test_OptionalNumberParamWithDefault(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { request := createMCPRequest(tc.params) - result, err := optionalIntParamWithDefault(request, tc.paramName, tc.defaultVal) + result, err := OptionalIntParamWithDefault(request, tc.paramName, tc.defaultVal) if tc.expectError { assert.Error(t, err) @@ -472,7 +472,7 @@ func Test_OptionalBooleanParam(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { request := createMCPRequest(tc.params) - result, err := optionalParam[bool](request, tc.paramName) + result, err := OptionalParam[bool](request, tc.paramName) if tc.expectError { assert.Error(t, err) @@ -540,7 +540,7 @@ func TestOptionalStringArrayParam(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { request := createMCPRequest(tc.params) - result, err := optionalStringArrayParam(request, tc.paramName) + result, err := OptionalStringArrayParam(request, tc.paramName) if tc.expectError { assert.Error(t, err) @@ -556,13 +556,13 @@ func TestOptionalPaginationParams(t *testing.T) { tests := []struct { name string params map[string]any - expected paginationParams + expected PaginationParams expectError bool }{ { name: "no pagination parameters, default values", params: map[string]any{}, - expected: paginationParams{ + expected: PaginationParams{ page: 1, perPage: 30, }, @@ -573,7 +573,7 @@ func TestOptionalPaginationParams(t *testing.T) { params: map[string]any{ "page": float64(2), }, - expected: paginationParams{ + expected: PaginationParams{ page: 2, perPage: 30, }, @@ -584,7 +584,7 @@ func TestOptionalPaginationParams(t *testing.T) { params: map[string]any{ "perPage": float64(50), }, - expected: paginationParams{ + expected: PaginationParams{ page: 1, perPage: 50, }, @@ -596,7 +596,7 @@ func TestOptionalPaginationParams(t *testing.T) { "page": float64(2), "perPage": float64(50), }, - expected: paginationParams{ + expected: PaginationParams{ page: 2, perPage: 50, }, @@ -607,7 +607,7 @@ func TestOptionalPaginationParams(t *testing.T) { params: map[string]any{ "page": "not-a-number", }, - expected: paginationParams{}, + expected: PaginationParams{}, expectError: true, }, { @@ -615,7 +615,7 @@ func TestOptionalPaginationParams(t *testing.T) { params: map[string]any{ "perPage": "not-a-number", }, - expected: paginationParams{}, + expected: PaginationParams{}, expectError: true, }, } @@ -623,7 +623,7 @@ func TestOptionalPaginationParams(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { request := createMCPRequest(tc.params) - result, err := optionalPaginationParams(request) + result, err := OptionalPaginationParams(request) if tc.expectError { assert.Error(t, err) From 778b84069d1dc6adeb98602ed4792b93e83e974c Mon Sep 17 00:00:00 2001 From: MayorFaj Date: Mon, 7 Apr 2025 19:01:26 +0100 Subject: [PATCH 09/41] fix: enhance Docker publish workflow with additional tagging options --- .github/workflows/docker-publish.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 4c370ebe..2ba64f06 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -66,6 +66,20 @@ jobs: uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=schedule + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha + type=edge + type=match + type=pep440 + # Custom rule to prevent pre-releases from getting latest tag + type=raw,value=latest,enable=${{ !contains(github.ref, '-') && github.ref_type == 'tag' }} - name: Go Build Cache for Docker uses: actions/cache@v4 From 6b059c78f12a0db96452497bd8877675b7e2860d Mon Sep 17 00:00:00 2001 From: MayorFaj Date: Mon, 7 Apr 2025 20:18:30 +0100 Subject: [PATCH 10/41] fix: update Docker publish workflow to correctly handle version tagging --- .github/workflows/docker-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 2ba64f06..78f54233 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -79,7 +79,7 @@ jobs: type=match type=pep440 # Custom rule to prevent pre-releases from getting latest tag - type=raw,value=latest,enable=${{ !contains(github.ref, '-') && github.ref_type == 'tag' }} + type=raw,value=latest,enable=${{ github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-') }} - name: Go Build Cache for Docker uses: actions/cache@v4 From 35567c3f97268c861836746a7c08c3549e62112e Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Tue, 8 Apr 2025 09:51:10 +0200 Subject: [PATCH 11/41] Update .github/workflows/docker-publish.yml --- .github/workflows/docker-publish.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 78f54233..36fda3c5 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -76,7 +76,6 @@ jobs: type=semver,pattern={{major}} type=sha type=edge - type=match type=pep440 # Custom rule to prevent pre-releases from getting latest tag type=raw,value=latest,enable=${{ github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-') }} From 3f62483c778281bf813dcd96527869e7dd19f593 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Tue, 8 Apr 2025 09:54:31 +0200 Subject: [PATCH 12/41] Update .github/workflows/docker-publish.yml --- .github/workflows/docker-publish.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 36fda3c5..35ffc47d 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -76,7 +76,6 @@ jobs: type=semver,pattern={{major}} type=sha type=edge - type=pep440 # Custom rule to prevent pre-releases from getting latest tag type=raw,value=latest,enable=${{ github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-') }} From 923e1b065d7158d08cfceecc2466b99af17a6e2f Mon Sep 17 00:00:00 2001 From: Shunsuke Suzuki Date: Tue, 8 Apr 2025 15:31:33 +0900 Subject: [PATCH 13/41] Generate GitHub Artifact Attestations --- .github/workflows/goreleaser.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index a25a3469..263607ee 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -5,6 +5,8 @@ on: - "v*" permissions: contents: write + id-token: write + attestations: write jobs: release: @@ -33,3 +35,11 @@ jobs: workdir: . env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate signed build provenance attestations for workflow artifacts + uses: actions/attest-build-provenance@v2 + with: + subject-path: | + dist/*.tar.gz + dist/*.zip + dist/*.txt From 4cf96ab2a2d6fc9fd0822b96523377023f60c031 Mon Sep 17 00:00:00 2001 From: William Martin Date: Tue, 8 Apr 2025 11:03:21 +0200 Subject: [PATCH 14/41] Indicate Go API stability in README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index cf53ce2a..86e516ce 100644 --- a/README.md +++ b/README.md @@ -435,6 +435,10 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `prNumber`: Pull request number (string, required) - `path`: File or directory path (string, optional) +## Library Usage + +The exported Go API of this module should currently be considered unstable, and subject to breaking changes. In the future, we may offer stability; please file an issue if there is a use case where this would be valuable. + ## License This project is licensed under the terms of the MIT open source license. Please refer to [MIT](./LICENSE) for the full terms. From 936b24c4c3b48b7d5ae18b36f6a264f3a418c3aa Mon Sep 17 00:00:00 2001 From: Ariel Deitcher <1149246+mntlty@users.noreply.github.com> Date: Tue, 8 Apr 2025 08:19:42 -0700 Subject: [PATCH 15/41] remove pretty print json flag (#179) --- cmd/github-mcp-server/main.go | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index dd4d41a7..d7b42bec 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -1,9 +1,7 @@ package main import ( - "bytes" "context" - "encoding/json" "fmt" "io" stdlog "log" @@ -41,7 +39,6 @@ var ( logFile := viper.GetString("log-file") readOnly := viper.GetBool("read-only") exportTranslations := viper.GetBool("export-translations") - prettyPrintJSON := viper.GetBool("pretty-print-json") logger, err := initLogger(logFile) if err != nil { stdlog.Fatal("Failed to initialize logger:", err) @@ -52,7 +49,6 @@ var ( logger: logger, logCommands: logCommands, exportTranslations: exportTranslations, - prettyPrintJSON: prettyPrintJSON, } if err := runStdioServer(cfg); err != nil { stdlog.Fatal("failed to run stdio server:", err) @@ -70,7 +66,6 @@ func init() { rootCmd.PersistentFlags().Bool("enable-command-logging", false, "When enabled, the server will log all command requests and responses to the log file") rootCmd.PersistentFlags().Bool("export-translations", false, "Save translations to a JSON file") rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)") - rootCmd.PersistentFlags().Bool("pretty-print-json", false, "Pretty print JSON output") // Bind flag to viper _ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only")) @@ -78,7 +73,6 @@ func init() { _ = viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging")) _ = viper.BindPFlag("export-translations", rootCmd.PersistentFlags().Lookup("export-translations")) _ = viper.BindPFlag("gh-host", rootCmd.PersistentFlags().Lookup("gh-host")) - _ = viper.BindPFlag("pretty-print-json", rootCmd.PersistentFlags().Lookup("pretty-print-json")) // Add subcommands rootCmd.AddCommand(stdioCmd) @@ -112,20 +106,6 @@ type runConfig struct { logger *log.Logger logCommands bool exportTranslations bool - prettyPrintJSON bool -} - -// JSONPrettyPrintWriter is a Writer that pretty prints input to indented JSON -type JSONPrettyPrintWriter struct { - writer io.Writer -} - -func (j JSONPrettyPrintWriter) Write(p []byte) (n int, err error) { - var prettyJSON bytes.Buffer - if err := json.Indent(&prettyJSON, p, "", "\t"); err != nil { - return 0, err - } - return j.writer.Write(prettyJSON.Bytes()) } func runStdioServer(cfg runConfig) error { @@ -179,9 +159,6 @@ func runStdioServer(cfg runConfig) error { in, out = loggedIO, loggedIO } - if cfg.prettyPrintJSON { - out = JSONPrettyPrintWriter{writer: out} - } errC <- stdioServer.Listen(ctx, in, out) }() From 56c1fce434d98cc5d4935979cc4c661a29bd0ed6 Mon Sep 17 00:00:00 2001 From: monotykamary Date: Wed, 9 Apr 2025 13:11:10 +0700 Subject: [PATCH 16/41] feat: Add update_pull_request tool (#122) * feat: add update_pull_request tool * refactor: address feedback on optionalParamOK helper * docs: add update_pull_request tool documentation * refactor: update optionalParamsOK as exported member * fix: rename to exported function --- README.md | 11 ++ pkg/github/helper_test.go | 112 ++++++++++++++++++++ pkg/github/pullrequests.go | 113 ++++++++++++++++++++ pkg/github/pullrequests_test.go | 182 ++++++++++++++++++++++++++++++++ pkg/github/server.go | 25 +++++ 5 files changed, 443 insertions(+) diff --git a/README.md b/README.md index 86e516ce..b78be380 100644 --- a/README.md +++ b/README.md @@ -287,6 +287,17 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `draft`: Create as draft PR (boolean, optional) - `maintainer_can_modify`: Allow maintainer edits (boolean, optional) +- **update_pull_request** - Update an existing pull request in a GitHub repository + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `pullNumber`: Pull request number to update (number, required) + - `title`: New title (string, optional) + - `body`: New description (string, optional) + - `state`: New state ('open' or 'closed') (string, optional) + - `base`: New base branch name (string, optional) + - `maintainer_can_modify`: Allow maintainer edits (boolean, optional) + ### Repositories - **create_or_update_file** - Create or update a single file in a repository diff --git a/pkg/github/helper_test.go b/pkg/github/helper_test.go index 9dcffa42..40fc0b94 100644 --- a/pkg/github/helper_test.go +++ b/pkg/github/helper_test.go @@ -93,3 +93,115 @@ func getTextResult(t *testing.T, result *mcp.CallToolResult) mcp.TextContent { assert.Equal(t, "text", textContent.Type) return textContent } + +func TestOptionalParamOK(t *testing.T) { + tests := []struct { + name string + args map[string]interface{} + paramName string + expectedVal interface{} + expectedOk bool + expectError bool + errorMsg string + }{ + { + name: "present and correct type (string)", + args: map[string]interface{}{"myParam": "hello"}, + paramName: "myParam", + expectedVal: "hello", + expectedOk: true, + expectError: false, + }, + { + name: "present and correct type (bool)", + args: map[string]interface{}{"myParam": true}, + paramName: "myParam", + expectedVal: true, + expectedOk: true, + expectError: false, + }, + { + name: "present and correct type (number)", + args: map[string]interface{}{"myParam": float64(123)}, + paramName: "myParam", + expectedVal: float64(123), + expectedOk: true, + expectError: false, + }, + { + name: "present but wrong type (string expected, got bool)", + args: map[string]interface{}{"myParam": true}, + paramName: "myParam", + expectedVal: "", // Zero value for string + expectedOk: true, // ok is true because param exists + expectError: true, + errorMsg: "parameter myParam is not of type string, is bool", + }, + { + name: "present but wrong type (bool expected, got string)", + args: map[string]interface{}{"myParam": "true"}, + paramName: "myParam", + expectedVal: false, // Zero value for bool + expectedOk: true, // ok is true because param exists + expectError: true, + errorMsg: "parameter myParam is not of type bool, is string", + }, + { + name: "parameter not present", + args: map[string]interface{}{"anotherParam": "value"}, + paramName: "myParam", + expectedVal: "", // Zero value for string + expectedOk: false, + expectError: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + request := createMCPRequest(tc.args) + + // Test with string type assertion + if _, isString := tc.expectedVal.(string); isString || tc.errorMsg == "parameter myParam is not of type string, is bool" { + val, ok, err := OptionalParamOK[string](request, tc.paramName) + if tc.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.errorMsg) + assert.Equal(t, tc.expectedOk, ok) // Check ok even on error + assert.Equal(t, tc.expectedVal, val) // Check zero value on error + } else { + require.NoError(t, err) + assert.Equal(t, tc.expectedOk, ok) + assert.Equal(t, tc.expectedVal, val) + } + } + + // Test with bool type assertion + if _, isBool := tc.expectedVal.(bool); isBool || tc.errorMsg == "parameter myParam is not of type bool, is string" { + val, ok, err := OptionalParamOK[bool](request, tc.paramName) + if tc.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.errorMsg) + assert.Equal(t, tc.expectedOk, ok) // Check ok even on error + assert.Equal(t, tc.expectedVal, val) // Check zero value on error + } else { + require.NoError(t, err) + assert.Equal(t, tc.expectedOk, ok) + assert.Equal(t, tc.expectedVal, val) + } + } + + // Test with float64 type assertion (for number case) + if _, isFloat := tc.expectedVal.(float64); isFloat { + val, ok, err := OptionalParamOK[float64](request, tc.paramName) + if tc.expectError { + // This case shouldn't happen for float64 in the defined tests + require.Fail(t, "Unexpected error case for float64") + } else { + require.NoError(t, err) + assert.Equal(t, tc.expectedOk, ok) + assert.Equal(t, tc.expectedVal, val) + } + } + }) + } +} diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index 65b87154..c5f9d9fa 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -67,6 +67,119 @@ func GetPullRequest(client *github.Client, t translations.TranslationHelperFunc) } } +// UpdatePullRequest creates a tool to update an existing pull request. +func UpdatePullRequest(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("update_pull_request", + mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_DESCRIPTION", "Update an existing pull request in a GitHub repository")), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithNumber("pullNumber", + mcp.Required(), + mcp.Description("Pull request number to update"), + ), + mcp.WithString("title", + mcp.Description("New title"), + ), + mcp.WithString("body", + mcp.Description("New description"), + ), + mcp.WithString("state", + mcp.Description("New state ('open' or 'closed')"), + mcp.Enum("open", "closed"), + ), + mcp.WithString("base", + mcp.Description("New base branch name"), + ), + mcp.WithBoolean("maintainer_can_modify", + mcp.Description("Allow maintainer edits"), + ), + ), + func(ctx context.Context, 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 + } + + // Build the update struct only with provided fields + update := &github.PullRequest{} + updateNeeded := false + + if title, ok, err := OptionalParamOK[string](request, "title"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if ok { + update.Title = github.Ptr(title) + updateNeeded = true + } + + if body, ok, err := OptionalParamOK[string](request, "body"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if ok { + update.Body = github.Ptr(body) + updateNeeded = true + } + + if state, ok, err := OptionalParamOK[string](request, "state"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if ok { + update.State = github.Ptr(state) + updateNeeded = true + } + + if base, ok, err := OptionalParamOK[string](request, "base"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if ok { + update.Base = &github.PullRequestBranch{Ref: github.Ptr(base)} + updateNeeded = true + } + + if maintainerCanModify, ok, err := OptionalParamOK[bool](request, "maintainer_can_modify"); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } else if ok { + update.MaintainerCanModify = github.Ptr(maintainerCanModify) + updateNeeded = true + } + + if !updateNeeded { + return mcp.NewToolResultError("No update parameters provided."), nil + } + + pr, resp, err := client.PullRequests.Edit(ctx, owner, repo, pullNumber, update) + if err != nil { + return nil, fmt.Errorf("failed to update pull request: %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 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 + } +} + // ListPullRequests creates a tool to list and filter repository pull requests. func ListPullRequests(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_pull_requests", diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go index 34c41cc7..e9647029 100644 --- a/pkg/github/pullrequests_test.go +++ b/pkg/github/pullrequests_test.go @@ -126,6 +126,188 @@ func Test_GetPullRequest(t *testing.T) { } } +func Test_UpdatePullRequest(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := UpdatePullRequest(mockClient, translations.NullTranslationHelper) + + assert.Equal(t, "update_pull_request", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "owner") + assert.Contains(t, tool.InputSchema.Properties, "repo") + assert.Contains(t, tool.InputSchema.Properties, "pullNumber") + assert.Contains(t, tool.InputSchema.Properties, "title") + assert.Contains(t, tool.InputSchema.Properties, "body") + assert.Contains(t, tool.InputSchema.Properties, "state") + assert.Contains(t, tool.InputSchema.Properties, "base") + assert.Contains(t, tool.InputSchema.Properties, "maintainer_can_modify") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pullNumber"}) + + // Setup mock PR for success case + mockUpdatedPR := &github.PullRequest{ + Number: github.Ptr(42), + Title: github.Ptr("Updated Test PR Title"), + State: github.Ptr("open"), + HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42"), + Body: github.Ptr("Updated test PR body."), + MaintainerCanModify: github.Ptr(false), + Base: &github.PullRequestBranch{ + Ref: github.Ptr("develop"), + }, + } + + mockClosedPR := &github.PullRequest{ + Number: github.Ptr(42), + Title: github.Ptr("Test PR"), + State: github.Ptr("closed"), // State updated + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedPR *github.PullRequest + expectedErrMsg string + }{ + { + name: "successful PR update (title, body, base, maintainer_can_modify)", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PatchReposPullsByOwnerByRepoByPullNumber, + // Expect the flat string based on previous test failure output and API docs + expectRequestBody(t, map[string]interface{}{ + "title": "Updated Test PR Title", + "body": "Updated test PR body.", + "base": "develop", + "maintainer_can_modify": false, + }).andThen( + mockResponse(t, http.StatusOK, mockUpdatedPR), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "pullNumber": float64(42), + "title": "Updated Test PR Title", + "body": "Updated test PR body.", + "base": "develop", + "maintainer_can_modify": false, + }, + expectError: false, + expectedPR: mockUpdatedPR, + }, + { + name: "successful PR update (state)", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PatchReposPullsByOwnerByRepoByPullNumber, + expectRequestBody(t, map[string]interface{}{ + "state": "closed", + }).andThen( + mockResponse(t, http.StatusOK, mockClosedPR), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "pullNumber": float64(42), + "state": "closed", + }, + expectError: false, + expectedPR: mockClosedPR, + }, + { + name: "no update parameters provided", + mockedClient: mock.NewMockedHTTPClient(), // No API call expected + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "pullNumber": float64(42), + // No update fields + }, + expectError: false, // Error is returned in the result, not as Go error + expectedErrMsg: "No update parameters provided", + }, + { + name: "PR update fails (API error)", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PatchReposPullsByOwnerByRepoByPullNumber, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnprocessableEntity) + _, _ = w.Write([]byte(`{"message": "Validation Failed"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "pullNumber": float64(42), + "title": "Invalid Title Causing Error", + }, + expectError: true, + expectedErrMsg: "failed to update pull request", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := UpdatePullRequest(client, translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErrMsg) + return + } + + require.NoError(t, err) + + // Parse the result and get the text content + textContent := getTextResult(t, result) + + // Check for expected error message within the result text + if tc.expectedErrMsg != "" { + assert.Contains(t, textContent.Text, tc.expectedErrMsg) + return + } + + // Unmarshal and verify the successful result + var returnedPR github.PullRequest + err = json.Unmarshal([]byte(textContent.Text), &returnedPR) + require.NoError(t, err) + assert.Equal(t, *tc.expectedPR.Number, *returnedPR.Number) + if tc.expectedPR.Title != nil { + assert.Equal(t, *tc.expectedPR.Title, *returnedPR.Title) + } + if tc.expectedPR.Body != nil { + assert.Equal(t, *tc.expectedPR.Body, *returnedPR.Body) + } + if tc.expectedPR.State != nil { + assert.Equal(t, *tc.expectedPR.State, *returnedPR.State) + } + if tc.expectedPR.Base != nil && tc.expectedPR.Base.Ref != nil { + assert.NotNil(t, returnedPR.Base) + assert.Equal(t, *tc.expectedPR.Base.Ref, *returnedPR.Base.Ref) + } + if tc.expectedPR.MaintainerCanModify != nil { + assert.Equal(t, *tc.expectedPR.MaintainerCanModify, *returnedPR.MaintainerCanModify) + } + }) + } +} + func Test_ListPullRequests(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) diff --git a/pkg/github/server.go b/pkg/github/server.go index 5852d581..84c15f50 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -53,6 +53,7 @@ func NewServer(client *github.Client, version string, readOnly bool, t translati s.AddTool(UpdatePullRequestBranch(client, t)) s.AddTool(CreatePullRequestReview(client, t)) s.AddTool(CreatePullRequest(client, t)) + s.AddTool(UpdatePullRequest(client, t)) } // Add GitHub tools - Repositories @@ -112,6 +113,30 @@ func GetMe(client *github.Client, t translations.TranslationHelperFunc) (tool mc } } +// OptionalParamOK is a helper function that can be used to fetch a requested parameter from the request. +// It returns the value, a boolean indicating if the parameter was present, and an error if the type is wrong. +func OptionalParamOK[T any](r mcp.CallToolRequest, p string) (value T, ok bool, err error) { + // Check if the parameter is present in the request + val, exists := r.Params.Arguments[p] + if !exists { + // Not present, return zero value, false, no error + return + } + + // Check if the parameter is of the expected type + value, ok = val.(T) + if !ok { + // Present but wrong type + err = fmt.Errorf("parameter %s is not of type %T, is %T", p, value, val) + ok = true // Set ok to true because the parameter *was* present, even if wrong type + return + } + + // Present and correct type + ok = true + return +} + // isAcceptedError checks if the error is an accepted error. func isAcceptedError(err error) bool { var acceptedError *github.AcceptedError From 86fbc8504fa5cd11fdb8f805137ffdb405db3f8d Mon Sep 17 00:00:00 2001 From: Andrew Georgiou Date: Wed, 9 Apr 2025 17:25:59 +0100 Subject: [PATCH 17/41] Fix handling nil values for optional string array parameters, (#194) * Fix handling nil values for optional string array parameters, nil values should be equivalent to an empty string, currently we return an error but Claude passes nil for optional values. * lint fixes --- pkg/github/issues_test.go | 7 ++++--- pkg/github/server.go | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 04a2ae19..e8b16e02 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -468,9 +468,10 @@ func Test_CreateIssue(t *testing.T) { ), ), requestArgs: map[string]interface{}{ - "owner": "owner", - "repo": "repo", - "title": "Minimal Issue", + "owner": "owner", + "repo": "repo", + "title": "Minimal Issue", + "assignees": nil, // Expect no failure with nil optional value. }, expectError: false, expectedIssue: &github.Issue{ diff --git a/pkg/github/server.go b/pkg/github/server.go index 84c15f50..80457a54 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -238,6 +238,8 @@ func OptionalStringArrayParam(r mcp.CallToolRequest, p string) ([]string, error) } switch v := r.Params.Arguments[p].(type) { + case nil: + return []string{}, nil case []string: return v, nil case []any: From 3ec8699deb6e1df5ab443c63fab024ecadb959d8 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 9 Apr 2025 15:45:14 +0200 Subject: [PATCH 18/41] chore: groundwork for multi-user to server --- cmd/github-mcp-server/main.go | 5 +- pkg/github/code_scanning.go | 13 +++- pkg/github/code_scanning_test.go | 8 +-- pkg/github/issues.go | 42 ++++++++++--- pkg/github/issues_test.go | 28 ++++----- pkg/github/pullrequests.go | 66 +++++++++++++++++---- pkg/github/pullrequests_test.go | 44 +++++++------- pkg/github/repositories.go | 44 +++++++++++--- pkg/github/repositories_test.go | 28 ++++----- pkg/github/repository_resource.go | 26 ++++---- pkg/github/repository_resource_test.go | 2 +- pkg/github/search.go | 20 ++++++- pkg/github/search_test.go | 12 ++-- pkg/github/server.go | 82 ++++++++++++++------------ pkg/github/server_test.go | 10 +++- 15 files changed, 287 insertions(+), 143 deletions(-) diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index d7b42bec..f5539529 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -137,8 +137,11 @@ func runStdioServer(cfg runConfig) error { t, dumpTranslations := translations.TranslationHelper() + getClient := func(_ context.Context) (*gogithub.Client, error) { + return ghClient, nil // closing over client + } // Create - ghServer := github.NewServer(ghClient, version, cfg.readOnly, t) + ghServer := github.NewServer(getClient, version, cfg.readOnly, t) stdioServer := server.NewStdioServer(ghServer) stdLogger := stdlog.New(cfg.logger.Writer(), "stdioserver", 0) diff --git a/pkg/github/code_scanning.go b/pkg/github/code_scanning.go index dc48bdb3..4fc029bf 100644 --- a/pkg/github/code_scanning.go +++ b/pkg/github/code_scanning.go @@ -13,7 +13,7 @@ import ( "github.com/mark3labs/mcp-go/server" ) -func GetCodeScanningAlert(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +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.")), mcp.WithString("owner", @@ -43,6 +43,11 @@ func GetCodeScanningAlert(client *github.Client, t translations.TranslationHelpe 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 nil, fmt.Errorf("failed to get alert: %w", err) @@ -66,7 +71,7 @@ func GetCodeScanningAlert(client *github.Client, t translations.TranslationHelpe } } -func ListCodeScanningAlerts(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func ListCodeScanningAlerts(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_code_scanning_alerts", mcp.WithDescription(t("TOOL_LIST_CODE_SCANNING_ALERTS_DESCRIPTION", "List code scanning alerts in a GitHub repository.")), mcp.WithString("owner", @@ -110,6 +115,10 @@ func ListCodeScanningAlerts(client *github.Client, t translations.TranslationHel 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}) if err != nil { return nil, fmt.Errorf("failed to list alerts: %w", err) diff --git a/pkg/github/code_scanning_test.go b/pkg/github/code_scanning_test.go index f1f3a1de..c9895e26 100644 --- a/pkg/github/code_scanning_test.go +++ b/pkg/github/code_scanning_test.go @@ -16,7 +16,7 @@ import ( func Test_GetCodeScanningAlert(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := GetCodeScanningAlert(mockClient, translations.NullTranslationHelper) + tool, _ := GetCodeScanningAlert(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "get_code_scanning_alert", tool.Name) assert.NotEmpty(t, tool.Description) @@ -82,7 +82,7 @@ func Test_GetCodeScanningAlert(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := GetCodeScanningAlert(client, translations.NullTranslationHelper) + _, handler := GetCodeScanningAlert(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -118,7 +118,7 @@ func Test_GetCodeScanningAlert(t *testing.T) { func Test_ListCodeScanningAlerts(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := ListCodeScanningAlerts(mockClient, translations.NullTranslationHelper) + tool, _ := ListCodeScanningAlerts(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "list_code_scanning_alerts", tool.Name) assert.NotEmpty(t, tool.Description) @@ -201,7 +201,7 @@ func Test_ListCodeScanningAlerts(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := ListCodeScanningAlerts(client, translations.NullTranslationHelper) + _, handler := ListCodeScanningAlerts(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index c983fa26..16c34141 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -15,7 +15,7 @@ import ( ) // GetIssue creates a tool to get details of a specific issue in a GitHub repository. -func GetIssue(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func GetIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_issue", mcp.WithDescription(t("TOOL_GET_ISSUE_DESCRIPTION", "Get details of a specific issue in a GitHub repository")), mcp.WithString("owner", @@ -45,6 +45,10 @@ func GetIssue(client *github.Client, t translations.TranslationHelperFunc) (tool 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) @@ -69,7 +73,7 @@ func GetIssue(client *github.Client, t translations.TranslationHelperFunc) (tool } // AddIssueComment creates a tool to add a comment to an issue. -func AddIssueComment(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("add_issue_comment", mcp.WithDescription(t("TOOL_ADD_ISSUE_COMMENT_DESCRIPTION", "Add a comment to an existing issue")), mcp.WithString("owner", @@ -111,6 +115,10 @@ func AddIssueComment(client *github.Client, t translations.TranslationHelperFunc Body: github.Ptr(body), } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } createdComment, resp, err := client.Issues.CreateComment(ctx, owner, repo, issueNumber, comment) if err != nil { return nil, fmt.Errorf("failed to create comment: %w", err) @@ -135,7 +143,7 @@ func AddIssueComment(client *github.Client, t translations.TranslationHelperFunc } // SearchIssues creates a tool to search for issues and pull requests. -func SearchIssues(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func SearchIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("search_issues", mcp.WithDescription(t("TOOL_SEARCH_ISSUES_DESCRIPTION", "Search for issues and pull requests across GitHub repositories")), mcp.WithString("q", @@ -191,6 +199,10 @@ func SearchIssues(client *github.Client, t translations.TranslationHelperFunc) ( }, } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } result, resp, err := client.Search.Issues(ctx, query, opts) if err != nil { return nil, fmt.Errorf("failed to search issues: %w", err) @@ -215,7 +227,7 @@ func SearchIssues(client *github.Client, t translations.TranslationHelperFunc) ( } // CreateIssue creates a tool to create a new issue in a GitHub repository. -func CreateIssue(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func CreateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("create_issue", mcp.WithDescription(t("TOOL_CREATE_ISSUE_DESCRIPTION", "Create a new issue in a GitHub repository")), mcp.WithString("owner", @@ -305,6 +317,10 @@ func CreateIssue(client *github.Client, t translations.TranslationHelperFunc) (t Milestone: milestoneNum, } + 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) @@ -329,7 +345,7 @@ func CreateIssue(client *github.Client, t translations.TranslationHelperFunc) (t } // ListIssues creates a tool to list and filter repository issues -func ListIssues(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func ListIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_issues", mcp.WithDescription(t("TOOL_LIST_ISSUES_DESCRIPTION", "List issues in a GitHub repository with filtering options")), mcp.WithString("owner", @@ -419,6 +435,10 @@ func ListIssues(client *github.Client, t translations.TranslationHelperFunc) (to opts.PerPage = int(perPage) } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } issues, resp, err := client.Issues.ListByRepo(ctx, owner, repo, opts) if err != nil { return nil, fmt.Errorf("failed to list issues: %w", err) @@ -443,7 +463,7 @@ func ListIssues(client *github.Client, t translations.TranslationHelperFunc) (to } // UpdateIssue creates a tool to update an existing issue in a GitHub repository. -func UpdateIssue(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func UpdateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("update_issue", mcp.WithDescription(t("TOOL_UPDATE_ISSUE_DESCRIPTION", "Update an existing issue in a GitHub repository")), mcp.WithString("owner", @@ -557,6 +577,10 @@ func UpdateIssue(client *github.Client, t translations.TranslationHelperFunc) (t issueRequest.Milestone = &milestoneNum } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } updatedIssue, resp, err := client.Issues.Edit(ctx, owner, repo, issueNumber, issueRequest) if err != nil { return nil, fmt.Errorf("failed to update issue: %w", err) @@ -581,7 +605,7 @@ func UpdateIssue(client *github.Client, t translations.TranslationHelperFunc) (t } // GetIssueComments creates a tool to get comments for a GitHub issue. -func GetIssueComments(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func GetIssueComments(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_issue_comments", mcp.WithDescription(t("TOOL_GET_ISSUE_COMMENTS_DESCRIPTION", "Get comments for a GitHub issue")), mcp.WithString("owner", @@ -632,6 +656,10 @@ func GetIssueComments(client *github.Client, t translations.TranslationHelperFun }, } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } comments, resp, err := client.Issues.ListComments(ctx, owner, repo, issueNumber, opts) if err != nil { return nil, fmt.Errorf("failed to get issue comments: %w", err) diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index e8b16e02..61ca0ae7 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -18,7 +18,7 @@ import ( func Test_GetIssue(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := GetIssue(mockClient, translations.NullTranslationHelper) + tool, _ := GetIssue(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "get_issue", tool.Name) assert.NotEmpty(t, tool.Description) @@ -82,7 +82,7 @@ func Test_GetIssue(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := GetIssue(client, translations.NullTranslationHelper) + _, handler := GetIssue(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -114,7 +114,7 @@ func Test_GetIssue(t *testing.T) { func Test_AddIssueComment(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := AddIssueComment(mockClient, translations.NullTranslationHelper) + tool, _ := AddIssueComment(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "add_issue_comment", tool.Name) assert.NotEmpty(t, tool.Description) @@ -185,7 +185,7 @@ func Test_AddIssueComment(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := AddIssueComment(client, translations.NullTranslationHelper) + _, handler := AddIssueComment(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := mcp.CallToolRequest{ @@ -237,7 +237,7 @@ func Test_AddIssueComment(t *testing.T) { func Test_SearchIssues(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := SearchIssues(mockClient, translations.NullTranslationHelper) + tool, _ := SearchIssues(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "search_issues", tool.Name) assert.NotEmpty(t, tool.Description) @@ -352,7 +352,7 @@ func Test_SearchIssues(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := SearchIssues(client, translations.NullTranslationHelper) + _, handler := SearchIssues(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -393,7 +393,7 @@ func Test_SearchIssues(t *testing.T) { func Test_CreateIssue(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := CreateIssue(mockClient, translations.NullTranslationHelper) + tool, _ := CreateIssue(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "create_issue", tool.Name) assert.NotEmpty(t, tool.Description) @@ -506,7 +506,7 @@ func Test_CreateIssue(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := CreateIssue(client, translations.NullTranslationHelper) + _, handler := CreateIssue(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -567,7 +567,7 @@ func Test_CreateIssue(t *testing.T) { func Test_ListIssues(t *testing.T) { // Verify tool definition mockClient := github.NewClient(nil) - tool, _ := ListIssues(mockClient, translations.NullTranslationHelper) + tool, _ := ListIssues(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "list_issues", tool.Name) assert.NotEmpty(t, tool.Description) @@ -698,7 +698,7 @@ func Test_ListIssues(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := ListIssues(client, translations.NullTranslationHelper) + _, handler := ListIssues(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -743,7 +743,7 @@ func Test_ListIssues(t *testing.T) { func Test_UpdateIssue(t *testing.T) { // Verify tool definition mockClient := github.NewClient(nil) - tool, _ := UpdateIssue(mockClient, translations.NullTranslationHelper) + tool, _ := UpdateIssue(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "update_issue", tool.Name) assert.NotEmpty(t, tool.Description) @@ -882,7 +882,7 @@ func Test_UpdateIssue(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := UpdateIssue(client, translations.NullTranslationHelper) + _, handler := UpdateIssue(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -1000,7 +1000,7 @@ func Test_ParseISOTimestamp(t *testing.T) { func Test_GetIssueComments(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := GetIssueComments(mockClient, translations.NullTranslationHelper) + tool, _ := GetIssueComments(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "get_issue_comments", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1100,7 +1100,7 @@ func Test_GetIssueComments(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := GetIssueComments(client, translations.NullTranslationHelper) + _, handler := GetIssueComments(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index c5f9d9fa..14aeb918 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -14,7 +14,7 @@ import ( ) // GetPullRequest creates a tool to get details of a specific pull request. -func GetPullRequest(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func GetPullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_pull_request", mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_DESCRIPTION", "Get details of a specific pull request")), mcp.WithString("owner", @@ -44,6 +44,10 @@ func GetPullRequest(client *github.Client, t translations.TranslationHelperFunc) 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 nil, fmt.Errorf("failed to get pull request: %w", err) @@ -68,7 +72,7 @@ func GetPullRequest(client *github.Client, t translations.TranslationHelperFunc) } // UpdatePullRequest creates a tool to update an existing pull request. -func UpdatePullRequest(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func UpdatePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("update_pull_request", mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_DESCRIPTION", "Update an existing pull request in a GitHub repository")), mcp.WithString("owner", @@ -157,6 +161,10 @@ func UpdatePullRequest(client *github.Client, t translations.TranslationHelperFu return mcp.NewToolResultError("No update parameters provided."), nil } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } pr, resp, err := client.PullRequests.Edit(ctx, owner, repo, pullNumber, update) if err != nil { return nil, fmt.Errorf("failed to update pull request: %w", err) @@ -181,7 +189,7 @@ func UpdatePullRequest(client *github.Client, t translations.TranslationHelperFu } // ListPullRequests creates a tool to list and filter repository pull requests. -func ListPullRequests(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_pull_requests", mcp.WithDescription(t("TOOL_LIST_PULL_REQUESTS_DESCRIPTION", "List and filter repository pull requests")), mcp.WithString("owner", @@ -255,6 +263,10 @@ func ListPullRequests(client *github.Client, t translations.TranslationHelperFun }, } + 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 nil, fmt.Errorf("failed to list pull requests: %w", err) @@ -279,7 +291,7 @@ func ListPullRequests(client *github.Client, t translations.TranslationHelperFun } // MergePullRequest creates a tool to merge a pull request. -func MergePullRequest(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("merge_pull_request", mcp.WithDescription(t("TOOL_MERGE_PULL_REQUEST_DESCRIPTION", "Merge a pull request")), mcp.WithString("owner", @@ -335,6 +347,10 @@ func MergePullRequest(client *github.Client, t translations.TranslationHelperFun 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 nil, fmt.Errorf("failed to merge pull request: %w", err) @@ -359,7 +375,7 @@ func MergePullRequest(client *github.Client, t translations.TranslationHelperFun } // GetPullRequestFiles creates a tool to get the list of files changed in a pull request. -func GetPullRequestFiles(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func GetPullRequestFiles(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_pull_request_files", mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_FILES_DESCRIPTION", "Get the list of files changed in a pull request")), mcp.WithString("owner", @@ -389,6 +405,10 @@ func GetPullRequestFiles(client *github.Client, t translations.TranslationHelper return mcp.NewToolResultError(err.Error()), nil } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } opts := &github.ListOptions{} files, resp, err := client.PullRequests.ListFiles(ctx, owner, repo, pullNumber, opts) if err != nil { @@ -414,7 +434,7 @@ func GetPullRequestFiles(client *github.Client, t translations.TranslationHelper } // GetPullRequestStatus creates a tool to get the combined status of all status checks for a pull request. -func GetPullRequestStatus(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func GetPullRequestStatus(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_pull_request_status", mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_STATUS_DESCRIPTION", "Get the combined status of all status checks for a pull request")), mcp.WithString("owner", @@ -444,6 +464,10 @@ func GetPullRequestStatus(client *github.Client, t translations.TranslationHelpe return mcp.NewToolResultError(err.Error()), nil } // First get the PR to find the head SHA + 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 nil, fmt.Errorf("failed to get pull request: %w", err) @@ -483,7 +507,7 @@ func GetPullRequestStatus(client *github.Client, t translations.TranslationHelpe } // UpdatePullRequestBranch creates a tool to update a pull request branch with the latest changes from the base branch. -func UpdatePullRequestBranch(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("update_pull_request_branch", mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_BRANCH_DESCRIPTION", "Update a pull request branch with the latest changes from the base branch")), mcp.WithString("owner", @@ -524,6 +548,10 @@ func UpdatePullRequestBranch(client *github.Client, t translations.TranslationHe opts.ExpectedHeadSHA = github.Ptr(expectedHeadSHA) } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } result, resp, err := client.PullRequests.UpdateBranch(ctx, owner, repo, pullNumber, opts) if err != nil { // Check if it's an acceptedError. An acceptedError indicates that the update is in progress, @@ -553,7 +581,7 @@ func UpdatePullRequestBranch(client *github.Client, t translations.TranslationHe } // GetPullRequestComments creates a tool to get the review comments on a pull request. -func GetPullRequestComments(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func GetPullRequestComments(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_pull_request_comments", mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_COMMENTS_DESCRIPTION", "Get the review comments on a pull request")), mcp.WithString("owner", @@ -589,6 +617,10 @@ func GetPullRequestComments(client *github.Client, t translations.TranslationHel }, } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } comments, resp, err := client.PullRequests.ListComments(ctx, owner, repo, pullNumber, opts) if err != nil { return nil, fmt.Errorf("failed to get pull request comments: %w", err) @@ -613,7 +645,7 @@ func GetPullRequestComments(client *github.Client, t translations.TranslationHel } // GetPullRequestReviews creates a tool to get the reviews on a pull request. -func GetPullRequestReviews(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func GetPullRequestReviews(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_pull_request_reviews", mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_REVIEWS_DESCRIPTION", "Get the reviews on a pull request")), mcp.WithString("owner", @@ -643,6 +675,10 @@ func GetPullRequestReviews(client *github.Client, t translations.TranslationHelp return mcp.NewToolResultError(err.Error()), nil } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } reviews, resp, err := client.PullRequests.ListReviews(ctx, owner, repo, pullNumber, nil) if err != nil { return nil, fmt.Errorf("failed to get pull request reviews: %w", err) @@ -667,7 +703,7 @@ func GetPullRequestReviews(client *github.Client, t translations.TranslationHelp } // CreatePullRequestReview creates a tool to submit a review on a pull request. -func CreatePullRequestReview(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func CreatePullRequestReview(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("create_pull_request_review", mcp.WithDescription(t("TOOL_CREATE_PULL_REQUEST_REVIEW_DESCRIPTION", "Create a review on a pull request")), mcp.WithString("owner", @@ -835,6 +871,10 @@ func CreatePullRequestReview(client *github.Client, t translations.TranslationHe reviewRequest.Comments = comments } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } review, resp, err := client.PullRequests.CreateReview(ctx, owner, repo, pullNumber, reviewRequest) if err != nil { return nil, fmt.Errorf("failed to create pull request review: %w", err) @@ -859,7 +899,7 @@ func CreatePullRequestReview(client *github.Client, t translations.TranslationHe } // CreatePullRequest creates a tool to create a new pull request. -func CreatePullRequest(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("create_pull_request", mcp.WithDescription(t("TOOL_CREATE_PULL_REQUEST_DESCRIPTION", "Create a new pull request in a GitHub repository")), mcp.WithString("owner", @@ -942,6 +982,10 @@ func CreatePullRequest(client *github.Client, t translations.TranslationHelperFu newPR.Draft = github.Ptr(draft) newPR.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 nil, fmt.Errorf("failed to create pull request: %w", err) diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go index e9647029..3c20dfc2 100644 --- a/pkg/github/pullrequests_test.go +++ b/pkg/github/pullrequests_test.go @@ -17,7 +17,7 @@ import ( func Test_GetPullRequest(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := GetPullRequest(mockClient, translations.NullTranslationHelper) + tool, _ := GetPullRequest(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "get_pull_request", tool.Name) assert.NotEmpty(t, tool.Description) @@ -94,7 +94,7 @@ func Test_GetPullRequest(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := GetPullRequest(client, translations.NullTranslationHelper) + _, handler := GetPullRequest(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -129,7 +129,7 @@ func Test_GetPullRequest(t *testing.T) { func Test_UpdatePullRequest(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := UpdatePullRequest(mockClient, translations.NullTranslationHelper) + tool, _ := UpdatePullRequest(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "update_pull_request", tool.Name) assert.NotEmpty(t, tool.Description) @@ -257,7 +257,7 @@ func Test_UpdatePullRequest(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := UpdatePullRequest(client, translations.NullTranslationHelper) + _, handler := UpdatePullRequest(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -311,7 +311,7 @@ func Test_UpdatePullRequest(t *testing.T) { func Test_ListPullRequests(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := ListPullRequests(mockClient, translations.NullTranslationHelper) + tool, _ := ListPullRequests(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "list_pull_requests", tool.Name) assert.NotEmpty(t, tool.Description) @@ -403,7 +403,7 @@ func Test_ListPullRequests(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := ListPullRequests(client, translations.NullTranslationHelper) + _, handler := ListPullRequests(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -441,7 +441,7 @@ func Test_ListPullRequests(t *testing.T) { func Test_MergePullRequest(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := MergePullRequest(mockClient, translations.NullTranslationHelper) + tool, _ := MergePullRequest(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "merge_pull_request", tool.Name) assert.NotEmpty(t, tool.Description) @@ -518,7 +518,7 @@ func Test_MergePullRequest(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := MergePullRequest(client, translations.NullTranslationHelper) + _, handler := MergePullRequest(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -552,7 +552,7 @@ func Test_MergePullRequest(t *testing.T) { func Test_GetPullRequestFiles(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := GetPullRequestFiles(mockClient, translations.NullTranslationHelper) + tool, _ := GetPullRequestFiles(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "get_pull_request_files", tool.Name) assert.NotEmpty(t, tool.Description) @@ -630,7 +630,7 @@ func Test_GetPullRequestFiles(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := GetPullRequestFiles(client, translations.NullTranslationHelper) + _, handler := GetPullRequestFiles(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -668,7 +668,7 @@ func Test_GetPullRequestFiles(t *testing.T) { func Test_GetPullRequestStatus(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := GetPullRequestStatus(mockClient, translations.NullTranslationHelper) + tool, _ := GetPullRequestStatus(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "get_pull_request_status", tool.Name) assert.NotEmpty(t, tool.Description) @@ -790,7 +790,7 @@ func Test_GetPullRequestStatus(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := GetPullRequestStatus(client, translations.NullTranslationHelper) + _, handler := GetPullRequestStatus(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -829,7 +829,7 @@ func Test_GetPullRequestStatus(t *testing.T) { func Test_UpdatePullRequestBranch(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := UpdatePullRequestBranch(mockClient, translations.NullTranslationHelper) + tool, _ := UpdatePullRequestBranch(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "update_pull_request_branch", tool.Name) assert.NotEmpty(t, tool.Description) @@ -917,7 +917,7 @@ func Test_UpdatePullRequestBranch(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := UpdatePullRequestBranch(client, translations.NullTranslationHelper) + _, handler := UpdatePullRequestBranch(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -945,7 +945,7 @@ func Test_UpdatePullRequestBranch(t *testing.T) { func Test_GetPullRequestComments(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := GetPullRequestComments(mockClient, translations.NullTranslationHelper) + tool, _ := GetPullRequestComments(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "get_pull_request_comments", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1033,7 +1033,7 @@ func Test_GetPullRequestComments(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := GetPullRequestComments(client, translations.NullTranslationHelper) + _, handler := GetPullRequestComments(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -1072,7 +1072,7 @@ func Test_GetPullRequestComments(t *testing.T) { func Test_GetPullRequestReviews(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := GetPullRequestReviews(mockClient, translations.NullTranslationHelper) + tool, _ := GetPullRequestReviews(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "get_pull_request_reviews", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1156,7 +1156,7 @@ func Test_GetPullRequestReviews(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := GetPullRequestReviews(client, translations.NullTranslationHelper) + _, handler := GetPullRequestReviews(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -1195,7 +1195,7 @@ func Test_GetPullRequestReviews(t *testing.T) { func Test_CreatePullRequestReview(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := CreatePullRequestReview(mockClient, translations.NullTranslationHelper) + tool, _ := CreatePullRequestReview(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "create_pull_request_review", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1523,7 +1523,7 @@ func Test_CreatePullRequestReview(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := CreatePullRequestReview(client, translations.NullTranslationHelper) + _, handler := CreatePullRequestReview(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -1566,7 +1566,7 @@ func Test_CreatePullRequestReview(t *testing.T) { func Test_CreatePullRequest(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := CreatePullRequest(mockClient, translations.NullTranslationHelper) + tool, _ := CreatePullRequest(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "create_pull_request", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1678,7 +1678,7 @@ func Test_CreatePullRequest(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := CreatePullRequest(client, translations.NullTranslationHelper) + _, handler := CreatePullRequest(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 2dafd4ce..f52c0341 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -14,7 +14,7 @@ import ( ) // ListCommits creates a tool to get commits of a branch in a repository. -func ListCommits(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_commits", mcp.WithDescription(t("TOOL_LIST_COMMITS_DESCRIPTION", "Get list of commits of a branch in a GitHub repository")), mcp.WithString("owner", @@ -56,6 +56,10 @@ func ListCommits(client *github.Client, t translations.TranslationHelperFunc) (t }, } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } commits, resp, err := client.Repositories.ListCommits(ctx, owner, repo, opts) if err != nil { return nil, fmt.Errorf("failed to list commits: %w", err) @@ -80,7 +84,7 @@ func ListCommits(client *github.Client, t translations.TranslationHelperFunc) (t } // CreateOrUpdateFile creates a tool to create or update a file in a GitHub repository. -func CreateOrUpdateFile(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func CreateOrUpdateFile(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("create_or_update_file", mcp.WithDescription(t("TOOL_CREATE_OR_UPDATE_FILE_DESCRIPTION", "Create or update a single file in a GitHub repository")), mcp.WithString("owner", @@ -157,6 +161,10 @@ func CreateOrUpdateFile(client *github.Client, t translations.TranslationHelperF } // Create or update the file + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } fileContent, resp, err := client.Repositories.CreateFile(ctx, owner, repo, path, opts) if err != nil { return nil, fmt.Errorf("failed to create/update file: %w", err) @@ -181,7 +189,7 @@ func CreateOrUpdateFile(client *github.Client, t translations.TranslationHelperF } // CreateRepository creates a tool to create a new GitHub repository. -func CreateRepository(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("create_repository", mcp.WithDescription(t("TOOL_CREATE_REPOSITORY_DESCRIPTION", "Create a new GitHub repository in your account")), mcp.WithString("name", @@ -223,6 +231,10 @@ func CreateRepository(client *github.Client, t translations.TranslationHelperFun 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 nil, fmt.Errorf("failed to create repository: %w", err) @@ -247,7 +259,7 @@ func CreateRepository(client *github.Client, t translations.TranslationHelperFun } // GetFileContents creates a tool to get the contents of a file or directory from a GitHub repository. -func GetFileContents(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func GetFileContents(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_file_contents", mcp.WithDescription(t("TOOL_GET_FILE_CONTENTS_DESCRIPTION", "Get the contents of a file or directory from a GitHub repository")), mcp.WithString("owner", @@ -284,6 +296,10 @@ func GetFileContents(client *github.Client, t translations.TranslationHelperFunc return mcp.NewToolResultError(err.Error()), nil } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } opts := &github.RepositoryContentGetOptions{Ref: branch} fileContent, dirContent, resp, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) if err != nil { @@ -316,7 +332,7 @@ func GetFileContents(client *github.Client, t translations.TranslationHelperFunc } // ForkRepository creates a tool to fork a repository. -func ForkRepository(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func ForkRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("fork_repository", mcp.WithDescription(t("TOOL_FORK_REPOSITORY_DESCRIPTION", "Fork a GitHub repository to your account or specified organization")), mcp.WithString("owner", @@ -350,6 +366,10 @@ func ForkRepository(client *github.Client, t translations.TranslationHelperFunc) opts.Organization = org } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } forkedRepo, resp, err := client.Repositories.CreateFork(ctx, owner, repo, opts) if err != nil { // Check if it's an acceptedError. An acceptedError indicates that the update is in progress, @@ -379,7 +399,7 @@ func ForkRepository(client *github.Client, t translations.TranslationHelperFunc) } // CreateBranch creates a tool to create a new branch. -func CreateBranch(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func CreateBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("create_branch", mcp.WithDescription(t("TOOL_CREATE_BRANCH_DESCRIPTION", "Create a new branch in a GitHub repository")), mcp.WithString("owner", @@ -416,6 +436,11 @@ func CreateBranch(client *github.Client, t translations.TranslationHelperFunc) ( return mcp.NewToolResultError(err.Error()), nil } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + // Get the source branch SHA var ref *github.Reference @@ -459,7 +484,7 @@ func CreateBranch(client *github.Client, t translations.TranslationHelperFunc) ( } // PushFiles creates a tool to push multiple files in a single commit to a GitHub repository. -func PushFiles(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func PushFiles(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("push_files", mcp.WithDescription(t("TOOL_PUSH_FILES_DESCRIPTION", "Push multiple files to a GitHub repository in a single commit")), mcp.WithString("owner", @@ -523,6 +548,11 @@ func PushFiles(client *github.Client, t translations.TranslationHelperFunc) (too return mcp.NewToolResultError("files parameter must be an array of objects with path and content"), nil } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + // Get the reference for the branch ref, resp, err := client.Git.GetRef(ctx, owner, repo, "refs/heads/"+branch) if err != nil { diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index 5c47183d..2dc0cff9 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -18,7 +18,7 @@ import ( func Test_GetFileContents(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := GetFileContents(mockClient, translations.NullTranslationHelper) + tool, _ := GetFileContents(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "get_file_contents", tool.Name) assert.NotEmpty(t, tool.Description) @@ -132,7 +132,7 @@ func Test_GetFileContents(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := GetFileContents(client, translations.NullTranslationHelper) + _, handler := GetFileContents(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := mcp.CallToolRequest{ @@ -189,7 +189,7 @@ func Test_GetFileContents(t *testing.T) { func Test_ForkRepository(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := ForkRepository(mockClient, translations.NullTranslationHelper) + tool, _ := ForkRepository(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "fork_repository", tool.Name) assert.NotEmpty(t, tool.Description) @@ -259,7 +259,7 @@ func Test_ForkRepository(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := ForkRepository(client, translations.NullTranslationHelper) + _, handler := ForkRepository(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -287,7 +287,7 @@ func Test_ForkRepository(t *testing.T) { func Test_CreateBranch(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := CreateBranch(mockClient, translations.NullTranslationHelper) + tool, _ := CreateBranch(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "create_branch", tool.Name) assert.NotEmpty(t, tool.Description) @@ -445,7 +445,7 @@ func Test_CreateBranch(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := CreateBranch(client, translations.NullTranslationHelper) + _, handler := CreateBranch(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -478,7 +478,7 @@ func Test_CreateBranch(t *testing.T) { func Test_ListCommits(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := ListCommits(mockClient, translations.NullTranslationHelper) + tool, _ := ListCommits(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "list_commits", tool.Name) assert.NotEmpty(t, tool.Description) @@ -614,7 +614,7 @@ func Test_ListCommits(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := ListCommits(client, translations.NullTranslationHelper) + _, handler := ListCommits(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -652,7 +652,7 @@ func Test_ListCommits(t *testing.T) { func Test_CreateOrUpdateFile(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := CreateOrUpdateFile(mockClient, translations.NullTranslationHelper) + tool, _ := CreateOrUpdateFile(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "create_or_update_file", tool.Name) assert.NotEmpty(t, tool.Description) @@ -775,7 +775,7 @@ func Test_CreateOrUpdateFile(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := CreateOrUpdateFile(client, translations.NullTranslationHelper) + _, handler := CreateOrUpdateFile(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -815,7 +815,7 @@ func Test_CreateOrUpdateFile(t *testing.T) { func Test_CreateRepository(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := CreateRepository(mockClient, translations.NullTranslationHelper) + tool, _ := CreateRepository(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "create_repository", tool.Name) assert.NotEmpty(t, tool.Description) @@ -923,7 +923,7 @@ func Test_CreateRepository(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := CreateRepository(client, translations.NullTranslationHelper) + _, handler := CreateRepository(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -961,7 +961,7 @@ func Test_CreateRepository(t *testing.T) { func Test_PushFiles(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := PushFiles(mockClient, translations.NullTranslationHelper) + tool, _ := PushFiles(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "push_files", tool.Name) assert.NotEmpty(t, tool.Description) @@ -1256,7 +1256,7 @@ func Test_PushFiles(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := PushFiles(client, translations.NullTranslationHelper) + _, handler := PushFiles(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index 47cb8bf6..949157f5 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -18,52 +18,52 @@ import ( ) // GetRepositoryResourceContent defines the resource template and handler for getting repository content. -func GetRepositoryResourceContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +func GetRepositoryResourceContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { return mcp.NewResourceTemplate( "repo://{owner}/{repo}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_DESCRIPTION", "Repository Content"), ), - RepositoryResourceContentsHandler(client) + RepositoryResourceContentsHandler(getClient) } // GetRepositoryResourceBranchContent defines the resource template and handler for getting repository content for a branch. -func GetRepositoryResourceBranchContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +func GetRepositoryResourceBranchContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { return mcp.NewResourceTemplate( "repo://{owner}/{repo}/refs/heads/{branch}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_BRANCH_DESCRIPTION", "Repository Content for specific branch"), ), - RepositoryResourceContentsHandler(client) + RepositoryResourceContentsHandler(getClient) } // GetRepositoryResourceCommitContent defines the resource template and handler for getting repository content for a commit. -func GetRepositoryResourceCommitContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +func GetRepositoryResourceCommitContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { return mcp.NewResourceTemplate( "repo://{owner}/{repo}/sha/{sha}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_COMMIT_DESCRIPTION", "Repository Content for specific commit"), ), - RepositoryResourceContentsHandler(client) + RepositoryResourceContentsHandler(getClient) } // GetRepositoryResourceTagContent defines the resource template and handler for getting repository content for a tag. -func GetRepositoryResourceTagContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +func GetRepositoryResourceTagContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { return mcp.NewResourceTemplate( "repo://{owner}/{repo}/refs/tags/{tag}/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_TAG_DESCRIPTION", "Repository Content for specific tag"), ), - RepositoryResourceContentsHandler(client) + RepositoryResourceContentsHandler(getClient) } // GetRepositoryResourcePrContent defines the resource template and handler for getting repository content for a pull request. -func GetRepositoryResourcePrContent(client *github.Client, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { +func GetRepositoryResourcePrContent(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { return mcp.NewResourceTemplate( "repo://{owner}/{repo}/refs/pull/{prNumber}/head/contents{/path*}", // Resource template t("RESOURCE_REPOSITORY_CONTENT_PR_DESCRIPTION", "Repository Content for specific pull request"), ), - RepositoryResourceContentsHandler(client) + RepositoryResourceContentsHandler(getClient) } // RepositoryResourceContentsHandler returns a handler function for repository content requests. -func RepositoryResourceContentsHandler(client *github.Client) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { +func RepositoryResourceContentsHandler(getClient GetClientFn) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { return func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { // the matcher will give []string with one element // https://github.com/mark3labs/mcp-go/pull/54 @@ -107,6 +107,10 @@ func RepositoryResourceContentsHandler(client *github.Client) func(ctx context.C opts.Ref = "refs/pull/" + prNumber[0] + "/head" } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } fileContent, directoryContent, _, err := client.Repositories.GetContents(ctx, owner, repo, path, opts) if err != nil { return nil, err diff --git a/pkg/github/repository_resource_test.go b/pkg/github/repository_resource_test.go index c274d1b5..ffd14be3 100644 --- a/pkg/github/repository_resource_test.go +++ b/pkg/github/repository_resource_test.go @@ -234,7 +234,7 @@ func Test_repositoryResourceContentsHandler(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := github.NewClient(tc.mockedClient) - handler := RepositoryResourceContentsHandler(client) + handler := RepositoryResourceContentsHandler((stubGetClientFn(client))) request := mcp.ReadResourceRequest{ Params: struct { diff --git a/pkg/github/search.go b/pkg/github/search.go index cd2ab434..75810e24 100644 --- a/pkg/github/search.go +++ b/pkg/github/search.go @@ -13,7 +13,7 @@ import ( ) // SearchRepositories creates a tool to search for GitHub repositories. -func SearchRepositories(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func SearchRepositories(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("search_repositories", mcp.WithDescription(t("TOOL_SEARCH_REPOSITORIES_DESCRIPTION", "Search for GitHub repositories")), mcp.WithString("query", @@ -39,6 +39,10 @@ func SearchRepositories(client *github.Client, t translations.TranslationHelperF }, } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } result, resp, err := client.Search.Repositories(ctx, query, opts) if err != nil { return nil, fmt.Errorf("failed to search repositories: %w", err) @@ -63,7 +67,7 @@ func SearchRepositories(client *github.Client, t translations.TranslationHelperF } // SearchCode creates a tool to search for code across GitHub repositories. -func SearchCode(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("search_code", mcp.WithDescription(t("TOOL_SEARCH_CODE_DESCRIPTION", "Search for code across GitHub repositories")), mcp.WithString("q", @@ -106,6 +110,11 @@ func SearchCode(client *github.Client, t translations.TranslationHelperFunc) (to }, } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + result, resp, err := client.Search.Code(ctx, query, opts) if err != nil { return nil, fmt.Errorf("failed to search code: %w", err) @@ -130,7 +139,7 @@ func SearchCode(client *github.Client, t translations.TranslationHelperFunc) (to } // SearchUsers creates a tool to search for GitHub users. -func SearchUsers(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func SearchUsers(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("search_users", mcp.WithDescription(t("TOOL_SEARCH_USERS_DESCRIPTION", "Search for GitHub users")), mcp.WithString("q", @@ -174,6 +183,11 @@ func SearchUsers(client *github.Client, t translations.TranslationHelperFunc) (t }, } + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + result, resp, err := client.Search.Users(ctx, query, opts) if err != nil { return nil, fmt.Errorf("failed to search users: %w", err) diff --git a/pkg/github/search_test.go b/pkg/github/search_test.go index b000a0bf..b61518e4 100644 --- a/pkg/github/search_test.go +++ b/pkg/github/search_test.go @@ -16,7 +16,7 @@ import ( func Test_SearchRepositories(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := SearchRepositories(mockClient, translations.NullTranslationHelper) + tool, _ := SearchRepositories(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "search_repositories", tool.Name) assert.NotEmpty(t, tool.Description) @@ -122,7 +122,7 @@ func Test_SearchRepositories(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := SearchRepositories(client, translations.NullTranslationHelper) + _, handler := SearchRepositories(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -163,7 +163,7 @@ func Test_SearchRepositories(t *testing.T) { func Test_SearchCode(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := SearchCode(mockClient, translations.NullTranslationHelper) + tool, _ := SearchCode(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "search_code", tool.Name) assert.NotEmpty(t, tool.Description) @@ -273,7 +273,7 @@ func Test_SearchCode(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := SearchCode(client, translations.NullTranslationHelper) + _, handler := SearchCode(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) @@ -314,7 +314,7 @@ func Test_SearchCode(t *testing.T) { func Test_SearchUsers(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) - tool, _ := SearchUsers(mockClient, translations.NullTranslationHelper) + tool, _ := SearchUsers(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "search_users", tool.Name) assert.NotEmpty(t, tool.Description) @@ -428,7 +428,7 @@ func Test_SearchUsers(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := SearchUsers(client, translations.NullTranslationHelper) + _, handler := SearchUsers(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) diff --git a/pkg/github/server.go b/pkg/github/server.go index 80457a54..9dee1596 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -14,8 +14,10 @@ import ( "github.com/mark3labs/mcp-go/server" ) +type GetClientFn func(context.Context) (*github.Client, error) + // NewServer creates a new GitHub MCP server with the specified GH client and logger. -func NewServer(client *github.Client, version string, readOnly bool, t translations.TranslationHelperFunc) *server.MCPServer { +func NewServer(getClient GetClientFn, version string, readOnly bool, t translations.TranslationHelperFunc) *server.MCPServer { // Create a new MCP server s := server.NewMCPServer( "github-mcp-server", @@ -24,65 +26,65 @@ func NewServer(client *github.Client, version string, readOnly bool, t translati server.WithLogging()) // Add GitHub Resources - s.AddResourceTemplate(GetRepositoryResourceContent(client, t)) - s.AddResourceTemplate(GetRepositoryResourceBranchContent(client, t)) - s.AddResourceTemplate(GetRepositoryResourceCommitContent(client, t)) - s.AddResourceTemplate(GetRepositoryResourceTagContent(client, t)) - s.AddResourceTemplate(GetRepositoryResourcePrContent(client, t)) + s.AddResourceTemplate(GetRepositoryResourceContent(getClient, t)) + s.AddResourceTemplate(GetRepositoryResourceBranchContent(getClient, t)) + s.AddResourceTemplate(GetRepositoryResourceCommitContent(getClient, t)) + s.AddResourceTemplate(GetRepositoryResourceTagContent(getClient, t)) + s.AddResourceTemplate(GetRepositoryResourcePrContent(getClient, t)) // Add GitHub tools - Issues - s.AddTool(GetIssue(client, t)) - s.AddTool(SearchIssues(client, t)) - s.AddTool(ListIssues(client, t)) - s.AddTool(GetIssueComments(client, t)) + s.AddTool(GetIssue(getClient, t)) + s.AddTool(SearchIssues(getClient, t)) + s.AddTool(ListIssues(getClient, t)) + s.AddTool(GetIssueComments(getClient, t)) if !readOnly { - s.AddTool(CreateIssue(client, t)) - s.AddTool(AddIssueComment(client, t)) - s.AddTool(UpdateIssue(client, t)) + s.AddTool(CreateIssue(getClient, t)) + s.AddTool(AddIssueComment(getClient, t)) + s.AddTool(UpdateIssue(getClient, t)) } // Add GitHub tools - Pull Requests - s.AddTool(GetPullRequest(client, t)) - s.AddTool(ListPullRequests(client, t)) - s.AddTool(GetPullRequestFiles(client, t)) - s.AddTool(GetPullRequestStatus(client, t)) - s.AddTool(GetPullRequestComments(client, t)) - s.AddTool(GetPullRequestReviews(client, t)) + s.AddTool(GetPullRequest(getClient, t)) + s.AddTool(ListPullRequests(getClient, t)) + s.AddTool(GetPullRequestFiles(getClient, t)) + s.AddTool(GetPullRequestStatus(getClient, t)) + s.AddTool(GetPullRequestComments(getClient, t)) + s.AddTool(GetPullRequestReviews(getClient, t)) if !readOnly { - s.AddTool(MergePullRequest(client, t)) - s.AddTool(UpdatePullRequestBranch(client, t)) - s.AddTool(CreatePullRequestReview(client, t)) - s.AddTool(CreatePullRequest(client, t)) - s.AddTool(UpdatePullRequest(client, t)) + s.AddTool(MergePullRequest(getClient, t)) + s.AddTool(UpdatePullRequestBranch(getClient, t)) + s.AddTool(CreatePullRequestReview(getClient, t)) + s.AddTool(CreatePullRequest(getClient, t)) + s.AddTool(UpdatePullRequest(getClient, t)) } // Add GitHub tools - Repositories - s.AddTool(SearchRepositories(client, t)) - s.AddTool(GetFileContents(client, t)) - s.AddTool(ListCommits(client, t)) + s.AddTool(SearchRepositories(getClient, t)) + s.AddTool(GetFileContents(getClient, t)) + s.AddTool(ListCommits(getClient, t)) if !readOnly { - s.AddTool(CreateOrUpdateFile(client, t)) - s.AddTool(CreateRepository(client, t)) - s.AddTool(ForkRepository(client, t)) - s.AddTool(CreateBranch(client, t)) - s.AddTool(PushFiles(client, t)) + s.AddTool(CreateOrUpdateFile(getClient, t)) + s.AddTool(CreateRepository(getClient, t)) + s.AddTool(ForkRepository(getClient, t)) + s.AddTool(CreateBranch(getClient, t)) + s.AddTool(PushFiles(getClient, t)) } // Add GitHub tools - Search - s.AddTool(SearchCode(client, t)) - s.AddTool(SearchUsers(client, t)) + s.AddTool(SearchCode(getClient, t)) + s.AddTool(SearchUsers(getClient, t)) // Add GitHub tools - Users - s.AddTool(GetMe(client, t)) + s.AddTool(GetMe(getClient, t)) // Add GitHub tools - Code Scanning - s.AddTool(GetCodeScanningAlert(client, t)) - s.AddTool(ListCodeScanningAlerts(client, t)) + s.AddTool(GetCodeScanningAlert(getClient, t)) + s.AddTool(ListCodeScanningAlerts(getClient, t)) return s } // GetMe creates a tool to get details of the authenticated user. -func GetMe(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_me", mcp.WithDescription(t("TOOL_GET_ME_DESCRIPTION", "Get details of the authenticated GitHub user. Use this when a request include \"me\", \"my\"...")), mcp.WithString("reason", @@ -90,6 +92,10 @@ func GetMe(client *github.Client, t translations.TranslationHelperFunc) (tool mc ), ), func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } user, resp, err := client.Users.Get(ctx, "") if err != nil { return nil, fmt.Errorf("failed to get user: %w", err) diff --git a/pkg/github/server_test.go b/pkg/github/server_test.go index 979046fc..3ee9851a 100644 --- a/pkg/github/server_test.go +++ b/pkg/github/server_test.go @@ -15,10 +15,16 @@ import ( "github.com/stretchr/testify/require" ) +func stubGetClientFn(client *github.Client) GetClientFn { + return func(_ context.Context) (*github.Client, error) { + return client, nil + } +} + func Test_GetMe(t *testing.T) { // Verify tool definition mockClient := github.NewClient(nil) - tool, _ := GetMe(mockClient, translations.NullTranslationHelper) + tool, _ := GetMe(stubGetClientFn(mockClient), translations.NullTranslationHelper) assert.Equal(t, "get_me", tool.Name) assert.NotEmpty(t, tool.Description) @@ -96,7 +102,7 @@ func Test_GetMe(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup client with mock client := github.NewClient(tc.mockedClient) - _, handler := GetMe(client, translations.NullTranslationHelper) + _, handler := GetMe(stubGetClientFn(client), translations.NullTranslationHelper) // Create call request request := createMCPRequest(tc.requestArgs) From 7145142dc6b59f1806c3179d30fb970c91f4005a Mon Sep 17 00:00:00 2001 From: Eng Zer Jun Date: Thu, 10 Apr 2025 19:15:55 +0800 Subject: [PATCH 19/41] docs: fix CODEOWNERS syntax (#184) Currently PRs are not being automatically requested for review because the CODEOWNERS file is missing a `*`. Co-authored-by: Javier Uruen Val --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 998d87b2..954bc41c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -@juruen @sammorrowdrums @williammartin @toby +* @juruen @sammorrowdrums @williammartin @toby From 651a3aaac5cfbd32ab6c4a92814da36533740a35 Mon Sep 17 00:00:00 2001 From: Chiedo John <2156688+chiedo@users.noreply.github.com> Date: Thu, 10 Apr 2025 09:09:38 -0400 Subject: [PATCH 20/41] Assume less (#214) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b78be380..3ca44428 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ automation and interaction capabilities for developers and tools. ## Prerequisites 1. To run the server in a container, you will need to have [Docker](https://www.docker.com/) installed. -2. [Create a GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new). +2. Once Docker is installed, you will also need to ensure Docker is running. +3. Lastly you will need to [Create a GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new). The MCP server can use many of the GitHub APIs, so enable the permissions that you feel comfortable granting your AI tools (to learn more about access tokens, please check out the [documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)). From 919a10c18781807a8a36f90c63efe47380e4d147 Mon Sep 17 00:00:00 2001 From: Jake Shorty Date: Fri, 11 Apr 2025 00:09:19 -0600 Subject: [PATCH 21/41] Add tool for getting a single commit (includes stats, files) (#216) * Add tool for getting a commit * Split mock back out, use RepositoryCommit with Files/Stats --- README.md | 9 ++- pkg/github/repositories.go | 67 ++++++++++++++++ pkg/github/repositories_test.go | 130 ++++++++++++++++++++++++++++++++ pkg/github/server.go | 1 + 4 files changed, 206 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ca44428..f85663ab 100644 --- a/README.md +++ b/README.md @@ -354,7 +354,7 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `branch`: New branch name (string, required) - `sha`: SHA to create branch from (string, required) -- **list_commits** - Gets commits of a branch in a repository +- **list_commits** - Get a list of commits of a branch in a repository - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `sha`: Branch name, tag, or commit SHA (string, optional) @@ -362,6 +362,13 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `page`: Page number (number, optional) - `perPage`: Results per page (number, optional) +- **get_commit** - Get details for a commit from a repository + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `sha`: Commit SHA, branch name, or tag name (string, required) + - `page`: Page number, for files in the commit (number, optional) + - `perPage`: Results per page, for files in the commit (number, optional) + ### Search - **search_code** - Search for code across GitHub repositories diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index f52c0341..56500eaf 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -13,6 +13,73 @@ import ( "github.com/mark3labs/mcp-go/server" ) +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")), + 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, branch name, or tag name"), + ), + WithPagination(), + ), + func(ctx context.Context, 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 + } + sha, err := requiredParam[string](request, "sha") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + pagination, err := OptionalPaginationParams(request) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + opts := &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) + } + commit, resp, err := client.Repositories.GetCommit(ctx, owner, repo, sha, opts) + if err != nil { + return nil, fmt.Errorf("failed to get commit: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != 200 { + 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 commit: %s", string(body))), nil + } + + r, err := json.Marshal(commit) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + // ListCommits creates a tool to get commits of a branch in a repository. func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_commits", diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index 2dc0cff9..20f96dde 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -475,6 +475,136 @@ func Test_CreateBranch(t *testing.T) { } } +func Test_GetCommit(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := GetCommit(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "get_commit", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "owner") + assert.Contains(t, tool.InputSchema.Properties, "repo") + assert.Contains(t, tool.InputSchema.Properties, "sha") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "sha"}) + + mockCommit := &github.RepositoryCommit{ + SHA: github.Ptr("abc123def456"), + Commit: &github.Commit{ + Message: github.Ptr("First commit"), + Author: &github.CommitAuthor{ + Name: github.Ptr("Test User"), + Email: github.Ptr("test@example.com"), + Date: &github.Timestamp{Time: time.Now().Add(-48 * time.Hour)}, + }, + }, + Author: &github.User{ + Login: github.Ptr("testuser"), + }, + HTMLURL: github.Ptr("https://github.com/owner/repo/commit/abc123def456"), + Stats: &github.CommitStats{ + Additions: github.Ptr(10), + Deletions: github.Ptr(2), + Total: github.Ptr(12), + }, + Files: []*github.CommitFile{ + { + Filename: github.Ptr("file1.go"), + Status: github.Ptr("modified"), + Additions: github.Ptr(10), + Deletions: github.Ptr(2), + Changes: github.Ptr(12), + Patch: github.Ptr("@@ -1,2 +1,10 @@"), + }, + }, + } + // This one currently isn't defined in the mock package we're using. + var mockEndpointPattern = mock.EndpointPattern{ + Pattern: "/repos/{owner}/{repo}/commits/{sha}", + Method: "GET", + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedCommit *github.RepositoryCommit + expectedErrMsg string + }{ + { + name: "successful commit fetch", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mockEndpointPattern, + mockResponse(t, http.StatusOK, mockCommit), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "sha": "abc123def456", + }, + expectError: false, + expectedCommit: mockCommit, + }, + { + name: "commit fetch fails", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mockEndpointPattern, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message": "Not Found"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "sha": "nonexistent-sha", + }, + expectError: true, + expectedErrMsg: "failed to get commit", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := GetCommit(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErrMsg) + return + } + + require.NoError(t, err) + + // Parse the result and get the text content if no error + textContent := getTextResult(t, result) + + // Unmarshal and verify the result + var returnedCommit github.RepositoryCommit + err = json.Unmarshal([]byte(textContent.Text), &returnedCommit) + require.NoError(t, err) + + assert.Equal(t, *tc.expectedCommit.SHA, *returnedCommit.SHA) + assert.Equal(t, *tc.expectedCommit.Commit.Message, *returnedCommit.Commit.Message) + assert.Equal(t, *tc.expectedCommit.Author.Login, *returnedCommit.Author.Login) + assert.Equal(t, *tc.expectedCommit.HTMLURL, *returnedCommit.HTMLURL) + }) + } +} + func Test_ListCommits(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) diff --git a/pkg/github/server.go b/pkg/github/server.go index 9dee1596..2d252b29 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -61,6 +61,7 @@ func NewServer(getClient GetClientFn, version string, readOnly bool, t translati // Add GitHub tools - Repositories s.AddTool(SearchRepositories(getClient, t)) s.AddTool(GetFileContents(getClient, t)) + s.AddTool(GetCommit(getClient, t)) s.AddTool(ListCommits(getClient, t)) if !readOnly { s.AddTool(CreateOrUpdateFile(getClient, t)) From 3c18a342c9bc5c503552caf669d5ef7b31f67f8c Mon Sep 17 00:00:00 2001 From: Adam Holt Date: Fri, 11 Apr 2025 10:52:05 +0200 Subject: [PATCH 22/41] Allow passing through server options (#218) * Allow passing through server options Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/github/server.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/github/server.go b/pkg/github/server.go index 2d252b29..490a8105 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -17,13 +17,20 @@ import ( type GetClientFn func(context.Context) (*github.Client, error) // NewServer creates a new GitHub MCP server with the specified GH client and logger. -func NewServer(getClient GetClientFn, version string, readOnly bool, t translations.TranslationHelperFunc) *server.MCPServer { +func NewServer(getClient GetClientFn, version string, readOnly bool, t translations.TranslationHelperFunc, opts ...server.ServerOption) *server.MCPServer { + // Add default options + defaultOpts := []server.ServerOption{ + server.WithResourceCapabilities(true, true), + server.WithLogging(), + } + opts = append(defaultOpts, opts...) + // Create a new MCP server s := server.NewMCPServer( "github-mcp-server", version, - server.WithResourceCapabilities(true, true), - server.WithLogging()) + opts..., + ) // Add GitHub Resources s.AddResourceTemplate(GetRepositoryResourceContent(getClient, t)) From 01aefd3d7a17cbaa33bc888004590bf5f991c6a9 Mon Sep 17 00:00:00 2001 From: Arya Soni <18515597+aryasoni98@users.noreply.github.com> Date: Fri, 11 Apr 2025 18:01:29 +0530 Subject: [PATCH 23/41] Add ability to view branches for a repo #141 (#205) * Add ability to view branches for a repo #141 * fix: update ListBranches test to use InputSchema and correct translation helper * fix: update ListBranches test to use InputSchema and correct translation helper * fix: update ListBranches test to handle errors in tool result * fix: replace deprecated github.String with github.Ptr * docs: add list_branches tool documentation to README --- README.md | 7 ++ pkg/github/repositories.go | 63 ++++++++++++++++++ pkg/github/repositories_test.go | 110 ++++++++++++++++++++++++++++++++ pkg/github/server.go | 1 + 4 files changed, 181 insertions(+) diff --git a/README.md b/README.md index f85663ab..00f49b64 100644 --- a/README.md +++ b/README.md @@ -311,6 +311,13 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `branch`: Branch name (string, optional) - `sha`: File SHA if updating (string, optional) +- **list_branches** - List branches in a GitHub repository + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `page`: Page number (number, optional) + - `perPage`: Results per page (number, optional) + - **push_files** - Push multiple files in a single commit - `owner`: Repository owner (string, required) diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 56500eaf..1d74dcfb 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -150,6 +150,69 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (t } } +// ListBranches creates a tool to list branches in a GitHub repository. +func ListBranches(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("list_branches", + mcp.WithDescription(t("TOOL_LIST_BRANCHES_DESCRIPTION", "List branches in a GitHub repository")), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + WithPagination(), + ), + func(ctx context.Context, 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 + } + pagination, err := OptionalPaginationParams(request) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + opts := &github.BranchListOptions{ + 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) + } + + branches, resp, err := client.Repositories.ListBranches(ctx, owner, repo, opts) + if err != nil { + return nil, fmt.Errorf("failed to list branches: %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 branches: %s", string(body))), nil + } + + r, err := json.Marshal(branches) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + // CreateOrUpdateFile creates a tool to create or update a file in a GitHub repository. func CreateOrUpdateFile(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("create_or_update_file", diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index 20f96dde..2410a31d 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -1423,3 +1423,113 @@ func Test_PushFiles(t *testing.T) { }) } } + +func Test_ListBranches(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := ListBranches(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "list_branches", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "owner") + assert.Contains(t, tool.InputSchema.Properties, "repo") + assert.Contains(t, tool.InputSchema.Properties, "page") + assert.Contains(t, tool.InputSchema.Properties, "perPage") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"}) + + // Setup mock branches for success case + mockBranches := []*github.Branch{ + { + Name: github.Ptr("main"), + Commit: &github.RepositoryCommit{SHA: github.Ptr("abc123")}, + }, + { + Name: github.Ptr("develop"), + Commit: &github.RepositoryCommit{SHA: github.Ptr("def456")}, + }, + } + + // Test cases + tests := []struct { + name string + args map[string]interface{} + mockResponses []mock.MockBackendOption + wantErr bool + errContains string + }{ + { + name: "success", + args: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "page": float64(2), + }, + mockResponses: []mock.MockBackendOption{ + mock.WithRequestMatch( + mock.GetReposBranchesByOwnerByRepo, + mockBranches, + ), + }, + wantErr: false, + }, + { + name: "missing owner", + args: map[string]interface{}{ + "repo": "repo", + }, + mockResponses: []mock.MockBackendOption{}, + wantErr: false, + errContains: "missing required parameter: owner", + }, + { + name: "missing repo", + args: map[string]interface{}{ + "owner": "owner", + }, + mockResponses: []mock.MockBackendOption{}, + wantErr: false, + errContains: "missing required parameter: repo", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create mock client + mockClient := github.NewClient(mock.NewMockedHTTPClient(tt.mockResponses...)) + _, handler := ListBranches(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + // Create request + request := createMCPRequest(tt.args) + + // Call handler + result, err := handler(context.Background(), request) + if tt.wantErr { + require.Error(t, err) + if tt.errContains != "" { + assert.Contains(t, err.Error(), tt.errContains) + } + return + } + + require.NoError(t, err) + require.NotNil(t, result) + + if tt.errContains != "" { + textContent := getTextResult(t, result) + assert.Contains(t, textContent.Text, tt.errContains) + return + } + + textContent := getTextResult(t, result) + require.NotEmpty(t, textContent.Text) + + // Verify response + var branches []*github.Branch + err = json.Unmarshal([]byte(textContent.Text), &branches) + require.NoError(t, err) + assert.Len(t, branches, 2) + assert.Equal(t, "main", *branches[0].Name) + assert.Equal(t, "develop", *branches[1].Name) + }) + } +} diff --git a/pkg/github/server.go b/pkg/github/server.go index 490a8105..f7eea97e 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -70,6 +70,7 @@ func NewServer(getClient GetClientFn, version string, readOnly bool, t translati s.AddTool(GetFileContents(getClient, t)) s.AddTool(GetCommit(getClient, t)) s.AddTool(ListCommits(getClient, t)) + s.AddTool(ListBranches(getClient, t)) if !readOnly { s.AddTool(CreateOrUpdateFile(getClient, t)) s.AddTool(CreateRepository(getClient, t)) From 865f9bf4d7133130ae112d0e15858831974a48af Mon Sep 17 00:00:00 2001 From: Jake Shorty Date: Fri, 11 Apr 2025 12:25:40 -0600 Subject: [PATCH 24/41] Prefer already-defined endpoint mock (#226) --- pkg/github/repositories_test.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pkg/github/repositories_test.go b/pkg/github/repositories_test.go index 2410a31d..5b8129fe 100644 --- a/pkg/github/repositories_test.go +++ b/pkg/github/repositories_test.go @@ -517,11 +517,6 @@ func Test_GetCommit(t *testing.T) { }, }, } - // This one currently isn't defined in the mock package we're using. - var mockEndpointPattern = mock.EndpointPattern{ - Pattern: "/repos/{owner}/{repo}/commits/{sha}", - Method: "GET", - } tests := []struct { name string @@ -535,7 +530,7 @@ func Test_GetCommit(t *testing.T) { name: "successful commit fetch", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( - mockEndpointPattern, + mock.GetReposCommitsByOwnerByRepoByRef, mockResponse(t, http.StatusOK, mockCommit), ), ), @@ -551,7 +546,7 @@ func Test_GetCommit(t *testing.T) { name: "commit fetch fails", mockedClient: mock.NewMockedHTTPClient( mock.WithRequestMatchHandler( - mockEndpointPattern, + mock.GetReposCommitsByOwnerByRepoByRef, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message": "Not Found"}`)) From 8343fa5900942c85cce4fb18f4af2d00bd889ad1 Mon Sep 17 00:00:00 2001 From: Hirofumi Omi <99390907+omihirofumi@users.noreply.github.com> Date: Sat, 12 Apr 2025 15:15:38 +0900 Subject: [PATCH 25/41] chore:update CONTRIBUTING.md style guide link (#234) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7e176e70..fe307d1d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ [fork]: https://github.com/github/github-mcp-server/fork [pr]: https://github.com/github/github-mcp-server/compare -[style]: https://github.com/github/github-mcp-server/blob/main/.golangci.yaml +[style]: https://github.com/github/github-mcp-server/blob/main/.golangci.yml Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. From 6c05b4009ab7a73fa690e3c11fb97b6bf6df07db Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Sat, 12 Apr 2025 02:45:10 -0700 Subject: [PATCH 26/41] Add tools for one-off PR comments and replying to PR review comments (#143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add add_pull_request_review_comment tool for PR review comments Adds the ability to add review comments to pull requests with support for line, multi-line, and file-level comments, as well as replying to existing comments. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Add reply_to_pull_request_review_comment tool Adds a new tool to reply to existing pull request review comments using the GitHub API's comment reply endpoint. This allows for threaded discussions on pull request reviews. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Update README with new PR review comment tools * rebase * use new getClient function inadd and reply pr review tools * Unify PR review comment tools into a single consolidated tool The separate AddPullRequestReviewComment and ReplyToPullRequestReviewComment tools have been merged into a single tool that handles both creating new comments and replying to existing ones. This approach simplifies the API and provides a more consistent interface for users. - Made commit_id and path optional when using in_reply_to for replies - Updated the tests to verify both comment and reply functionality - Removed the separate ReplyToPullRequestReviewComment tool - Fixed test expectations to match how errors are returned 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Update README to reflect the unified PR review comment tool --------- Co-authored-by: Claude Co-authored-by: Javier Uruen Val --- README.md | 15 +++ pkg/github/pullrequests.go | 170 +++++++++++++++++++++++++++ pkg/github/pullrequests_test.go | 197 ++++++++++++++++++++++++++++++++ pkg/github/server.go | 1 + 4 files changed, 383 insertions(+) diff --git a/README.md b/README.md index 00f49b64..ec8018a0 100644 --- a/README.md +++ b/README.md @@ -288,6 +288,21 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `draft`: Create as draft PR (boolean, optional) - `maintainer_can_modify`: Allow maintainer edits (boolean, optional) +- **add_pull_request_review_comment** - Add a review comment to a pull request or reply to an existing comment + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `pull_number`: Pull request number (number, required) + - `body`: The text of the review comment (string, required) + - `commit_id`: The SHA of the commit to comment on (string, required unless using in_reply_to) + - `path`: The relative path to the file that necessitates a comment (string, required unless using in_reply_to) + - `line`: The line of the blob in the pull request diff that the comment applies to (number, optional) + - `side`: The side of the diff to comment on (LEFT or RIGHT) (string, optional) + - `start_line`: For multi-line comments, the first line of the range (number, optional) + - `start_side`: For multi-line comments, the starting side of the diff (LEFT or RIGHT) (string, optional) + - `subject_type`: The level at which the comment is targeted (line or file) (string, optional) + - `in_reply_to`: The ID of the review comment to reply to (number, optional). When specified, only body is required and other parameters are ignored. + - **update_pull_request** - Update an existing pull request in a GitHub repository - `owner`: Repository owner (string, required) diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index 14aeb918..fd9420d7 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -644,6 +644,176 @@ func GetPullRequestComments(getClient GetClientFn, t translations.TranslationHel } } +// AddPullRequestReviewComment creates a tool to add a review comment to a pull request. +func AddPullRequestReviewComment(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("add_pull_request_review_comment", + mcp.WithDescription(t("TOOL_ADD_PULL_REQUEST_COMMENT_DESCRIPTION", "Add a review comment to a pull request")), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithNumber("pull_number", + mcp.Required(), + mcp.Description("Pull request number"), + ), + mcp.WithString("body", + mcp.Required(), + mcp.Description("The text of the review comment"), + ), + mcp.WithString("commit_id", + mcp.Description("The SHA of the commit to comment on. Required unless in_reply_to is specified."), + ), + mcp.WithString("path", + mcp.Description("The relative path to the file that necessitates a comment. Required unless in_reply_to is specified."), + ), + mcp.WithString("subject_type", + mcp.Description("The level at which the comment is targeted, 'line' or 'file'"), + mcp.Enum("line", "file"), + ), + mcp.WithNumber("line", + mcp.Description("The line of the blob in the pull request diff that the comment applies to. For multi-line comments, the last line of the range"), + ), + mcp.WithString("side", + mcp.Description("The side of the diff to comment on. Can be LEFT or RIGHT"), + mcp.Enum("LEFT", "RIGHT"), + ), + mcp.WithNumber("start_line", + mcp.Description("For multi-line comments, the first line of the range that the comment applies to"), + ), + mcp.WithString("start_side", + mcp.Description("For multi-line comments, the starting side of the diff that the comment applies to. Can be LEFT or RIGHT"), + mcp.Enum("LEFT", "RIGHT"), + ), + mcp.WithNumber("in_reply_to", + mcp.Description("The ID of the review comment to reply to. When specified, only body is required and all other parameters are ignored"), + ), + ), + func(ctx context.Context, 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, "pull_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) + } + + // Check if this is a reply to an existing comment + if replyToFloat, ok := request.Params.Arguments["in_reply_to"].(float64); ok { + // Use the specialized method for reply comments due to inconsistency in underlying go-github library: https://github.com/google/go-github/pull/950 + commentID := int64(replyToFloat) + createdReply, resp, err := client.PullRequests.CreateCommentInReplyTo(ctx, owner, repo, pullNumber, body, commentID) + if err != nil { + return nil, fmt.Errorf("failed to reply to pull request comment: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusCreated { + respBody, 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 reply to pull request comment: %s", string(respBody))), nil + } + + r, err := json.Marshal(createdReply) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } + + // This is a new comment, not a reply + // Verify required parameters for a new comment + commitID, err := requiredParam[string](request, "commit_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + path, err := requiredParam[string](request, "path") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + comment := &github.PullRequestComment{ + Body: github.Ptr(body), + CommitID: github.Ptr(commitID), + Path: github.Ptr(path), + } + + subjectType, err := OptionalParam[string](request, "subject_type") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + if subjectType != "file" { + line, lineExists := request.Params.Arguments["line"].(float64) + startLine, startLineExists := request.Params.Arguments["start_line"].(float64) + side, sideExists := request.Params.Arguments["side"].(string) + startSide, startSideExists := request.Params.Arguments["start_side"].(string) + + if !lineExists { + return mcp.NewToolResultError("line parameter is required unless using subject_type:file"), nil + } + + comment.Line = github.Ptr(int(line)) + if sideExists { + comment.Side = github.Ptr(side) + } + if startLineExists { + comment.StartLine = github.Ptr(int(startLine)) + } + if startSideExists { + comment.StartSide = github.Ptr(startSide) + } + + if startLineExists && !lineExists { + return mcp.NewToolResultError("if start_line is provided, line must also be provided"), nil + } + if startSideExists && !sideExists { + return mcp.NewToolResultError("if start_side is provided, side must also be provided"), nil + } + } + + createdComment, resp, err := client.PullRequests.CreateComment(ctx, owner, repo, pullNumber, comment) + if err != nil { + return nil, fmt.Errorf("failed to create pull request comment: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusCreated { + respBody, 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 pull request comment: %s", string(respBody))), nil + } + + r, err := json.Marshal(createdComment) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + // GetPullRequestReviews creates a tool to get the reviews on a pull request. func GetPullRequestReviews(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_pull_request_reviews", diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go index 3c20dfc2..bb372624 100644 --- a/pkg/github/pullrequests_test.go +++ b/pkg/github/pullrequests_test.go @@ -1719,3 +1719,200 @@ func Test_CreatePullRequest(t *testing.T) { }) } } + +func Test_AddPullRequestReviewComment(t *testing.T) { + mockClient := github.NewClient(nil) + tool, _ := AddPullRequestReviewComment(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "add_pull_request_review_comment", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "owner") + assert.Contains(t, tool.InputSchema.Properties, "repo") + assert.Contains(t, tool.InputSchema.Properties, "pull_number") + assert.Contains(t, tool.InputSchema.Properties, "body") + assert.Contains(t, tool.InputSchema.Properties, "commit_id") + assert.Contains(t, tool.InputSchema.Properties, "path") + // Since we've updated commit_id and path to be optional when using in_reply_to + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pull_number", "body"}) + + mockComment := &github.PullRequestComment{ + ID: github.Ptr(int64(123)), + Body: github.Ptr("Great stuff!"), + Path: github.Ptr("file1.txt"), + Line: github.Ptr(2), + Side: github.Ptr("RIGHT"), + } + + mockReply := &github.PullRequestComment{ + ID: github.Ptr(int64(456)), + Body: github.Ptr("Good point, will fix!"), + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedComment *github.PullRequestComment + expectedErrMsg string + }{ + { + name: "successful line comment creation", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PostReposPullsCommentsByOwnerByRepoByPullNumber, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusCreated) + err := json.NewEncoder(w).Encode(mockComment) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "pull_number": float64(1), + "body": "Great stuff!", + "commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e", + "path": "file1.txt", + "line": float64(2), + "side": "RIGHT", + }, + expectError: false, + expectedComment: mockComment, + }, + { + name: "successful reply using in_reply_to", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PostReposPullsCommentsByOwnerByRepoByPullNumber, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusCreated) + err := json.NewEncoder(w).Encode(mockReply) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "pull_number": float64(1), + "body": "Good point, will fix!", + "in_reply_to": float64(123), + }, + expectError: false, + expectedComment: mockReply, + }, + { + name: "comment creation fails", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PostReposPullsCommentsByOwnerByRepoByPullNumber, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnprocessableEntity) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"message": "Validation Failed"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "pull_number": float64(1), + "body": "Great stuff!", + "commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e", + "path": "file1.txt", + "line": float64(2), + }, + expectError: true, + expectedErrMsg: "failed to create pull request comment", + }, + { + name: "reply creation fails", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PostReposPullsCommentsByOwnerByRepoByPullNumber, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"message": "Comment not found"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "pull_number": float64(1), + "body": "Good point, will fix!", + "in_reply_to": float64(999), + }, + expectError: true, + expectedErrMsg: "failed to reply to pull request comment", + }, + { + name: "missing required parameters for comment", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "pull_number": float64(1), + "body": "Great stuff!", + // missing commit_id and path + }, + expectError: false, + expectedErrMsg: "missing required parameter: commit_id", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockClient := github.NewClient(tc.mockedClient) + + _, handler := AddPullRequestReviewComment(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + request := createMCPRequest(tc.requestArgs) + + result, err := handler(context.Background(), request) + + if tc.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErrMsg) + return + } + + require.NoError(t, err) + assert.NotNil(t, result) + require.Len(t, result.Content, 1) + + textContent := getTextResult(t, result) + if tc.expectedErrMsg != "" { + assert.Contains(t, textContent.Text, tc.expectedErrMsg) + return + } + + var returnedComment github.PullRequestComment + err = json.Unmarshal([]byte(getTextResult(t, result).Text), &returnedComment) + require.NoError(t, err) + + assert.Equal(t, *tc.expectedComment.ID, *returnedComment.ID) + assert.Equal(t, *tc.expectedComment.Body, *returnedComment.Body) + + // Only check Path, Line, and Side if they exist in the expected comment + if tc.expectedComment.Path != nil { + assert.Equal(t, *tc.expectedComment.Path, *returnedComment.Path) + } + if tc.expectedComment.Line != nil { + assert.Equal(t, *tc.expectedComment.Line, *returnedComment.Line) + } + if tc.expectedComment.Side != nil { + assert.Equal(t, *tc.expectedComment.Side, *returnedComment.Side) + } + }) + } +} diff --git a/pkg/github/server.go b/pkg/github/server.go index f7eea97e..da916b98 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -63,6 +63,7 @@ func NewServer(getClient GetClientFn, version string, readOnly bool, t translati s.AddTool(CreatePullRequestReview(getClient, t)) s.AddTool(CreatePullRequest(getClient, t)) s.AddTool(UpdatePullRequest(getClient, t)) + s.AddTool(AddPullRequestReviewComment(getClient, t)) } // Add GitHub tools - Repositories From 9dacf70b6e82edbc4bd0f9d1a0ccb91a5c599816 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Sat, 12 Apr 2025 11:23:49 +0200 Subject: [PATCH 27/41] chore: extend user agent --- cmd/github-mcp-server/main.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index f5539529..354ec3a9 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -13,6 +13,7 @@ import ( iolog "github.com/github/github-mcp-server/pkg/log" "github.com/github/github-mcp-server/pkg/translations" gogithub "github.com/google/go-github/v69/github" + "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -137,11 +138,19 @@ func runStdioServer(cfg runConfig) error { t, dumpTranslations := translations.TranslationHelper() + beforeInit := func(_ context.Context, _ any, message *mcp.InitializeRequest) { + ghClient.UserAgent = fmt.Sprintf("github-mcp-server/%s (%s/%s)", version, message.Params.ClientInfo.Name, message.Params.ClientInfo.Version) + } + getClient := func(_ context.Context) (*gogithub.Client, error) { return ghClient, nil // closing over client } + + hooks := &server.Hooks{ + OnBeforeInitialize: []server.OnBeforeInitializeFunc{beforeInit}, + } // Create - ghServer := github.NewServer(getClient, version, cfg.readOnly, t) + ghServer := github.NewServer(getClient, version, cfg.readOnly, t, server.WithHooks(hooks)) stdioServer := server.NewStdioServer(ghServer) stdLogger := stdlog.New(cfg.logger.Writer(), "stdioserver", 0) From bbba3bb7b06b5da2b5ac70003c9f9508c72255b7 Mon Sep 17 00:00:00 2001 From: Simon Danielsson <70206058+simondanielsson@users.noreply.github.com> Date: Sat, 12 Apr 2025 15:08:14 +0200 Subject: [PATCH 28/41] feat: pretty-print JSONL text responses in `mcpcurl` (#239) * Pretty-print jsonl text responses * Remove else in favour of continue --------- Co-authored-by: danielssonsimon --- cmd/mcpcurl/main.go | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/cmd/mcpcurl/main.go b/cmd/mcpcurl/main.go index b6676bfe..dfc639b9 100644 --- a/cmd/mcpcurl/main.go +++ b/cmd/mcpcurl/main.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "crypto/rand" "encoding/json" "fmt" "io" @@ -11,8 +12,6 @@ import ( "slices" "strings" - "crypto/rand" - "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -161,7 +160,7 @@ func main() { _ = rootCmd.MarkPersistentFlagRequired("stdio-server-cmd") // Add global flag for pretty printing - rootCmd.PersistentFlags().Bool("pretty", true, "Pretty print MCP response (only for JSON responses)") + rootCmd.PersistentFlags().Bool("pretty", true, "Pretty print MCP response (only for JSON or JSONL responses)") // Add the tools command to the root command rootCmd.AddCommand(toolsCmd) @@ -426,15 +425,26 @@ func printResponse(response string, prettyPrint bool) error { // Extract text from content items of type "text" for _, content := range resp.Result.Content { if content.Type == "text" { - // Unmarshal the text content - var textContent map[string]interface{} - if err := json.Unmarshal([]byte(content.Text), &textContent); err != nil { - return fmt.Errorf("failed to parse text content: %w", err) + var textContentObj map[string]interface{} + err := json.Unmarshal([]byte(content.Text), &textContentObj) + + if err == nil { + prettyText, err := json.MarshalIndent(textContentObj, "", " ") + if err != nil { + return fmt.Errorf("failed to pretty print text content: %w", err) + } + fmt.Println(string(prettyText)) + continue + } + + // Fallback parsing as JSONL + var textContentList []map[string]interface{} + if err := json.Unmarshal([]byte(content.Text), &textContentList); err != nil { + return fmt.Errorf("failed to parse text content as a list: %w", err) } - // Pretty print the text content - prettyText, err := json.MarshalIndent(textContent, "", " ") + prettyText, err := json.MarshalIndent(textContentList, "", " ") if err != nil { - return fmt.Errorf("failed to pretty print text content: %w", err) + return fmt.Errorf("failed to pretty print array content: %w", err) } fmt.Println(string(prettyText)) } From 62eed343e0947f5fe1f5cc4ce239665a96653eef Mon Sep 17 00:00:00 2001 From: Ricardo Fearing <9965014+rfearing@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:18:26 -0400 Subject: [PATCH 29/41] Clarify local build (#264) --- README.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ec8018a0..6bfc6ab5 100644 --- a/README.md +++ b/README.md @@ -91,10 +91,24 @@ More about using MCP server tools in VS Code's [agent mode documentation](https: ### Build from source -If you don't have Docker, you can use `go` to build the binary in the -`cmd/github-mcp-server` directory, and use the `github-mcp-server stdio` -command with the `GITHUB_PERSONAL_ACCESS_TOKEN` environment variable set to -your token. +If you don't have Docker, you can use `go build` to build the binary in the +`cmd/github-mcp-server` directory, and use the `github-mcp-server stdio` command with the `GITHUB_PERSONAL_ACCESS_TOKEN` environment variable set to your token. To specify the output location of the build, use the `-o` flag. You should configure your server to use the built executable as its `command`. For example: + +```JSON +{ + "mcp": { + "servers": { + "github": { + "command": "/path/to/github-mcp-server", + "args": ["stdio"], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "" + } + } + } + } +} +``` ## GitHub Enterprise Server From ff3036d596250648813066fefddc478c766a6167 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 9 Apr 2025 01:14:30 +0200 Subject: [PATCH 30/41] feat: partition tools by product/feature --- cmd/github-mcp-server/main.go | 57 ++++++-- go.mod | 2 +- go.sum | 4 +- pkg/github/context_tools.go | 49 +++++++ pkg/github/context_tools_test.go | 132 ++++++++++++++++++ pkg/github/dynamic_tools.go | 125 +++++++++++++++++ pkg/github/resources.go | 14 ++ pkg/github/server.go | 106 +------------- pkg/github/server_test.go | 123 ----------------- pkg/github/tools.go | 117 ++++++++++++++++ pkg/toolsets/toolsets.go | 154 +++++++++++++++++++++ pkg/toolsets/toolsets_test.go | 230 +++++++++++++++++++++++++++++++ pkg/translations/translations.go | 3 - third-party-licenses.darwin.md | 2 +- third-party-licenses.linux.md | 2 +- third-party-licenses.windows.md | 2 +- 16 files changed, 877 insertions(+), 245 deletions(-) create mode 100644 pkg/github/context_tools.go create mode 100644 pkg/github/context_tools_test.go create mode 100644 pkg/github/dynamic_tools.go create mode 100644 pkg/github/resources.go create mode 100644 pkg/github/tools.go create mode 100644 pkg/toolsets/toolsets.go create mode 100644 pkg/toolsets/toolsets_test.go diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index 354ec3a9..4d5368ec 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -44,12 +44,16 @@ var ( if err != nil { stdlog.Fatal("Failed to initialize logger:", err) } + + enabledToolsets := viper.GetStringSlice("toolsets") + logCommands := viper.GetBool("enable-command-logging") cfg := runConfig{ readOnly: readOnly, logger: logger, logCommands: logCommands, exportTranslations: exportTranslations, + enabledToolsets: enabledToolsets, } if err := runStdioServer(cfg); err != nil { stdlog.Fatal("failed to run stdio server:", err) @@ -62,6 +66,8 @@ func init() { cobra.OnInitialize(initConfig) // Add global flags that will be shared by all commands + rootCmd.PersistentFlags().StringSlice("toolsets", github.DefaultTools, "An optional comma separated list of groups of tools to allow, defaults to enabling all") + rootCmd.PersistentFlags().Bool("dynamic-toolsets", false, "Enable dynamic toolsets") rootCmd.PersistentFlags().Bool("read-only", false, "Restrict the server to read-only operations") rootCmd.PersistentFlags().String("log-file", "", "Path to log file") rootCmd.PersistentFlags().Bool("enable-command-logging", false, "When enabled, the server will log all command requests and responses to the log file") @@ -69,11 +75,13 @@ func init() { rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)") // Bind flag to viper + _ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets")) + _ = viper.BindPFlag("dynamic_toolsets", rootCmd.PersistentFlags().Lookup("dynamic-toolsets")) _ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only")) _ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file")) _ = viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging")) _ = viper.BindPFlag("export-translations", rootCmd.PersistentFlags().Lookup("export-translations")) - _ = viper.BindPFlag("gh-host", rootCmd.PersistentFlags().Lookup("gh-host")) + _ = viper.BindPFlag("host", rootCmd.PersistentFlags().Lookup("gh-host")) // Add subcommands rootCmd.AddCommand(stdioCmd) @@ -81,7 +89,7 @@ func init() { func initConfig() { // Initialize Viper configuration - viper.SetEnvPrefix("APP") + viper.SetEnvPrefix("github") viper.AutomaticEnv() } @@ -107,6 +115,7 @@ type runConfig struct { logger *log.Logger logCommands bool exportTranslations bool + enabledToolsets []string } func runStdioServer(cfg runConfig) error { @@ -115,18 +124,14 @@ func runStdioServer(cfg runConfig) error { defer stop() // Create GH client - token := os.Getenv("GITHUB_PERSONAL_ACCESS_TOKEN") + token := viper.GetString("personal_access_token") if token == "" { cfg.logger.Fatal("GITHUB_PERSONAL_ACCESS_TOKEN not set") } ghClient := gogithub.NewClient(nil).WithAuthToken(token) ghClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", version) - // Check GH_HOST env var first, then fall back to viper config - host := os.Getenv("GH_HOST") - if host == "" { - host = viper.GetString("gh-host") - } + host := viper.GetString("host") if host != "" { var err error @@ -149,8 +154,40 @@ func runStdioServer(cfg runConfig) error { hooks := &server.Hooks{ OnBeforeInitialize: []server.OnBeforeInitializeFunc{beforeInit}, } - // Create - ghServer := github.NewServer(getClient, version, cfg.readOnly, t, server.WithHooks(hooks)) + // Create server + ghServer := github.NewServer(version, server.WithHooks(hooks)) + + enabled := cfg.enabledToolsets + dynamic := viper.GetBool("dynamic_toolsets") + if dynamic { + // filter "all" from the enabled toolsets + enabled = make([]string, 0, len(cfg.enabledToolsets)) + for _, toolset := range cfg.enabledToolsets { + if toolset != "all" { + enabled = append(enabled, toolset) + } + } + } + + // Create default toolsets + toolsets, err := github.InitToolsets(enabled, cfg.readOnly, getClient, t) + context := github.InitContextToolset(getClient, t) + + if err != nil { + stdlog.Fatal("Failed to initialize toolsets:", err) + } + + // Register resources with the server + github.RegisterResources(ghServer, getClient, t) + // Register the tools with the server + toolsets.RegisterTools(ghServer) + context.RegisterTools(ghServer) + + if dynamic { + dynamic := github.InitDynamicToolset(ghServer, toolsets, t) + dynamic.RegisterTools(ghServer) + } + stdioServer := server.NewStdioServer(ghServer) stdLogger := stdlog.New(cfg.logger.Writer(), "stdioserver", 0) diff --git a/go.mod b/go.mod index 858690cd..7c09fba9 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/docker/docker v28.0.4+incompatible github.com/google/go-cmp v0.7.0 github.com/google/go-github/v69 v69.2.0 - github.com/mark3labs/mcp-go v0.18.0 + github.com/mark3labs/mcp-go v0.20.1 github.com/migueleliasweb/go-github-mock v1.1.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.9.1 diff --git a/go.sum b/go.sum index 19d368de..3378b4fd 100644 --- a/go.sum +++ b/go.sum @@ -57,8 +57,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mark3labs/mcp-go v0.18.0 h1:YuhgIVjNlTG2ZOwmrkORWyPTp0dz1opPEqvsPtySXao= -github.com/mark3labs/mcp-go v0.18.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= +github.com/mark3labs/mcp-go v0.20.1 h1:E1Bbx9K8d8kQmDZ1QHblM38c7UU2evQ2LlkANk1U/zw= +github.com/mark3labs/mcp-go v0.20.1/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= github.com/migueleliasweb/go-github-mock v1.1.0 h1:GKaOBPsrPGkAKgtfuWY8MclS1xR6MInkx1SexJucMwE= github.com/migueleliasweb/go-github-mock v1.1.0/go.mod h1:pYe/XlGs4BGMfRY4vmeixVsODHnVDDhJ9zoi0qzSMHc= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= diff --git a/pkg/github/context_tools.go b/pkg/github/context_tools.go new file mode 100644 index 00000000..1c91d703 --- /dev/null +++ b/pkg/github/context_tools.go @@ -0,0 +1,49 @@ +package github + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/github/github-mcp-server/pkg/translations" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +// GetMe creates a tool to get details of the authenticated user. +func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("get_me", + mcp.WithDescription(t("TOOL_GET_ME_DESCRIPTION", "Get details of the authenticated GitHub user. Use this when a request include \"me\", \"my\"...")), + mcp.WithString("reason", + mcp.Description("Optional: reason the session was created"), + ), + ), + func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + user, resp, err := client.Users.Get(ctx, "") + if err != nil { + return nil, fmt.Errorf("failed to get user: %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 user: %s", string(body))), nil + } + + r, err := json.Marshal(user) + if err != nil { + return nil, fmt.Errorf("failed to marshal user: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} diff --git a/pkg/github/context_tools_test.go b/pkg/github/context_tools_test.go new file mode 100644 index 00000000..c9d220dd --- /dev/null +++ b/pkg/github/context_tools_test.go @@ -0,0 +1,132 @@ +package github + +import ( + "context" + "encoding/json" + "net/http" + "testing" + "time" + + "github.com/github/github-mcp-server/pkg/translations" + "github.com/google/go-github/v69/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_GetMe(t *testing.T) { + // Verify tool definition + mockClient := github.NewClient(nil) + tool, _ := GetMe(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "get_me", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "reason") + assert.Empty(t, tool.InputSchema.Required) // No required parameters + + // Setup mock user response + mockUser := &github.User{ + Login: github.Ptr("testuser"), + Name: github.Ptr("Test User"), + Email: github.Ptr("test@example.com"), + Bio: github.Ptr("GitHub user for testing"), + Company: github.Ptr("Test Company"), + Location: github.Ptr("Test Location"), + HTMLURL: github.Ptr("https://github.com/testuser"), + CreatedAt: &github.Timestamp{Time: time.Now().Add(-365 * 24 * time.Hour)}, + Type: github.Ptr("User"), + Plan: &github.Plan{ + Name: github.Ptr("pro"), + }, + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedUser *github.User + expectedErrMsg string + }{ + { + name: "successful get user", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetUser, + mockUser, + ), + ), + requestArgs: map[string]interface{}{}, + expectError: false, + expectedUser: mockUser, + }, + { + name: "successful get user with reason", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetUser, + mockUser, + ), + ), + requestArgs: map[string]interface{}{ + "reason": "Testing API", + }, + expectError: false, + expectedUser: mockUser, + }, + { + name: "get user fails", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetUser, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"message": "Unauthorized"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{}, + expectError: true, + expectedErrMsg: "failed to get user", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := GetMe(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErrMsg) + return + } + + require.NoError(t, err) + + // Parse result and get text content if no error + textContent := getTextResult(t, result) + + // Unmarshal and verify the result + var returnedUser github.User + err = json.Unmarshal([]byte(textContent.Text), &returnedUser) + require.NoError(t, err) + + // Verify user details + assert.Equal(t, *tc.expectedUser.Login, *returnedUser.Login) + assert.Equal(t, *tc.expectedUser.Name, *returnedUser.Name) + assert.Equal(t, *tc.expectedUser.Email, *returnedUser.Email) + assert.Equal(t, *tc.expectedUser.Bio, *returnedUser.Bio) + assert.Equal(t, *tc.expectedUser.HTMLURL, *returnedUser.HTMLURL) + assert.Equal(t, *tc.expectedUser.Type, *returnedUser.Type) + }) + } +} diff --git a/pkg/github/dynamic_tools.go b/pkg/github/dynamic_tools.go new file mode 100644 index 00000000..d4d5f27a --- /dev/null +++ b/pkg/github/dynamic_tools.go @@ -0,0 +1,125 @@ +package github + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/github/github-mcp-server/pkg/toolsets" + "github.com/github/github-mcp-server/pkg/translations" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func ToolsetEnum(toolsetGroup *toolsets.ToolsetGroup) mcp.PropertyOption { + toolsetNames := make([]string, 0, len(toolsetGroup.Toolsets)) + for name := range toolsetGroup.Toolsets { + toolsetNames = append(toolsetNames, name) + } + return mcp.Enum(toolsetNames...) +} + +func EnableToolset(s *server.MCPServer, toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("enable_toolset", + mcp.WithDescription(t("TOOL_ENABLE_TOOLSET_DESCRIPTION", "Enable one of the sets of tools the GitHub MCP server provides, use get_toolset_tools and list_available_toolsets first to see what this will enable")), + mcp.WithString("toolset", + mcp.Required(), + mcp.Description("The name of the toolset to enable"), + ToolsetEnum(toolsetGroup), + ), + ), + func(_ context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // We need to convert the toolsets back to a map for JSON serialization + toolsetName, err := requiredParam[string](request, "toolset") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + toolset := toolsetGroup.Toolsets[toolsetName] + if toolset == nil { + return mcp.NewToolResultError(fmt.Sprintf("Toolset %s not found", toolsetName)), nil + } + if toolset.Enabled { + return mcp.NewToolResultText(fmt.Sprintf("Toolset %s is already enabled", toolsetName)), nil + } + + toolset.Enabled = true + + // caution: this currently affects the global tools and notifies all clients: + // + // Send notification to all initialized sessions + // s.sendNotificationToAllClients("notifications/tools/list_changed", nil) + s.AddTools(toolset.GetActiveTools()...) + + return mcp.NewToolResultText(fmt.Sprintf("Toolset %s enabled", toolsetName)), nil + } +} + +func ListAvailableToolsets(toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("list_available_toolsets", + mcp.WithDescription(t("TOOL_LIST_AVAILABLE_TOOLSETS_DESCRIPTION", "List all available toolsets this GitHub MCP server can offer, providing the enabled status of each. Use this when a task could be achieved with a GitHub tool and the currently available tools aren't enough. Call get_toolset_tools with these toolset names to discover specific tools you can call")), + ), + func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // We need to convert the toolsetGroup back to a map for JSON serialization + + payload := []map[string]string{} + + for name, ts := range toolsetGroup.Toolsets { + { + t := map[string]string{ + "name": name, + "description": ts.Description, + "can_enable": "true", + "currently_enabled": fmt.Sprintf("%t", ts.Enabled), + } + payload = append(payload, t) + } + } + + r, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("failed to marshal features: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +func GetToolsetsTools(toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("get_toolset_tools", + mcp.WithDescription(t("TOOL_GET_TOOLSET_TOOLS_DESCRIPTION", "Lists all the capabilities that are enabled with the specified toolset, use this to get clarity on whether enabling a toolset would help you to complete a task")), + mcp.WithString("toolset", + mcp.Required(), + mcp.Description("The name of the toolset you want to get the tools for"), + ToolsetEnum(toolsetGroup), + ), + ), + func(_ context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // We need to convert the toolsetGroup back to a map for JSON serialization + toolsetName, err := requiredParam[string](request, "toolset") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + toolset := toolsetGroup.Toolsets[toolsetName] + if toolset == nil { + return mcp.NewToolResultError(fmt.Sprintf("Toolset %s not found", toolsetName)), nil + } + payload := []map[string]string{} + + for _, st := range toolset.GetAvailableTools() { + tool := map[string]string{ + "name": st.Tool.Name, + "description": st.Tool.Description, + "can_enable": "true", + "toolset": toolsetName, + } + payload = append(payload, tool) + } + + r, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("failed to marshal features: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} diff --git a/pkg/github/resources.go b/pkg/github/resources.go new file mode 100644 index 00000000..774261e9 --- /dev/null +++ b/pkg/github/resources.go @@ -0,0 +1,14 @@ +package github + +import ( + "github.com/github/github-mcp-server/pkg/translations" + "github.com/mark3labs/mcp-go/server" +) + +func RegisterResources(s *server.MCPServer, getClient GetClientFn, t translations.TranslationHelperFunc) { + s.AddResourceTemplate(GetRepositoryResourceContent(getClient, t)) + s.AddResourceTemplate(GetRepositoryResourceBranchContent(getClient, t)) + s.AddResourceTemplate(GetRepositoryResourceCommitContent(getClient, t)) + s.AddResourceTemplate(GetRepositoryResourceTagContent(getClient, t)) + s.AddResourceTemplate(GetRepositoryResourcePrContent(getClient, t)) +} diff --git a/pkg/github/server.go b/pkg/github/server.go index da916b98..e4c24171 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -1,25 +1,20 @@ package github import ( - "context" - "encoding/json" "errors" "fmt" - "io" - "net/http" - "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v69/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) -type GetClientFn func(context.Context) (*github.Client, error) - // NewServer creates a new GitHub MCP server with the specified GH client and logger. -func NewServer(getClient GetClientFn, version string, readOnly bool, t translations.TranslationHelperFunc, opts ...server.ServerOption) *server.MCPServer { + +func NewServer(version string, opts ...server.ServerOption) *server.MCPServer { // Add default options defaultOpts := []server.ServerOption{ + server.WithToolCapabilities(true), server.WithResourceCapabilities(true, true), server.WithLogging(), } @@ -31,104 +26,9 @@ func NewServer(getClient GetClientFn, version string, readOnly bool, t translati version, opts..., ) - - // Add GitHub Resources - s.AddResourceTemplate(GetRepositoryResourceContent(getClient, t)) - s.AddResourceTemplate(GetRepositoryResourceBranchContent(getClient, t)) - s.AddResourceTemplate(GetRepositoryResourceCommitContent(getClient, t)) - s.AddResourceTemplate(GetRepositoryResourceTagContent(getClient, t)) - s.AddResourceTemplate(GetRepositoryResourcePrContent(getClient, t)) - - // Add GitHub tools - Issues - s.AddTool(GetIssue(getClient, t)) - s.AddTool(SearchIssues(getClient, t)) - s.AddTool(ListIssues(getClient, t)) - s.AddTool(GetIssueComments(getClient, t)) - if !readOnly { - s.AddTool(CreateIssue(getClient, t)) - s.AddTool(AddIssueComment(getClient, t)) - s.AddTool(UpdateIssue(getClient, t)) - } - - // Add GitHub tools - Pull Requests - s.AddTool(GetPullRequest(getClient, t)) - s.AddTool(ListPullRequests(getClient, t)) - s.AddTool(GetPullRequestFiles(getClient, t)) - s.AddTool(GetPullRequestStatus(getClient, t)) - s.AddTool(GetPullRequestComments(getClient, t)) - s.AddTool(GetPullRequestReviews(getClient, t)) - if !readOnly { - s.AddTool(MergePullRequest(getClient, t)) - s.AddTool(UpdatePullRequestBranch(getClient, t)) - s.AddTool(CreatePullRequestReview(getClient, t)) - s.AddTool(CreatePullRequest(getClient, t)) - s.AddTool(UpdatePullRequest(getClient, t)) - s.AddTool(AddPullRequestReviewComment(getClient, t)) - } - - // Add GitHub tools - Repositories - s.AddTool(SearchRepositories(getClient, t)) - s.AddTool(GetFileContents(getClient, t)) - s.AddTool(GetCommit(getClient, t)) - s.AddTool(ListCommits(getClient, t)) - s.AddTool(ListBranches(getClient, t)) - if !readOnly { - s.AddTool(CreateOrUpdateFile(getClient, t)) - s.AddTool(CreateRepository(getClient, t)) - s.AddTool(ForkRepository(getClient, t)) - s.AddTool(CreateBranch(getClient, t)) - s.AddTool(PushFiles(getClient, t)) - } - - // Add GitHub tools - Search - s.AddTool(SearchCode(getClient, t)) - s.AddTool(SearchUsers(getClient, t)) - - // Add GitHub tools - Users - s.AddTool(GetMe(getClient, t)) - - // Add GitHub tools - Code Scanning - s.AddTool(GetCodeScanningAlert(getClient, t)) - s.AddTool(ListCodeScanningAlerts(getClient, t)) return s } -// GetMe creates a tool to get details of the authenticated user. -func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("get_me", - mcp.WithDescription(t("TOOL_GET_ME_DESCRIPTION", "Get details of the authenticated GitHub user. Use this when a request include \"me\", \"my\"...")), - mcp.WithString("reason", - mcp.Description("Optional: reason the session was created"), - ), - ), - func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { - client, err := getClient(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) - } - user, resp, err := client.Users.Get(ctx, "") - if err != nil { - return nil, fmt.Errorf("failed to get user: %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 user: %s", string(body))), nil - } - - r, err := json.Marshal(user) - if err != nil { - return nil, fmt.Errorf("failed to marshal user: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil - } -} - // OptionalParamOK is a helper function that can be used to fetch a requested parameter from the request. // It returns the value, a boolean indicating if the parameter was present, and an error if the type is wrong. func OptionalParamOK[T any](r mcp.CallToolRequest, p string) (value T, ok bool, err error) { diff --git a/pkg/github/server_test.go b/pkg/github/server_test.go index 3ee9851a..58bcb9db 100644 --- a/pkg/github/server_test.go +++ b/pkg/github/server_test.go @@ -2,17 +2,11 @@ package github import ( "context" - "encoding/json" "fmt" - "net/http" "testing" - "time" - "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v69/github" - "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func stubGetClientFn(client *github.Client) GetClientFn { @@ -21,123 +15,6 @@ func stubGetClientFn(client *github.Client) GetClientFn { } } -func Test_GetMe(t *testing.T) { - // Verify tool definition - mockClient := github.NewClient(nil) - tool, _ := GetMe(stubGetClientFn(mockClient), translations.NullTranslationHelper) - - assert.Equal(t, "get_me", tool.Name) - assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "reason") - assert.Empty(t, tool.InputSchema.Required) // No required parameters - - // Setup mock user response - mockUser := &github.User{ - Login: github.Ptr("testuser"), - Name: github.Ptr("Test User"), - Email: github.Ptr("test@example.com"), - Bio: github.Ptr("GitHub user for testing"), - Company: github.Ptr("Test Company"), - Location: github.Ptr("Test Location"), - HTMLURL: github.Ptr("https://github.com/testuser"), - CreatedAt: &github.Timestamp{Time: time.Now().Add(-365 * 24 * time.Hour)}, - Type: github.Ptr("User"), - Plan: &github.Plan{ - Name: github.Ptr("pro"), - }, - } - - tests := []struct { - name string - mockedClient *http.Client - requestArgs map[string]interface{} - expectError bool - expectedUser *github.User - expectedErrMsg string - }{ - { - name: "successful get user", - mockedClient: mock.NewMockedHTTPClient( - mock.WithRequestMatch( - mock.GetUser, - mockUser, - ), - ), - requestArgs: map[string]interface{}{}, - expectError: false, - expectedUser: mockUser, - }, - { - name: "successful get user with reason", - mockedClient: mock.NewMockedHTTPClient( - mock.WithRequestMatch( - mock.GetUser, - mockUser, - ), - ), - requestArgs: map[string]interface{}{ - "reason": "Testing API", - }, - expectError: false, - expectedUser: mockUser, - }, - { - name: "get user fails", - mockedClient: mock.NewMockedHTTPClient( - mock.WithRequestMatchHandler( - mock.GetUser, - http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusUnauthorized) - _, _ = w.Write([]byte(`{"message": "Unauthorized"}`)) - }), - ), - ), - requestArgs: map[string]interface{}{}, - expectError: true, - expectedErrMsg: "failed to get user", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Setup client with mock - client := github.NewClient(tc.mockedClient) - _, handler := GetMe(stubGetClientFn(client), translations.NullTranslationHelper) - - // Create call request - request := createMCPRequest(tc.requestArgs) - - // Call handler - result, err := handler(context.Background(), request) - - // Verify results - if tc.expectError { - require.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedErrMsg) - return - } - - require.NoError(t, err) - - // Parse result and get text content if no error - textContent := getTextResult(t, result) - - // Unmarshal and verify the result - var returnedUser github.User - err = json.Unmarshal([]byte(textContent.Text), &returnedUser) - require.NoError(t, err) - - // Verify user details - assert.Equal(t, *tc.expectedUser.Login, *returnedUser.Login) - assert.Equal(t, *tc.expectedUser.Name, *returnedUser.Name) - assert.Equal(t, *tc.expectedUser.Email, *returnedUser.Email) - assert.Equal(t, *tc.expectedUser.Bio, *returnedUser.Bio) - assert.Equal(t, *tc.expectedUser.HTMLURL, *returnedUser.HTMLURL) - assert.Equal(t, *tc.expectedUser.Type, *returnedUser.Type) - }) - } -} - func Test_IsAcceptedError(t *testing.T) { tests := []struct { name string diff --git a/pkg/github/tools.go b/pkg/github/tools.go new file mode 100644 index 00000000..ce10c4ad --- /dev/null +++ b/pkg/github/tools.go @@ -0,0 +1,117 @@ +package github + +import ( + "context" + + "github.com/github/github-mcp-server/pkg/toolsets" + "github.com/github/github-mcp-server/pkg/translations" + "github.com/google/go-github/v69/github" + "github.com/mark3labs/mcp-go/server" +) + +type GetClientFn func(context.Context) (*github.Client, error) + +var DefaultTools = []string{"all"} + +func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn, t translations.TranslationHelperFunc) (*toolsets.ToolsetGroup, error) { + // Create a new toolset group + tsg := toolsets.NewToolsetGroup(readOnly) + + // Define all available features with their default state (disabled) + // Create toolsets + repos := toolsets.NewToolset("repos", "GitHub Repository related tools"). + AddReadTools( + toolsets.NewServerTool(SearchRepositories(getClient, t)), + toolsets.NewServerTool(GetFileContents(getClient, t)), + toolsets.NewServerTool(ListCommits(getClient, t)), + toolsets.NewServerTool(SearchCode(getClient, t)), + toolsets.NewServerTool(GetCommit(getClient, t)), + toolsets.NewServerTool(ListBranches(getClient, t)), + ). + AddWriteTools( + 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)), + ) + issues := toolsets.NewToolset("issues", "GitHub Issues related tools"). + AddReadTools( + toolsets.NewServerTool(GetIssue(getClient, t)), + toolsets.NewServerTool(SearchIssues(getClient, t)), + toolsets.NewServerTool(ListIssues(getClient, t)), + toolsets.NewServerTool(GetIssueComments(getClient, t)), + ). + AddWriteTools( + toolsets.NewServerTool(CreateIssue(getClient, t)), + toolsets.NewServerTool(AddIssueComment(getClient, t)), + toolsets.NewServerTool(UpdateIssue(getClient, t)), + ) + users := toolsets.NewToolset("users", "GitHub User related tools"). + AddReadTools( + toolsets.NewServerTool(SearchUsers(getClient, t)), + ) + 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(GetPullRequestStatus(getClient, t)), + toolsets.NewServerTool(GetPullRequestComments(getClient, t)), + toolsets.NewServerTool(GetPullRequestReviews(getClient, t)), + ). + AddWriteTools( + toolsets.NewServerTool(MergePullRequest(getClient, t)), + toolsets.NewServerTool(UpdatePullRequestBranch(getClient, t)), + toolsets.NewServerTool(CreatePullRequestReview(getClient, t)), + toolsets.NewServerTool(CreatePullRequest(getClient, t)), + toolsets.NewServerTool(UpdatePullRequest(getClient, t)), + toolsets.NewServerTool(AddPullRequestReviewComment(getClient, t)), + ) + 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)), + ) + // Keep experiments alive so the system doesn't error out when it's always enabled + experiments := toolsets.NewToolset("experiments", "Experimental features that are not considered stable yet") + + // Add toolsets to the group + tsg.AddToolset(repos) + tsg.AddToolset(issues) + tsg.AddToolset(users) + tsg.AddToolset(pullRequests) + tsg.AddToolset(codeSecurity) + tsg.AddToolset(experiments) + // Enable the requested features + + if err := tsg.EnableToolsets(passedToolsets); err != nil { + return nil, err + } + + return tsg, nil +} + +func InitContextToolset(getClient GetClientFn, t translations.TranslationHelperFunc) *toolsets.Toolset { + // Create a new context toolset + contextTools := toolsets.NewToolset("context", "Tools that provide context about the current user and GitHub context you are operating in"). + AddReadTools( + toolsets.NewServerTool(GetMe(getClient, t)), + ) + contextTools.Enabled = true + return contextTools +} + +// InitDynamicToolset creates a dynamic toolset that can be used to enable other toolsets, and so requires the server and toolset group as arguments +func InitDynamicToolset(s *server.MCPServer, tsg *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) *toolsets.Toolset { + // Create a new dynamic toolset + // Need to add the dynamic toolset last so it can be used to enable other toolsets + dynamicToolSelection := toolsets.NewToolset("dynamic", "Discover GitHub MCP tools that can help achieve tasks by enabling additional sets of tools, you can control the enablement of any toolset to access its tools when this toolset is enabled."). + AddReadTools( + toolsets.NewServerTool(ListAvailableToolsets(tsg, t)), + toolsets.NewServerTool(GetToolsetsTools(tsg, t)), + toolsets.NewServerTool(EnableToolset(s, tsg, t)), + ) + dynamicToolSelection.Enabled = true + return dynamicToolSelection +} diff --git a/pkg/toolsets/toolsets.go b/pkg/toolsets/toolsets.go new file mode 100644 index 00000000..d4397fc9 --- /dev/null +++ b/pkg/toolsets/toolsets.go @@ -0,0 +1,154 @@ +package toolsets + +import ( + "fmt" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func NewServerTool(tool mcp.Tool, handler server.ToolHandlerFunc) server.ServerTool { + return server.ServerTool{Tool: tool, Handler: handler} +} + +type Toolset struct { + Name string + Description string + Enabled bool + readOnly bool + writeTools []server.ServerTool + readTools []server.ServerTool +} + +func (t *Toolset) GetActiveTools() []server.ServerTool { + if t.Enabled { + if t.readOnly { + return t.readTools + } + return append(t.readTools, t.writeTools...) + } + return nil +} + +func (t *Toolset) GetAvailableTools() []server.ServerTool { + if t.readOnly { + return t.readTools + } + return append(t.readTools, t.writeTools...) +} + +func (t *Toolset) RegisterTools(s *server.MCPServer) { + if !t.Enabled { + return + } + for _, tool := range t.readTools { + s.AddTool(tool.Tool, tool.Handler) + } + if !t.readOnly { + for _, tool := range t.writeTools { + s.AddTool(tool.Tool, tool.Handler) + } + } +} + +func (t *Toolset) SetReadOnly() { + // Set the toolset to read-only + t.readOnly = true +} + +func (t *Toolset) AddWriteTools(tools ...server.ServerTool) *Toolset { + // Silently ignore if the toolset is read-only to avoid any breach of that contract + if !t.readOnly { + t.writeTools = append(t.writeTools, tools...) + } + return t +} + +func (t *Toolset) AddReadTools(tools ...server.ServerTool) *Toolset { + t.readTools = append(t.readTools, tools...) + return t +} + +type ToolsetGroup struct { + Toolsets map[string]*Toolset + everythingOn bool + readOnly bool +} + +func NewToolsetGroup(readOnly bool) *ToolsetGroup { + return &ToolsetGroup{ + Toolsets: make(map[string]*Toolset), + everythingOn: false, + readOnly: readOnly, + } +} + +func (tg *ToolsetGroup) AddToolset(ts *Toolset) { + if tg.readOnly { + ts.SetReadOnly() + } + tg.Toolsets[ts.Name] = ts +} + +func NewToolset(name string, description string) *Toolset { + return &Toolset{ + Name: name, + Description: description, + Enabled: false, + readOnly: false, + } +} + +func (tg *ToolsetGroup) IsEnabled(name string) bool { + // If everythingOn is true, all features are enabled + if tg.everythingOn { + return true + } + + feature, exists := tg.Toolsets[name] + if !exists { + return false + } + return feature.Enabled +} + +func (tg *ToolsetGroup) EnableToolsets(names []string) error { + // Special case for "all" + for _, name := range names { + if name == "all" { + tg.everythingOn = true + break + } + err := tg.EnableToolset(name) + if err != nil { + return err + } + } + // Do this after to ensure all toolsets are enabled if "all" is present anywhere in list + if tg.everythingOn { + for name := range tg.Toolsets { + err := tg.EnableToolset(name) + if err != nil { + return err + } + } + return nil + } + return nil +} + +func (tg *ToolsetGroup) EnableToolset(name string) error { + toolset, exists := tg.Toolsets[name] + if !exists { + return fmt.Errorf("toolset %s does not exist", name) + } + toolset.Enabled = true + tg.Toolsets[name] = toolset + return nil +} + +func (tg *ToolsetGroup) RegisterTools(s *server.MCPServer) { + for _, toolset := range tg.Toolsets { + toolset.RegisterTools(s) + } +} diff --git a/pkg/toolsets/toolsets_test.go b/pkg/toolsets/toolsets_test.go new file mode 100644 index 00000000..7ece1df1 --- /dev/null +++ b/pkg/toolsets/toolsets_test.go @@ -0,0 +1,230 @@ +package toolsets + +import ( + "testing" +) + +func TestNewToolsetGroup(t *testing.T) { + tsg := NewToolsetGroup(false) + if tsg == nil { + t.Fatal("Expected NewToolsetGroup to return a non-nil pointer") + } + if tsg.Toolsets == nil { + t.Fatal("Expected Toolsets map to be initialized") + } + if len(tsg.Toolsets) != 0 { + t.Fatalf("Expected Toolsets map to be empty, got %d items", len(tsg.Toolsets)) + } + if tsg.everythingOn { + t.Fatal("Expected everythingOn to be initialized as false") + } +} + +func TestAddToolset(t *testing.T) { + tsg := NewToolsetGroup(false) + + // Test adding a toolset + toolset := NewToolset("test-toolset", "A test toolset") + toolset.Enabled = true + tsg.AddToolset(toolset) + + // Verify toolset was added correctly + if len(tsg.Toolsets) != 1 { + t.Errorf("Expected 1 toolset, got %d", len(tsg.Toolsets)) + } + + toolset, exists := tsg.Toolsets["test-toolset"] + if !exists { + t.Fatal("Feature was not added to the map") + } + + if toolset.Name != "test-toolset" { + t.Errorf("Expected toolset name to be 'test-toolset', got '%s'", toolset.Name) + } + + if toolset.Description != "A test toolset" { + t.Errorf("Expected toolset description to be 'A test toolset', got '%s'", toolset.Description) + } + + if !toolset.Enabled { + t.Error("Expected toolset to be enabled") + } + + // Test adding another toolset + anotherToolset := NewToolset("another-toolset", "Another test toolset") + tsg.AddToolset(anotherToolset) + + if len(tsg.Toolsets) != 2 { + t.Errorf("Expected 2 toolsets, got %d", len(tsg.Toolsets)) + } + + // Test overriding existing toolset + updatedToolset := NewToolset("test-toolset", "Updated description") + tsg.AddToolset(updatedToolset) + + toolset = tsg.Toolsets["test-toolset"] + if toolset.Description != "Updated description" { + t.Errorf("Expected toolset description to be updated to 'Updated description', got '%s'", toolset.Description) + } + + if toolset.Enabled { + t.Error("Expected toolset to be disabled after update") + } +} + +func TestIsEnabled(t *testing.T) { + tsg := NewToolsetGroup(false) + + // Test with non-existent toolset + if tsg.IsEnabled("non-existent") { + t.Error("Expected IsEnabled to return false for non-existent toolset") + } + + // Test with disabled toolset + disabledToolset := NewToolset("disabled-toolset", "A disabled toolset") + tsg.AddToolset(disabledToolset) + if tsg.IsEnabled("disabled-toolset") { + t.Error("Expected IsEnabled to return false for disabled toolset") + } + + // Test with enabled toolset + enabledToolset := NewToolset("enabled-toolset", "An enabled toolset") + enabledToolset.Enabled = true + tsg.AddToolset(enabledToolset) + if !tsg.IsEnabled("enabled-toolset") { + t.Error("Expected IsEnabled to return true for enabled toolset") + } +} + +func TestEnableFeature(t *testing.T) { + tsg := NewToolsetGroup(false) + + // Test enabling non-existent toolset + err := tsg.EnableToolset("non-existent") + if err == nil { + t.Error("Expected error when enabling non-existent toolset") + } + + // Test enabling toolset + testToolset := NewToolset("test-toolset", "A test toolset") + tsg.AddToolset(testToolset) + + if tsg.IsEnabled("test-toolset") { + t.Error("Expected toolset to be disabled initially") + } + + err = tsg.EnableToolset("test-toolset") + if err != nil { + t.Errorf("Expected no error when enabling toolset, got: %v", err) + } + + if !tsg.IsEnabled("test-toolset") { + t.Error("Expected toolset to be enabled after EnableFeature call") + } + + // Test enabling already enabled toolset + err = tsg.EnableToolset("test-toolset") + if err != nil { + t.Errorf("Expected no error when enabling already enabled toolset, got: %v", err) + } +} + +func TestEnableToolsets(t *testing.T) { + tsg := NewToolsetGroup(false) + + // Prepare toolsets + toolset1 := NewToolset("toolset1", "Feature 1") + toolset2 := NewToolset("toolset2", "Feature 2") + tsg.AddToolset(toolset1) + tsg.AddToolset(toolset2) + + // Test enabling multiple toolsets + err := tsg.EnableToolsets([]string{"toolset1", "toolset2"}) + if err != nil { + t.Errorf("Expected no error when enabling toolsets, got: %v", err) + } + + if !tsg.IsEnabled("toolset1") { + t.Error("Expected toolset1 to be enabled") + } + + if !tsg.IsEnabled("toolset2") { + t.Error("Expected toolset2 to be enabled") + } + + // Test with non-existent toolset in the list + err = tsg.EnableToolsets([]string{"toolset1", "non-existent"}) + if err == nil { + t.Error("Expected error when enabling list with non-existent toolset") + } + + // Test with empty list + err = tsg.EnableToolsets([]string{}) + if err != nil { + t.Errorf("Expected no error with empty toolset list, got: %v", err) + } + + // Test enabling everything through EnableToolsets + tsg = NewToolsetGroup(false) + err = tsg.EnableToolsets([]string{"all"}) + if err != nil { + t.Errorf("Expected no error when enabling 'all', got: %v", err) + } + + if !tsg.everythingOn { + t.Error("Expected everythingOn to be true after enabling 'all' via EnableToolsets") + } +} + +func TestEnableEverything(t *testing.T) { + tsg := NewToolsetGroup(false) + + // Add a disabled toolset + testToolset := NewToolset("test-toolset", "A test toolset") + tsg.AddToolset(testToolset) + + // Verify it's disabled + if tsg.IsEnabled("test-toolset") { + t.Error("Expected toolset to be disabled initially") + } + + // Enable "all" + err := tsg.EnableToolsets([]string{"all"}) + if err != nil { + t.Errorf("Expected no error when enabling 'eall', got: %v", err) + } + + // Verify everythingOn was set + if !tsg.everythingOn { + t.Error("Expected everythingOn to be true after enabling 'eall'") + } + + // Verify the previously disabled toolset is now enabled + if !tsg.IsEnabled("test-toolset") { + t.Error("Expected toolset to be enabled when everythingOn is true") + } + + // Verify a non-existent toolset is also enabled + if !tsg.IsEnabled("non-existent") { + t.Error("Expected non-existent toolset to be enabled when everythingOn is true") + } +} + +func TestIsEnabledWithEverythingOn(t *testing.T) { + tsg := NewToolsetGroup(false) + + // Enable "everything" + err := tsg.EnableToolsets([]string{"all"}) + if err != nil { + t.Errorf("Expected no error when enabling 'all', got: %v", err) + } + + // Test that any toolset name returns true with IsEnabled + if !tsg.IsEnabled("some-toolset") { + t.Error("Expected IsEnabled to return true for any toolset when everythingOn is true") + } + + if !tsg.IsEnabled("another-toolset") { + t.Error("Expected IsEnabled to return true for any toolset when everythingOn is true") + } +} diff --git a/pkg/translations/translations.go b/pkg/translations/translations.go index 6d910525..741ee2b5 100644 --- a/pkg/translations/translations.go +++ b/pkg/translations/translations.go @@ -20,9 +20,6 @@ func TranslationHelper() (TranslationHelperFunc, func()) { var translationKeyMap = map[string]string{} v := viper.New() - v.SetEnvPrefix("GITHUB_MCP_") - v.AutomaticEnv() - // Load from JSON file v.SetConfigName("github-mcp-server-config") v.SetConfigType("json") diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md index 80c6d1c4..389bb966 100644 --- a/third-party-licenses.darwin.md +++ b/third-party-licenses.darwin.md @@ -13,7 +13,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/google/go-github/v69/github](https://pkg.go.dev/github.com/google/go-github/v69/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v69.2.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) - - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.18.0/LICENSE)) + - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.20.1/LICENSE)) - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE)) - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE)) - [github.com/sirupsen/logrus](https://pkg.go.dev/github.com/sirupsen/logrus) ([MIT](https://github.com/sirupsen/logrus/blob/v1.9.3/LICENSE)) diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md index 80c6d1c4..389bb966 100644 --- a/third-party-licenses.linux.md +++ b/third-party-licenses.linux.md @@ -13,7 +13,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/google/go-github/v69/github](https://pkg.go.dev/github.com/google/go-github/v69/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v69.2.0/LICENSE)) - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) - - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.18.0/LICENSE)) + - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.20.1/LICENSE)) - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE)) - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE)) - [github.com/sirupsen/logrus](https://pkg.go.dev/github.com/sirupsen/logrus) ([MIT](https://github.com/sirupsen/logrus/blob/v1.9.3/LICENSE)) diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md index 5fc973d7..96d037cc 100644 --- a/third-party-licenses.windows.md +++ b/third-party-licenses.windows.md @@ -14,7 +14,7 @@ Some packages may only be included on certain architectures or operating systems - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE)) - [github.com/inconshreveable/mousetrap](https://pkg.go.dev/github.com/inconshreveable/mousetrap) ([Apache-2.0](https://github.com/inconshreveable/mousetrap/blob/v1.1.0/LICENSE)) - - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.18.0/LICENSE)) + - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.20.1/LICENSE)) - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE)) - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE)) - [github.com/sirupsen/logrus](https://pkg.go.dev/github.com/sirupsen/logrus) ([MIT](https://github.com/sirupsen/logrus/blob/v1.9.3/LICENSE)) From ea19581ef02e4dadde3c196499dc8a390bf6e1a4 Mon Sep 17 00:00:00 2001 From: Joe Ton Date: Sat, 5 Apr 2025 15:03:04 -0700 Subject: [PATCH 31/41] refactor: improve version output using SetVersionTemplate --- cmd/github-mcp-server/main.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index 4d5368ec..89bd956e 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -29,7 +29,7 @@ var ( Use: "server", Short: "GitHub MCP Server", Long: `A GitHub MCP server that handles various tools and resources.`, - Version: fmt.Sprintf("%s (%s) %s", version, commit, date), + Version: version, } stdioCmd = &cobra.Command{ @@ -65,6 +65,9 @@ var ( func init() { cobra.OnInitialize(initConfig) + rootCmd.SetVersionTemplate(fmt.Sprintf( + "GitHub MCP Server\nVersion: %s\nCommit: %s\nBuild Date: %s\n", version, commit, date)) + // Add global flags that will be shared by all commands rootCmd.PersistentFlags().StringSlice("toolsets", github.DefaultTools, "An optional comma separated list of groups of tools to allow, defaults to enabling all") rootCmd.PersistentFlags().Bool("dynamic-toolsets", false, "Enable dynamic toolsets") From 7eff90460565753cb0f2d60b60ed9dd6db1496ec Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Tue, 15 Apr 2025 14:25:29 +0200 Subject: [PATCH 32/41] chore: improve --version command --- cmd/github-mcp-server/main.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index 89bd956e..15f92a73 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -29,7 +29,7 @@ var ( Use: "server", Short: "GitHub MCP Server", Long: `A GitHub MCP server that handles various tools and resources.`, - Version: version, + Version: fmt.Sprintf("Version: %s\nCommit: %s\nBuild Date: %s", version, commit, date), } stdioCmd = &cobra.Command{ @@ -65,8 +65,7 @@ var ( func init() { cobra.OnInitialize(initConfig) - rootCmd.SetVersionTemplate(fmt.Sprintf( - "GitHub MCP Server\nVersion: %s\nCommit: %s\nBuild Date: %s\n", version, commit, date)) + rootCmd.SetVersionTemplate("{{.Short}}\n{{.Version}}\n") // Add global flags that will be shared by all commands rootCmd.PersistentFlags().StringSlice("toolsets", github.DefaultTools, "An optional comma separated list of groups of tools to allow, defaults to enabling all") From b72a591ce5ca3c32bed641e97bb1f1190f29db96 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Tue, 15 Apr 2025 14:29:45 +0200 Subject: [PATCH 33/41] chore: use tabs not spaces --- cmd/github-mcp-server/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index 15f92a73..5ca0e21c 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -65,7 +65,7 @@ var ( func init() { cobra.OnInitialize(initConfig) - rootCmd.SetVersionTemplate("{{.Short}}\n{{.Version}}\n") + rootCmd.SetVersionTemplate("{{.Short}}\n{{.Version}}\n") // Add global flags that will be shared by all commands rootCmd.PersistentFlags().StringSlice("toolsets", github.DefaultTools, "An optional comma separated list of groups of tools to allow, defaults to enabling all") From 4457d0ac3039fab27a1a3b9f8d4850922548de76 Mon Sep 17 00:00:00 2001 From: Tony Truong Date: Tue, 15 Apr 2025 17:01:33 +0200 Subject: [PATCH 34/41] Add missing enum constraints (#278) * Add missing enum constraints * Remove redundant listing of additional enums in descriptions --------- Co-authored-by: Sam Morrow --- pkg/github/code_scanning.go | 6 ++++-- pkg/github/issues.go | 14 +++++++------- pkg/github/pullrequests.go | 23 ++++++++++++++--------- pkg/github/repositories.go | 2 +- pkg/github/search.go | 6 +++--- 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/pkg/github/code_scanning.go b/pkg/github/code_scanning.go index 4fc029bf..5dbac0d5 100644 --- a/pkg/github/code_scanning.go +++ b/pkg/github/code_scanning.go @@ -86,11 +86,13 @@ func ListCodeScanningAlerts(getClient GetClientFn, t translations.TranslationHel mcp.Description("The Git reference for the results you want to list."), ), mcp.WithString("state", - mcp.Description("State of the code scanning alerts to list. Set to closed to list only closed code scanning alerts. Default: open"), + mcp.Description("Filter code scanning alerts by state. Defaults to open"), mcp.DefaultString("open"), + mcp.Enum("open", "closed", "dismissed", "fixed"), ), mcp.WithString("severity", - mcp.Description("Only code scanning alerts with this severity will be returned. Possible values are: critical, high, medium, low, warning, note, error."), + mcp.Description("Filter code scanning alerts by severity"), + mcp.Enum("critical", "high", "medium", "low", "warning", "note", "error"), ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 16c34141..1324bd56 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -90,7 +90,7 @@ func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc ), mcp.WithString("body", mcp.Required(), - mcp.Description("Comment text"), + mcp.Description("Comment content"), ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -151,7 +151,7 @@ func SearchIssues(getClient GetClientFn, t translations.TranslationHelperFunc) ( mcp.Description("Search query using GitHub issues search syntax"), ), mcp.WithString("sort", - mcp.Description("Sort field (comments, reactions, created, etc.)"), + mcp.Description("Sort field by number of matches of categories, defaults to best match"), mcp.Enum( "comments", "reactions", @@ -167,7 +167,7 @@ func SearchIssues(getClient GetClientFn, t translations.TranslationHelperFunc) ( ), ), mcp.WithString("order", - mcp.Description("Sort order ('asc' or 'desc')"), + mcp.Description("Sort order"), mcp.Enum("asc", "desc"), ), WithPagination(), @@ -357,7 +357,7 @@ func ListIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (to mcp.Description("Repository name"), ), mcp.WithString("state", - mcp.Description("Filter by state ('open', 'closed', 'all')"), + mcp.Description("Filter by state"), mcp.Enum("open", "closed", "all"), ), mcp.WithArray("labels", @@ -369,11 +369,11 @@ func ListIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (to ), ), mcp.WithString("sort", - mcp.Description("Sort by ('created', 'updated', 'comments')"), + mcp.Description("Sort order"), mcp.Enum("created", "updated", "comments"), ), mcp.WithString("direction", - mcp.Description("Sort direction ('asc', 'desc')"), + mcp.Description("Sort direction"), mcp.Enum("asc", "desc"), ), mcp.WithString("since", @@ -485,7 +485,7 @@ func UpdateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t mcp.Description("New description"), ), mcp.WithString("state", - mcp.Description("New state ('open' or 'closed')"), + mcp.Description("New state"), mcp.Enum("open", "closed"), ), mcp.WithArray("labels", diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index fd9420d7..2be249c8 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -94,7 +94,7 @@ func UpdatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu mcp.Description("New description"), ), mcp.WithString("state", - mcp.Description("New state ('open' or 'closed')"), + mcp.Description("New state"), mcp.Enum("open", "closed"), ), mcp.WithString("base", @@ -201,7 +201,8 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun mcp.Description("Repository name"), ), mcp.WithString("state", - mcp.Description("Filter by state ('open', 'closed', 'all')"), + mcp.Description("Filter by state"), + mcp.Enum("open", "closed", "all"), ), mcp.WithString("head", mcp.Description("Filter by head user/org and branch"), @@ -210,10 +211,12 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun mcp.Description("Filter by base branch"), ), mcp.WithString("sort", - mcp.Description("Sort by ('created', 'updated', 'popularity', 'long-running')"), + mcp.Description("Sort by"), + mcp.Enum("created", "updated", "popularity", "long-running"), ), mcp.WithString("direction", - mcp.Description("Sort direction ('asc', 'desc')"), + mcp.Description("Sort direction"), + mcp.Enum("asc", "desc"), ), WithPagination(), ), @@ -313,7 +316,8 @@ func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFun mcp.Description("Extra detail for merge commit"), ), mcp.WithString("merge_method", - mcp.Description("Merge method ('merge', 'squash', 'rebase')"), + mcp.Description("Merge method"), + mcp.Enum("merge", "squash", "rebase"), ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -671,21 +675,21 @@ func AddPullRequestReviewComment(getClient GetClientFn, t translations.Translati mcp.Description("The relative path to the file that necessitates a comment. Required unless in_reply_to is specified."), ), mcp.WithString("subject_type", - mcp.Description("The level at which the comment is targeted, 'line' or 'file'"), + mcp.Description("The level at which the comment is targeted"), mcp.Enum("line", "file"), ), mcp.WithNumber("line", mcp.Description("The line of the blob in the pull request diff that the comment applies to. For multi-line comments, the last line of the range"), ), mcp.WithString("side", - mcp.Description("The side of the diff to comment on. Can be LEFT or RIGHT"), + mcp.Description("The side of the diff to comment on"), mcp.Enum("LEFT", "RIGHT"), ), mcp.WithNumber("start_line", mcp.Description("For multi-line comments, the first line of the range that the comment applies to"), ), mcp.WithString("start_side", - mcp.Description("For multi-line comments, the starting side of the diff that the comment applies to. Can be LEFT or RIGHT"), + mcp.Description("For multi-line comments, the starting side of the diff that the comment applies to"), mcp.Enum("LEFT", "RIGHT"), ), mcp.WithNumber("in_reply_to", @@ -893,7 +897,8 @@ func CreatePullRequestReview(getClient GetClientFn, t translations.TranslationHe ), mcp.WithString("event", mcp.Required(), - mcp.Description("Review action ('APPROVE', 'REQUEST_CHANGES', 'COMMENT')"), + mcp.Description("Review action to perform"), + mcp.Enum("APPROVE", "REQUEST_CHANGES", "COMMENT"), ), mcp.WithString("commitId", mcp.Description("SHA of commit to review"), diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 1d74dcfb..51948730 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -93,7 +93,7 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (t mcp.Description("Repository name"), ), mcp.WithString("sha", - mcp.Description("Branch name"), + mcp.Description("SHA or Branch name"), ), WithPagination(), ), diff --git a/pkg/github/search.go b/pkg/github/search.go index 75810e24..dc85c177 100644 --- a/pkg/github/search.go +++ b/pkg/github/search.go @@ -78,7 +78,7 @@ func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (to mcp.Description("Sort field ('indexed' only)"), ), mcp.WithString("order", - mcp.Description("Sort order ('asc' or 'desc')"), + mcp.Description("Sort order"), mcp.Enum("asc", "desc"), ), WithPagination(), @@ -147,11 +147,11 @@ func SearchUsers(getClient GetClientFn, t translations.TranslationHelperFunc) (t mcp.Description("Search query using GitHub users search syntax"), ), mcp.WithString("sort", - mcp.Description("Sort field (followers, repositories, joined)"), + mcp.Description("Sort field by category"), mcp.Enum("followers", "repositories", "joined"), ), mcp.WithString("order", - mcp.Description("Sort order ('asc' or 'desc')"), + mcp.Description("Sort order"), mcp.Enum("asc", "desc"), ), WithPagination(), From 614f2267249ee9e4aeb8696d29caa93f3752196a Mon Sep 17 00:00:00 2001 From: Dylan Rinker Date: Tue, 15 Apr 2025 13:36:16 -0400 Subject: [PATCH 35/41] Add Tool Name property for List Code Scanning Alerts tool (#272) --- README.md | 1 + pkg/github/code_scanning.go | 9 ++++++++- pkg/github/code_scanning_test.go | 19 +++++++++++-------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6bfc6ab5..288d7548 100644 --- a/README.md +++ b/README.md @@ -436,6 +436,7 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `ref`: Git reference (string, optional) - `state`: Alert state (string, optional) - `severity`: Alert severity (string, optional) + - `tool_name`: The name of the tool used for code scanning (string, optional) ## Resources diff --git a/pkg/github/code_scanning.go b/pkg/github/code_scanning.go index 5dbac0d5..b33f32c1 100644 --- a/pkg/github/code_scanning.go +++ b/pkg/github/code_scanning.go @@ -94,6 +94,9 @@ func ListCodeScanningAlerts(getClient GetClientFn, t translations.TranslationHel mcp.Description("Filter code scanning alerts by severity"), mcp.Enum("critical", "high", "medium", "low", "warning", "note", "error"), ), + mcp.WithString("tool_name", + mcp.Description("The name of the tool used for code scanning."), + ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := requiredParam[string](request, "owner") @@ -116,12 +119,16 @@ func ListCodeScanningAlerts(getClient GetClientFn, t translations.TranslationHel 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}) + alerts, resp, err := client.CodeScanning.ListAlertsForRepo(ctx, owner, repo, &github.AlertListOptions{Ref: ref, State: state, Severity: severity, ToolName: toolName}) if err != nil { return nil, fmt.Errorf("failed to list alerts: %w", err) } diff --git a/pkg/github/code_scanning_test.go b/pkg/github/code_scanning_test.go index c9895e26..40dabebd 100644 --- a/pkg/github/code_scanning_test.go +++ b/pkg/github/code_scanning_test.go @@ -127,6 +127,7 @@ func Test_ListCodeScanningAlerts(t *testing.T) { assert.Contains(t, tool.InputSchema.Properties, "ref") assert.Contains(t, tool.InputSchema.Properties, "state") assert.Contains(t, tool.InputSchema.Properties, "severity") + assert.Contains(t, tool.InputSchema.Properties, "tool_name") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"}) // Setup mock alerts for success case @@ -159,20 +160,22 @@ func Test_ListCodeScanningAlerts(t *testing.T) { mock.WithRequestMatchHandler( mock.GetReposCodeScanningAlertsByOwnerByRepo, expectQueryParams(t, map[string]string{ - "ref": "main", - "state": "open", - "severity": "high", + "ref": "main", + "state": "open", + "severity": "high", + "tool_name": "codeql", }).andThen( mockResponse(t, http.StatusOK, mockAlerts), ), ), ), requestArgs: map[string]interface{}{ - "owner": "owner", - "repo": "repo", - "ref": "main", - "state": "open", - "severity": "high", + "owner": "owner", + "repo": "repo", + "ref": "main", + "state": "open", + "severity": "high", + "tool_name": "codeql", }, expectError: false, expectedAlerts: mockAlerts, From 7c197f5850296f721ed4762163a9da1c71f5bb2e Mon Sep 17 00:00:00 2001 From: Tony Truong Date: Thu, 17 Apr 2025 10:19:59 +0200 Subject: [PATCH 36/41] feat: Adding SecretScanning Toolset (#280) * Adding GetSecretScanningAlert and ListSecretScanningAlerts * adding wip for tests * fix tests * add readme section * Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update .gitignore Co-authored-by: Sam Morrow * fix product name --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Sam Morrow --- .gitignore | 9 +- README.md | 17 +- pkg/github/secret_scanning.go | 146 +++++++++++++++++ pkg/github/secret_scanning_test.go | 243 +++++++++++++++++++++++++++++ pkg/github/tools.go | 6 + 5 files changed, 418 insertions(+), 3 deletions(-) create mode 100644 pkg/github/secret_scanning.go create mode 100644 pkg/github/secret_scanning_test.go diff --git a/.gitignore b/.gitignore index 9fb1dca9..9371be3e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,12 @@ .idea cmd/github-mcp-server/github-mcp-server + +# VSCode +.vscode/mcp.json + # Added by goreleaser init: dist/ -__debug_bin* \ No newline at end of file +__debug_bin* + +# Go +vendor diff --git a/README.md b/README.md index 288d7548..8a5ee539 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,6 @@ automation and interaction capabilities for developers and tools. 3. Lastly you will need to [Create a GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new). The MCP server can use many of the GitHub APIs, so enable the permissions that you feel comfortable granting your AI tools (to learn more about access tokens, please check out the [documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)). - - ## Installation ### Usage with VS Code @@ -438,6 +436,21 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `severity`: Alert severity (string, optional) - `tool_name`: The name of the tool used for code scanning (string, optional) +### Secret Scanning + +- **get_secret_scanning_alert** - Get a secret scanning alert + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `alertNumber`: Alert number (number, required) + +- **list_secret_scanning_alerts** - List secret scanning alerts for a repository + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `state`: Alert state (string, optional) + - `secret_type`: The secret types to be filtered for in a comma-separated list (string, optional) + - `resolution`: The resolution status (string, optional) + ## Resources ### Repository Content diff --git a/pkg/github/secret_scanning.go b/pkg/github/secret_scanning.go new file mode 100644 index 00000000..ee344061 --- /dev/null +++ b/pkg/github/secret_scanning.go @@ -0,0 +1,146 @@ +package github + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/github/github-mcp-server/pkg/translations" + "github.com/google/go-github/v69/github" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func GetSecretScanningAlert(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool( + "get_secret_scanning_alert", + mcp.WithDescription(t("TOOL_GET_SECRET_SCANNING_ALERT_DESCRIPTION", "Get details of a specific secret scanning alert in a GitHub repository.")), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("The owner of the repository."), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("The name of the repository."), + ), + mcp.WithNumber("alertNumber", + mcp.Required(), + mcp.Description("The number of the alert."), + ), + ), + func(ctx context.Context, 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 nil, fmt.Errorf("failed to get alert: %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 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 ListSecretScanningAlerts(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool( + "list_secret_scanning_alerts", + mcp.WithDescription(t("TOOL_LIST_SECRET_SCANNING_ALERTS_DESCRIPTION", "List secret scanning alerts in a GitHub repository.")), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("The owner of the repository."), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("The name of the repository."), + ), + mcp.WithString("state", + mcp.Description("Filter by state"), + mcp.Enum("open", "resolved"), + ), + mcp.WithString("secret_type", + mcp.Description("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."), + ), + mcp.WithString("resolution", + mcp.Description("Filter by resolution"), + mcp.Enum("false_positive", "wont_fix", "revoked", "pattern_edited", "pattern_deleted", "used_in_tests"), + ), + ), + func(ctx context.Context, 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 nil, fmt.Errorf("failed to list alerts: %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 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 + } +} diff --git a/pkg/github/secret_scanning_test.go b/pkg/github/secret_scanning_test.go new file mode 100644 index 00000000..d32cbca9 --- /dev/null +++ b/pkg/github/secret_scanning_test.go @@ -0,0 +1,243 @@ +package github + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/github/github-mcp-server/pkg/translations" + "github.com/google/go-github/v69/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_GetSecretScanningAlert(t *testing.T) { + mockClient := github.NewClient(nil) + tool, _ := GetSecretScanningAlert(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "get_secret_scanning_alert", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "owner") + assert.Contains(t, tool.InputSchema.Properties, "repo") + assert.Contains(t, tool.InputSchema.Properties, "alertNumber") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "alertNumber"}) + + // Setup mock alert for success case + mockAlert := &github.SecretScanningAlert{ + Number: github.Ptr(42), + State: github.Ptr("open"), + HTMLURL: github.Ptr("https://github.com/owner/private-repo/security/secret-scanning/42"), + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedAlert *github.SecretScanningAlert + expectedErrMsg string + }{ + { + name: "successful alert fetch", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposSecretScanningAlertsByOwnerByRepoByAlertNumber, + mockAlert, + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "alertNumber": float64(42), + }, + expectError: false, + expectedAlert: mockAlert, + }, + { + name: "alert fetch fails", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposSecretScanningAlertsByOwnerByRepoByAlertNumber, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message": "Not Found"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "alertNumber": float64(9999), + }, + expectError: true, + expectedErrMsg: "failed to get alert", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := GetSecretScanningAlert(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErrMsg) + return + } + + require.NoError(t, err) + + // Parse the result and get the text content if no error + textContent := getTextResult(t, result) + + // Unmarshal and verify the result + var returnedAlert github.Alert + err = json.Unmarshal([]byte(textContent.Text), &returnedAlert) + assert.NoError(t, err) + assert.Equal(t, *tc.expectedAlert.Number, *returnedAlert.Number) + assert.Equal(t, *tc.expectedAlert.State, *returnedAlert.State) + assert.Equal(t, *tc.expectedAlert.HTMLURL, *returnedAlert.HTMLURL) + + }) + } +} + +func Test_ListSecretScanningAlerts(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := ListSecretScanningAlerts(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "list_secret_scanning_alerts", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "owner") + assert.Contains(t, tool.InputSchema.Properties, "repo") + assert.Contains(t, tool.InputSchema.Properties, "state") + assert.Contains(t, tool.InputSchema.Properties, "secret_type") + assert.Contains(t, tool.InputSchema.Properties, "resolution") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"}) + + // Setup mock alerts for success case + resolvedAlert := github.SecretScanningAlert{ + Number: github.Ptr(2), + HTMLURL: github.Ptr("https://github.com/owner/private-repo/security/secret-scanning/2"), + State: github.Ptr("resolved"), + Resolution: github.Ptr("false_positive"), + SecretType: github.Ptr("adafruit_io_key"), + } + openAlert := github.SecretScanningAlert{ + Number: github.Ptr(2), + HTMLURL: github.Ptr("https://github.com/owner/private-repo/security/secret-scanning/3"), + State: github.Ptr("open"), + Resolution: github.Ptr("false_positive"), + SecretType: github.Ptr("adafruit_io_key"), + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedAlerts []*github.SecretScanningAlert + expectedErrMsg string + }{ + { + name: "successful resolved alerts listing", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposSecretScanningAlertsByOwnerByRepo, + expectQueryParams(t, map[string]string{ + "state": "resolved", + }).andThen( + mockResponse(t, http.StatusOK, []*github.SecretScanningAlert{&resolvedAlert}), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "state": "resolved", + }, + expectError: false, + expectedAlerts: []*github.SecretScanningAlert{&resolvedAlert}, + }, + { + name: "successful alerts listing", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposSecretScanningAlertsByOwnerByRepo, + expectQueryParams(t, map[string]string{}).andThen( + mockResponse(t, http.StatusOK, []*github.SecretScanningAlert{&resolvedAlert, &openAlert}), + ), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + }, + expectError: false, + expectedAlerts: []*github.SecretScanningAlert{&resolvedAlert, &openAlert}, + }, + { + name: "alerts listing fails", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposSecretScanningAlertsByOwnerByRepo, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"message": "Unauthorized access"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + }, + expectError: true, + expectedErrMsg: "failed to list alerts", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + client := github.NewClient(tc.mockedClient) + _, handler := ListSecretScanningAlerts(stubGetClientFn(client), translations.NullTranslationHelper) + + request := createMCPRequest(tc.requestArgs) + + result, err := handler(context.Background(), request) + + if tc.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErrMsg) + return + } + + require.NoError(t, err) + + textContent := getTextResult(t, result) + + // Unmarshal and verify the result + var returnedAlerts []*github.SecretScanningAlert + err = json.Unmarshal([]byte(textContent.Text), &returnedAlerts) + assert.NoError(t, err) + assert.Len(t, returnedAlerts, len(tc.expectedAlerts)) + for i, alert := range returnedAlerts { + assert.Equal(t, *tc.expectedAlerts[i].Number, *alert.Number) + assert.Equal(t, *tc.expectedAlerts[i].HTMLURL, *alert.HTMLURL) + assert.Equal(t, *tc.expectedAlerts[i].State, *alert.State) + assert.Equal(t, *tc.expectedAlerts[i].Resolution, *alert.Resolution) + assert.Equal(t, *tc.expectedAlerts[i].SecretType, *alert.SecretType) + } + }) + } +} diff --git a/pkg/github/tools.go b/pkg/github/tools.go index ce10c4ad..35dabaef 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -73,6 +73,11 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn, toolsets.NewServerTool(GetCodeScanningAlert(getClient, t)), toolsets.NewServerTool(ListCodeScanningAlerts(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)), + ) // Keep experiments alive so the system doesn't error out when it's always enabled experiments := toolsets.NewToolset("experiments", "Experimental features that are not considered stable yet") @@ -82,6 +87,7 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn, tsg.AddToolset(users) tsg.AddToolset(pullRequests) tsg.AddToolset(codeSecurity) + tsg.AddToolset(secretProtection) tsg.AddToolset(experiments) // Enable the requested features From 54bd5ec239937fd01436f8645f0e81211e5b1764 Mon Sep 17 00:00:00 2001 From: William Martin Date: Thu, 17 Apr 2025 12:05:04 +0200 Subject: [PATCH 37/41] Update CODEOWNERS to github-mcp-server team --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 954bc41c..04812330 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @juruen @sammorrowdrums @williammartin @toby +* @github/github-mcp-server From 22e493620cd7c64af54571e1d6920ab583f93841 Mon Sep 17 00:00:00 2001 From: Toby Padilla Date: Thu, 17 Apr 2025 04:50:19 -0600 Subject: [PATCH 38/41] fix: update json schema for `create_pull_request_review` to make OpenAI compatible (#300) Co-authored-by: Sam Morrow --- pkg/github/pullrequests.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index 2be249c8..b1584c0e 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -908,30 +908,30 @@ func CreatePullRequestReview(getClient GetClientFn, t translations.TranslationHe map[string]interface{}{ "type": "object", "additionalProperties": false, - "required": []string{"path", "body"}, + "required": []string{"path", "body", "position", "line", "side", "start_line", "start_side"}, "properties": map[string]interface{}{ "path": map[string]interface{}{ "type": "string", "description": "path to the file", }, "position": map[string]interface{}{ - "type": "number", + "type": []string{"number", "null"}, "description": "position of the comment in the diff", }, "line": map[string]interface{}{ - "type": "number", + "type": []string{"number", "null"}, "description": "line number in the file to comment on. For multi-line comments, the end of the line range", }, "side": map[string]interface{}{ - "type": "string", + "type": []string{"string", "null"}, "description": "The side of the diff on which the line resides. For multi-line comments, this is the side for the end of the line range. (LEFT or RIGHT)", }, "start_line": map[string]interface{}{ - "type": "number", + "type": []string{"number", "null"}, "description": "The first line of the range to which the comment refers. Required for multi-line comments.", }, "start_side": map[string]interface{}{ - "type": "string", + "type": []string{"string", "null"}, "description": "The side of the diff on which the start line resides for multi-line comments. (LEFT or RIGHT)", }, "body": map[string]interface{}{ From aa573152726abd942a617ba0805171e29ccaa54c Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Tue, 15 Apr 2025 00:57:53 +0200 Subject: [PATCH 39/41] chore: update readme for tool paritioning --- README.md | 104 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 92 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8a5ee539..703df68b 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,95 @@ If you don't have Docker, you can use `go build` to build the binary in the } ``` +## Tool Configuration + +The GitHub MCP Server supports enabling or disabling specific groups of functionalities via the `--toolsets` flag. This allows you to control which GitHub API capabilities are available to your AI tools. + +### Available Toolsets + +The following sets of tools are available (all are on by default): + +| Toolset | Description | +| ----------------------- | ------------------------------------------------------------- | +| `repos` | Repository-related tools (file operations, branches, commits) | +| `issues` | Issue-related tools (create, read, update, comment) | +| `users ` | Anything relating to GitHub Users | +| `pull_requests` | Pull request operations (create, merge, review) | +| `code_security` | Code scanning alerts and security features | +| `experiments` | Experimental features (not considered stable) | + +#### Specifying Toolsets + +To reduce the available tools, you can pass an allow-list in two ways: + +1. **Using Command Line Argument**: + + ```bash + github-mcp-server --toolsets repos,issues,pull_requests,code_security + ``` + +2. **Using Environment Variable**: + ```bash + GITHUB_TOOLSETS="repos,issues,pull_requests,code_security" ./github-mcp-server + ``` + +The environment variable `GITHUB_TOOLSETS` takes precedence over the command line argument if both are provided. + +Any toolsets you specify will be enabled from the start, including when `--dynamic-toolsets` is on. + +You might want to do this if the model is confused about which tools to call and you only require a subset. + + +### Using Toolsets With Docker + +When using Docker, you can pass the toolsets as environment variables: + +```bash +docker run -i --rm \ + -e GITHUB_PERSONAL_ACCESS_TOKEN= \ + -e GITHUB_TOOLSETS="repos,issues,pull_requests,code_security,experiments" \ + ghcr.io/github/github-mcp-server +``` + +### The "all" Toolset + +The special toolset `all` can be provided to enable all available toolsets regardless of any other configuration: + +```bash +./github-mcp-server --toolsets all +``` + +Or using the environment variable: + +```bash +GITHUB_TOOLSETS="all" ./github-mcp-server +``` + +## Dynamic Tool Discovery + +Instead of starting with all tools enabled, you can turn on Dynamic Toolset Discovery. +This feature provides tools that help the MCP Host application to discover and enable sets of GitHub tools only when needed. +This helps to avoid situations where models get confused by the shear number of tools available to them, which varies by model. + +### Using Dynamic Tool Discovery + +When using the binary, you can pass the `--dynamic-toolsets` flag. + +```bash +./github-mcp-server --dynamic-toolsets +``` + +When using Docker, you can pass the toolsets as environment variables: + +```bash +docker run -i --rm \ + -e GITHUB_PERSONAL_ACCESS_TOKEN= \ + -e GITHUB_DYNAMIC_TOOLSETS=1 \ + ghcr.io/github/github-mcp-server +``` + + + ## GitHub Enterprise Server The flag `--gh-host` and the environment variable `GH_HOST` can be used to set @@ -329,7 +418,6 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description ### Repositories - **create_or_update_file** - Create or update a single file in a repository - - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `path`: File path (string, required) @@ -339,14 +427,12 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `sha`: File SHA if updating (string, optional) - **list_branches** - List branches in a GitHub repository - - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `page`: Page number (number, optional) - `perPage`: Results per page (number, optional) - **push_files** - Push multiple files in a single commit - - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `branch`: Branch to push to (string, required) @@ -354,7 +440,6 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `message`: Commit message (string, required) - **search_repositories** - Search for GitHub repositories - - `query`: Search query (string, required) - `sort`: Sort field (string, optional) - `order`: Sort order (string, optional) @@ -362,27 +447,23 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `perPage`: Results per page (number, optional) - **create_repository** - Create a new GitHub repository - - `name`: Repository name (string, required) - `description`: Repository description (string, optional) - `private`: Whether the repository is private (boolean, optional) - `autoInit`: Auto-initialize with README (boolean, optional) - **get_file_contents** - Get contents of a file or directory - - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `path`: File path (string, required) - `ref`: Git reference (string, optional) - **fork_repository** - Fork a repository - - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `organization`: Target organization name (string, optional) - **create_branch** - Create a new branch - - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `branch`: New branch name (string, required) @@ -403,16 +484,15 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `page`: Page number, for files in the commit (number, optional) - `perPage`: Results per page, for files in the commit (number, optional) -### Search - -- **search_code** - Search for code across GitHub repositories - + - **search_code** - Search for code across GitHub repositories - `query`: Search query (string, required) - `sort`: Sort field (string, optional) - `order`: Sort order (string, optional) - `page`: Page number (number, optional) - `perPage`: Results per page (number, optional) +### Users + - **search_users** - Search for GitHub users - `query`: Search query (string, required) - `sort`: Sort field (string, optional) From 11b18286d13bc8d386159a53ec063f16137abd56 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Tue, 15 Apr 2025 01:00:06 +0200 Subject: [PATCH 40/41] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 703df68b..574c60f6 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ The following sets of tools are available (all are on by default): | ----------------------- | ------------------------------------------------------------- | | `repos` | Repository-related tools (file operations, branches, commits) | | `issues` | Issue-related tools (create, read, update, comment) | -| `users ` | Anything relating to GitHub Users | +| `users` | Anything relating to GitHub Users | | `pull_requests` | Pull request operations (create, merge, review) | | `code_security` | Code scanning alerts and security features | | `experiments` | Experimental features (not considered stable) | From 89914364974088cda9bec4fd559afaf1123e462f Mon Sep 17 00:00:00 2001 From: Toby Padilla Date: Wed, 16 Apr 2025 14:08:36 -0600 Subject: [PATCH 41/41] docs: update toolset copy --- README.md | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 574c60f6..5493adba 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ If you don't have Docker, you can use `go build` to build the binary in the ## Tool Configuration -The GitHub MCP Server supports enabling or disabling specific groups of functionalities via the `--toolsets` flag. This allows you to control which GitHub API capabilities are available to your AI tools. +The GitHub MCP Server supports enabling or disabling specific groups of functionalities via the `--toolsets` flag. This allows you to control which GitHub API capabilities are available to your AI tools. Enabling only the toolsets that you need can help the LLM with tool choice and reduce the context size. ### Available Toolsets @@ -127,7 +127,7 @@ The following sets of tools are available (all are on by default): #### Specifying Toolsets -To reduce the available tools, you can pass an allow-list in two ways: +To specify toolsets you want available to the LLM, you can pass an allow-list in two ways: 1. **Using Command Line Argument**: @@ -142,11 +142,6 @@ To reduce the available tools, you can pass an allow-list in two ways: The environment variable `GITHUB_TOOLSETS` takes precedence over the command line argument if both are provided. -Any toolsets you specify will be enabled from the start, including when `--dynamic-toolsets` is on. - -You might want to do this if the model is confused about which tools to call and you only require a subset. - - ### Using Toolsets With Docker When using Docker, you can pass the toolsets as environment variables: @@ -174,9 +169,9 @@ GITHUB_TOOLSETS="all" ./github-mcp-server ## Dynamic Tool Discovery -Instead of starting with all tools enabled, you can turn on Dynamic Toolset Discovery. -This feature provides tools that help the MCP Host application to discover and enable sets of GitHub tools only when needed. -This helps to avoid situations where models get confused by the shear number of tools available to them, which varies by model. +**Note**: This feature is currently in beta and may not be available in all environments. Please test it out and let us know if you encounter any issues. + +Instead of starting with all tools enabled, you can turn on dynamic toolset discovery. Dynamic toolsets allow the MCP host to list and enable toolsets in response to a user prompt. This should help to avoid situations where the model gets confused by the shear number of tools available. ### Using Dynamic Tool Discovery @@ -195,8 +190,6 @@ docker run -i --rm \ ghcr.io/github/github-mcp-server ``` - - ## GitHub Enterprise Server The flag `--gh-host` and the environment variable `GH_HOST` can be used to set