From 275d31525a3bf410b21374f8c5ab3e172bf2d485 Mon Sep 17 00:00:00 2001 From: Arya Soni Date: Fri, 11 Apr 2025 23:02:42 +0530 Subject: [PATCH 1/2] docs: add required GitHub token permissions per action (#128) --- README.md | 79 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 00f49b64..f423c869 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,67 @@ 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. 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)). +2. [Create a GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new). + Each tool requires specific permissions to function. See the [Required Token Permissions](#required-token-permissions) section below for details. +## Required Token Permissions +Each tool requires specific GitHub Personal Access Token permissions to function. Below are the required permissions for each tool category: + +### Users +- **get_me** + - Required permissions: + - `read:user` - Read access to profile info + +### Issues +- **get_issue**, **get_issue_comments**, **list_issues** + - Required permissions: + - `repo` - Full control of private repositories (for private repos) + - `public_repo` - Access public repositories (for public repos) + +- **create_issue**, **add_issue_comment**, **update_issue** + - Required permissions: + - `repo` - Full control of private repositories (for private repos) + - `public_repo` - Access public repositories (for public repos) + - `write:discussion` - Write access to repository discussions (if using discussions) + +### Pull Requests +- **get_pull_request**, **list_pull_requests**, **get_pull_request_files**, **get_pull_request_status** + - Required permissions: + - `repo` - Full control of private repositories (for private repos) + - `public_repo` - Access public repositories (for public repos) + +- **merge_pull_request**, **update_pull_request_branch**, **create_pull_request**, **update_pull_request** + - Required permissions: + - `repo` - Full control of private repositories (for private repos) + - `public_repo` - Access public repositories (for public repos) + - `write:discussion` - Write access to repository discussions (if using discussions) + +### Repositories +- **get_file_contents**, **search_repositories**, **list_commits** + - Required permissions: + - `repo` - Full control of private repositories (for private repos) + - `public_repo` - Access public repositories (for public repos) + +- **create_or_update_file**, **push_files**, **create_repository**, **fork_repository**, **create_branch** + - Required permissions: + - `repo` - Full control of private repositories (for private repos) + - `public_repo` - Access public repositories (for public repos) + - `delete_repo` - Delete repositories (if needed) + +### Search +- **search_code**, **search_users** + - Required permissions: + - No special permissions required for public data + - `repo` - Required for searching private repositories + +### Code Scanning +- **get_code_scanning_alert**, **list_code_scanning_alerts** + - Required permissions: + - `security_events` - Read and write security events + - `repo` - Full control of private repositories (for private repos) + +Note: For organization repositories, additional organization-specific permissions may be required. ## Installation @@ -311,13 +367,6 @@ 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) @@ -361,7 +410,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** - Get a list of commits of a branch in a repository +- **list_commits** - Gets 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) @@ -369,13 +418,6 @@ 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 @@ -468,3 +510,4 @@ The exported Go API of this module should currently be considered unstable, and ## License This project is licensed under the terms of the MIT open source license. Please refer to [MIT](./LICENSE) for the full terms. + From 908e3b05fab6777cf3aae284896a61d3b727c844 Mon Sep 17 00:00:00 2001 From: Arya Soni Date: Sat, 12 Apr 2025 12:51:57 +0530 Subject: [PATCH 2/2] Issue #196: Document add_sub_issue tool functionality --- README.md | 10 +- pkg/github/issues.go | 192 ++++++++++++++++++++++++ pkg/github/issues_test.go | 306 ++++++++++++++++++++++++++++++++++++++ pkg/github/server.go | 2 + 4 files changed, 508 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f423c869..ed33dc36 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,13 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description - `issue_number`: Issue number (number, required) - `body`: Comment text (string, required) +- **add_sub_issue** - Add a sub-issue to an existing issue + + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - `parent_issue_number`: Parent issue number (number, required) + - `child_issue_number`: Child issue number to add as sub-issue (number, required) + - **list_issues** - List and filter repository issues - `owner`: Repository owner (string, required) @@ -509,5 +516,4 @@ The exported Go API of this module should currently be considered unstable, and ## License -This project is licensed under the terms of the MIT open source license. Please refer to [MIT](./LICENSE) for the full terms. - +This project is licensed under the terms of the MIT open source license. Please refer to [MIT](./LICENSE) for the full terms. \ No newline at end of file diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 16c34141..50a849e4 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -11,9 +11,15 @@ import ( "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/mock" "github.com/mark3labs/mcp-go/server" ) +const ( + PostReposSubIssuesByOwnerByRepoByParentIssueNumberByChildIssueNumber mock.EndpointPattern = "POST /repos/{owner}/{repo}/issues/{parent_issue_number}/sub-issues/{child_issue_number}" + GetReposSubIssuesByOwnerByRepoByIssueNumber mock.EndpointPattern = "GET /repos/{owner}/{repo}/issues/{issue_number}/sub-issues" +) + // GetIssue creates a tool to get details of a specific issue in a GitHub repository. func GetIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_issue", @@ -49,6 +55,8 @@ func GetIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool if err != nil { return nil, fmt.Errorf("failed to get GitHub client: %w", err) } + + // Get issue details issue, resp, err := client.Issues.Get(ctx, owner, repo, issueNumber) if err != nil { return nil, fmt.Errorf("failed to get issue: %w", err) @@ -63,6 +71,37 @@ func GetIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool return mcp.NewToolResultError(fmt.Sprintf("failed to get issue: %s", string(body))), nil } + // Get sub-issues + url := fmt.Sprintf("repos/%v/%v/issues/%d/sub-issues", owner, repo, issueNumber) + req, err := client.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + var subIssues []*github.Issue + resp, err = client.Do(ctx, req, &subIssues) + if err == nil && resp.StatusCode == http.StatusOK { + // Only include sub-issues if the request was successful + // Create a custom response struct that includes sub-issues + type IssueWithSubIssues struct { + *github.Issue + SubIssues []*github.Issue `json:"sub_issues,omitempty"` + } + + issueWithSubs := &IssueWithSubIssues{ + Issue: issue, + SubIssues: subIssues, + } + + r, err := json.Marshal(issueWithSubs) + if err != nil { + return nil, fmt.Errorf("failed to marshal issue with sub-issues: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } + + // If getting sub-issues failed, just return the main issue r, err := json.Marshal(issue) if err != nil { return nil, fmt.Errorf("failed to marshal issue: %w", err) @@ -683,6 +722,159 @@ func GetIssueComments(getClient GetClientFn, t translations.TranslationHelperFun } } +// AddSubIssue creates a tool to add a sub-issue to an existing issue. +func AddSubIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("add_sub_issue", + mcp.WithDescription(t("TOOL_ADD_SUB_ISSUE_DESCRIPTION", "Add a sub-issue to an existing issue")), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithNumber("parent_issue_number", + mcp.Required(), + mcp.Description("Parent issue number"), + ), + mcp.WithNumber("child_issue_number", + mcp.Required(), + mcp.Description("Child issue number to add as sub-issue"), + ), + ), + 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 + } + parentIssueNumber, err := RequiredInt(request, "parent_issue_number") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + childIssueNumber, err := RequiredInt(request, "child_issue_number") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + // First verify both issues exist + _, resp, err := client.Issues.Get(ctx, owner, repo, parentIssueNumber) + if err != nil { + return nil, fmt.Errorf("failed to get parent issue: %w", err) + } + if resp.StatusCode != http.StatusOK { + return mcp.NewToolResultError("parent issue not found"), nil + } + + _, resp, err = client.Issues.Get(ctx, owner, repo, childIssueNumber) + if err != nil { + return nil, fmt.Errorf("failed to get child issue: %w", err) + } + if resp.StatusCode != http.StatusOK { + return mcp.NewToolResultError("child issue not found"), nil + } + + // Add sub-issue relationship + url := fmt.Sprintf("repos/%v/%v/issues/%d/sub-issues/%d", owner, repo, parentIssueNumber, childIssueNumber) + req, err := client.NewRequest("POST", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + resp, err = client.Do(ctx, req, nil) + if err != nil { + return nil, fmt.Errorf("failed to add sub-issue: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusCreated { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return mcp.NewToolResultError(fmt.Sprintf("failed to add sub-issue: %s", string(body))), nil + } + + return mcp.NewToolResultText(fmt.Sprintf("Successfully added issue #%d as a sub-issue of #%d", childIssueNumber, parentIssueNumber)), nil + } +} + +// GetSubIssues creates a tool to get sub-issues of a specific issue. +func GetSubIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("get_sub_issues", + mcp.WithDescription(t("TOOL_GET_SUB_ISSUES_DESCRIPTION", "Get sub-issues of a specific issue")), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithNumber("issue_number", + mcp.Required(), + mcp.Description("Parent issue number"), + ), + ), + 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 + } + issueNumber, err := RequiredInt(request, "issue_number") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + // Get sub-issues + url := fmt.Sprintf("repos/%v/%v/issues/%d/sub-issues", owner, repo, issueNumber) + req, err := client.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + var subIssues []*github.Issue + resp, err := client.Do(ctx, req, &subIssues) + if err != nil { + return nil, fmt.Errorf("failed to get sub-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 get sub-issues: %s", string(body))), nil + } + + r, err := json.Marshal(subIssues) + if err != nil { + return nil, fmt.Errorf("failed to marshal sub-issues: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + // parseISOTimestamp parses an ISO 8601 timestamp string into a time.Time object. // Returns the parsed time or an error if parsing fails. // Example formats supported: "2023-01-15T14:30:00Z", "2023-01-15" diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 61ca0ae7..21307a4c 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -15,6 +15,12 @@ import ( "github.com/stretchr/testify/require" ) +// Custom endpoint patterns for sub-issues +const ( + PostReposSubIssuesByOwnerByRepoByParentIssueNumberByChildIssueNumber mock.EndpointPattern = "POST /repos/{owner}/{repo}/issues/{parent_issue_number}/sub-issues/{child_issue_number}" + GetReposSubIssuesByOwnerByRepoByIssueNumber mock.EndpointPattern = "GET /repos/{owner}/{repo}/issues/{issue_number}/sub-issues" +) + func Test_GetIssue(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) @@ -76,6 +82,33 @@ func Test_GetIssue(t *testing.T) { expectError: true, expectedErrMsg: "failed to get issue", }, + { + name: "successful issue retrieval with sub-issues", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposIssuesByOwnerByRepoByIssueNumber, + mockIssue, + ), + mock.WithRequestMatchHandler( + GetReposSubIssuesByOwnerByRepoByIssueNumber, + mockResponse(t, http.StatusOK, []*github.Issue{ + { + Number: github.Ptr(43), + Title: github.Ptr("Sub Issue"), + State: github.Ptr("open"), + HTMLURL: github.Ptr("https://github.com/owner/repo/issues/43"), + }, + }), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "issue_number": float64(42), + }, + expectError: false, + expectedIssue: mockIssue, + }, } for _, tc := range tests { @@ -1130,3 +1163,276 @@ func Test_GetIssueComments(t *testing.T) { }) } } + +func Test_AddSubIssue(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := AddSubIssue(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "add_sub_issue", 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, "parent_issue_number") + assert.Contains(t, tool.InputSchema.Properties, "child_issue_number") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "parent_issue_number", "child_issue_number"}) + + // Setup mock issues for success case + mockParentIssue := &github.Issue{ + Number: github.Ptr(42), + Title: github.Ptr("Parent Issue"), + State: github.Ptr("open"), + HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42"), + } + + mockChildIssue := &github.Issue{ + Number: github.Ptr(43), + Title: github.Ptr("Child Issue"), + State: github.Ptr("open"), + HTMLURL: github.Ptr("https://github.com/owner/repo/issues/43"), + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedErrMsg string + }{ + { + name: "successful sub-issue addition", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposIssuesByOwnerByRepoByIssueNumber, + mockParentIssue, + ), + mock.WithRequestMatch( + mock.GetReposIssuesByOwnerByRepoByIssueNumber, + mockChildIssue, + ), + mock.WithRequestMatchHandler( + PostReposSubIssuesByOwnerByRepoByParentIssueNumberByChildIssueNumber, + mockResponse(t, http.StatusCreated, nil), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "parent_issue_number": float64(42), + "child_issue_number": float64(43), + }, + expectError: false, + }, + { + name: "parent issue not found", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetReposIssuesByOwnerByRepoByIssueNumber, + mockResponse(t, http.StatusNotFound, `{"message": "Issue not found"}`), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "parent_issue_number": float64(999), + "child_issue_number": float64(43), + }, + expectError: false, + expectedErrMsg: "parent issue not found", + }, + { + name: "child issue not found", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposIssuesByOwnerByRepoByIssueNumber, + mockParentIssue, + ), + mock.WithRequestMatchHandler( + mock.GetReposIssuesByOwnerByRepoByIssueNumber, + mockResponse(t, http.StatusNotFound, `{"message": "Issue not found"}`), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "parent_issue_number": float64(42), + "child_issue_number": float64(999), + }, + expectError: false, + expectedErrMsg: "child issue not found", + }, + { + name: "sub-issue addition fails", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposIssuesByOwnerByRepoByIssueNumber, + mockParentIssue, + ), + mock.WithRequestMatch( + mock.GetReposIssuesByOwnerByRepoByIssueNumber, + mockChildIssue, + ), + mock.WithRequestMatchHandler( + PostReposSubIssuesByOwnerByRepoByParentIssueNumberByChildIssueNumber, + mockResponse(t, http.StatusUnprocessableEntity, `{"message": "Issues cannot be nested more than one level deep"}`), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "parent_issue_number": float64(42), + "child_issue_number": float64(43), + }, + expectError: false, + expectedErrMsg: "failed to add sub-issue", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := AddSubIssue(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 + } + + if tc.expectedErrMsg != "" { + require.NotNil(t, result) + textContent := getTextResult(t, result) + assert.Contains(t, textContent.Text, tc.expectedErrMsg) + return + } + + require.NoError(t, err) + require.NotNil(t, result) + textContent := getTextResult(t, result) + assert.Contains(t, textContent.Text, "Successfully added issue") + }) + } +} + +func Test_GetSubIssues(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := GetSubIssues(stubGetClientFn(mockClient), translations.NullTranslationHelper) + + assert.Equal(t, "get_sub_issues", 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, "issue_number") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "issue_number"}) + + // Setup mock issues for success case + mockSubIssues := []*github.Issue{ + { + Number: github.Ptr(43), + Title: github.Ptr("Sub Issue 1"), + State: github.Ptr("open"), + HTMLURL: github.Ptr("https://github.com/owner/repo/issues/43"), + }, + { + Number: github.Ptr(44), + Title: github.Ptr("Sub Issue 2"), + State: github.Ptr("closed"), + HTMLURL: github.Ptr("https://github.com/owner/repo/issues/44"), + }, + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedIssues []*github.Issue + expectedErrMsg string + }{ + { + name: "successful sub-issues retrieval", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + GetReposSubIssuesByOwnerByRepoByIssueNumber, + mockResponse(t, http.StatusOK, mockSubIssues), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "issue_number": float64(42), + }, + expectError: false, + expectedIssues: mockSubIssues, + }, + { + name: "parent issue not found", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + GetReposSubIssuesByOwnerByRepoByIssueNumber, + mockResponse(t, http.StatusNotFound, `{"message": "Issue not found"}`), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "issue_number": float64(999), + }, + expectError: false, + expectedErrMsg: "failed to get sub-issues", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := GetSubIssues(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 + } + + if tc.expectedErrMsg != "" { + require.NotNil(t, result) + textContent := getTextResult(t, result) + assert.Contains(t, textContent.Text, tc.expectedErrMsg) + return + } + + require.NoError(t, err) + textContent := getTextResult(t, result) + + // Unmarshal and verify the result + var returnedIssues []*github.Issue + err = json.Unmarshal([]byte(textContent.Text), &returnedIssues) + require.NoError(t, err) + + assert.Len(t, returnedIssues, len(tc.expectedIssues)) + for i, issue := range returnedIssues { + assert.Equal(t, *tc.expectedIssues[i].Number, *issue.Number) + assert.Equal(t, *tc.expectedIssues[i].Title, *issue.Title) + assert.Equal(t, *tc.expectedIssues[i].State, *issue.State) + assert.Equal(t, *tc.expectedIssues[i].HTMLURL, *issue.HTMLURL) + } + }) + } +} diff --git a/pkg/github/server.go b/pkg/github/server.go index f7eea97e..6f97a718 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -44,10 +44,12 @@ func NewServer(getClient GetClientFn, version string, readOnly bool, t translati s.AddTool(SearchIssues(getClient, t)) s.AddTool(ListIssues(getClient, t)) s.AddTool(GetIssueComments(getClient, t)) + s.AddTool(GetSubIssues(getClient, t)) if !readOnly { s.AddTool(CreateIssue(getClient, t)) s.AddTool(AddIssueComment(getClient, t)) s.AddTool(UpdateIssue(getClient, t)) + s.AddTool(AddSubIssue(getClient, t)) } // Add GitHub tools - Pull Requests