jevgrep
★ 866dzhng/jevgrep
Finds code by what it does rather than what it matches: a CLI for coding agents that asks which files and which regions are relevant to a description.
Rust CLI and MCP server for SEO and GEO work, scraping DuckDuckGo instead of paying for a search API.
View on GitHub →Hands-on review
A Rust SEO toolkit with ten commands and an MCP server. Everything we reported was fixed within a day — but not yet released.
Good for
Watch out for
Tested Sep 21, 2026 at c9e3491 · re · Rust in Docker, offline; the re-check drove the real MCP server over stdio and the CLI with no network at all
How we reviewed this: we read the source at c9e3491 and ran it in Docker offline against a local stand-in — its endpoint is a hard-coded constant with no override, so we patched that one line, then reverted and diffed. We re-ran everything at 5120aac after the fixes landed, driving the real MCP server over stdio and the CLI with --network none. We made no Jev calls.
A single Rust binary with ten subcommands covering the usual SEO surface: audit a directory of Markdown or HTML, validate Schema.org blocks, check robots.txt for AI crawler rules, inspect a sitemap, track a keyword’s DuckDuckGo rank over time in a local SQLite file, and generate a content brief. Five of them send text to Jev for judgement — keywords, query, audit, geo and brief; the rest are local parsing. It also ships an MCP server so an agent can call eight of the same tools.
For a repository that is two days old, the breadth is real, and the local half works. The SEO checks are ordinary but sound, and a single binary with no runtime is a genuinely nice way to ship this.
src/mcp.rs:53 dispatches on the method name and handles exactly two:
match req.method.as_str() {
"tools/list" => …
"tools/call" => …
_ => … Some(json!({ "code": -32601, "message": "Method not found" })),
}
initialize is not there. Every MCP client sends initialize first and waits for the result before it sends anything else, so the first thing any compliant client gets back is -32601 and the connection is over. The eight tools behind it are unreachable — not degraded, unreachable — and the README’s MCP section is the headline feature.
This was the cheapest fix in this review: one match arm returning a protocolVersion, capabilities and serverInfo.
Fixed in f8cb7c5, hours after we reported it. We drove the built binary over stdio with a compliant opening sequence, offline:
initialize -> result | protocolVersion: 2024-11-05 | serverInfo: {"name":"jev-seo","version":"0.1.0"}
tools/list -> 8 tools: seo_keywords, seo_serp_inspect, seo_audit, seo_geo,
seo_schema, seo_robots, seo_brief, seo_sitemap
notifications/initialized is accepted as well, so the whole client-side opening goes through and all eight tools are reachable.
The directory audit is careful about what it opens. src/audit.rs:702:
if file_name.starts_with('.') || file_name == "node_modules" || file_name == "target" || file_name == "dist" || file_name == "build" {
continue;
}
and then it takes only .md, .mdx, .markdown, .html and .htm.
geo runs none of that. src/main.rs:270:
let content = std::fs::read_to_string(&target).unwrap_or_else(|_| target.clone());
No extension check, no dot-file rule, no root. Whatever path you name is read whole and posted as "content". src/mcp.rs:210 is the same line again for seo_geo, which means an agent holding this MCP server can name the path.
This is the eighth tool in this directory where the exclusion list protects the directory walk and naming a file explicitly walks around it — the same shape as JevLint, perch, jegrep and jevscan-evm. It keeps recurring because the walk is where authors think about safety, and a named path never passes through it.
The unwrap_or_else had a second effect worth knowing. Give geo a URL and read_to_string fails, so the fallback sent the URL string itself as the page content. You got a score, and it was computed on about forty characters. Nothing in the output said the page was never fetched.
Also fixed in f8cb7c5, and fixed in the right place: paths::read_user_file is one guard shared by the CLI and the MCP tool, rather than a check bolted onto each caller. Same four targets through jev-seo geo and through tools/call, in a container with no network:
| target | CLI | seo_geo over MCP |
|---|---|---|
.env |
refusing dot-file |
isError: true, refusing dot-file |
secrets.json |
refusing non-content file |
isError: true, refusing non-content file |
https://example.com/pricing |
pass a local file path or an inline snippet, not a URL |
same |
page.md |
passes the guard, then asks for the key | same |
Rejecting a URL outright is better than what we suggested: scoring forty characters of URL as if it were a page is gone rather than papered over.
rank scrapes DuckDuckGo and writes the position to SQLite. src/serp.rs:48 starts with an empty vector and fills it by regex from whatever HTML came back. There is no check that the response is a results page, and no check that anything matched.
If DuckDuckGo serves its anomaly page — which returns HTTP 200, so ureq does not error — no block matches, items is empty, position is None, and track_keyword writes NULL. The output reads:
Current: Not in top 30
and the exit code is 0. “We searched and you weren’t there” and “the search never happened” were stored identically and printed identically. For a tool whose whole point is a trend line you check over weeks, that is the failure that matters: a week of rate-limiting looks like a week of falling off the results.
Fixed in the same commit. serp.rs:49 now bails when the page carries a challenge marker and :95 bails when nothing parses, and because the rank command propagates the error, track_keyword is never reached — so no NULL row is written. This is the one fix we could not drive ourselves: scrape_serp builds its own request to a hardcoded URL, so exercising the blocked branch means asking DuckDuckGo for its anomaly page on purpose, which we won’t do. We’ve offered a pull request that lifts the parsing into a pure function so the branch can be tested hermetically.
A related catch of the author’s own, in the same range: rank matching now compares hosts exactly, so a lookalike domain can no longer take credit for your ranking.
One correction to something we nearly published: the SERP scrape itself is fine by robots. It posts to html.duckduckgo.com/html/, and that host’s robots.txt says Allow: /. The autocomplete call behind keywords is the one that isn’t — it hits duckduckgo.com/ac/?q=…, and duckduckgo.com/robots.txt carries Disallow: /*? with an Allow: /?* that only covers the root. Both requests go out under a spoofed Chrome user-agent string (src/serp.rs:5).
cargo install jev-seo succeeds, and what you get is 0.1.0, published 2026-09-18 13:10 UTC. It was five commits behind when we reviewed it; it is much further behind now, and — this is the part that matters — it contains none of the fixes above. Someone installing today from the README still gets the broken handshake. We diffed the two at review time:
| crates.io 0.1.0 | GitHub c9e3491 |
|
|---|---|---|
| commands | 6 | 10 |
| MCP tools | 3 | 8 |
schema, robots, brief and sitemap do not exist in the published crate. The README documents all ten.
Three days old, MIT, 27 test functions passing, and a workflow of its own. The key handling is the best part and deserves saying: TYPESAFE_API_KEY is read from the environment at src/engine.rs:22, never written to disk and never logged. Note that it is the only route — the endpoint at src/engine.rs:28 is a constant, so there is no OpenRouter or Vercel option and no way to point it at a gateway.
We’d said to wait. Within a day of the report the author had fixed all three — the handshake, the path guard and the blocked search — and kept going through five more rounds of work on top. That is the fastest turnaround we have seen in this directory, and the review above is now a record of what was rather than what is.
What still holds: install from the repository, not from crates.io, until a release catches up; and keywords reaches an autocomplete endpoint that DuckDuckGo’s own robots.txt disallows, under a spoofed user agent, which is a decision worth making deliberately.
For an SEO-adjacent tool that is further along, see jevcal.
See how it compares with other tools in Best Jev tools, tested hands-on.
Review updated Sep 21, 2026. Numbers quoted from the project are its author's own; we don't publish our own measurements of Jev.
dzhng/jevgrep
Finds code by what it does rather than what it matches: a CLI for coding agents that asks which files and which regions are relevant to a description.
Alex314618-create/JevRev
Puts an LLM and Jev in one workflow, with sift, loop and long modes, a TUI, and a CLI that can be pointed at any /v1/systemone host. English and Chinese.
uehaj/jev-semgrep
Grep by meaning: Jev scores each line against a description in any language, with AND, OR, and NOT. A single dependency-free Node file.
New Jev releases, pricing changes, and the best new projects, once a week. No spam; unsubscribe anytime.
Powered by Buttondown. See our privacy policy.