package gitexplorer import ( "context" "fmt" "code.t-juice.club/torjus/labmcp/internal/mcp" ) // RegisterHandlers registers all git-explorer tool handlers on the MCP server. func RegisterHandlers(server *mcp.Server, client *GitClient) { server.RegisterTool(resolveRefTool(), makeResolveRefHandler(client)) server.RegisterTool(getLogTool(), makeGetLogHandler(client)) server.RegisterTool(getCommitInfoTool(), makeGetCommitInfoHandler(client)) server.RegisterTool(getDiffFilesTool(), makeGetDiffFilesHandler(client)) server.RegisterTool(getFileAtCommitTool(), makeGetFileAtCommitHandler(client)) server.RegisterTool(isAncestorTool(), makeIsAncestorHandler(client)) server.RegisterTool(commitsBetweenTool(), makeCommitsBetweenHandler(client)) server.RegisterTool(listBranchesTool(), makeListBranchesHandler(client)) server.RegisterTool(searchCommitsTool(), makeSearchCommitsHandler(client)) } // Tool definitions func resolveRefTool() mcp.Tool { return mcp.Tool{ Name: "resolve_ref", Description: "Resolve a git ref (branch, tag, or commit hash) to its full commit hash", InputSchema: mcp.InputSchema{ Type: "object", Properties: map[string]mcp.Property{ "ref": { Type: "string", Description: "Git ref to resolve (e.g., 'main', 'v1.0.0', 'HEAD', commit hash)", }, }, Required: []string{"ref"}, }, } } func getLogTool() mcp.Tool { return mcp.Tool{ Name: "get_log", Description: "Get commit log starting from a ref, with optional filters", InputSchema: mcp.InputSchema{ Type: "object", Properties: map[string]mcp.Property{ "ref": { Type: "string", Description: "Starting ref for the log (default: HEAD)", }, "limit": { Type: "integer", Description: fmt.Sprintf("Maximum number of commits to return (default: 20, max: %d)", Limits.MaxLogEntries), Default: 20, }, "author": { Type: "string", Description: "Filter by author name or email (substring match)", }, "since": { Type: "string", Description: "Stop log at this ref (exclusive)", }, "path": { Type: "string", Description: "Filter commits that affect this path", }, }, }, } } func getCommitInfoTool() mcp.Tool { return mcp.Tool{ Name: "get_commit_info", Description: "Get full details for a specific commit including message, author, and optionally file statistics", InputSchema: mcp.InputSchema{ Type: "object", Properties: map[string]mcp.Property{ "ref": { Type: "string", Description: "Commit ref (hash, branch, tag, or HEAD)", }, "include_stats": { Type: "boolean", Description: "Include file change statistics (default: true)", Default: true, }, }, Required: []string{"ref"}, }, } } func getDiffFilesTool() mcp.Tool { return mcp.Tool{ Name: "get_diff_files", Description: "Get list of files changed between two commits with change type and line counts", InputSchema: mcp.InputSchema{ Type: "object", Properties: map[string]mcp.Property{ "from_ref": { Type: "string", Description: "Starting commit ref (the older commit)", }, "to_ref": { Type: "string", Description: "Ending commit ref (the newer commit)", }, }, Required: []string{"from_ref", "to_ref"}, }, } } func getFileAtCommitTool() mcp.Tool { return mcp.Tool{ Name: "get_file_at_commit", Description: fmt.Sprintf("Get the contents of a file at a specific commit (max %dKB)", Limits.MaxFileContent/1024), InputSchema: mcp.InputSchema{ Type: "object", Properties: map[string]mcp.Property{ "ref": { Type: "string", Description: "Commit ref (hash, branch, tag, or HEAD)", }, "path": { Type: "string", Description: "Path to the file relative to repository root", }, }, Required: []string{"ref", "path"}, }, } } func isAncestorTool() mcp.Tool { return mcp.Tool{ Name: "is_ancestor", Description: "Check if one commit is an ancestor of another", InputSchema: mcp.InputSchema{ Type: "object", Properties: map[string]mcp.Property{ "ancestor": { Type: "string", Description: "Potential ancestor commit ref", }, "descendant": { Type: "string", Description: "Potential descendant commit ref", }, }, Required: []string{"ancestor", "descendant"}, }, } } func commitsBetweenTool() mcp.Tool { return mcp.Tool{ Name: "commits_between", Description: "Get all commits between two refs (from is exclusive, to is inclusive)", InputSchema: mcp.InputSchema{ Type: "object", Properties: map[string]mcp.Property{ "from_ref": { Type: "string", Description: "Starting commit ref (exclusive - commits after this)", }, "to_ref": { Type: "string", Description: "Ending commit ref (inclusive - up to and including this)", }, "limit": { Type: "integer", Description: fmt.Sprintf("Maximum number of commits (default: %d)", Limits.MaxLogEntries), Default: Limits.MaxLogEntries, }, }, Required: []string{"from_ref", "to_ref"}, }, } } func listBranchesTool() mcp.Tool { return mcp.Tool{ Name: "list_branches", Description: "List all branches in the repository", InputSchema: mcp.InputSchema{ Type: "object", Properties: map[string]mcp.Property{ "include_remote": { Type: "boolean", Description: "Include remote-tracking branches (default: false)", Default: false, }, }, }, } } func searchCommitsTool() mcp.Tool { return mcp.Tool{ Name: "search_commits", Description: "Search commit messages for a pattern (case-insensitive)", InputSchema: mcp.InputSchema{ Type: "object", Properties: map[string]mcp.Property{ "query": { Type: "string", Description: "Search pattern to match in commit messages", }, "ref": { Type: "string", Description: "Starting ref for the search (default: HEAD)", }, "limit": { Type: "integer", Description: fmt.Sprintf("Maximum number of results (default: 20, max: %d)", Limits.MaxSearchResult), Default: 20, }, }, Required: []string{"query"}, }, } } // Handler constructors func makeResolveRefHandler(client *GitClient) mcp.ToolHandler { return func(ctx context.Context, args map[string]interface{}) (mcp.CallToolResult, error) { ref, _ := args["ref"].(string) if ref == "" { return mcp.ErrorContent(fmt.Errorf("ref is required")), nil } result, err := client.ResolveRef(ref) if err != nil { return mcp.ErrorContent(err), nil } return mcp.CallToolResult{ Content: []mcp.Content{mcp.TextContent(FormatResolveResult(result))}, }, nil } } func makeGetLogHandler(client *GitClient) mcp.ToolHandler { return func(ctx context.Context, args map[string]interface{}) (mcp.CallToolResult, error) { ref := "HEAD" if r, ok := args["ref"].(string); ok && r != "" { ref = r } limit := 20 if l, ok := args["limit"].(float64); ok && l > 0 { limit = int(l) } author, _ := args["author"].(string) since, _ := args["since"].(string) path, _ := args["path"].(string) entries, err := client.GetLog(ref, limit, author, since, path) if err != nil { return mcp.ErrorContent(err), nil } return mcp.CallToolResult{ Content: []mcp.Content{mcp.TextContent(FormatLogEntries(entries))}, }, nil } } func makeGetCommitInfoHandler(client *GitClient) mcp.ToolHandler { return func(ctx context.Context, args map[string]interface{}) (mcp.CallToolResult, error) { ref, _ := args["ref"].(string) if ref == "" { return mcp.ErrorContent(fmt.Errorf("ref is required")), nil } includeStats := true if s, ok := args["include_stats"].(bool); ok { includeStats = s } info, err := client.GetCommitInfo(ref, includeStats) if err != nil { return mcp.ErrorContent(err), nil } return mcp.CallToolResult{ Content: []mcp.Content{mcp.TextContent(FormatCommitInfo(info))}, }, nil } } func makeGetDiffFilesHandler(client *GitClient) mcp.ToolHandler { return func(ctx context.Context, args map[string]interface{}) (mcp.CallToolResult, error) { fromRef, _ := args["from_ref"].(string) if fromRef == "" { return mcp.ErrorContent(fmt.Errorf("from_ref is required")), nil } toRef, _ := args["to_ref"].(string) if toRef == "" { return mcp.ErrorContent(fmt.Errorf("to_ref is required")), nil } result, err := client.GetDiffFiles(fromRef, toRef) if err != nil { return mcp.ErrorContent(err), nil } return mcp.CallToolResult{ Content: []mcp.Content{mcp.TextContent(FormatDiffResult(result))}, }, nil } } func makeGetFileAtCommitHandler(client *GitClient) mcp.ToolHandler { return func(ctx context.Context, args map[string]interface{}) (mcp.CallToolResult, error) { ref, _ := args["ref"].(string) if ref == "" { return mcp.ErrorContent(fmt.Errorf("ref is required")), nil } path, _ := args["path"].(string) if path == "" { return mcp.ErrorContent(fmt.Errorf("path is required")), nil } content, err := client.GetFileAtCommit(ref, path) if err != nil { return mcp.ErrorContent(err), nil } return mcp.CallToolResult{ Content: []mcp.Content{mcp.TextContent(FormatFileContent(content))}, }, nil } } func makeIsAncestorHandler(client *GitClient) mcp.ToolHandler { return func(ctx context.Context, args map[string]interface{}) (mcp.CallToolResult, error) { ancestor, _ := args["ancestor"].(string) if ancestor == "" { return mcp.ErrorContent(fmt.Errorf("ancestor is required")), nil } descendant, _ := args["descendant"].(string) if descendant == "" { return mcp.ErrorContent(fmt.Errorf("descendant is required")), nil } result, err := client.IsAncestor(ancestor, descendant) if err != nil { return mcp.ErrorContent(err), nil } return mcp.CallToolResult{ Content: []mcp.Content{mcp.TextContent(FormatAncestryResult(result))}, }, nil } } func makeCommitsBetweenHandler(client *GitClient) mcp.ToolHandler { return func(ctx context.Context, args map[string]interface{}) (mcp.CallToolResult, error) { fromRef, _ := args["from_ref"].(string) if fromRef == "" { return mcp.ErrorContent(fmt.Errorf("from_ref is required")), nil } toRef, _ := args["to_ref"].(string) if toRef == "" { return mcp.ErrorContent(fmt.Errorf("to_ref is required")), nil } limit := Limits.MaxLogEntries if l, ok := args["limit"].(float64); ok && l > 0 { limit = int(l) } result, err := client.CommitsBetween(fromRef, toRef, limit) if err != nil { return mcp.ErrorContent(err), nil } return mcp.CallToolResult{ Content: []mcp.Content{mcp.TextContent(FormatCommitRange(result))}, }, nil } } func makeListBranchesHandler(client *GitClient) mcp.ToolHandler { return func(ctx context.Context, args map[string]interface{}) (mcp.CallToolResult, error) { includeRemote := false if r, ok := args["include_remote"].(bool); ok { includeRemote = r } result, err := client.ListBranches(includeRemote) if err != nil { return mcp.ErrorContent(err), nil } return mcp.CallToolResult{ Content: []mcp.Content{mcp.TextContent(FormatBranchList(result))}, }, nil } } func makeSearchCommitsHandler(client *GitClient) mcp.ToolHandler { return func(ctx context.Context, args map[string]interface{}) (mcp.CallToolResult, error) { query, _ := args["query"].(string) if query == "" { return mcp.ErrorContent(fmt.Errorf("query is required")), nil } ref := "HEAD" if r, ok := args["ref"].(string); ok && r != "" { ref = r } limit := 20 if l, ok := args["limit"].(float64); ok && l > 0 { limit = int(l) } result, err := client.SearchCommits(ref, query, limit) if err != nil { return mcp.ErrorContent(err), nil } return mcp.CallToolResult{ Content: []mcp.Content{mcp.TextContent(FormatSearchResult(result))}, }, nil } }