Engineering
How to Add Web Search to a Local LLM Agent
A local agent doesn’t need a full search product bolted on to become useful on fresh information. It needs a narrow research loop: search for candidate pages, crawl the ones worth opening, extract the relevant evidence, and hand the model a compact prompt with sources attached. Everything past that is optional.
Why “just fetch the page” doesn’t scale
The tempting first version of web search for an agent is a single tool: give the model a fetch(url) function and let it read whatever comes back. That works for a demo and breaks down fast in practice, for two separate reasons.
The model still has to find the URL, which usually means a second tool for search, and the two tools need to be composed correctly on every call: search, pick a result, then fetch it. And once it has a page, raw HTML or rendered text is mostly noise: navigation, boilerplate, ads, and unrelated sections, with the useful paragraph buried somewhere inside. A model reading the whole thing is spending most of its context budget on text that was never going to inform the answer.
The shape that holds up: search, crawl, chunk, rerank
A research loop that scales past a demo needs four steps, in order. Search turns the question into a ranked list of candidate URLs. Crawl fetches and extracts clean content from the pages worth opening, not every result, just the ones likely to matter. Chunk splits that content into pieces small enough to rank independently, since one page can contain both the exact answer and several paragraphs of nothing useful. Rerank scores those chunks against the actual query and keeps only the ones that clear a relevance bar.
What comes out the other end is a small set of passages, each tied to the URL it came from, ready to drop into a prompt. The model reads evidence, not a page, and its citations point at the text that was actually retrieved for the question.
Wiring this into an MCP agent
TinySearch packages that loop as three MCP tools, which is a reasonable shape to copy even if you’re not using it directly.
get_current_datetime() runs first for anything time-sensitive, so the agent has a concrete UTC anchor for words like “latest” or “this week” instead of guessing from training data. search(query) handles fast, top-level discovery on its own: it returns backend-ordered titles, URLs, previews, and upstream dates, without crawling anything yet, which keeps a client’s initial exploration cheap. scrape_urls(items) does the heavier work, taking one to five URLs, whether picked from a search result or already known to the agent, and running the full crawl, chunk, and rerank sequence against an optional per-item query, returning focused evidence for pages that clearly matter.
Keeping the tool surface that small matters for two reasons. A client with three narrow, predictable tools routes correctly more often than one with a single do-everything tool that has to infer intent from a vague query: deciding whether to search first or fetch a known URL is a much easier choice than deciding, inside one tool call, how much of the page to read and how to rank it. And a research layer with a small, inspectable surface is easier to debug when something in the pipeline returns a bad result, since you know which step, search or scrape, to look at first.
Getting it running
TinySearch’s fast path is a single command any stdio MCP client can launch:
uvx --from "tinysuite-search[server]" tinysearchThat starts the server with DDGS as the default search backend, no API key or account required. Crawling and extraction run on Crawl4AI underneath, and chunk ranking uses local embeddings, so the whole loop runs on infrastructure you control. Fast search calls skip Chromium and the embedding model entirely; the first scrape_urls call is what initializes both, which is worth pre-warming with tinysearch setup if you don’t want that delay landing on a user’s first real request. tinysearch doctor checks an existing installation’s config, browser, and model bundle without downloading anything, which is the first thing to run if a client can’t connect.
For a shared, network-reachable endpoint instead of a per-client stdio process, the Docker path runs the same pipeline against a bundled SearXNG instance, reachable over Streamable HTTP once the client is pointed at /mcp.
The design choice underneath all of it
The important constraint isn’t any single step; it’s restraint on what gets returned. Search is only useful to an agent when the result fits inside its context budget and still preserves enough of the source to be checked. A research tool that returns too much just moves the noise problem one layer up instead of solving it. TinySearch enforces that restraint with server-side limits rather than leaving it to the client model to guess: a default of ten results per search call, and a default token budget per scrape_urls item, both configurable but small by default. Read more on why that budget mindset applies past search too, or see the full breakdown of TinySearch’s pipeline.