mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-10-12 04:54:27 +00:00
fix(ast_query): skip dependency dirs (node_modules/vendor) by default
ast_query walked every language-matching file under the search root, including node_modules, vendor and bower_components — parsing third-party code and flooding results with irrelevant matches (large token cost on every call, worst for JS/TS node_modules). Skip these by name during the walk, alongside the existing hidden-dir skip. The walk root is now exempt from both skips, so targeting a dependency dir explicitly (paths:["vendor/foo"]) still works — previously naming a hidden or dependency dir as the root would have skipped everything. Documents the scope in the tool description and help resource. Adds a regression test covering default-skip and explicit-target inclusion.
This commit is contained in:
@@ -150,6 +150,69 @@ func TestASTQueryFormatLocation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestASTQuerySkipsDependencyDirs verifies ast_query excludes node_modules/vendor
|
||||||
|
// by default (they flood results with third-party code) but still searches them
|
||||||
|
// when the caller targets one explicitly via paths (the walk root is never skipped).
|
||||||
|
func TestASTQuerySkipsDependencyDirs(t *testing.T) {
|
||||||
|
srv, tmpDir := newFeaturesServer(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
deps := map[string]string{
|
||||||
|
"node_modules": "NodeModulesDep",
|
||||||
|
"vendor": "VendorDep",
|
||||||
|
}
|
||||||
|
for dir, fn := range deps {
|
||||||
|
full := filepath.Join(tmpDir, dir)
|
||||||
|
if err := os.MkdirAll(full, 0o750); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
src := "package dep\n\nfunc " + fn + "() string { return \"\" }\n"
|
||||||
|
if err := os.WriteFile(filepath.Join(full, "dep.go"), []byte(src), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: workspace-root query must skip the dependency dirs.
|
||||||
|
req := mcp.CallToolRequest{}
|
||||||
|
req.Params.Arguments = map[string]interface{}{
|
||||||
|
"pattern": "func $NAME() string",
|
||||||
|
"language": "go",
|
||||||
|
"paths": []interface{}{tmpDir},
|
||||||
|
"format": "location",
|
||||||
|
}
|
||||||
|
res, err := srv.handleASTQuery(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("default query error: %v", err)
|
||||||
|
}
|
||||||
|
// location format prints file:line, so assert on paths.
|
||||||
|
text := res.Content[0].(mcp.TextContent).Text
|
||||||
|
if !strings.Contains(text, "a.go") {
|
||||||
|
t.Errorf("default query should find top-level a.go, got:\n%s", text)
|
||||||
|
}
|
||||||
|
for dir := range deps {
|
||||||
|
if strings.Contains(text, dir+string(filepath.Separator)) {
|
||||||
|
t.Errorf("default query must skip %s, got:\n%s", dir, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit target: querying inside node_modules must still find its code.
|
||||||
|
req2 := mcp.CallToolRequest{}
|
||||||
|
req2.Params.Arguments = map[string]interface{}{
|
||||||
|
"pattern": "func $NAME() string",
|
||||||
|
"language": "go",
|
||||||
|
"paths": []interface{}{filepath.Join(tmpDir, "node_modules")},
|
||||||
|
"format": "location",
|
||||||
|
}
|
||||||
|
res2, err := srv.handleASTQuery(ctx, req2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("explicit-target query error: %v", err)
|
||||||
|
}
|
||||||
|
text2 := res2.Content[0].(mcp.TextContent).Text
|
||||||
|
if !strings.Contains(text2, "node_modules"+string(filepath.Separator)+"dep.go") {
|
||||||
|
t.Errorf("explicit node_modules target must find node_modules/dep.go, got:\n%s", text2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Feature 3 (ast_query): pagination cursor ----
|
// ---- Feature 3 (ast_query): pagination cursor ----
|
||||||
|
|
||||||
func TestASTQueryPaginationCursor(t *testing.T) {
|
func TestASTQueryPaginationCursor(t *testing.T) {
|
||||||
|
|||||||
@@ -131,7 +131,10 @@ func (s *Server) runASTQueryWalk(ctx context.Context, p *astQueryParams, exts []
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
if strings.HasPrefix(info.Name(), ".") {
|
// Skip hidden and dependency dirs (third-party code that would
|
||||||
|
// flood results), but never the walk root itself — the caller
|
||||||
|
// explicitly targeted it, e.g. paths:["vendor/foo"].
|
||||||
|
if path != searchPath && (strings.HasPrefix(info.Name(), ".") || isDependencyDir(info.Name())) {
|
||||||
return filepath.SkipDir
|
return filepath.SkipDir
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -172,6 +175,19 @@ func (s *Server) runASTQueryWalk(ctx context.Context, p *astQueryParams, exts []
|
|||||||
return allResults
|
return allResults
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isDependencyDir reports whether name is a well-known third-party dependency
|
||||||
|
// directory that ast_query skips by default. These hold vendored/installed code,
|
||||||
|
// not the caller's source, and parsing them wastes time and floods results with
|
||||||
|
// irrelevant matches. Target one explicitly via paths to query inside it.
|
||||||
|
func isDependencyDir(name string) bool {
|
||||||
|
switch name {
|
||||||
|
case "node_modules", "vendor", "bower_components":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// hasAnySuffix reports whether path ends with any of the given suffixes.
|
// hasAnySuffix reports whether path ends with any of the given suffixes.
|
||||||
func hasAnySuffix(path string, suffixes []string) bool {
|
func hasAnySuffix(path string, suffixes []string) bool {
|
||||||
for _, ext := range suffixes {
|
for _, ext := range suffixes {
|
||||||
|
|||||||
@@ -96,6 +96,11 @@ const helpASTQuery = "# ast_query — flags and examples\n\n" +
|
|||||||
"Captured `$VAR` values appear in output but do not constrain which nodes match.\n\n" +
|
"Captured `$VAR` values appear in output but do not constrain which nodes match.\n\n" +
|
||||||
"For precise selection, combine the pattern with `name_exact`, `name_matches`, and/or `kind_in` — " +
|
"For precise selection, combine the pattern with `name_exact`, `name_matches`, and/or `kind_in` — " +
|
||||||
"these ARE applied as filters.\n\n" +
|
"these ARE applied as filters.\n\n" +
|
||||||
|
"## Scope\n\n" +
|
||||||
|
"The walk skips hidden directories (dot-prefixed) and dependency directories " +
|
||||||
|
"(`node_modules`, `vendor`, `bower_components`) — these hold third-party code that would " +
|
||||||
|
"flood results. To query inside one, name it explicitly in `paths` (e.g. `\"paths\": [\"vendor/foo\"]`); " +
|
||||||
|
"the targeted root is never skipped. Note: unlike file_search, ast_query does not consult .gitignore.\n\n" +
|
||||||
"## Examples\n\n" +
|
"## Examples\n\n" +
|
||||||
"```json\n" +
|
"```json\n" +
|
||||||
"// All Go functions/methods (the 'error' return is NOT enforced — add name_/kind_in to narrow)\n" +
|
"// All Go functions/methods (the 'error' return is NOT enforced — add name_/kind_in to narrow)\n" +
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ func (s *Server) registerTools() {
|
|||||||
mcp.NewTool("ast_query",
|
mcp.NewTool("ast_query",
|
||||||
mcp.WithDescription("Search for AST patterns in code files. Use code patterns with $VAR placeholders to match and capture code structures like functions, classes, and types. "+
|
mcp.WithDescription("Search for AST patterns in code files. Use code patterns with $VAR placeholders to match and capture code structures like functions, classes, and types. "+
|
||||||
"Matching is keyword/kind-based, not fully structural: argument and return-type constraints in the pattern are not enforced — use name_exact/name_matches/kind_in to filter precisely. "+
|
"Matching is keyword/kind-based, not fully structural: argument and return-type constraints in the pattern are not enforced — use name_exact/name_matches/kind_in to filter precisely. "+
|
||||||
|
"Hidden dirs and dependency dirs (node_modules, vendor, bower_components) are skipped unless targeted explicitly via paths. "+
|
||||||
"See resource filepuff://help/ast_query for flags and examples."),
|
"See resource filepuff://help/ast_query for flags and examples."),
|
||||||
mcp.WithReadOnlyHintAnnotation(true),
|
mcp.WithReadOnlyHintAnnotation(true),
|
||||||
mcp.WithString("pattern",
|
mcp.WithString("pattern",
|
||||||
|
|||||||
Reference in New Issue
Block a user