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_uri and 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).

to_dict() dict[source]

A plain-dict view of the card (JSON-friendly, extra flattened out).

class toolery.Catalog(cards: Iterable[Card] = (), *, search_backend: SearchBackend = <function lexical_search>)[source]

A searchable catalog of Card s.

>>> 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']
by_kind(kind: str) list[Card][source]

All cards of a given kind.

property cards: list[Card]

All cards in the catalog.

property kinds: dict[str, int]

Mapping of kind to the number of cards of that kind.

search(query: str, *, limit: int = 10) list[tuple[Card, float]][source]

Ranked (card, score) results for query via the search backend.

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 ir retrieval substrate.

Satisfies the (query, cards, *, limit) -> [(card, score), ...] search-backend contract, so it is a drop-in for toolery.Catalog’s search_backend. The ir corpus is built lazily on first search and cached, and rebuilt only when the cards change. Only hits above min_score are returned (precision-favoring).

Args (keyword-only):

name: corpus name handed to ir. embedder: "default" (MiniLM) for real semantics, "light" for a

hermetic pure-numpy hashing embedder (no download/network).

mode: ir retrieval mode — "dense", "lexical", or "hybrid". persist: if True, use ir’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 ir corpus 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: ir mode — "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 the

ledger) instead of the in-memory store.

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) under root.

Agent specs declare a YAML-frontmatter name; .md files without one are skipped. Pass a project root (finds **/agents/*.md) or an agents dir directly.

toolery.catalog(*sources, search_backend: SearchBackend = <function lexical_search>) Catalog[source]

Build a Catalog from one or more sources.

A source is a folder path (harvested as markdown), a harvester iterable of cards (e.g. toolery.skills()), or bare Card s.

>>> 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 root into cards.

The card id is the path relative to root; name and description are read from frontmatter when present, else derived from the first heading and paragraph. pattern is a pathlib.Path.glob() pattern (recursive by default via **).

Rank cards against query by 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.json or similar).

source may 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) under root.

Scans root/pyproject.toml and root/*/pyproject.toml (the common folder-of-packages layout); name/description come from the [project] table.

toolery.search_tool(catalog: Catalog)[source]

Return a search(query, limit=10) function (an MCP-shaped tool) bound to catalog.

Its signature and docstring become the MCP tool schema and description.

toolery.skills(root, *, kind: str = 'skill') Iterator[Card][source]

Harvest Claude Agent Skills (SKILL.md files) under root into cards.