Skip to content

Commit 8e494d9

Browse files
committed
feat: add get_file_blame tool for retrieving git blame information
1 parent f197a9f commit 8e494d9

File tree

6 files changed

+575
-1
lines changed

6 files changed

+575
-1
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,12 @@ Possible options:
10901090
- `repo`: Repository name (string, required)
10911091
- `sha`: Commit SHA, branch name, or tag name (string, required)
10921092

1093+
- **get_file_blame** - Get file blame information
1094+
- `owner`: Repository owner (username or organization) (string, required)
1095+
- `path`: Path to the file in the repository (string, required)
1096+
- `ref`: Git reference (branch, tag, or commit SHA). Defaults to the repository's default branch (string, optional)
1097+
- `repo`: Repository name (string, required)
1098+
10931099
- **get_file_contents** - Get file or directory contents
10941100
- `owner`: Repository owner (username or organization) (string, required)
10951101
- `path`: Path to file/directory (directories must end with a slash '/') (string, optional)

docs/remote-server.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to
1919
<!-- START AUTOMATED TOOLSETS -->
2020
| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |
2121
|----------------|--------------------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
22-
| 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) |
22+
| 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) |
2323
| 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) |
2424
| 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) |
2525
| 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) |
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"annotations": {
3+
"readOnlyHint": true,
4+
"title": "Get file blame information"
5+
},
6+
"description": "Get git blame information for a file, showing who last modified each line",
7+
"inputSchema": {
8+
"type": "object",
9+
"required": [
10+
"owner",
11+
"repo",
12+
"path"
13+
],
14+
"properties": {
15+
"owner": {
16+
"type": "string",
17+
"description": "Repository owner (username or organization)"
18+
},
19+
"path": {
20+
"type": "string",
21+
"description": "Path to the file in the repository"
22+
},
23+
"ref": {
24+
"type": "string",
25+
"description": "Git reference (branch, tag, or commit SHA). Defaults to the repository's default branch"
26+
},
27+
"repo": {
28+
"type": "string",
29+
"description": "Repository name"
30+
}
31+
}
32+
},
33+
"name": "get_file_blame"
34+
}

pkg/github/repositories.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/google/go-github/v79/github"
1717
"github.com/google/jsonschema-go/jsonschema"
1818
"github.com/modelcontextprotocol/go-sdk/mcp"
19+
"github.com/shurcooL/githubv4"
1920
)
2021

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

21142115
return tool, handler
21152116
}
2117+
2118+
func GetFileBlame(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) {
2119+
tool := mcp.Tool{
2120+
Name: "get_file_blame",
2121+
Description: t("TOOL_GET_FILE_BLAME_DESCRIPTION", "Get git blame information for a file, showing who last modified each line"),
2122+
Annotations: &mcp.ToolAnnotations{
2123+
Title: t("TOOL_GET_FILE_BLAME_USER_TITLE", "Get file blame information"),
2124+
ReadOnlyHint: true,
2125+
},
2126+
InputSchema: &jsonschema.Schema{
2127+
Type: "object",
2128+
Properties: map[string]*jsonschema.Schema{
2129+
"owner": {
2130+
Type: "string",
2131+
Description: "Repository owner (username or organization)",
2132+
},
2133+
"repo": {
2134+
Type: "string",
2135+
Description: "Repository name",
2136+
},
2137+
"path": {
2138+
Type: "string",
2139+
Description: "Path to the file in the repository",
2140+
},
2141+
"ref": {
2142+
Type: "string",
2143+
Description: "Git reference (branch, tag, or commit SHA). Defaults to the repository's default branch",
2144+
},
2145+
},
2146+
Required: []string{"owner", "repo", "path"},
2147+
},
2148+
}
2149+
2150+
handler := mcp.ToolHandlerFor[map[string]any, any](func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
2151+
owner, err := RequiredParam[string](args, "owner")
2152+
if err != nil {
2153+
return utils.NewToolResultError(err.Error()), nil, nil
2154+
}
2155+
repo, err := RequiredParam[string](args, "repo")
2156+
if err != nil {
2157+
return utils.NewToolResultError(err.Error()), nil, nil
2158+
}
2159+
path, err := RequiredParam[string](args, "path")
2160+
if err != nil {
2161+
return utils.NewToolResultError(err.Error()), nil, nil
2162+
}
2163+
ref, err := OptionalParam[string](args, "ref")
2164+
if err != nil {
2165+
return utils.NewToolResultError(err.Error()), nil, nil
2166+
}
2167+
2168+
client, err := getGQLClient(ctx)
2169+
if err != nil {
2170+
return nil, nil, fmt.Errorf("failed to get GitHub GraphQL client: %w", err)
2171+
}
2172+
2173+
// First, get the default branch if ref is not specified
2174+
if ref == "" {
2175+
var repoQuery struct {
2176+
Repository struct {
2177+
DefaultBranchRef struct {
2178+
Name githubv4.String
2179+
}
2180+
} `graphql:"repository(owner: $owner, name: $repo)"`
2181+
}
2182+
2183+
vars := map[string]interface{}{
2184+
"owner": githubv4.String(owner),
2185+
"repo": githubv4.String(repo),
2186+
}
2187+
2188+
if err := client.Query(ctx, &repoQuery, vars); err != nil {
2189+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
2190+
"failed to get default branch",
2191+
err,
2192+
), nil, nil
2193+
}
2194+
2195+
// Validate that the repository has a default branch
2196+
if repoQuery.Repository.DefaultBranchRef.Name == "" {
2197+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
2198+
"repository has no default branch",
2199+
fmt.Errorf("repository %s/%s has no default branch or is empty", owner, repo),
2200+
), nil, nil
2201+
}
2202+
2203+
ref = string(repoQuery.Repository.DefaultBranchRef.Name)
2204+
}
2205+
// Now query the blame information
2206+
var blameQuery struct {
2207+
Repository struct {
2208+
Object struct {
2209+
Commit struct {
2210+
Blame struct {
2211+
Ranges []struct {
2212+
StartingLine githubv4.Int
2213+
EndingLine githubv4.Int
2214+
Age githubv4.Int
2215+
Commit struct {
2216+
OID githubv4.String
2217+
Message githubv4.String
2218+
CommittedDate githubv4.DateTime
2219+
Author struct {
2220+
Name githubv4.String
2221+
Email githubv4.String
2222+
User *struct {
2223+
Login githubv4.String
2224+
URL githubv4.String
2225+
}
2226+
}
2227+
}
2228+
}
2229+
} `graphql:"blame(path: $path)"`
2230+
} `graphql:"... on Commit"`
2231+
} `graphql:"object(expression: $ref)"`
2232+
} `graphql:"repository(owner: $owner, name: $repo)"`
2233+
}
2234+
2235+
vars := map[string]interface{}{
2236+
"owner": githubv4.String(owner),
2237+
"repo": githubv4.String(repo),
2238+
"ref": githubv4.String(ref),
2239+
"path": githubv4.String(path),
2240+
}
2241+
2242+
if err := client.Query(ctx, &blameQuery, vars); err != nil {
2243+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
2244+
fmt.Sprintf("failed to get blame for file: %s", path),
2245+
err,
2246+
), nil, nil
2247+
}
2248+
2249+
// Convert the blame ranges to a more readable format
2250+
type BlameRange struct {
2251+
StartingLine int `json:"starting_line"`
2252+
EndingLine int `json:"ending_line"`
2253+
Age int `json:"age"`
2254+
Commit struct {
2255+
SHA string `json:"sha"`
2256+
Message string `json:"message"`
2257+
CommittedDate string `json:"committed_date"`
2258+
Author struct {
2259+
Name string `json:"name"`
2260+
Email string `json:"email"`
2261+
Login *string `json:"login,omitempty"`
2262+
URL *string `json:"url,omitempty"`
2263+
} `json:"author"`
2264+
} `json:"commit"`
2265+
}
2266+
2267+
type BlameResult struct {
2268+
Repository string `json:"repository"`
2269+
Path string `json:"path"`
2270+
Ref string `json:"ref"`
2271+
Ranges []BlameRange `json:"ranges"`
2272+
}
2273+
2274+
result := BlameResult{
2275+
Repository: fmt.Sprintf("%s/%s", owner, repo),
2276+
Path: path,
2277+
Ref: ref,
2278+
Ranges: make([]BlameRange, 0, len(blameQuery.Repository.Object.Commit.Blame.Ranges)),
2279+
}
2280+
2281+
for _, r := range blameQuery.Repository.Object.Commit.Blame.Ranges {
2282+
br := BlameRange{
2283+
StartingLine: int(r.StartingLine),
2284+
EndingLine: int(r.EndingLine),
2285+
Age: int(r.Age),
2286+
}
2287+
br.Commit.SHA = string(r.Commit.OID)
2288+
br.Commit.Message = string(r.Commit.Message)
2289+
br.Commit.CommittedDate = r.Commit.CommittedDate.Format("2006-01-02T15:04:05Z")
2290+
br.Commit.Author.Name = string(r.Commit.Author.Name)
2291+
br.Commit.Author.Email = string(r.Commit.Author.Email)
2292+
if r.Commit.Author.User != nil {
2293+
login := string(r.Commit.Author.User.Login)
2294+
url := string(r.Commit.Author.User.URL)
2295+
br.Commit.Author.Login = &login
2296+
br.Commit.Author.URL = &url
2297+
}
2298+
2299+
result.Ranges = append(result.Ranges, br)
2300+
}
2301+
2302+
r, err := json.Marshal(result)
2303+
if err != nil {
2304+
return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
2305+
}
2306+
2307+
return utils.NewToolResultText(string(r)), nil, nil
2308+
})
2309+
2310+
return tool, handler
2311+
}

0 commit comments

Comments
 (0)