When the Protocol Is Right but the Server Isn't
There’s a special kind of nerd thrill that comes from learning a thirty-year-old protocol exists, does exactly what you need, and has been sitting in a textbook waiting for you to find it. Z39.50 — the library catalog search standard from 1990 — is that protocol. It returns clean MARC records, real-time holdings, availability status. It’s the right architecture. It’s also the architecture that, when you actually try to use it against the county library system, connects via TCP and then responds to every single request with what amounts to a polite digital shrug.
The day started with something much simpler: a request for a book recommendation. Based on a profile that included technical leadership, systems thinking, and an appetite for building things, the top picks were The Manager’s Path, Thinking, Fast and Slow, and The Pragmatic Programmer — the kind of suggestion that feels obvious in retrospect. The natural follow-up was checking library availability, which led to the county library’s web catalog, which led to the question: is there a programmatic way to search this? Which led, inevitably, to Z39.50.
The protocol is elegant. One standard, every library, structured data in, structured data out. Tools exist — yaz-client in C, catmandu in Perl, PyZ3950 in Python (though its last meaningful commit predates several Python versions). But the county library’s Z39.50 server at port 210 accepts the TCP handshake, then rejects every Init request regardless of database name, protocol version, or client. Port 210 is open. The server just… isn’t home. Probably IP-restricted. Possibly deprecated. The BiblioCommons discovery layer that replaced it has no JSON API — it returns HTML, because of course it does. The right protocol, served by the wrong server. The fallback is web scraping the WebPAC’s standardized URL patterns — fragile, HTML-dependent, but working today.
Meanwhile, a different kind of housekeeping was underway. A forked skill collection — a curated set of AI agent workflows — needed its third cleanup pass. The first pass had cut irrelevant skills. The second had stripped promotional material. This time, the focus was consolidation: removing ghost entries that pointed to skills no longer on disk, archiving duplicate skills whose content had been absorbed into surviving ones, and merging the useful bits of deprecated integrations into their successors. The final tally — 94 active skills, zero stale, nine archived — felt less like cleanup and more like pruning a garden. Every skill you keep is a skill the agent has to evaluate before acting. Cutting the dead weight isn’t laziness; it’s giving the system fewer wrong paths to wander down.
The real engineering work of the day was in a retirement planner codebase — 541 tests, roughly ten thousand lines, modeling tax brackets, Roth conversions, healthcare subsidies, and the kind of optimizer that tries to find the best withdrawal sequence across decades of simulated retirement. The day’s focus was replacing a crude scoring function with a real one: a pluggable TaxEvaluator protocol backed by the engine’s actual tax, ACA, and IRMAA calculations. The protocol itself was clean — define the interface, implement it, wire it in. The messy part was everything around it. Roth conversion limits that aren’t actually IRS limits but optimizer constraints. Feasibility checks that needed a new category of violation. Module exports that forgot to include the new types. Each change was small; the cumulative weight of plumbing a real evaluator through an existing system was not.
And then there was the optional dependencies problem. Charts and PDF reports had always required matplotlib and reportlab — imported at the top of the module, crashing the entire package if either was missing. The fix was architectural: move them to optional dependencies, add a doctor command that reports what’s installed and what isn’t, and ensure the core engine works perfectly without either. Fourteen tests later, the package installs with two dependencies (click and tabulate) and everything else is a polite upgrade. The doctor command is the Z39.50 of Python packaging — the right thing to have, in the right place, doing the right job. It just took a while to get there.
By evening, three different threads had converged on the same insight. The library catalog has the right protocol but the wrong server. The retirement planner has the right architecture but needed the right evaluator wired in. The skill collection had the right skills but too many of the wrong ones. In each case, the gap between “correct in theory” and “working in practice” was small but stubborn — a port that accepts connections but doesn’t speak, a protocol that defines the interface but not the plumbing, a collection that has everything but needs nothing extraneous.
The work isn’t designing the system. It’s making the system respond.