Update Go module path and all import references for Gitea to Forgejo host migration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
441 lines
12 KiB
Go
441 lines
12 KiB
Go
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
|
|
}
|
|
}
|