Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,7 @@ The following sets of tools are available:
| --- | ----------------------- | ------------------------------------------------------------- |
| <picture><source media="(prefers-color-scheme: dark)" srcset="pkg/octicons/icons/person-dark.png"><source media="(prefers-color-scheme: light)" srcset="pkg/octicons/icons/person-light.png"><img src="pkg/octicons/icons/person-light.png" width="20" height="20" alt="person"></picture> | `context` | **Strongly recommended**: Tools that provide context about the current user and GitHub context you are operating in |
| <picture><source media="(prefers-color-scheme: dark)" srcset="pkg/octicons/icons/workflow-dark.png"><source media="(prefers-color-scheme: light)" srcset="pkg/octicons/icons/workflow-light.png"><img src="pkg/octicons/icons/workflow-light.png" width="20" height="20" alt="workflow"></picture> | `actions` | GitHub Actions workflows and CI/CD operations |
| <picture><source media="(prefers-color-scheme: dark)" srcset="pkg/octicons/icons/code-square-dark.png"><source media="(prefers-color-scheme: light)" srcset="pkg/octicons/icons/code-square-light.png"><img src="pkg/octicons/icons/code-square-light.png" width="20" height="20" alt="code-square"></picture> | `code_quality` | GitHub Code Quality related tools |
| <picture><source media="(prefers-color-scheme: dark)" srcset="pkg/octicons/icons/codescan-dark.png"><source media="(prefers-color-scheme: light)" srcset="pkg/octicons/icons/codescan-light.png"><img src="pkg/octicons/icons/codescan-light.png" width="20" height="20" alt="codescan"></picture> | `code_security` | Code security related tools, such as GitHub Code Scanning |
| <picture><source media="(prefers-color-scheme: dark)" srcset="pkg/octicons/icons/copilot-dark.png"><source media="(prefers-color-scheme: light)" srcset="pkg/octicons/icons/copilot-light.png"><img src="pkg/octicons/icons/copilot-light.png" width="20" height="20" alt="copilot"></picture> | `copilot` | Copilot related tools |
| <picture><source media="(prefers-color-scheme: dark)" srcset="pkg/octicons/icons/dependabot-dark.png"><source media="(prefers-color-scheme: light)" srcset="pkg/octicons/icons/dependabot-light.png"><img src="pkg/octicons/icons/dependabot-light.png" width="20" height="20" alt="dependabot"></picture> | `dependabot` | Dependabot tools |
Expand Down Expand Up @@ -640,6 +641,18 @@ The following sets of tools are available:

<details>

<summary><picture><source media="(prefers-color-scheme: dark)" srcset="pkg/octicons/icons/code-square-dark.png"><source media="(prefers-color-scheme: light)" srcset="pkg/octicons/icons/code-square-light.png"><img src="pkg/octicons/icons/code-square-light.png" width="20" height="20" alt="code-square"></picture> Code Quality</summary>

- **get_code_quality_finding** - Get code quality finding
- **Required OAuth Scopes**: `repo`
- `findingNumber`: The number of the finding. (number, required)
- `owner`: The owner of the repository. (string, required)
- `repo`: The name of the repository. (string, required)

</details>

<details>

<summary><picture><source media="(prefers-color-scheme: dark)" srcset="pkg/octicons/icons/codescan-dark.png"><source media="(prefers-color-scheme: light)" srcset="pkg/octicons/icons/codescan-light.png"><img src="pkg/octicons/icons/codescan-light.png" width="20" height="20" alt="codescan"></picture> Code Security</summary>

- **get_code_scanning_alert** - Get code scanning alert
Expand Down
2 changes: 1 addition & 1 deletion docs/feature-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ runtime behavior (such as output formatting) won't appear here.
- `owner`: Repository owner (username or organization) (string, required)
- `pullNumber`: The pull request number (number, required)
- `repo`: Repository name (string, required)
- `reviewers`: GitHub usernames to request reviews from (string[], required)
- `reviewers`: GitHub usernames or ORG/team-slug team reviewers to request reviews from (string[], required)

