Skip to content

Search Tool

file_search_tool finds files by name, or searches file contents for a pattern. It's backed by ripgrep (rg) if installed, falls back to grep, and falls back further to a pure-Python walk + regex scan — so it works out of the box with zero system dependencies, and gets faster automatically if a better engine is available.

from langchain_mero_tools import make_search_tool

search_tool = make_search_tool(ctx)   # ctx=None for unrestricted access

Input schema

{
    "operation": "files" | "content",
    "path": str,
    "query": str | None,        # required for "content"
    "glob": str | None,         # filename filter, e.g. "*.py"
    "ignore": list[str] | None, # regex or plain substrings to skip
    "include_hidden": bool,      # default False
    "max_results": int,          # default 100, hard cap 1000
    "case_sensitive": bool       # default True
}

Both operations require only Permission.READ — this tool never writes anything.

Operations

files — find files by name

search_tool.invoke({
    "operation": "files",
    "path": "./src",
    "glob": "*.py",
})

Returns one file path per line, or "No files matched." if nothing was found.

content — search file contents

search_tool.invoke({
    "operation": "content",
    "path": "./src",
    "query": "def handle_request",
    "glob": "*.py",
    "case_sensitive": False,
})

Returns matching lines in path:line_number:content format, or "No matches found." if nothing matched.

query is treated as a regular expression when the pure-Python fallback is used (it falls back to a literal/escaped match if the pattern doesn't compile). When ripgrep/grep are used, query is passed through to them directly, so their native regex syntax applies.

Which engine is used, and why it matters

Engine Used when Notes
ripgrep (rg) Always preferred if rg is on PATH Fastest, respects .gitignore-style ignores by nature of how it's invoked here.
grep Used for content search if rg isn't available Standard POSIX grep -rn. Not used for files search — that case falls straight to the Python walker if rg is absent.
pure Python Used if neither rg nor grep is available Path.rglob("*") plus a compiled regex scan. Always available, no system dependencies.

You never need to configure which engine is used — it's detected per call with shutil.which(...). Install ripgrep (see Installation) if you want the speed benefit; the tool's behavior is otherwise identical either way.

On max_results and external tools

This is worth understanding if you're tuning max_results for a very large codebase.

rg's own -m N flag caps matches per file searched, not globally. With many files each under that per-file cap, rg can still emit far more than max_results lines in total before this tool gets a chance to slice the list down — and it keeps scanning every file in the tree to produce that output. That's wasted work at best, and a resource-exhaustion risk on a very large tree at worst.

To fix this, files/content operations backed by rg or grep stream the subprocess's stdout line by line and stop — killing the process — the moment max_results lines have been collected. That makes max_results a real global cap, independent of how any one file's matches are distributed, and avoids letting the subprocess keep working past what's actually needed. The pure-Python fallback already counted a running global total correctly and keeps that same behavior.

There's also a hard ceiling: even if you request max_results=5000, the tool caps it at 1000 internally, since results are meant to be read by an LLM in a single response, not paginated through by a human.

There's a 30-second timeout on the underlying subprocess as a further safety net against a search that somehow still runs long (e.g. a very slow network filesystem).

Examples

Find all Python test files

search_tool.invoke({
    "operation": "files",
    "path": ".",
    "glob": "test_*.py",
})

Search for a TODO across the codebase, ignoring vendored code

search_tool.invoke({
    "operation": "content",
    "path": ".",
    "query": "TODO",
    "ignore": ["node_modules", "vendor", r"\.min\.js$"],
})

Case-insensitive search, capped to 20 results

search_tool.invoke({
    "operation": "content",
    "path": "./docs",
    "query": "getting started",
    "case_sensitive": False,
    "max_results": 20,
})

Error responses

search_tool.invoke({"operation": "content", "path": "./src"})
# "Error: 'query' is required for content search."

search_tool.invoke({"operation": "files", "path": "/etc"})
# "Denied: Path '/etc' is outside the allowed scope [...] for context '...'."

See also