Skip to content
Open
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1090,6 +1090,12 @@ Possible options:
- `repo`: Repository name (string, required)
- `sha`: Commit SHA, branch name, or tag name (string, required)

- **get_file_blame** - Get file blame information
- `owner`: Repository owner (username or organization) (string, required)
- `path`: Path to the file in the repository (string, required)
- `ref`: Git reference (branch, tag, or commit SHA). Defaults to the repository's default branch (string, optional)
- `repo`: Repository name (string, required)

- **get_file_contents** - Get file or directory contents
- `owner`: Repository owner (username or organization) (string, required)
- `path`: Path to file/directory (directories must end with a slash '/') (string, optional)
Expand Down
2 changes: 1 addition & 1 deletion docs/remote-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to
<!-- START AUTOMATED TOOLSETS -->
| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |
|----------------|--------------------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Default | ["Default" toolset](../README.md#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/ | [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) |
| 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 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) |
| 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
34 changes: 34 additions & 0 deletions pkg/github/__toolsnaps__/get_file_blame.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"annotations": {
"readOnlyHint": true,
"title": "Get file blame information"
},
"description": "Get git blame information for a file, showing who last modified each line",
"inputSchema": {
"type": "object",
"required": [
"owner",
"repo",
"path"
],
"properties": {
"owner": {
"type": "string",
"description": "Repository owner (username or organization)"
},
"path": {
"type": "string",
"description": "Path to the file in the repository"
},
"ref": {
"type": "string",
"description": "Git reference (branch, tag, or commit SHA). Defaults to the repository's default branch"
},
"repo": {
"type": "string",
"description": "Repository name"
}
}
},
"name": "get_file_blame"
}
196 changes: 196 additions & 0 deletions pkg/github/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/google/go-github/v79/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/shurcooL/githubv4"
)

func GetCommit(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
Expand Down Expand Up @@ -2113,3 +2114,198 @@ func UnstarRepository(getClient GetClientFn, t translations.TranslationHelperFun

return tool, handler
}

func GetFileBlame(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
tool := mcp.Tool{
Name: "get_file_blame",
Description: t("TOOL_GET_FILE_BLAME_DESCRIPTION", "Get git blame information for a file, showing who last modified each line"),
Annotations: &mcp.ToolAnnotations{
Title: t("TOOL_GET_FILE_BLAME_USER_TITLE", "Get file blame information"),
ReadOnlyHint: true,
},
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner (username or organization)",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"path": {
Type: "string",
Description: "Path to the file in the repository",
},
"ref": {
Type: "string",
Description: "Git reference (branch, tag, or commit SHA). Defaults to the repository's default branch",
},
},
Required: []string{"owner", "repo", "path"},
},
}

handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *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
}
path, err := RequiredParam[string](args, "path")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
ref, err := OptionalParam[string](args, "ref")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

client, err := getGQLClient(ctx)
if err != nil {
return nil, nil, fmt.Errorf("failed to get GitHub GraphQL client: %w", err)
}

// First, get the default branch if ref is not specified
if ref == "" {
var repoQuery struct {
Repository struct {
DefaultBranchRef struct {
Name githubv4.String
}
} `graphql:"repository(owner: $owner, name: $repo)"`
}

vars := map[string]interface{}{
"owner": githubv4.String(owner),
"repo": githubv4.String(repo),
}

if err := client.Query(ctx, &repoQuery, vars); err != nil {
return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
"failed to get default branch",
err,
), nil, nil
}

// Validate that the repository has a default branch
if repoQuery.Repository.DefaultBranchRef.Name == "" {
return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
"repository has no default branch",
fmt.Errorf("repository %s/%s has no default branch or is empty", owner, repo),
), nil, nil
}

ref = string(repoQuery.Repository.DefaultBranchRef.Name)
}
// Now query the blame information
var blameQuery struct {
Repository struct {
Object struct {
Commit struct {
Blame struct {
Ranges []struct {
StartingLine githubv4.Int
EndingLine githubv4.Int
Age githubv4.Int
Commit struct {
OID githubv4.String
Message githubv4.String
CommittedDate githubv4.DateTime
Author struct {
Name githubv4.String
Email githubv4.String
User *struct {
Login githubv4.String
URL githubv4.String
}
}
}
}
} `graphql:"blame(path: $path)"`
} `graphql:"... on Commit"`
} `graphql:"object(expression: $ref)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}

vars := map[string]interface{}{
"owner": githubv4.String(owner),
"repo": githubv4.String(repo),
"ref": githubv4.String(ref),
"path": githubv4.String(path),
}

if err := client.Query(ctx, &blameQuery, vars); err != nil {
return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
fmt.Sprintf("failed to get blame for file: %s", path),
err,
), nil, nil
}

// Convert the blame ranges to a more readable format
type BlameRange struct {
StartingLine int `json:"starting_line"`
EndingLine int `json:"ending_line"`
Age int `json:"age"`
Commit struct {
SHA string `json:"sha"`
Message string `json:"message"`
CommittedDate string `json:"committed_date"`
Author struct {
Name string `json:"name"`
Email string `json:"email"`
Login *string `json:"login,omitempty"`
URL *string `json:"url,omitempty"`
} `json:"author"`
} `json:"commit"`
}

type BlameResult struct {
Repository string `json:"repository"`
Path string `json:"path"`
Ref string `json:"ref"`
Ranges []BlameRange `json:"ranges"`
}

result := BlameResult{
Repository: fmt.Sprintf("%s/%s", owner, repo),
Path: path,
Ref: ref,
Ranges: make([]BlameRange, 0, len(blameQuery.Repository.Object.Commit.Blame.Ranges)),
}

for _, r := range blameQuery.Repository.Object.Commit.Blame.Ranges {
br := BlameRange{
StartingLine: int(r.StartingLine),
EndingLine: int(r.EndingLine),
Age: int(r.Age),
}
br.Commit.SHA = string(r.Commit.OID)
br.Commit.Message = string(r.Commit.Message)
br.Commit.CommittedDate = r.Commit.CommittedDate.Format("2006-01-02T15:04:05Z")
br.Commit.Author.Name = string(r.Commit.Author.Name)
br.Commit.Author.Email = string(r.Commit.Author.Email)
if r.Commit.Author.User != nil {
login := string(r.Commit.Author.User.Login)
url := string(r.Commit.Author.User.URL)
br.Commit.Author.Login = &login
br.Commit.Author.URL = &url
}

result.Ranges = append(result.Ranges, br)
}

r, err := json.Marshal(result)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
}

return utils.NewToolResultText(string(r)), nil, nil
})

return tool, handler
}
Loading