- **resolve_review_thread** - Resolve Review Thread
- **Required OAuth Scopes**: `repo`
Expand Down
1 change: 1 addition & 0 deletions docs/remote-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to
| <picture><source media="(prefers-color-scheme: dark)" srcset="../pkg/octicons/icons/apps-dark.png"><source media="(prefers-color-scheme: light)" srcset="../pkg/octicons/icons/apps-light.png"><img src="../pkg/octicons/icons/apps-light.png" width="20" height="20" alt="apps"></picture><br>`default` | Default toolset | https://api.githubcopilot.com/mcp/ | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2F%22%7D) | [read-only](https://api.githubcopilot.com/mcp/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Freadonly%22%7D) |
| <picture><source media="(prefers-color-scheme: dark)" srcset="../pkg/octicons/icons/apps-dark.png"><source media="(prefers-color-scheme: light)" srcset="../pkg/octicons/icons/apps-light.png"><img src="../pkg/octicons/icons/apps-light.png" width="20" height="20" alt="apps"></picture><br>`all` | All available GitHub MCP tools | https://api.githubcopilot.com/mcp/x/all | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-all&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fall%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/all/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-all&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fall%2Freadonly%22%7D) |
| <picture><source media="(prefers-color-scheme: dark)" srcset="../pkg/octicons/icons/workflow-dark.png"><source media="(prefers-color-scheme: light)" srcset="../pkg/octicons/icons/workflow-light.png"><img src="../pkg/octicons/icons/workflow-light.png" width="20" height="20" alt="workflow"></picture><br>`actions` | GitHub Actions workflows and CI/CD operations | https://api.githubcopilot.com/mcp/x/actions | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-actions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Factions%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/actions/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-actions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Factions%2Freadonly%22%7D) |
| <picture><source media="(prefers-color-scheme: dark)" srcset="../pkg/octicons/icons/code-square-dark.png"><source media="(prefers-color-scheme: light)" srcset="../pkg/octicons/icons/code-square-light.png"><img src="../pkg/octicons/icons/code-square-light.png" width="20" height="20" alt="code-square"></picture><br>`code_quality` | GitHub Code Quality related tools | https://api.githubcopilot.com/mcp/x/code_quality | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-code_quality&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcode_quality%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/code_quality/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-code_quality&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcode_quality%2Freadonly%22%7D) |
| <picture><source media="(prefers-color-scheme: dark)" srcset="../pkg/octicons/icons/codescan-dark.png"><source media="(prefers-color-scheme: light)" srcset="../pkg/octicons/icons/codescan-light.png"><img src="../pkg/octicons/icons/codescan-light.png" width="20" height="20" alt="codescan"></picture><br>`code_security` | Code security related tools, such as GitHub Code Scanning | https://api.githubcopilot.com/mcp/x/code_security | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-code_security&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcode_security%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/code_security/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-code_security&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcode_security%2Freadonly%22%7D) |
| <picture><source media="(prefers-color-scheme: dark)" srcset="../pkg/octicons/icons/copilot-dark.png"><source media="(prefers-color-scheme: light)" srcset="../pkg/octicons/icons/copilot-light.png"><img src="../pkg/octicons/icons/copilot-light.png" width="20" height="20" alt="copilot"></picture><br>`copilot` | Copilot related tools | https://api.githubcopilot.com/mcp/x/copilot | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-copilot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcopilot%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/copilot/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-copilot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcopilot%2Freadonly%22%7D) |
| <picture><source media="(prefers-color-scheme: dark)" srcset="../pkg/octicons/icons/dependabot-dark.png"><source media="(prefers-color-scheme: light)" srcset="../pkg/octicons/icons/dependabot-light.png"><img src="../pkg/octicons/icons/dependabot-light.png" width="20" height="20" alt="dependabot"></picture><br>`dependabot` | Dependabot tools | https://api.githubcopilot.com/mcp/x/dependabot | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-dependabot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdependabot%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/dependabot/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-dependabot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdependabot%2Freadonly%22%7D) |
Expand Down
1 change: 1 addition & 0 deletions docs/toolsets-and-icons.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ icons := octicons.Icons("repo")
| Users | `people` |
| Organizations | `organization` |
| Actions | `workflow` |
| Code Quality | `code-square` |
| Code Security | `codescan` |
| Secret Protection | `shield-lock` |
| Dependabot | `dependabot` |
Expand Down
30 changes: 30 additions & 0 deletions pkg/github/__toolsnaps__/get_code_quality_finding.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"annotations": {
"readOnlyHint": true,
"title": "Get code quality finding"
},
"description": "Get details of a specific code quality finding in a GitHub repository.",
"inputSchema": {
"properties": {
"findingNumber": {
"description": "The number of the finding.",
"type": "number"
},
"owner": {
"description": "The owner of the repository.",
"type": "string"
},
"repo": {
"description": "The name of the repository.",
"type": "string"
}
},
"required": [
"owner",
"repo",
"findingNumber"
],
"type": "object"
},
"name": "get_code_quality_finding"
}
99 changes: 99 additions & 0 deletions pkg/github/code_quality.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package github

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"

