Blog

jurl as an agent's web tool: fewer tokens, all from the page

jurl mcp gives Claude Code, Claude Desktop or Cursor five web tools. On 34 pages they handed back a median 27.5 times fewer tokens than the page, every line from the page, and a plain "Not found" when the page doesn't say it.

When an agent needs something from the web, it calls a tool, and whatever the tool returns goes into its context. jurl can be that tool:

claude mcp add jurl -- jurl mcp

Claude Desktop and Cursor take the same entry in their config files. The agent gets five tools, one for each thing jurl already does on the command line:

Tool Like Returns
answer --precise -q The exact answer, its paragraph, a link that highlights it
read_page jurl, -q The blocks that carry the page, or that answer question
find_links --links The links worth following
find_code --code Code blocks, as written
find_image --find, --image, --vision Image URLs

Asked how much memory a Cloudflare Worker can use, answer returns this from Cloudflare’s limits page:

128 MB

Each [isolate](https://developers.cloudflare.com/workers/reference/how-workers-works/#isolates) can consume up to 128 MB of memory, including the JavaScript heap and [WebAssembly](https://developers.cloudflare.com/workers/runtime-apis/webassembly/) allocations. This limit is per-isolate, not per-invocation. A single isolate can handle many concurrent requests.

<https://developers.cloudflare.com/workers/platform/limits/#:~:text=consume%20up%20to-,128%20MB,-of%20memory%2C%20including>

496 characters. To find them, jurl’s models read 18,680 tokens of the page. The agent reads the answer, the paragraph it’s in, and a link to the line.

34 pages, one question each

Docs, API references, pricing pages, Wikipedia, GDPR, the IRS and GOV.UK, release notes, READMEs and news, measured on 7 October with jurl 0.1.11. For each page we counted the tokens the agent reads, checked whether the answer is in them, and checked every line against the page.

Tokens the agent reads (median) Answer in it Lines not on the page Code lines not on the page
Whole page as markdown 8,178 34/34 none none
read_page 230 34/34 (102 of 102 runs) 0 of 1,632 0 of 315
answer 110 28/34 (79 of 102 runs) 0 of 373 0 of 14
Claude Code’s WebFetch 132 30/34, one wrong 198 of 324 33 of 75

The automatic line check flagged 8 of jurl’s lines; we looked each one up by hand, and all were Wikipedia text with a hidden span in the middle of it.

Against the whole page, read_page gave the agent a median 27.5 times fewer tokens, and the answer was there in every run.

Against WebFetch, jurl isn’t shorter. WebFetch has a small model read the page and write a reply, and replies are short. The difference is whose words they are: 61% of WebFetch’s lines and 44% of its code lines aren’t on the page. Most of that is harmless framing, like “According to the pricing page…”. Some isn’t. On “What’s New in Python 3.13” it gave PCbuild/build.bat --experimental-jit, the flag for the experimental JIT, as the Windows command for the free-threaded build.

When the page doesn’t say it (two of the 34 questions), jurl exited with “not found” in all 12 runs, in one line of about 20 tokens. WebFetch said the page didn’t cover it and then answered anyway, with 14 lines of aiofiles code, 11 of them not on the page.

Where jurl does worse

“Not found” is an answer

When the page doesn’t say it, the tool returns a normal result that starts with Not found:, the way the CLI exits 1. When something breaks (a 404, a bad key), it’s a tool error, the way the CLI exits 2. The agent can tell “the page doesn’t say” from “I couldn’t read the page”, and it acts on it. From a real Claude Code session:

tool_use: mcp__jurl__answer {"url": "docs.github.com/…/rate-limits-for-the-rest-api", "question": "primary rate limit for authenticated users"}
tool_result: Not found: no part of … is exactly the answer (closest: "5,000 requests per hour", p=0.36)
tool_use: mcp__jurl__answer {… "question": "rate limit authenticated users requests per hour", "follow": true}
tool_result: 5,000 requests per hour …

It reworded the question, turned on follow and got the answer, without being told to.

The server is about 250 lines of Rust with no new dependencies; the binary grew by 97 KB. The code is in src/mcp.rs and the design notes in pull request #40.