-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Add Repository Tree Navigation Tool #1164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
{ | ||
"annotations": { | ||
"title": "Get repository tree", | ||
"readOnlyHint": true | ||
}, | ||
"description": "Get the tree structure (files and directories) of a GitHub repository at a specific ref or SHA", | ||
"inputSchema": { | ||
"properties": { | ||
"owner": { | ||
"description": "Repository owner (username or organization)", | ||
"type": "string" | ||
}, | ||
"path_filter": { | ||
"description": "Optional path prefix to filter the tree results (e.g., 'src/' to only show files in the src directory)", | ||
"type": "string" | ||
}, | ||
"recursive": { | ||
"default": false, | ||
"description": "Setting this parameter to true returns the objects or subtrees referenced by the tree. Default is false", | ||
"type": "boolean" | ||
}, | ||
"repo": { | ||
"description": "Repository name", | ||
"type": "string" | ||
}, | ||
"tree_sha": { | ||
"description": "The SHA1 value or ref (branch or tag) name of the tree. Defaults to the repository's default branch", | ||
"type": "string" | ||
} | ||
}, | ||
"required": [ | ||
"owner", | ||
"repo" | ||
], | ||
"type": "object" | ||
}, | ||
"name": "get_repository_tree" | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -677,6 +677,146 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t | |||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
// GetRepositoryTree creates a tool to get the tree structure of a GitHub repository. | ||||||||||||||||||||||||||||||||||||||
func GetRepositoryTree(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { | ||||||||||||||||||||||||||||||||||||||
return mcp.NewTool("get_repository_tree", | ||||||||||||||||||||||||||||||||||||||
mcp.WithDescription(t("TOOL_GET_REPOSITORY_TREE_DESCRIPTION", "Get the tree structure (files and directories) of a GitHub repository at a specific ref or SHA")), | ||||||||||||||||||||||||||||||||||||||
mcp.WithToolAnnotation(mcp.ToolAnnotation{ | ||||||||||||||||||||||||||||||||||||||
Title: t("TOOL_GET_REPOSITORY_TREE_USER_TITLE", "Get repository tree"), | ||||||||||||||||||||||||||||||||||||||
ReadOnlyHint: ToBoolPtr(true), | ||||||||||||||||||||||||||||||||||||||
}), | ||||||||||||||||||||||||||||||||||||||
mcp.WithString("owner", | ||||||||||||||||||||||||||||||||||||||
mcp.Required(), | ||||||||||||||||||||||||||||||||||||||
mcp.Description("Repository owner (username or organization)"), | ||||||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||||||
mcp.WithString("repo", | ||||||||||||||||||||||||||||||||||||||
mcp.Required(), | ||||||||||||||||||||||||||||||||||||||
mcp.Description("Repository name"), | ||||||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||||||
mcp.WithString("tree_sha", | ||||||||||||||||||||||||||||||||||||||
mcp.Description("The SHA1 value or ref (branch or tag) name of the tree. Defaults to the repository's default branch"), | ||||||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||||||
mcp.WithBoolean("recursive", | ||||||||||||||||||||||||||||||||||||||
mcp.Description("Setting this parameter to true returns the objects or subtrees referenced by the tree. Default is false"), | ||||||||||||||||||||||||||||||||||||||
mcp.DefaultBool(false), | ||||||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||||||
mcp.WithString("path_filter", | ||||||||||||||||||||||||||||||||||||||
mcp.Description("Optional path prefix to filter the tree results (e.g., 'src/' to only show files in the src directory)"), | ||||||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||||||
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||||||||||||||||||||||||||||||||||||||
owner, err := RequiredParam[string](request, "owner") | ||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||
return mcp.NewToolResultError(err.Error()), nil | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
repo, err := RequiredParam[string](request, "repo") | ||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||
return mcp.NewToolResultError(err.Error()), nil | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
treeSHA, err := OptionalParam[string](request, "tree_sha") | ||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||
return mcp.NewToolResultError(err.Error()), nil | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
recursive, err := OptionalBoolParamWithDefault(request, "recursive", false) | ||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||
return mcp.NewToolResultError(err.Error()), nil | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
pathFilter, err := OptionalParam[string](request, "path_filter") | ||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||
return mcp.NewToolResultError(err.Error()), nil | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
client, err := getClient(ctx) | ||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||
return mcp.NewToolResultError("failed to get GitHub client"), nil | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
// If no tree_sha is provided, use the repository's default branch | ||||||||||||||||||||||||||||||||||||||
if treeSHA == "" { | ||||||||||||||||||||||||||||||||||||||
repoInfo, _, err := client.Repositories.Get(ctx, owner, repo) | ||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||
return mcp.NewToolResultError(fmt.Sprintf("failed to get repository info: %s", err)), nil | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
treeSHA = *repoInfo.DefaultBranch | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
// Get the tree using the GitHub Git Tree API | ||||||||||||||||||||||||||||||||||||||
tree, resp, err := client.Git.GetTree(ctx, owner, repo, treeSHA, recursive) | ||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||
return ghErrors.NewGitHubAPIErrorResponse(ctx, | ||||||||||||||||||||||||||||||||||||||
"failed to get repository tree", | ||||||||||||||||||||||||||||||||||||||
resp, | ||||||||||||||||||||||||||||||||||||||
err, | ||||||||||||||||||||||||||||||||||||||
), nil | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
defer func() { _ = resp.Body.Close() }() | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
// Filter tree entries if path_filter is provided | ||||||||||||||||||||||||||||||||||||||
var filteredEntries []*github.TreeEntry | ||||||||||||||||||||||||||||||||||||||
if pathFilter != "" { | ||||||||||||||||||||||||||||||||||||||
for _, entry := range tree.Entries { | ||||||||||||||||||||||||||||||||||||||
if strings.HasPrefix(entry.GetPath(), pathFilter) { | ||||||||||||||||||||||||||||||||||||||
filteredEntries = append(filteredEntries, entry) | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||
filteredEntries = tree.Entries | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
type TreeEntryResponse struct { | ||||||||||||||||||||||||||||||||||||||
Path string `json:"path"` | ||||||||||||||||||||||||||||||||||||||
Type string `json:"type"` | ||||||||||||||||||||||||||||||||||||||
Size *int `json:"size,omitempty"` | ||||||||||||||||||||||||||||||||||||||
Mode string `json:"mode"` | ||||||||||||||||||||||||||||||||||||||
SHA string `json:"sha"` | ||||||||||||||||||||||||||||||||||||||
URL string `json:"url"` | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
type TreeResponse struct { | ||||||||||||||||||||||||||||||||||||||
SHA string `json:"sha"` | ||||||||||||||||||||||||||||||||||||||
Truncated bool `json:"truncated"` | ||||||||||||||||||||||||||||||||||||||
Tree []TreeEntryResponse `json:"tree"` | ||||||||||||||||||||||||||||||||||||||
TreeSHA string `json:"tree_sha"` | ||||||||||||||||||||||||||||||||||||||
Owner string `json:"owner"` | ||||||||||||||||||||||||||||||||||||||
Repo string `json:"repo"` | ||||||||||||||||||||||||||||||||||||||
Recursive bool `json:"recursive"` | ||||||||||||||||||||||||||||||||||||||
Count int `json:"count"` | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
treeEntries := make([]TreeEntryResponse, len(filteredEntries)) | ||||||||||||||||||||||||||||||||||||||
for i, entry := range filteredEntries { | ||||||||||||||||||||||||||||||||||||||
treeEntries[i] = TreeEntryResponse{ | ||||||||||||||||||||||||||||||||||||||
Path: entry.GetPath(), | ||||||||||||||||||||||||||||||||||||||
Type: entry.GetType(), | ||||||||||||||||||||||||||||||||||||||
Mode: entry.GetMode(), | ||||||||||||||||||||||||||||||||||||||
SHA: entry.GetSHA(), | ||||||||||||||||||||||||||||||||||||||
URL: entry.GetURL(), | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
if entry.Size != nil { | ||||||||||||||||||||||||||||||||||||||
treeEntries[i].Size = entry.Size | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
natagdunbar marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
response := TreeResponse{ | ||||||||||||||||||||||||||||||||||||||
SHA: *tree.SHA, | ||||||||||||||||||||||||||||||||||||||
Truncated: *tree.Truncated, | ||||||||||||||||||||||||||||||||||||||
Comment on lines
+800
to
+802
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential nil pointer dereference when dereferencing tree.SHA and tree.Truncated from the GitHub API response without null checks.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||||||||||||||||||||||||||||||
Tree: treeEntries, | ||||||||||||||||||||||||||||||||||||||
TreeSHA: treeSHA, | ||||||||||||||||||||||||||||||||||||||
Owner: owner, | ||||||||||||||||||||||||||||||||||||||
Repo: repo, | ||||||||||||||||||||||||||||||||||||||
Recursive: recursive, | ||||||||||||||||||||||||||||||||||||||
Count: len(filteredEntries), | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
r, err := json.Marshal(response) | ||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||
return nil, fmt.Errorf("failed to marshal response: %w", err) | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
return mcp.NewToolResultText(string(r)), nil | ||||||||||||||||||||||||||||||||||||||
Comment on lines
+800
to
+816
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have support for Output Schemas now, so you could move |
||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
// ForkRepository creates a tool to fork a repository. | ||||||||||||||||||||||||||||||||||||||
func ForkRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { | ||||||||||||||||||||||||||||||||||||||
return mcp.NewTool("fork_repository", | ||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the client handle
recursive: false
such that it won't return recursive entries? The docs suggest that we'll need to omit therecursive
parameter completely in order to prevent recursion:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yep, it looks like the client will only include the recursive parameter if it's
true
: https://github.com/google/go-github/blob/46f1bf23e6f9659d04f9eaebff5d25902cddfd8e/github/git_trees.go#L102-L104👍