"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"

ghErrors "github.com/github/github-mcp-server/pkg/errors"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/scopes"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
)

func GetCodeQualityFinding(t translations.TranslationHelperFunc) inventory.ServerTool {
return NewTool(
ToolsetMetadataCodeQuality,
mcp.Tool{
Name: "get_code_quality_finding",
Description: t("TOOL_GET_CODE_QUALITY_FINDING_DESCRIPTION", "Get details of a specific code quality finding in a GitHub repository."),
Annotations: &mcp.ToolAnnotations{
Title: t("TOOL_GET_CODE_QUALITY_FINDING_USER_TITLE", "Get code quality finding"),
ReadOnlyHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "The owner of the repository.",
},
"repo": {
Type: "string",
Description: "The name of the repository.",
},
"findingNumber": {
Type: "number",
Description: "The number of the finding.",
},
},
Required: []string{"owner", "repo", "findingNumber"},
},
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
owner, err := RequiredParam[string](args, "owner")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
repo, err := RequiredParam[string](args, "repo")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
findingNumber, err := RequiredInt(args, "findingNumber")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

client, err := deps.GetClient(ctx)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
}

apiURL := fmt.Sprintf("repos/%s/%s/code-quality/findings/%d", owner, repo, findingNumber)
req, err := client.NewRequest(ctx, http.MethodGet, apiURL, nil)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to create request", err), nil, nil
}

finding := make(map[string]any)

resp, err := client.Do(req, &finding)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get finding", resp, err), nil, nil
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
}
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get finding", resp, body), nil, nil
}

r, err := json.Marshal(finding)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to marshal finding", err), nil, nil
}

return utils.NewToolResultText(string(r)), nil, nil
},
)
}
155 changes: 155 additions & 0 deletions pkg/github/code_quality_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package github

import (
"context"
"encoding/json"
"net/http"
"testing"

"github.com/google/go-github/v87/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/github/github-mcp-server/internal/toolsnaps"
"github.com/github/github-mcp-server/pkg/translations"
)

func Test_GetCodeQualityFinding(t *testing.T) {
// Verify tool definition once
toolDef := GetCodeQualityFinding(translations.NullTranslationHelper)
require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool))

assert.Equal(t, "get_code_quality_finding", toolDef.Tool.Name)
assert.NotEmpty(t, toolDef.Tool.Description)

// InputSchema is of type any, need to cast to *jsonschema.Schema
schema, ok := toolDef.Tool.InputSchema.(*jsonschema.Schema)
require.True(t, ok, "InputSchema should be *jsonschema.Schema")
assert.Contains(t, schema.Properties, "owner")
assert.Contains(t, schema.Properties, "repo")
assert.Contains(t, schema.Properties, "findingNumber")
assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "findingNumber"})

