@@ -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
2122func 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