@@ -19,6 +19,7 @@ import (
1919 "github.com/google/go-github/v82/github"
2020 "github.com/google/jsonschema-go/jsonschema"
2121 "github.com/modelcontextprotocol/go-sdk/mcp"
22+ "github.com/shurcooL/githubv4"
2223)
2324
2425func GetCommit (t translations.TranslationHelperFunc ) inventory.ServerTool {
@@ -2240,3 +2241,198 @@ func UnstarRepository(t translations.TranslationHelperFunc) inventory.ServerTool
22402241 },
22412242 )
22422243}
2244+
2245+ func GetFileBlame (getGQLClient GetGQLClientFn , t translations.TranslationHelperFunc ) (mcp.Tool , mcp.ToolHandlerFor [map [string ]any , any ]) {
2246+ tool := mcp.Tool {
2247+ Name : "get_file_blame" ,
2248+ Description : t ("TOOL_GET_FILE_BLAME_DESCRIPTION" , "Get git blame information for a file, showing who last modified each line" ),
2249+ Annotations : & mcp.ToolAnnotations {
2250+ Title : t ("TOOL_GET_FILE_BLAME_USER_TITLE" , "Get file blame information" ),
2251+ ReadOnlyHint : true ,
2252+ },
2253+ InputSchema : & jsonschema.Schema {
2254+ Type : "object" ,
2255+ Properties : map [string ]* jsonschema.Schema {
2256+ "owner" : {
2257+ Type : "string" ,
2258+ Description : "Repository owner (username or organization)" ,
2259+ },
2260+ "repo" : {
2261+ Type : "string" ,
2262+ Description : "Repository name" ,
2263+ },
2264+ "path" : {
2265+ Type : "string" ,
2266+ Description : "Path to the file in the repository" ,
2267+ },
2268+ "ref" : {
2269+ Type : "string" ,
2270+ Description : "Git reference (branch, tag, or commit SHA). Defaults to the repository's default branch" ,
2271+ },
2272+ },
2273+ Required : []string {"owner" , "repo" , "path" },
2274+ },
2275+ }
2276+
2277+ handler := mcp.ToolHandlerFor [map [string ]any , any ](func (ctx context.Context , _ * mcp.CallToolRequest , args map [string ]any ) (* mcp.CallToolResult , any , error ) {
2278+ owner , err := RequiredParam [string ](args , "owner" )
2279+ if err != nil {
2280+ return utils .NewToolResultError (err .Error ()), nil , nil
2281+ }
2282+ repo , err := RequiredParam [string ](args , "repo" )
2283+ if err != nil {
2284+ return utils .NewToolResultError (err .Error ()), nil , nil
2285+ }
2286+ path , err := RequiredParam [string ](args , "path" )
2287+ if err != nil {
2288+ return utils .NewToolResultError (err .Error ()), nil , nil
2289+ }
2290+ ref , err := OptionalParam [string ](args , "ref" )
2291+ if err != nil {
2292+ return utils .NewToolResultError (err .Error ()), nil , nil
2293+ }
2294+
2295+ client , err := getGQLClient (ctx )
2296+ if err != nil {
2297+ return nil , nil , fmt .Errorf ("failed to get GitHub GraphQL client: %w" , err )
2298+ }
2299+
2300+ // First, get the default branch if ref is not specified
2301+ if ref == "" {
2302+ var repoQuery struct {
2303+ Repository struct {
2304+ DefaultBranchRef struct {
2305+ Name githubv4.String
2306+ }
2307+ } `graphql:"repository(owner: $owner, name: $repo)"`
2308+ }
2309+
2310+ vars := map [string ]interface {}{
2311+ "owner" : githubv4 .String (owner ),
2312+ "repo" : githubv4 .String (repo ),
2313+ }
2314+
2315+ if err := client .Query (ctx , & repoQuery , vars ); err != nil {
2316+ return ghErrors .NewGitHubGraphQLErrorResponse (ctx ,
2317+ "failed to get default branch" ,
2318+ err ,
2319+ ), nil , nil
2320+ }
2321+
2322+ // Validate that the repository has a default branch
2323+ if repoQuery .Repository .DefaultBranchRef .Name == "" {
2324+ return ghErrors .NewGitHubGraphQLErrorResponse (ctx ,
2325+ "repository has no default branch" ,
2326+ fmt .Errorf ("repository %s/%s has no default branch or is empty" , owner , repo ),
2327+ ), nil , nil
2328+ }
2329+
2330+ ref = string (repoQuery .Repository .DefaultBranchRef .Name )
2331+ }
2332+ // Now query the blame information
2333+ var blameQuery struct {
2334+ Repository struct {
2335+ Object struct {
2336+ Commit struct {
2337+ Blame struct {
2338+ Ranges []struct {
2339+ StartingLine githubv4.Int
2340+ EndingLine githubv4.Int
2341+ Age githubv4.Int
2342+ Commit struct {
2343+ OID githubv4.String
2344+ Message githubv4.String
2345+ CommittedDate githubv4.DateTime
2346+ Author struct {
2347+ Name githubv4.String
2348+ Email githubv4.String
2349+ User * struct {
2350+ Login githubv4.String
2351+ URL githubv4.String
2352+ }
2353+ }
2354+ }
2355+ }
2356+ } `graphql:"blame(path: $path)"`
2357+ } `graphql:"... on Commit"`
2358+ } `graphql:"object(expression: $ref)"`
2359+ } `graphql:"repository(owner: $owner, name: $repo)"`
2360+ }
2361+
2362+ vars := map [string ]interface {}{
2363+ "owner" : githubv4 .String (owner ),
2364+ "repo" : githubv4 .String (repo ),
2365+ "ref" : githubv4 .String (ref ),
2366+ "path" : githubv4 .String (path ),
2367+ }
2368+
2369+ if err := client .Query (ctx , & blameQuery , vars ); err != nil {
2370+ return ghErrors .NewGitHubGraphQLErrorResponse (ctx ,
2371+ fmt .Sprintf ("failed to get blame for file: %s" , path ),
2372+ err ,
2373+ ), nil , nil
2374+ }
2375+
2376+ // Convert the blame ranges to a more readable format
2377+ type BlameRange struct {
2378+ StartingLine int `json:"starting_line"`
2379+ EndingLine int `json:"ending_line"`
2380+ Age int `json:"age"`
2381+ Commit struct {
2382+ SHA string `json:"sha"`
2383+ Message string `json:"message"`
2384+ CommittedDate string `json:"committed_date"`
2385+ Author struct {
2386+ Name string `json:"name"`
2387+ Email string `json:"email"`
2388+ Login * string `json:"login,omitempty"`
2389+ URL * string `json:"url,omitempty"`
2390+ } `json:"author"`
2391+ } `json:"commit"`
2392+ }
2393+
2394+ type BlameResult struct {
2395+ Repository string `json:"repository"`
2396+ Path string `json:"path"`
2397+ Ref string `json:"ref"`
2398+ Ranges []BlameRange `json:"ranges"`
2399+ }
2400+
2401+ result := BlameResult {
2402+ Repository : fmt .Sprintf ("%s/%s" , owner , repo ),
2403+ Path : path ,
2404+ Ref : ref ,
2405+ Ranges : make ([]BlameRange , 0 , len (blameQuery .Repository .Object .Commit .Blame .Ranges )),
2406+ }
2407+
2408+ for _ , r := range blameQuery .Repository .Object .Commit .Blame .Ranges {
2409+ br := BlameRange {
2410+ StartingLine : int (r .StartingLine ),
2411+ EndingLine : int (r .EndingLine ),
2412+ Age : int (r .Age ),
2413+ }
2414+ br .Commit .SHA = string (r .Commit .OID )
2415+ br .Commit .Message = string (r .Commit .Message )
2416+ br .Commit .CommittedDate = r .Commit .CommittedDate .Format ("2006-01-02T15:04:05Z" )
2417+ br .Commit .Author .Name = string (r .Commit .Author .Name )
2418+ br .Commit .Author .Email = string (r .Commit .Author .Email )
2419+ if r .Commit .Author .User != nil {
2420+ login := string (r .Commit .Author .User .Login )
2421+ url := string (r .Commit .Author .User .URL )
2422+ br .Commit .Author .Login = & login
2423+ br .Commit .Author .URL = & url
2424+ }
2425+
2426+ result .Ranges = append (result .Ranges , br )
2427+ }
2428+
2429+ r , err := json .Marshal (result )
2430+ if err != nil {
2431+ return nil , nil , fmt .Errorf ("failed to marshal response: %w" , err )
2432+ }
2433+
2434+ return utils .NewToolResultText (string (r )), nil , nil
2435+ })
2436+
2437+ return tool , handler
2438+ }
0 commit comments