type codeQualityRule struct {
ID *string `json:"id,omitempty"`
Title *string `json:"title,omitempty"`
Description *string `json:"description,omitempty"`
Help *string `json:"help,omitempty"`
Severity *string `json:"severity,omitempty"`
Category *string `json:"category,omitempty"`
}

type codeQualityLocation struct {
Path *string `json:"path,omitempty"`
StartLine *int `json:"start_line,omitempty"`
StartColumn *int `json:"start_column,omitempty"`
EndLine *int `json:"end_line,omitempty"`
EndColumn *int `json:"end_column,omitempty"`
}

type codeQualityMessage struct {
Text string `json:"text"`
Markdown string `json:"markdown"`
}

type codeQualityFinding struct {
Number *int `json:"number,omitempty"`
State *string `json:"state,omitempty"`
URL *string `json:"url,omitempty"`
Rule *codeQualityRule `json:"rule,omitempty"`
Location *codeQualityLocation `json:"location,omitempty"`
Message *codeQualityMessage `json:"message,omitempty"`
CreatedAt *github.Timestamp `json:"created_at,omitempty"`
}

// Setup mock finding for success case
mockFinding := &codeQualityFinding{
Number: github.Ptr(42),
State: github.Ptr("open"),
Rule: &codeQualityRule{
ID: github.Ptr("test-rule"),
Description: github.Ptr("Test Rule Description"),
},
}

tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedFinding *codeQualityFinding
expectedErrMsg string
}{
{
name: "successful finding fetch",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetReposCodeQualityFindingsByOwnerByRepoByFindingNumber: mockResponse(t, http.StatusOK, mockFinding),
}),
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"findingNumber": float64(42),
},
expectError: false,
expectedFinding: mockFinding,
},
{
name: "finding fetch fails",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetReposCodeQualityFindingsByOwnerByRepoByFindingNumber: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
},
}),
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"findingNumber": float64(9999),
},
expectError: true,
expectedErrMsg: "failed to get finding",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{
Client: client,
}
handler := toolDef.Handler(deps)

// Create call request
request := createMCPRequest(tc.requestArgs)

// Call handler with new signature
result, err := handler(ContextWithDeps(context.Background(), deps), &request)

// Verify results
if tc.expectError {
require.NoError(t, err)
require.True(t, result.IsError)
errorContent := getErrorResult(t, result)
assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
return
}

require.NoError(t, err)
require.False(t, result.IsError)

// Parse the result and get the text content if no error
textContent := getTextResult(t, result)

// Unmarshal and verify the result
var returnedFinding codeQualityFinding
err = json.Unmarshal([]byte(textContent.Text), &returnedFinding)
assert.NoError(t, err)
assert.Equal(t, *tc.expectedFinding.Number, *returnedFinding.Number)
assert.Equal(t, *tc.expectedFinding.State, *returnedFinding.State)
assert.Equal(t, *tc.expectedFinding.Rule.ID, *returnedFinding.Rule.ID)

})
}
}
3 changes: 3 additions & 0 deletions pkg/github/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ const (
GetReposReleasesLatestByOwnerByRepo = "GET /repos/{owner}/{repo}/releases/latest"
GetReposReleasesTagsByOwnerByRepoByTag = "GET /repos/{owner}/{repo}/releases/tags/{tag}"

// Code quality endpoints
GetReposCodeQualityFindingsByOwnerByRepoByFindingNumber = "GET /repos/{owner}/{repo}/code-quality/findings/{finding_number}"

// Code scanning endpoints
GetReposCodeScanningAlertsByOwnerByRepo = "GET /repos/{owner}/{repo}/code-scanning/alerts"
GetReposCodeScanningAlertsByOwnerByRepoByAlertNumber = "GET /repos/{owner}/{repo}/code-scanning/alerts/{alert_number}"
Expand Down
Loading
Loading