toolery
toolery — a searchable catalog over any corpus of tools, skills, agents, and components.
Point toolery at a collection of heterogeneous assets — Claude skills, agent
specs, MCP tools, docs, or packages — and get one searchable catalog: ask “what do
I already have for X?” and get a ranked answer, not fifty schemas.
Everything is projected onto a uniform Card; a pluggable search_backend
answers queries (the default is a zero-dependency lexical scorer, so it works out of
the box; a semantic backend built on the ir retrieval substrate drops into the
same seam).
>>> import toolery
>>> cat = toolery.catalog([
... toolery.Card('a', 'tool', 'csvdedupe', 'remove duplicate rows from a csv'),
... toolery.Card('b', 'tool', 'pdfread', 'extract text from pdf files'),
... ])
>>> [c.name for c, score in cat.search('deduplicate csv rows')]
['csvdedupe']
- class toolery.Card(id: str, kind: str, name: str, description: str = '', tags: tuple[str, ...]=(), source_uri: str | None = None, content_ref: str | None = None, extra: Mapping = <factory>)[source]
A normalized record for one catalogued asset.
The card is the cheap unit of discovery: small, uniform, and searchable. The full asset (a file, a package, a tool schema) is referenced by
content_ref/source_uriand loaded on demand.>>> c = Card(id='x', kind='skill', name='CSV deduper', ... description='remove duplicate rows from a csv', tags=('data',)) >>> c.text 'CSV deduper\nremove duplicate rows from a csv\ndata' >>> c.kind 'skill'
- property text: str
The searchable text surface of the card (name, description, tags).
- class toolery.Catalog(cards: Iterable[Card] = (), *, search_backend: SearchBackend = <function lexical_search>)[source]
A searchable catalog of
Cards.>>> cat = Catalog([ ... Card('a', 'skill', 'CSV deduper', 'removes duplicate rows from csv'), ... Card('b', 'skill', 'PDF reader', 'extract text from pdf files'), ... ]) >>> len(cat) 2 >>> [c.name for c, _ in cat.search('deduplicate a csv')] ['CSV deduper'] >>> [c.name for c in cat.by_kind('skill')] ['CSV deduper', 'PDF reader']
- property kinds: dict[str, int]
Mapping of
kindto the number of cards of that kind.
- class toolery.IrBackend(*, name: str = 'toolery', embedder: str = 'default', mode: str = 'dense', persist: bool = False, min_score: float = 0.0)[source]
Semantic search backend delegating to the
irretrieval substrate.Satisfies the
(query, cards, *, limit) -> [(card, score), ...]search-backend contract, so it is a drop-in fortoolery.Catalog’ssearch_backend. Theircorpus is built lazily on first search and cached, and rebuilt only when the cards change. Only hits abovemin_scoreare returned (precision-favoring).- Args (keyword-only):
name: corpus name handed to
ir. embedder:"default"(MiniLM) for real semantics,"light"for ahermetic pure-numpy hashing embedder (no download/network).
mode:
irretrieval mode —"dense","lexical", or"hybrid". persist: if True, useir’s file-backed store (incremental across runs)instead of the default in-memory store.
min_score: drop hits at or below this similarity (default 0.0).
- class toolery.IrFederatedBackend(*, name: str = 'toolery', embedder: str = 'default', mode: str = 'dense', min_score=None, max_k: int | None = None, persist: bool = False)[source]
Federated semantic backend: one
ircorpus per card kind, searched together.Delegates to
ir.discover([...]), which gates each per-kind corpus on its own (raw-score) abstention floor, fuses across corpora with Reciprocal Rank Fusion, then applies distractor-robust selection — so heterogeneous kinds (skills vs. packages vs. docs), whose similarity scores live on different scales, compare fairly. This is the multi-corpus discovery the catalog is designed around.- Same drop-in contract as
IrBackend. Args (keyword-only): name: prefix for the per-kind corpus names. embedder:
"default"(MiniLM) or"light"(hermetic hashing). mode:irmode —"dense"(warning-free),"lexical", or"hybrid". min_score: federated abstention floor —None,"auto", or a{corpus_name: float|"auto"|None}mapping (a bare float is invalid here).max_k: cap on committed results (defaults to the per-call
limit). persist: if True, use ir’s file-backed store (incremental across runs via theledger) instead of the in-memory store.
- Same drop-in contract as
- class toolery.SearchBackend(*args, **kwargs)[source]
Callable protocol every search backend satisfies.
- toolery.agents(root, *, kind='agent')[source]
Harvest Claude Code subagent specs (
.claude/agents/*.md) underroot.Agent specs declare a YAML-frontmatter
name;.mdfiles without one are skipped. Pass a project root (finds**/agents/*.md) or anagentsdir directly.
- toolery.catalog(*sources, search_backend: SearchBackend = <function lexical_search>) Catalog[source]
Build a
Catalogfrom one or more sources.A source is a folder path (harvested as markdown), a harvester iterable of cards (e.g.
toolery.skills()), or bareCards.>>> cat = catalog([Card('x', 'tool', 'grep', 'search text with patterns')]) >>> cat.search('pattern search')[0][0].name 'grep'
- toolery.folder(root, *, kind: str = 'doc', pattern: str = '**/*.md') Iterator[Card][source]
Harvest markdown/text documents under
rootinto cards.The card
idis the path relative toroot;nameanddescriptionare read from frontmatter when present, else derived from the first heading and paragraph.patternis apathlib.Path.glob()pattern (recursive by default via**).
- toolery.lexical_search(query: str, cards: Iterable[Card], *, limit: int = 10) list[tuple[Card, float]][source]
Rank
cardsagainstqueryby token overlap, with name/phrase boosts.Precision-favoring: a card is only returned if it shares a query token or contains the whole query as a substring — so an empty or irrelevant query yields nothing rather than noise.
>>> cards = [ ... Card('a', 'skill', 'CSV deduper', 'removes duplicate rows from csv'), ... Card('b', 'skill', 'PDF reader', 'extract text from pdf files'), ... ] >>> [(c.name, round(s, 2)) for c, s in lexical_search('dedupe csv', cards)] [('CSV deduper', 0.75)]
- toolery.make_server(catalog: Catalog, *, name: str = 'toolery')[source]
Build (but don’t run) an MCP server exposing
catalog’s search as one tool.
- toolery.mcp(source, *, kind='mcp')[source]
Harvest configured MCP servers from an MCP config (
.mcp.jsonor similar).sourcemay be the config file itself or a directory containing a.mcp.json.
- toolery.packages(root, *, kind='package')[source]
Harvest Python packages (dirs with a
pyproject.toml) underroot.Scans
root/pyproject.tomlandroot/*/pyproject.toml(the common folder-of-packages layout); name/description come from the[project]table.