diff --git a/README.md b/README.md
index dff62321b..aae1cd39e 100644
--- a/README.md
+++ b/README.md
@@ -555,6 +555,7 @@ The following sets of tools are available:
| --- | ----------------------- | ------------------------------------------------------------- |
|
| `context` | **Strongly recommended**: Tools that provide context about the current user and GitHub context you are operating in |
|
| `actions` | GitHub Actions workflows and CI/CD operations |
+|
| `code_quality` | GitHub Code Quality related tools |
|
| `code_security` | Code security related tools, such as GitHub Code Scanning |
|
| `copilot` | Copilot related tools |
|
| `dependabot` | Dependabot tools |
@@ -640,6 +641,18 @@ The following sets of tools are available:
+
Code Quality
+
+- **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)
+
+
+
+
+
Code Security
- **get_code_scanning_alert** - Get code scanning alert
diff --git a/docs/feature-flags.md b/docs/feature-flags.md
index 0b75a61ba..4c8654ce0 100644
--- a/docs/feature-flags.md
+++ b/docs/feature-flags.md
@@ -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`
diff --git a/docs/remote-server.md b/docs/remote-server.md
index aa083d2f2..4665ba804 100644
--- a/docs/remote-server.md
+++ b/docs/remote-server.md
@@ -22,6 +22,7 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to
| 
`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) |
| 
`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) |
| 
`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) |
+| 
`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) |
| 
`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) |
| 
`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) |
| 
`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) |
diff --git a/docs/toolsets-and-icons.md b/docs/toolsets-and-icons.md
index 9228248ec..0e54b1f16 100644
--- a/docs/toolsets-and-icons.md
+++ b/docs/toolsets-and-icons.md
@@ -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` |
diff --git a/pkg/github/__toolsnaps__/get_code_quality_finding.snap b/pkg/github/__toolsnaps__/get_code_quality_finding.snap
new file mode 100644
index 000000000..378efe835
--- /dev/null
+++ b/pkg/github/__toolsnaps__/get_code_quality_finding.snap
@@ -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"
+}
\ No newline at end of file
diff --git a/pkg/github/code_quality.go b/pkg/github/code_quality.go
new file mode 100644
index 000000000..41c791182
--- /dev/null
+++ b/pkg/github/code_quality.go
@@ -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
+ },
+ )
+}
diff --git a/pkg/github/code_quality_test.go b/pkg/github/code_quality_test.go
new file mode 100644
index 000000000..3971e5a0d
--- /dev/null
+++ b/pkg/github/code_quality_test.go
@@ -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)
+
+ })
+ }
+}
diff --git a/pkg/github/helper_test.go b/pkg/github/helper_test.go
index fdac78ce3..69921c674 100644
--- a/pkg/github/helper_test.go
+++ b/pkg/github/helper_test.go
@@ -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}"
diff --git a/pkg/github/tools.go b/pkg/github/tools.go
index d1d585b3f..7f383714c 100644
--- a/pkg/github/tools.go
+++ b/pkg/github/tools.go
@@ -5,10 +5,11 @@ import (
"slices"
"strings"
- "github.com/github/github-mcp-server/pkg/inventory"
- "github.com/github/github-mcp-server/pkg/translations"
"github.com/google/go-github/v87/github"
"github.com/shurcooL/githubv4"
+
+ "github.com/github/github-mcp-server/pkg/inventory"
+ "github.com/github/github-mcp-server/pkg/translations"
)
type GetClientFn func(context.Context) (*github.Client, error)
@@ -76,6 +77,11 @@ var (
Description: "GitHub Actions workflows and CI/CD operations",
Icon: "workflow",
}
+ ToolsetMetadataCodeQuality = inventory.ToolsetMetadata{
+ ID: "code_quality",
+ Description: "GitHub Code Quality related tools",
+ Icon: "code-square",
+ }
ToolsetMetadataCodeSecurity = inventory.ToolsetMetadata{
ID: "code_security",
Description: "Code security related tools, such as GitHub Code Scanning",
@@ -234,6 +240,9 @@ func AllTools(t translations.TranslationHelperFunc) []inventory.ServerTool {
AssignCopilotToIssue(t),
RequestCopilotReview(t),
+ // Code quality tools
+ GetCodeQualityFinding(t),
+
// Code security tools
GetCodeScanningAlert(t),
ListCodeScanningAlerts(t),
diff --git a/pkg/octicons/icons/code-square-dark.png b/pkg/octicons/icons/code-square-dark.png
new file mode 100644
index 000000000..8e2d8d0c9
Binary files /dev/null and b/pkg/octicons/icons/code-square-dark.png differ
diff --git a/pkg/octicons/icons/code-square-light.png b/pkg/octicons/icons/code-square-light.png
new file mode 100644
index 000000000..bccf0006a
Binary files /dev/null and b/pkg/octicons/icons/code-square-light.png differ
diff --git a/pkg/octicons/required_icons.txt b/pkg/octicons/required_icons.txt
index 7911b46eb..15dc44495 100644
--- a/pkg/octicons/required_icons.txt
+++ b/pkg/octicons/required_icons.txt
@@ -19,6 +19,7 @@ bell
book
check-circle
codescan
+code-square
comment-discussion
copilot
dependabot