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