A few weeks ago I pointed an agent at a ninety-page contract and asked for the termination notice period. It came back with nothing. The file was a scan: pictures of words, no text layer. The model could have answered in a second if anyone had handed it the words, and there was nobody in the loop whose job that was except me. This happens constantly once you start noticing it. A recording nobody is going to type up. A deck whose only numbers live inside a picture. A page that returns a bot challenge instead of the article. The model is not confused and it is not forgetful, it is stalled at the edge of a file format, and no amount of prompting gets it across. What follows is the layer I built and assembled so that stops happening: how a capability actually reaches an agent, the order I try things in, the tools I run, the seven I wrote myself, and what I would set up first if I were starting today. There are four, and picking the wrong one is how people end up maintaining a server for something that should have been a shell script. The distinction that decides it is what the tool has to remember between calls. An API remembers whatever the service behind it remembers and nothing on your side, which also means it is not a tool yet. Somebody has to write the client, hold the credential, and decide what a sensible call looks like. The other three are different answers to who does that work. An MCP server holds a live session: a connection, an authenticated identity, cursors, subscriptions. It hands the model typed tool definitions, so arguments are validated rather than guessed off a help page. That is the right shape for a ticket tracker, an issue feed, a warehouse, or a browser the agent is driving. The cost is that every server a session connects to pays rent in context, because the schemas land in the window before the first word of real work whether or not anything calls them. Good harnesses have started deferring them for that reason, loading a schema only once the agent asks. A language server is the stranger case, and the one most people leave on the table. It is not a function to call, it is a standing type-checked model of a codebase that answers structural questions: where this symbol is defined, who calls it, what type this expression has, what the compiler already believes is wrong. Grep approximates that by pattern matching. The language server knows, because it parsed the project and keeps the parse warm as files change. Agents are only now being handed one, and it changes what finding every caller costs. A command on the path remembers nothing at all. Each run is a fresh process with a string going in and a string coming out, discovery is one line in a README, and it costs zero context until the moment it is called. So the rule I use is simple. Live state, an identity, or a session to hold means an MCP server. Structural questions about code mean a language server. A pure conversion, where nothing needs to persist between one call and the next, means a command, and paying for a session there is paying for nothing. Most of the reach problem is web access, and most of the money people waste on it comes from starting too high up the ladder. There is a fork before the ladder, though. When what I want is a conclusion rather than material to reason over, Perplexity does the searching and the synthesis itself and hands back a cited answer, and the ladder never gets climbed at all. Everything below is the other branch: I want the sources, and I want them cheap. Five rungs, cheapest first, and I only climb when the rung below demonstrably fails. Native search and fetch are free and handle a surprising share of one-off lookups. The next rung is obscura, a free local headless browser, and it is where most javascript-rendered pages resolve. Only after that do I spend a metered credit. When I do spend on the sources branch, the choice between the two metered services is not about quality, it is about which half of the job you want done. [[Tavily::an agent-native search API that returns ranked, scored snippets shaped for a model to ground on, rather than a human-readable answer]] is search-first: ranked sources with relevance scores, recency and domain filters, and my own model reasoning on top of them. Firecrawl is scraper-first, with the better page-to-markdown conversion of the two and the only real browser interaction, so clicks, forms, and logins go there. The top rung is a real browser, which is slow and always works. That is the one to reach for when a page uses behavioral bot protection, when a single-page app serves an empty body to anything headless, or when there is a sign-in wall. Three engines do that job and they are not interchangeable. Playwright is the scripted headless one, for standalone flows and tests. The Chrome DevTools server is the one I wire into a repository to verify frontend changes against a running dev server. Claude in Chrome drives my actual logged-in Chrome, for the tasks that genuinely need my own session. Two categories sit outside the ladder because nothing else reaches them. Social platforms need ScrapeCreators, which reaches profiles, posts, comments, and transcripts across TikTok, Instagram, YouTube, X, LinkedIn, Reddit and about twenty more. Media downloads need yt-dlp. No amount of climbing the ladder gets you a TikTok transcript. Everything below sits at the root, meaning every session gets it regardless of which repository I opened. The MCP servers I keep on there are the ones holding something live. context7 for library documentation, so the model is not coding against training data that has drifted two versions ago. Tavily and Perplexity for the web. Playwright for a browser. Then, per repository, whichever systems of record the work actually touches: the issue tracker, the error monitor, the observability stack, the design files, the warehouse. Those are project-scoped and committed next to the code, so a clone works without my machine. One decision there is worth stealing. Project configuration does inherit down a directory tree, but the behaviour is undocumented and fragile, so I duplicate a shared server into each repository that needs it rather than relying on inheritance. A teammate cloning one repository gets a working setup instead of a mystery. The other thing I do is remove servers. I dropped the GitHub MCP entirely in favour of the command-line tool, which does more, costs no context, and was already installed. That is the general shape of the trade: if a mature CLI already exists and the work is stateless, the CLI wins. Everything else is a command. Firecrawl for hard pages, ScrapeCreators for the social platforms nothing else reaches, yt-dlp for media, gh for anything GitHub, and the seven I wrote myself. Laid out by shape, my root layer fits on one page. This is what every session starts with before a repository adds anything of its own, and the two at the bottom are the ones no agent ever calls. Two of the tools I lean on hardest have never been invoked by an agent, because they are not in the toolkit. They sit in the path of it. The first is rtk, a proxy wired into the shell hook that rewrites a command before it runs. When a session asks for a git status, what executes is the proxied version, and what comes back is the same information with the noise stripped out. The model is never told. That is the point: it costs nothing in the prompt because there is nothing in the prompt, and a hook does not have to be remembered. The savings are not marginal. Across 79,522 commands it has filtered out 386 million tokens, a little under 89 percent of what those commands would otherwise have printed straight into a context window. Linting alone accounts for 239 million of that across 655 runs, because a passing lint pass has nothing worth reporting and an unfiltered one prints thousands of lines saying so. The second is git-ai, which checkpoints edits as an agent makes them and records which changes came from a model rather than from me. Same mechanism, opposite purpose. rtk exists to make the loop cheaper; git-ai exists to make it auditable, so that in six months a commit I do not remember writing still says who wrote it. Both are the same trick, and it generalises past these two. A hook fires before or after a tool call and does something nobody asked the model to do, which means it happens every time rather than most of the time. Anything you would otherwise write into an instruction and hope for becomes a guarantee the moment it moves into the path. I built these because nothing off the shelf covered them and because each one is a pure conversion with no state worth holding. They are the reason a scan, a recording, or a three-hundred-gigabyte index stops being a dead end. That table is doing more work than it looks like. An agent does not browse a folder, it reads a page and decides. So the documentation is short on purpose, one table of what exists and one block of invocations that actually run, because a page that takes two minutes to skim gets skipped and the agent goes back to guessing. Underneath the seven there is a single shape, and it is most of why they keep working. Every tool is one file. No package, no shared library, no install step. The Python ones open with a shebang that runs the file through uv and a comment block declaring their own dependencies, which uv resolves on first execution and caches after. Adding a tool means dropping a file into a folder on the path. Removing one means deleting it. Filenames are full words, never acronyms. That sounds cosmetic and is not. Two of these shipped as img and vid, and I renamed them the same day, because an agent reaching for a tool guesses the obvious name and the obvious name is the whole word. None of them implements the hard part. ffmpeg does the video work, cwebp does the compression, DuckDB runs the query, pypdf and python-docx and python-pptx and openpyxl read the office formats, Chirp 3 does the transcription. What the tool contributes is policy: which path to take, which flags are correct, what the sane default is, and when to give up. The capability belongs to the binary underneath. The judgment is the part worth writing down, and it is exactly the part an agent should not be re-deriving in a prompt on every run. Configuration comes from the environment under a prefix of its own. A variable named for a vendor is the one that vendor's SDK picks up by default, so anything on the machine can end up using it by accident. Prefixed names mean nothing is inherited silently and the same tool runs under different credentials depending only on which shell started it. The last piece is the output contract, and it reads like housekeeping. Every tool prints its result, or the path to it, on stdout. Diagnostics go to stderr. Failure is a non-zero exit. Those three rules are what let something other than me chain them: a recording goes through the audio extractor, the result goes to the transcriber, and no wrapper or parse step is needed in between, because each stage says exactly one thing on the channel the next stage reads. The piece of this I am most pleased with is the one that never calls a model at all. Most PDFs are digital. They carry a real text layer, and pulling it takes a library, no network, and no cents. So the extractor tries that first and only falls through to Gemini vision when a file turns out to be a scan or an image. The ninety-page contract from the opening was the exception, not the rule, and the tool decides which case it is rather than asking. The crawl command has the same instinct in a different dimension. The index partition for a single monthly crawl is roughly three hundred gigabytes of parquet, and a naive query reads all of it. But the rows are sorted by a reversed-domain key, so a domain filter becomes a key prefix and the query engine skips whole row groups instead of scanning them. Reads against that bucket are free open data either way, but free is not the same as fast, and the difference between the two queries is seconds against minutes. Both cases point the same way. The cheap path should be the default path, and the tool should be the one that picks it rather than the agent. An agent told to be frugal will be frugal inconsistently, because frugality competes with every other instruction in the window. A tool that simply never spends when it does not have to is frugal every single time, and nobody has to remember. The fetch command is 28 lines of bash, and this is all of them. Underneath it is obscura, a Rust headless browser that is a few months old, has one maintainer, and ships as a tarball from a releases page with no package manager behind it. It is also the best free local fetcher I have tested, which is why it holds the second rung of the ladder. Those facts sit together uncomfortably and I use it anyway. The wrapper is the insurance policy. Every skill, every note, and every pipeline upstream calls fetch, and none of them mentions obscura. If it stalls out, or the maintainer moves on, or something better lands, I rewrite 28 lines and nothing above notices. Name the capability, never the vendor, and a dependency you would not bet a project on becomes safe to lean on daily. A tool layer is only trustworthy if its boundary is written down, so here is mine. Server-rendered pages come back clean. Soft bot shields come back clean. Javascript challenge pages come back clean with the right patience flags. Behavioral bot protection does not, and I learned that the honest way: a review site I had been reading every month was working in early June and returned nothing but an interstitial two weeks later, regardless of what I threw at it. Single-page apps that hydrate client-side serve an empty body to anything headless. Sign-in walls stay shut, on purpose. Then there is the failure that is worse than a blocked page, because it looks like success. I once ran a large batch of call recordings through the transcriber and got back clean, well-formatted, confident text. Then I went looking for mentions of a specific standardized assessment and the counts came back low. The speech engine had been transcribing a four-letter clinical acronym as a totally different word, an ordinary English one, and doing it inconsistently: correct in one sentence and mangled in the next, sometimes inside the same recording. Going the other way, a naive scan for a four-letter vendor name returned 458 hits, of which 2 were the vendor and the rest were the words invoice, involve, and invoke. The tool did its job and the analysis sitting on top of it was wrong anyway, and the reason is worth more than the anecdote. Exact string matching is the wrong instrument for transcribed speech. A speech engine does not fail randomly, it fails phonetically, substituting a word that sounds like the one that was actually said. So the matching has to run on sound rather than spelling, which is exactly what the phonetic-hashing family is for. Soundex is the oldest and crudest of them: it collapses a word to a short code so that near-homophones land on the same key. Run it across the pair from that corpus and the four-letter acronym and the ordinary word it was mangled into share a code exactly, which is the only reason those mentions were recoverable at all. It is a candidate generator, not an oracle, and knowing where it stops matters more than knowing that it works. Of three mangled instrument names I checked, Soundex put two on the right code and the third on a different one, and Metaphone did no better on that case. So the recipe that actually holds is three layers: a phonetic pass to propose candidates, edit distance to catch what phonetics misses, and a small reviewed alias list for whatever survives both. Then the opposite guard for precision, because any token under about five characters needs word-boundary matching before it is counted, or the short vendor name eats every invoice in the corpus. Recall from sound, precision from boundaries, and a sample you read with your own eyes before you trust the total. Skip it and the count is wrong by more than two hundredfold on its way into a slide, looking perfectly clean the whole time. Both lessons are written down now, in the knowledge base my agents read from, next to the note recording what the fetcher can and cannot reach. An agent reads the boundary before it wastes an hour rediscovering it. If you are building this layer from nothing, the order that would have saved me the most time is roughly this. Start with documentation access, because it is the cheapest correctness win available. context7 stops the model coding against an API that changed two versions ago, and it costs one connection. Then a document extractor, before any of the web tooling. Scans, decks, and spreadsheets block more work than hard web pages do, and almost none of that needs a model: the text layer is usually right there. Wire the free parsing path first and let a vision model be the fallback. Then the bottom two rungs of the web ladder, which cost nothing, and only add a metered service once you have watched something free fail. When you do add one, add Tavily before Firecrawl unless you already know your pages are hard, because ranked sources are what an agent needs most of the time. And if most of your questions are really requests for a conclusion, a synthesis service like Perplexity earns its keep before either of them. Add a language server as soon as you work in a typed codebase of any size. Structural questions answered wrongly by pattern matching are the expensive kind of wrong. Everything after that should be a command, and the test for whether to write one is short. If the job is a conversion with no state to hold, if a mature binary already does the hard part, and if you can express the policy in under a few hundred lines, write it. Otherwise use what exists. The crawl command was written to find product pages for one catalog. It has since answered which domains publish PDFs on a given topic, how many pages a competitor has indexed, and a domain audit unrelated to any of that. The transcriber was written for meeting recordings and is now how a podcast becomes a set of notes. None of it required a port or a second implementation, because a command takes a path and some flags and knows nothing else: no notion of which project called it, no configuration pinning it to a codebase, no identity of its own. That absence is the reusability. There was never anything specific in it to generalise. Reach is the layer that decides what an agent can touch, and it is cheaper to build than it looks. The seven came out of one day in June and one in July, and the rest is other people's software, arranged in the right order.