Content-Length: 38910 | pFad | http://github.com/github/github-mcp-server/pull/583.patch
thub.com
From 0c4c71c5bfd3b79321f79ae9375d0707a79d0be9 Mon Sep 17 00:00:00 2001
From: JoannaaKL
Date: Thu, 26 Jun 2025 10:40:54 +0200
Subject: [PATCH 1/3] Add search pull requests tool
---
pkg/github/issues.go | 2 +-
pkg/github/pullrequests.go | 88 ++++++++++++++++++
pkg/github/pullrequests_test.go | 156 ++++++++++++++++++++++++++++++++
pkg/github/tools.go | 1 +
4 files changed, 246 insertions(+), 1 deletion(-)
diff --git a/pkg/github/issues.go b/pkg/github/issues.go
index b4c64c8de..cc6869ffb 100644
--- a/pkg/github/issues.go
+++ b/pkg/github/issues.go
@@ -153,7 +153,7 @@ func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc
}
}
-// SearchIssues creates a tool to search for issues and pull requests.
+// SearchIssues creates a tool to search for issues.
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 in GitHub repositories.")),
diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go
index 7dcc2c4fd..0cdf72abc 100644
--- a/pkg/github/pullrequests.go
+++ b/pkg/github/pullrequests.go
@@ -533,6 +533,94 @@ func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFun
}
}
+// SearchPullRequests creates a tool to search for pull requests.
+func SearchPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+ return mcp.NewTool("search_pull_requests",
+ mcp.WithDescription(t("TOOL_SEARCH_PULL_REQUESTS_DESCRIPTION", "Search for pull requests in GitHub repositories.")),
+ mcp.WithToolAnnotation(mcp.ToolAnnotation{
+ Title: t("TOOL_SEARCH_PULL_REQUESTS_USER_TITLE", "Search pull requests"),
+ ReadOnlyHint: ToBoolPtr(true),
+ }),
+ mcp.WithString("q",
+ mcp.Required(),
+ mcp.Description("Search query using GitHub pull request search syntax"),
+ ),
+ mcp.WithString("sort",
+ mcp.Description("Sort field by number of matches of categories, defaults to best match"),
+ mcp.Enum(
+ "comments",
+ "reactions",
+ "reactions-+1",
+ "reactions--1",
+ "reactions-smile",
+ "reactions-thinking_face",
+ "reactions-heart",
+ "reactions-tada",
+ "interactions",
+ "created",
+ "updated",
+ ),
+ ),
+ mcp.WithString("order",
+ mcp.Description("Sort order"),
+ mcp.Enum("asc", "desc"),
+ ),
+ 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")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ order, err := OptionalParam[string](request, "order")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ pagination, err := OptionalPaginationParams(request)
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ opts := &github.SearchOptions{
+ Sort: sort,
+ Order: order,
+ ListOptions: github.ListOptions{
+ PerPage: pagination.perPage,
+ Page: pagination.page,
+ },
+ }
+
+ 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 pull requests: %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 search pull requests: %s", string(body))), nil
+ }
+
+ r, err := json.Marshal(result)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal response: %w", err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+ }
+}
+
// GetPullRequestFiles creates a tool to get the list of files changed in a pull request.
func GetPullRequestFiles(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
return mcp.NewTool("get_pull_request_files",
diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go
index 02575c439..21b76e8e8 100644
--- a/pkg/github/pullrequests_test.go
+++ b/pkg/github/pullrequests_test.go
@@ -565,6 +565,162 @@ func Test_MergePullRequest(t *testing.T) {
}
}
+func Test_SearchPullRequests(t *testing.T) {
+ mockClient := github.NewClient(nil)
+ tool, _ := SearchPullRequests(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+ require.NoError(t, toolsnaps.Test(tool.Name, tool))
+
+ assert.Equal(t, "search_pull_requests", tool.Name)
+ assert.NotEmpty(t, tool.Description)
+ assert.Contains(t, tool.InputSchema.Properties, "q")
+ assert.Contains(t, tool.InputSchema.Properties, "sort")
+ assert.Contains(t, tool.InputSchema.Properties, "order")
+ assert.Contains(t, tool.InputSchema.Properties, "perPage")
+ assert.Contains(t, tool.InputSchema.Properties, "page")
+ assert.ElementsMatch(t, tool.InputSchema.Required, []string{"q"})
+
+ mockSearchResult := &github.IssuesSearchResult{
+ Total: github.Ptr(2),
+ IncompleteResults: github.Ptr(false),
+ Issues: []*github.Issue{
+ {
+ Number: github.Ptr(42),
+ Title: github.Ptr("Test PR 1"),
+ Body: github.Ptr("Updated tests."),
+ State: github.Ptr("open"),
+ HTMLURL: github.Ptr("https://github.com/owner/repo/pull/1"),
+ Comments: github.Ptr(5),
+ User: &github.User{
+ Login: github.Ptr("user1"),
+ },
+ },
+ {
+ Number: github.Ptr(43),
+ Title: github.Ptr("Test PR 2"),
+ Body: github.Ptr("Updated build scripts."),
+ State: github.Ptr("open"),
+ HTMLURL: github.Ptr("https://github.com/owner/repo/pull/2"),
+ Comments: github.Ptr(3),
+ User: &github.User{
+ Login: github.Ptr("user2"),
+ },
+ },
+ },
+ }
+
+ tests := []struct {
+ name string
+ mockedClient *http.Client
+ requestArgs map[string]interface{}
+ expectError bool
+ expectedResult *github.IssuesSearchResult
+ expectedErrMsg string
+ }{
+ {
+ name: "successful pull request search with all parameters",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.GetSearchIssues,
+ expectQueryParams(
+ t,
+ map[string]string{
+ "q": "repo:owner/repo is:pr is:open",
+ "sort": "created",
+ "order": "desc",
+ "page": "1",
+ "per_page": "30",
+ },
+ ).andThen(
+ mockResponse(t, http.StatusOK, mockSearchResult),
+ ),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "q": "repo:owner/repo is:pr is:open",
+ "sort": "created",
+ "order": "desc",
+ "page": float64(1),
+ "perPage": float64(30),
+ },
+ expectError: false,
+ expectedResult: mockSearchResult,
+ },
+ {
+ name: "pull request search with minimal parameters",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatch(
+ mock.GetSearchIssues,
+ mockSearchResult,
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "q": "repo:owner/repo is:pr is:open",
+ },
+ expectError: false,
+ expectedResult: mockSearchResult,
+ },
+ {
+ name: "search pull requests fails",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.GetSearchIssues,
+ http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ w.WriteHeader(http.StatusBadRequest)
+ _, _ = w.Write([]byte(`{"message": "Validation Failed"}`))
+ }),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "q": "invalid:query",
+ },
+ expectError: true,
+ expectedErrMsg: "failed to search issues",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ // Setup client with mock
+ client := github.NewClient(tc.mockedClient)
+ _, handler := SearchIssues(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 returnedResult github.IssuesSearchResult
+ err = json.Unmarshal([]byte(textContent.Text), &returnedResult)
+ require.NoError(t, err)
+ assert.Equal(t, *tc.expectedResult.Total, *returnedResult.Total)
+ assert.Equal(t, *tc.expectedResult.IncompleteResults, *returnedResult.IncompleteResults)
+ assert.Len(t, returnedResult.Issues, len(tc.expectedResult.Issues))
+ for i, issue := range returnedResult.Issues {
+ assert.Equal(t, *tc.expectedResult.Issues[i].Number, *issue.Number)
+ assert.Equal(t, *tc.expectedResult.Issues[i].Title, *issue.Title)
+ assert.Equal(t, *tc.expectedResult.Issues[i].State, *issue.State)
+ assert.Equal(t, *tc.expectedResult.Issues[i].HTMLURL, *issue.HTMLURL)
+ assert.Equal(t, *tc.expectedResult.Issues[i].User.Login, *issue.User.Login)
+ }
+ })
+ }
+
+}
+
func Test_GetPullRequestFiles(t *testing.T) {
// Verify tool definition once
mockClient := github.NewClient(nil)
diff --git a/pkg/github/tools.go b/pkg/github/tools.go
index 5b970698c..502a381e9 100644
--- a/pkg/github/tools.go
+++ b/pkg/github/tools.go
@@ -51,6 +51,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
AddReadTools(
toolsets.NewServerTool(GetIssue(getClient, t)),
toolsets.NewServerTool(SearchIssues(getClient, t)),
+ toolsets.NewServerTool(SearchPullRequests(getClient, t)),
toolsets.NewServerTool(ListIssues(getClient, t)),
toolsets.NewServerTool(GetIssueComments(getClient, t)),
).
From 7b12e04057deb902008b4c3628eda7eb4457c243 Mon Sep 17 00:00:00 2001
From: JoannaaKL
Date: Thu, 26 Jun 2025 10:58:49 +0200
Subject: [PATCH 2/3] Split pr and issue search
Add description
Extract common code
Test fixes
Updated search description
Move search prs to prs toolset
Update tools snaps
---
pkg/github/__toolsnaps__/search_issues.snap | 6 +-
.../__toolsnaps__/search_pull_requests.snap | 56 ++++++++++++++
pkg/github/issues.go | 55 +-------------
pkg/github/issues_test.go | 12 +--
pkg/github/pullrequests.go | 55 +-------------
pkg/github/pullrequests_test.go | 16 ++--
pkg/github/search_utils.go | 73 +++++++++++++++++++
pkg/github/tools.go | 2 +-
8 files changed, 153 insertions(+), 122 deletions(-)
create mode 100644 pkg/github/__toolsnaps__/search_pull_requests.snap
create mode 100644 pkg/github/search_utils.go
diff --git a/pkg/github/__toolsnaps__/search_issues.snap b/pkg/github/__toolsnaps__/search_issues.snap
index 4e2382a3c..e81d18c41 100644
--- a/pkg/github/__toolsnaps__/search_issues.snap
+++ b/pkg/github/__toolsnaps__/search_issues.snap
@@ -3,7 +3,7 @@
"title": "Search issues",
"readOnlyHint": true
},
- "description": "Search for issues in GitHub repositories.",
+ "description": "Search for issues in GitHub repositories using issues search syntax already scoped to is:issue",
"inputSchema": {
"properties": {
"order": {
@@ -25,7 +25,7 @@
"minimum": 1,
"type": "number"
},
- "q": {
+ "query": {
"description": "Search query using GitHub issues search syntax",
"type": "string"
},
@@ -48,7 +48,7 @@
}
},
"required": [
- "q"
+ "query"
],
"type": "object"
},
diff --git a/pkg/github/__toolsnaps__/search_pull_requests.snap b/pkg/github/__toolsnaps__/search_pull_requests.snap
new file mode 100644
index 000000000..e33304bf7
--- /dev/null
+++ b/pkg/github/__toolsnaps__/search_pull_requests.snap
@@ -0,0 +1,56 @@
+{
+ "annotations": {
+ "title": "Search pull requests",
+ "readOnlyHint": true
+ },
+ "description": "Search for pull requests in GitHub repositories using issues search syntax already scoped to is:pr",
+ "inputSchema": {
+ "properties": {
+ "order": {
+ "description": "Sort order",
+ "enum": [
+ "asc",
+ "desc"
+ ],
+ "type": "string"
+ },
+ "page": {
+ "description": "Page number for pagination (min 1)",
+ "minimum": 1,
+ "type": "number"
+ },
+ "perPage": {
+ "description": "Results per page for pagination (min 1, max 100)",
+ "maximum": 100,
+ "minimum": 1,
+ "type": "number"
+ },
+ "query": {
+ "description": "Search query using GitHub pull request search syntax",
+ "type": "string"
+ },
+ "sort": {
+ "description": "Sort field by number of matches of categories, defaults to best match",
+ "enum": [
+ "comments",
+ "reactions",
+ "reactions-+1",
+ "reactions--1",
+ "reactions-smile",
+ "reactions-thinking_face",
+ "reactions-heart",
+ "reactions-tada",
+ "interactions",
+ "created",
+ "updated"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "query"
+ ],
+ "type": "object"
+ },
+ "name": "search_pull_requests"
+}
\ No newline at end of file
diff --git a/pkg/github/issues.go b/pkg/github/issues.go
index cc6869ffb..9a61102e4 100644
--- a/pkg/github/issues.go
+++ b/pkg/github/issues.go
@@ -156,12 +156,12 @@ func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc
// SearchIssues creates a tool to search for issues.
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 in GitHub repositories.")),
+ mcp.WithDescription(t("TOOL_SEARCH_ISSUES_DESCRIPTION", "Search for issues in GitHub repositories using issues search syntax already scoped to is:issue")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_SEARCH_ISSUES_USER_TITLE", "Search issues"),
ReadOnlyHint: ToBoolPtr(true),
}),
- mcp.WithString("q",
+ mcp.WithString("query",
mcp.Required(),
mcp.Description("Search query using GitHub issues search syntax"),
),
@@ -188,56 +188,7 @@ func SearchIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (
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")
- if err != nil {
- return mcp.NewToolResultError(err.Error()), nil
- }
- order, err := OptionalParam[string](request, "order")
- if err != nil {
- return mcp.NewToolResultError(err.Error()), nil
- }
- pagination, err := OptionalPaginationParams(request)
- if err != nil {
- return mcp.NewToolResultError(err.Error()), nil
- }
-
- opts := &github.SearchOptions{
- Sort: sort,
- Order: order,
- ListOptions: github.ListOptions{
- PerPage: pagination.perPage,
- Page: pagination.page,
- },
- }
-
- 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)
- }
- 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 search issues: %s", string(body))), nil
- }
-
- r, err := json.Marshal(result)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal response: %w", err)
- }
-
- return mcp.NewToolResultText(string(r)), nil
+ return searchHandler(ctx, getClient, request, "issue", "failed to search issues")
}
}
diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go
index 7c76d90f9..d1e13c0aa 100644
--- a/pkg/github/issues_test.go
+++ b/pkg/github/issues_test.go
@@ -237,12 +237,12 @@ func Test_SearchIssues(t *testing.T) {
assert.Equal(t, "search_issues", tool.Name)
assert.NotEmpty(t, tool.Description)
- assert.Contains(t, tool.InputSchema.Properties, "q")
+ assert.Contains(t, tool.InputSchema.Properties, "query")
assert.Contains(t, tool.InputSchema.Properties, "sort")
assert.Contains(t, tool.InputSchema.Properties, "order")
assert.Contains(t, tool.InputSchema.Properties, "perPage")
assert.Contains(t, tool.InputSchema.Properties, "page")
- assert.ElementsMatch(t, tool.InputSchema.Required, []string{"q"})
+ assert.ElementsMatch(t, tool.InputSchema.Required, []string{"query"})
// Setup mock search results
mockSearchResult := &github.IssuesSearchResult{
@@ -290,7 +290,7 @@ func Test_SearchIssues(t *testing.T) {
expectQueryParams(
t,
map[string]string{
- "q": "repo:owner/repo is:issue is:open",
+ "q": "is:issue repo:owner/repo is:open",
"sort": "created",
"order": "desc",
"page": "1",
@@ -302,7 +302,7 @@ func Test_SearchIssues(t *testing.T) {
),
),
requestArgs: map[string]interface{}{
- "q": "repo:owner/repo is:issue is:open",
+ "query": "repo:owner/repo is:open",
"sort": "created",
"order": "desc",
"page": float64(1),
@@ -320,7 +320,7 @@ func Test_SearchIssues(t *testing.T) {
),
),
requestArgs: map[string]interface{}{
- "q": "repo:owner/repo is:issue is:open",
+ "query": "is:issue repo:owner/repo is:open",
},
expectError: false,
expectedResult: mockSearchResult,
@@ -337,7 +337,7 @@ func Test_SearchIssues(t *testing.T) {
),
),
requestArgs: map[string]interface{}{
- "q": "invalid:query",
+ "query": "invalid:query",
},
expectError: true,
expectedErrMsg: "failed to search issues",
diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go
index 54bfe631e..fb9f720c9 100644
--- a/pkg/github/pullrequests.go
+++ b/pkg/github/pullrequests.go
@@ -536,12 +536,12 @@ func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFun
// SearchPullRequests creates a tool to search for pull requests.
func SearchPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("search_pull_requests",
- mcp.WithDescription(t("TOOL_SEARCH_PULL_REQUESTS_DESCRIPTION", "Search for pull requests in GitHub repositories.")),
+ mcp.WithDescription(t("TOOL_SEARCH_PULL_REQUESTS_DESCRIPTION", "Search for pull requests in GitHub repositories using issues search syntax already scoped to is:pr")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_SEARCH_PULL_REQUESTS_USER_TITLE", "Search pull requests"),
ReadOnlyHint: ToBoolPtr(true),
}),
- mcp.WithString("q",
+ mcp.WithString("query",
mcp.Required(),
mcp.Description("Search query using GitHub pull request search syntax"),
),
@@ -568,56 +568,7 @@ func SearchPullRequests(getClient GetClientFn, t translations.TranslationHelperF
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")
- if err != nil {
- return mcp.NewToolResultError(err.Error()), nil
- }
- order, err := OptionalParam[string](request, "order")
- if err != nil {
- return mcp.NewToolResultError(err.Error()), nil
- }
- pagination, err := OptionalPaginationParams(request)
- if err != nil {
- return mcp.NewToolResultError(err.Error()), nil
- }
-
- opts := &github.SearchOptions{
- Sort: sort,
- Order: order,
- ListOptions: github.ListOptions{
- PerPage: pagination.perPage,
- Page: pagination.page,
- },
- }
-
- 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 pull requests: %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 search pull requests: %s", string(body))), nil
- }
-
- r, err := json.Marshal(result)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal response: %w", err)
- }
-
- return mcp.NewToolResultText(string(r)), nil
+ return searchHandler(ctx, getClient, request, "pr", "failed to search pull requests")
}
}
diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go
index 8895f81de..f1f8394cc 100644
--- a/pkg/github/pullrequests_test.go
+++ b/pkg/github/pullrequests_test.go
@@ -572,12 +572,12 @@ func Test_SearchPullRequests(t *testing.T) {
assert.Equal(t, "search_pull_requests", tool.Name)
assert.NotEmpty(t, tool.Description)
- assert.Contains(t, tool.InputSchema.Properties, "q")
+ assert.Contains(t, tool.InputSchema.Properties, "query")
assert.Contains(t, tool.InputSchema.Properties, "sort")
assert.Contains(t, tool.InputSchema.Properties, "order")
assert.Contains(t, tool.InputSchema.Properties, "perPage")
assert.Contains(t, tool.InputSchema.Properties, "page")
- assert.ElementsMatch(t, tool.InputSchema.Required, []string{"q"})
+ assert.ElementsMatch(t, tool.InputSchema.Required, []string{"query"})
mockSearchResult := &github.IssuesSearchResult{
Total: github.Ptr(2),
@@ -624,7 +624,7 @@ func Test_SearchPullRequests(t *testing.T) {
expectQueryParams(
t,
map[string]string{
- "q": "repo:owner/repo is:pr is:open",
+ "q": "is:pr repo:owner/repo is:open",
"sort": "created",
"order": "desc",
"page": "1",
@@ -636,7 +636,7 @@ func Test_SearchPullRequests(t *testing.T) {
),
),
requestArgs: map[string]interface{}{
- "q": "repo:owner/repo is:pr is:open",
+ "query": "repo:owner/repo is:open",
"sort": "created",
"order": "desc",
"page": float64(1),
@@ -654,7 +654,7 @@ func Test_SearchPullRequests(t *testing.T) {
),
),
requestArgs: map[string]interface{}{
- "q": "repo:owner/repo is:pr is:open",
+ "query": "is:pr repo:owner/repo is:open",
},
expectError: false,
expectedResult: mockSearchResult,
@@ -671,10 +671,10 @@ func Test_SearchPullRequests(t *testing.T) {
),
),
requestArgs: map[string]interface{}{
- "q": "invalid:query",
+ "query": "invalid:query",
},
expectError: true,
- expectedErrMsg: "failed to search issues",
+ expectedErrMsg: "failed to search pull requests",
},
}
@@ -682,7 +682,7 @@ func Test_SearchPullRequests(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
- _, handler := SearchIssues(stubGetClientFn(client), translations.NullTranslationHelper)
+ _, handler := SearchPullRequests(stubGetClientFn(client), translations.NullTranslationHelper)
// Create call request
request := createMCPRequest(tc.requestArgs)
diff --git a/pkg/github/search_utils.go b/pkg/github/search_utils.go
new file mode 100644
index 000000000..4d60dbb0c
--- /dev/null
+++ b/pkg/github/search_utils.go
@@ -0,0 +1,73 @@
+package github
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/google/go-github/v72/github"
+ "github.com/mark3labs/mcp-go/mcp"
+)
+
+func searchHandler(
+ ctx context.Context,
+ getClient GetClientFn,
+ request mcp.CallToolRequest,
+ searchType string,
+ errorPrefix string,
+) (*mcp.CallToolResult, error) {
+ query, err := RequiredParam[string](request, "query")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ query = fmt.Sprintf("is:%s %s", searchType, query)
+
+ sort, err := OptionalParam[string](request, "sort")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ order, err := OptionalParam[string](request, "order")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+ pagination, err := OptionalPaginationParams(request)
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ opts := &github.SearchOptions{
+ Sort: sort,
+ Order: order,
+ ListOptions: github.ListOptions{
+ Page: pagination.page,
+ PerPage: pagination.perPage,
+ },
+ }
+
+ client, err := getClient(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("%s: failed to get GitHub client: %w", errorPrefix, err)
+ }
+ result, resp, err := client.Search.Issues(ctx, query, opts)
+ if err != nil {
+ return nil, fmt.Errorf("%s: %w", errorPrefix, err)
+ }
+ defer func() { _ = resp.Body.Close() }()
+
+ if resp.StatusCode != http.StatusOK {
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("%s: failed to read response body: %w", errorPrefix, err)
+ }
+ return mcp.NewToolResultError(fmt.Sprintf("%s: %s", errorPrefix, string(body))), nil
+ }
+
+ r, err := json.Marshal(result)
+ if err != nil {
+ return nil, fmt.Errorf("%s: failed to marshal response: %w", errorPrefix, err)
+ }
+
+ return mcp.NewToolResultText(string(r)), nil
+}
diff --git a/pkg/github/tools.go b/pkg/github/tools.go
index 502a381e9..6d723cef9 100644
--- a/pkg/github/tools.go
+++ b/pkg/github/tools.go
@@ -51,7 +51,6 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
AddReadTools(
toolsets.NewServerTool(GetIssue(getClient, t)),
toolsets.NewServerTool(SearchIssues(getClient, t)),
- toolsets.NewServerTool(SearchPullRequests(getClient, t)),
toolsets.NewServerTool(ListIssues(getClient, t)),
toolsets.NewServerTool(GetIssueComments(getClient, t)),
).
@@ -70,6 +69,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
toolsets.NewServerTool(GetPullRequest(getClient, t)),
toolsets.NewServerTool(ListPullRequests(getClient, t)),
toolsets.NewServerTool(GetPullRequestFiles(getClient, t)),
+ toolsets.NewServerTool(SearchPullRequests(getClient, t)),
toolsets.NewServerTool(GetPullRequestStatus(getClient, t)),
toolsets.NewServerTool(GetPullRequestComments(getClient, t)),
toolsets.NewServerTool(GetPullRequestReviews(getClient, t)),
From 9fc7fee65fd929033c74da285571cf3d1662a09e Mon Sep 17 00:00:00 2001
From: JoannaaKL
Date: Thu, 26 Jun 2025 13:24:07 +0200
Subject: [PATCH 3/3] Add repo and owner
---
pkg/github/__toolsnaps__/search_issues.snap | 8 ++
.../__toolsnaps__/search_pull_requests.snap | 8 ++
pkg/github/issues.go | 6 ++
pkg/github/issues_test.go | 79 +++++++++++++++++++
pkg/github/pullrequests.go | 6 ++
pkg/github/pullrequests_test.go | 79 +++++++++++++++++++
pkg/github/search_utils.go | 15 ++++
7 files changed, 201 insertions(+)
diff --git a/pkg/github/__toolsnaps__/search_issues.snap b/pkg/github/__toolsnaps__/search_issues.snap
index e81d18c41..7db502d94 100644
--- a/pkg/github/__toolsnaps__/search_issues.snap
+++ b/pkg/github/__toolsnaps__/search_issues.snap
@@ -14,6 +14,10 @@
],
"type": "string"
},
+ "owner": {
+ "description": "Optional repository owner. If provided with repo, only notifications for this repository are listed.",
+ "type": "string"
+ },
"page": {
"description": "Page number for pagination (min 1)",
"minimum": 1,
@@ -29,6 +33,10 @@
"description": "Search query using GitHub issues search syntax",
"type": "string"
},
+ "repo": {
+ "description": "Optional repository name. If provided with owner, only notifications for this repository are listed.",
+ "type": "string"
+ },
"sort": {
"description": "Sort field by number of matches of categories, defaults to best match",
"enum": [
diff --git a/pkg/github/__toolsnaps__/search_pull_requests.snap b/pkg/github/__toolsnaps__/search_pull_requests.snap
index e33304bf7..6a8d8e0e6 100644
--- a/pkg/github/__toolsnaps__/search_pull_requests.snap
+++ b/pkg/github/__toolsnaps__/search_pull_requests.snap
@@ -14,6 +14,10 @@
],
"type": "string"
},
+ "owner": {
+ "description": "Optional repository owner. If provided with repo, only notifications for this repository are listed.",
+ "type": "string"
+ },
"page": {
"description": "Page number for pagination (min 1)",
"minimum": 1,
@@ -29,6 +33,10 @@
"description": "Search query using GitHub pull request search syntax",
"type": "string"
},
+ "repo": {
+ "description": "Optional repository name. If provided with owner, only notifications for this repository are listed.",
+ "type": "string"
+ },
"sort": {
"description": "Sort field by number of matches of categories, defaults to best match",
"enum": [
diff --git a/pkg/github/issues.go b/pkg/github/issues.go
index 9a61102e4..3242c2be9 100644
--- a/pkg/github/issues.go
+++ b/pkg/github/issues.go
@@ -165,6 +165,12 @@ func SearchIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (
mcp.Required(),
mcp.Description("Search query using GitHub issues search syntax"),
),
+ mcp.WithString("owner",
+ mcp.Description("Optional repository owner. If provided with repo, only notifications for this repository are listed."),
+ ),
+ mcp.WithString("repo",
+ mcp.Description("Optional repository name. If provided with owner, only notifications for this repository are listed."),
+ ),
mcp.WithString("sort",
mcp.Description("Sort field by number of matches of categories, defaults to best match"),
mcp.Enum(
diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go
index d1e13c0aa..056fa7ed8 100644
--- a/pkg/github/issues_test.go
+++ b/pkg/github/issues_test.go
@@ -238,6 +238,8 @@ func Test_SearchIssues(t *testing.T) {
assert.Equal(t, "search_issues", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.Contains(t, tool.InputSchema.Properties, "query")
+ assert.Contains(t, tool.InputSchema.Properties, "owner")
+ assert.Contains(t, tool.InputSchema.Properties, "repo")
assert.Contains(t, tool.InputSchema.Properties, "sort")
assert.Contains(t, tool.InputSchema.Properties, "order")
assert.Contains(t, tool.InputSchema.Properties, "perPage")
@@ -311,6 +313,83 @@ func Test_SearchIssues(t *testing.T) {
expectError: false,
expectedResult: mockSearchResult,
},
+ {
+ name: "issues search with owner and repo parameters",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.GetSearchIssues,
+ expectQueryParams(
+ t,
+ map[string]string{
+ "q": "repo:test-owner/test-repo is:issue is:open",
+ "sort": "created",
+ "order": "asc",
+ "page": "1",
+ "per_page": "30",
+ },
+ ).andThen(
+ mockResponse(t, http.StatusOK, mockSearchResult),
+ ),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "query": "is:open",
+ "owner": "test-owner",
+ "repo": "test-repo",
+ "sort": "created",
+ "order": "asc",
+ },
+ expectError: false,
+ expectedResult: mockSearchResult,
+ },
+ {
+ name: "issues search with only owner parameter (should ignore it)",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.GetSearchIssues,
+ expectQueryParams(
+ t,
+ map[string]string{
+ "q": "is:issue bug",
+ "page": "1",
+ "per_page": "30",
+ },
+ ).andThen(
+ mockResponse(t, http.StatusOK, mockSearchResult),
+ ),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "query": "bug",
+ "owner": "test-owner",
+ },
+ expectError: false,
+ expectedResult: mockSearchResult,
+ },
+ {
+ name: "issues search with only repo parameter (should ignore it)",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.GetSearchIssues,
+ expectQueryParams(
+ t,
+ map[string]string{
+ "q": "is:issue feature",
+ "page": "1",
+ "per_page": "30",
+ },
+ ).andThen(
+ mockResponse(t, http.StatusOK, mockSearchResult),
+ ),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "query": "feature",
+ "repo": "test-repo",
+ },
+ expectError: false,
+ expectedResult: mockSearchResult,
+ },
{
name: "issues search with minimal parameters",
mockedClient: mock.NewMockedHTTPClient(
diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go
index fb9f720c9..bad822b13 100644
--- a/pkg/github/pullrequests.go
+++ b/pkg/github/pullrequests.go
@@ -545,6 +545,12 @@ func SearchPullRequests(getClient GetClientFn, t translations.TranslationHelperF
mcp.Required(),
mcp.Description("Search query using GitHub pull request search syntax"),
),
+ mcp.WithString("owner",
+ mcp.Description("Optional repository owner. If provided with repo, only notifications for this repository are listed."),
+ ),
+ mcp.WithString("repo",
+ mcp.Description("Optional repository name. If provided with owner, only notifications for this repository are listed."),
+ ),
mcp.WithString("sort",
mcp.Description("Sort field by number of matches of categories, defaults to best match"),
mcp.Enum(
diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go
index f1f8394cc..30341e86c 100644
--- a/pkg/github/pullrequests_test.go
+++ b/pkg/github/pullrequests_test.go
@@ -573,6 +573,8 @@ func Test_SearchPullRequests(t *testing.T) {
assert.Equal(t, "search_pull_requests", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.Contains(t, tool.InputSchema.Properties, "query")
+ assert.Contains(t, tool.InputSchema.Properties, "owner")
+ assert.Contains(t, tool.InputSchema.Properties, "repo")
assert.Contains(t, tool.InputSchema.Properties, "sort")
assert.Contains(t, tool.InputSchema.Properties, "order")
assert.Contains(t, tool.InputSchema.Properties, "perPage")
@@ -645,6 +647,83 @@ func Test_SearchPullRequests(t *testing.T) {
expectError: false,
expectedResult: mockSearchResult,
},
+ {
+ name: "pull request search with owner and repo parameters",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.GetSearchIssues,
+ expectQueryParams(
+ t,
+ map[string]string{
+ "q": "repo:test-owner/test-repo is:pr draft:false",
+ "sort": "updated",
+ "order": "asc",
+ "page": "1",
+ "per_page": "30",
+ },
+ ).andThen(
+ mockResponse(t, http.StatusOK, mockSearchResult),
+ ),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "query": "draft:false",
+ "owner": "test-owner",
+ "repo": "test-repo",
+ "sort": "updated",
+ "order": "asc",
+ },
+ expectError: false,
+ expectedResult: mockSearchResult,
+ },
+ {
+ name: "pull request search with only owner parameter (should ignore it)",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.GetSearchIssues,
+ expectQueryParams(
+ t,
+ map[string]string{
+ "q": "is:pr feature",
+ "page": "1",
+ "per_page": "30",
+ },
+ ).andThen(
+ mockResponse(t, http.StatusOK, mockSearchResult),
+ ),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "query": "feature",
+ "owner": "test-owner",
+ },
+ expectError: false,
+ expectedResult: mockSearchResult,
+ },
+ {
+ name: "pull request search with only repo parameter (should ignore it)",
+ mockedClient: mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.GetSearchIssues,
+ expectQueryParams(
+ t,
+ map[string]string{
+ "q": "is:pr review-required",
+ "page": "1",
+ "per_page": "30",
+ },
+ ).andThen(
+ mockResponse(t, http.StatusOK, mockSearchResult),
+ ),
+ ),
+ ),
+ requestArgs: map[string]interface{}{
+ "query": "review-required",
+ "repo": "test-repo",
+ },
+ expectError: false,
+ expectedResult: mockSearchResult,
+ },
{
name: "pull request search with minimal parameters",
mockedClient: mock.NewMockedHTTPClient(
diff --git a/pkg/github/search_utils.go b/pkg/github/search_utils.go
index 4d60dbb0c..6642dad8f 100644
--- a/pkg/github/search_utils.go
+++ b/pkg/github/search_utils.go
@@ -24,6 +24,20 @@ func searchHandler(
}
query = fmt.Sprintf("is:%s %s", searchType, query)
+ owner, err := OptionalParam[string](request, "owner")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ repo, err := OptionalParam[string](request, "repo")
+ if err != nil {
+ return mcp.NewToolResultError(err.Error()), nil
+ }
+
+ if owner != "" && repo != "" {
+ query = fmt.Sprintf("repo:%s/%s %s", owner, repo, query)
+ }
+
sort, err := OptionalParam[string](request, "sort")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
@@ -38,6 +52,7 @@ func searchHandler(
}
opts := &github.SearchOptions{
+ // Default to "created" if no sort is provided, as it's a common use case.
Sort: sort,
Order: order,
ListOptions: github.ListOptions{
--- a PPN by Garber Painting Akron. With Image Size Reduction included!Fetched URL: http://github.com/github/github-mcp-server/pull/583.patch
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy