Skip to content

Commit 1222293

Browse files
committed
feat: add get_file_blame tool for retrieving git blame information
1 parent 84ae009 commit 1222293

File tree

5 files changed

+560
-0
lines changed

5 files changed

+560
-0
lines changed

README.md

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

1028+
- **get_file_blame** - Get file blame information
1029+
- `owner`: Repository owner (username or organization) (string, required)
1030+
- `path`: Path to the file in the repository (string, required)
1031+
- `ref`: Git reference (branch, tag, or commit SHA). Defaults to the repository's default branch (string, optional)
1032+
- `repo`: Repository name (string, required)
1033+
10281034
- **get_file_contents** - Get file or directory contents
10291035
- `owner`: Repository owner (username or organization) (string, required)
10301036
- `path`: Path to file/directory (directories must end with a slash '/') (string, optional)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"annotations": {
3+
"title": "Get file blame information",
4+
"readOnlyHint": true
5+
},
6+
"description": "Get git blame information for a file, showing who last modified each line",
7+
"inputSchema": {
8+
"properties": {
9+
"owner": {
10+
"description": "Repository owner (username or organization)",
11+
"type": "string"
12+
},
13+
"path": {
14+
"description": "Path to the file in the repository",
15+
"type": "string"
16+
},
17+
"ref": {
18+
"description": "Git reference (branch, tag, or commit SHA). Defaults to the repository's default branch",
19+
"type": "string"
20+
},
21+
"repo": {
22+
"description": "Repository name",
23+
"type": "string"
24+
}
25+
},
26+
"required": [
27+
"owner",
28+
"repo",
29+
"path"
30+
],
31+
"type": "object"
32+
},
33+
"name": "get_file_blame"
34+
}

pkg/github/repositories.go

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

2122
func GetCommit(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
@@ -1920,3 +1921,187 @@ func UnstarRepository(getClient GetClientFn, t translations.TranslationHelperFun
19201921
return mcp.NewToolResultText(fmt.Sprintf("Successfully unstarred repository %s/%s", owner, repo)), nil
19211922
}
19221923
}
1924+
1925+
func GetFileBlame(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
1926+
return mcp.NewTool("get_file_blame",
1927+
mcp.WithDescription(t("TOOL_GET_FILE_BLAME_DESCRIPTION", "Get git blame information for a file, showing who last modified each line")),
1928+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
1929+
Title: t("TOOL_GET_FILE_BLAME_USER_TITLE", "Get file blame information"),
1930+
ReadOnlyHint: ToBoolPtr(true),
1931+
}),
1932+
mcp.WithString("owner",
1933+
mcp.Required(),
1934+
mcp.Description("Repository owner (username or organization)"),
1935+
),
1936+
mcp.WithString("repo",
1937+
mcp.Required(),
1938+
mcp.Description("Repository name"),
1939+
),
1940+
mcp.WithString("path",
1941+
mcp.Required(),
1942+
mcp.Description("Path to the file in the repository"),
1943+
),
1944+
mcp.WithString("ref",
1945+
mcp.Description("Git reference (branch, tag, or commit SHA). Defaults to the repository's default branch"),
1946+
),
1947+
),
1948+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
1949+
owner, err := RequiredParam[string](request, "owner")
1950+
if err != nil {
1951+
return mcp.NewToolResultError(err.Error()), nil
1952+
}
1953+
repo, err := RequiredParam[string](request, "repo")
1954+
if err != nil {
1955+
return mcp.NewToolResultError(err.Error()), nil
1956+
}
1957+
path, err := RequiredParam[string](request, "path")
1958+
if err != nil {
1959+
return mcp.NewToolResultError(err.Error()), nil
1960+
}
1961+
ref, err := OptionalParam[string](request, "ref")
1962+
if err != nil {
1963+
return mcp.NewToolResultError(err.Error()), nil
1964+
}
1965+
1966+
client, err := getGQLClient(ctx)
1967+
if err != nil {
1968+
return nil, fmt.Errorf("failed to get GitHub GraphQL client: %w", err)
1969+
}
1970+
1971+
// First, get the default branch if ref is not specified
1972+
if ref == "" {
1973+
var repoQuery struct {
1974+
Repository struct {
1975+
DefaultBranchRef struct {
1976+
Name githubv4.String
1977+
}
1978+
} `graphql:"repository(owner: $owner, name: $repo)"`
1979+
}
1980+
1981+
vars := map[string]interface{}{
1982+
"owner": githubv4.String(owner),
1983+
"repo": githubv4.String(repo),
1984+
}
1985+
1986+
if err := client.Query(ctx, &repoQuery, vars); err != nil {
1987+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
1988+
"failed to get default branch",
1989+
err,
1990+
), nil
1991+
}
1992+
1993+
// Validate that the repository has a default branch
1994+
if repoQuery.Repository.DefaultBranchRef.Name == "" {
1995+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
1996+
"repository has no default branch",
1997+
fmt.Errorf("repository %s/%s has no default branch or is empty", owner, repo),
1998+
), nil
1999+
}
2000+
2001+
ref = string(repoQuery.Repository.DefaultBranchRef.Name)
2002+
}
2003+
// Now query the blame information
2004+
var blameQuery struct {
2005+
Repository struct {
2006+
Object struct {
2007+
Commit struct {
2008+
Blame struct {
2009+
Ranges []struct {
2010+
StartingLine githubv4.Int
2011+
EndingLine githubv4.Int
2012+
Age githubv4.Int
2013+
Commit struct {
2014+
OID githubv4.String
2015+
Message githubv4.String
2016+
CommittedDate githubv4.DateTime
2017+
Author struct {
2018+
Name githubv4.String
2019+
Email githubv4.String
2020+
User *struct {
2021+
Login githubv4.String
2022+
URL githubv4.String
2023+
}
2024+
}
2025+
}
2026+
}
2027+
} `graphql:"blame(path: $path)"`
2028+
} `graphql:"... on Commit"`
2029+
} `graphql:"object(expression: $ref)"`
2030+
} `graphql:"repository(owner: $owner, name: $repo)"`
2031+
}
2032+
2033+
vars := map[string]interface{}{
2034+
"owner": githubv4.String(owner),
2035+
"repo": githubv4.String(repo),
2036+
"ref": githubv4.String(ref),
2037+
"path": githubv4.String(path),
2038+
}
2039+
2040+
if err := client.Query(ctx, &blameQuery, vars); err != nil {
2041+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx,
2042+
fmt.Sprintf("failed to get blame for file: %s", path),
2043+
err,
2044+
), nil
2045+
}
2046+
2047+
// Convert the blame ranges to a more readable format
2048+
type BlameRange struct {
2049+
StartingLine int `json:"starting_line"`
2050+
EndingLine int `json:"ending_line"`
2051+
Age int `json:"age"`
2052+
Commit struct {
2053+
SHA string `json:"sha"`
2054+
Message string `json:"message"`
2055+
CommittedDate string `json:"committed_date"`
2056+
Author struct {
2057+
Name string `json:"name"`
2058+
Email string `json:"email"`
2059+
Login *string `json:"login,omitempty"`
2060+
URL *string `json:"url,omitempty"`
2061+
} `json:"author"`
2062+
} `json:"commit"`
2063+
}
2064+
2065+
type BlameResult struct {
2066+
Repository string `json:"repository"`
2067+
Path string `json:"path"`
2068+
Ref string `json:"ref"`
2069+
Ranges []BlameRange `json:"ranges"`
2070+
}
2071+
2072+
result := BlameResult{
2073+
Repository: fmt.Sprintf("%s/%s", owner, repo),
2074+
Path: path,
2075+
Ref: ref,
2076+
Ranges: make([]BlameRange, 0, len(blameQuery.Repository.Object.Commit.Blame.Ranges)),
2077+
}
2078+
2079+
for _, r := range blameQuery.Repository.Object.Commit.Blame.Ranges {
2080+
br := BlameRange{
2081+
StartingLine: int(r.StartingLine),
2082+
EndingLine: int(r.EndingLine),
2083+
Age: int(r.Age),
2084+
}
2085+
br.Commit.SHA = string(r.Commit.OID)
2086+
br.Commit.Message = string(r.Commit.Message)
2087+
br.Commit.CommittedDate = r.Commit.CommittedDate.Format("2006-01-02T15:04:05Z")
2088+
br.Commit.Author.Name = string(r.Commit.Author.Name)
2089+
br.Commit.Author.Email = string(r.Commit.Author.Email)
2090+
if r.Commit.Author.User != nil {
2091+
login := string(r.Commit.Author.User.Login)
2092+
url := string(r.Commit.Author.User.URL)
2093+
br.Commit.Author.Login = &login
2094+
br.Commit.Author.URL = &url
2095+
}
2096+
2097+
result.Ranges = append(result.Ranges, br)
2098+
}
2099+
2100+
r, err := json.Marshal(result)
2101+
if err != nil {
2102+
return nil, fmt.Errorf("failed to marshal response: %w", err)
2103+
}
2104+
2105+
return mcp.NewToolResultText(string(r)), nil
2106+
}
2107+
}

0 commit comments

Comments
 (0)