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¶
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 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¶
- Directory Tool — for browsing structure rather than searching contents.
- Security & Sandboxing — path scoping applies to the search root the same as any other tool.