ard-kit
Self-hosted Agentic Resource Discovery (ARD)
for local and private resources: catalog your own scripts/tools into an
ai-catalog.json, then serve it through the surfaces the spec defines —
a static manifest at /.well-known/ard.json, a dynamic POST /search
endpoint and POST /explore facet aggregation. python3 selfcheck.py asserts the pipeline
end-to-end, and .gitlab-ci.yml runs it plus the official conformance CLI
(ards-project/ard-spec) on every push where runner quota allows.
ARD is the discovery layer that sits in front of MCP (tools), Skills (instructions), and A2A (agents). It answers one question: "what capability can help with this task?" — invocation stays with the resource's own mechanism.
Why this exists
The pieces started life as a private in-house integration — a catalogizer
over a large pile of local scripts and a registry serving them for
intent-based lookup. When the ARD specification was announced
(HF blog: Agentic Resource Discovery: Let agents search),
the salvageable pieces were pulled out, generalized, and aligned to the
spec. The original registry server code was lost in a workspace cleanup;
registry.py here is a faithful rebuild against the public shape.
Pieces
| File | Role |
|---|---|
ard_kit/catalogize.py |
Scans directories for .py/.sh scripts (AST docstrings, argparse flags, shell header comments), SKILL.md agent skills (YAML frontmatter → text/markdown; profile="urn:air:agent-skills" entries) and .mcp.json MCP client configs (→ application/mcp-server-card+json per server; env values never cataloged), emits ARD entries into ai-catalog.json plus an ai-catalog.inspect.json invoke-schema sidecar. Stdlib only. |
ard_kit/registry.py |
Minimal HTTP registry: serves the manifest and POST /search (token-overlap ranking, pageToken paging), GET /agents (deterministic listing), POST /explore (facet counts over the matched set), optional --upstreams federation fan-out, GET /inspect over the sidecar, optional --token bearer auth. Stdlib only. |
ard_kit/mcp_server.py |
MCP stdio front over the same catalog: ard_search (ranked summaries) and ard_inspect (invoke command + CLI arguments) as MCP tools, so an editor mounts a command instead of being handed a URL. Stdlib only. |
selfcheck.py |
End-to-end pipeline check: catalogize a temp dir, run search, assert ranking. |
systemd/ard-registry.service |
Unit template for running the registry as a user service. |
No dependencies beyond Python 3.10+ stdlib.
Install
pipx install agentic-ard-kit # isolated venv, three commands on PATH
pipx install . # same, from a checkout
This installs ard-catalogize, ard-registry and ard-mcp — the same
entry points as python3 -m ard_kit.catalogize, python3 -m ard_kit.registry
and python3 -m ard_kit.mcp_server. Running straight from a clone stays
supported and needs no install at all.
Qoder plugin
ard-kit also ships as a self-contained Qoder plugin — the same code plus an agent-facing wrapper:
| Component | What it gives the agent |
|---|---|
skills/ard-registry |
The discover → inspect → run → discard contract (env-driven endpoint: ARD_REGISTRY_URL, optional ARD_REGISTRY_TOKEN). |
/ard-catalogize |
Slash command: index a script directory into ai-catalog.json and re-verify live. |
/ard-serve |
Slash command: serve a catalog, confirm via /health. |
bin/ard-registry, bin/ard-catalogize, bin/ard-mcp |
Entry points added to PATH (stdlib-only, no install step). |
qoder plugins install ./ard-kit # then /plugins reload
qoder plugins validate ./ard-kit # manifest + component check
Quickstart
# 1. Catalog your scripts and agent skills (default scan dir: ./scripts)
python3 -m ard_kit.catalogize --dir scripts --dir .agents/skills --host myhost.example.com
# 2. Serve it
python3 -m ard_kit.registry --catalog ai-catalog.json --port 8390
# 3. Search (the ARD registry API shape)
curl -s http://127.0.0.1:8390/search \
-H 'Content-Type: application/json' \
-d '{"query": {"text": "scan subdomains"}, "pageSize": 5}'
# 4. Explore what the catalog holds (facet counts, no ranking)
curl -s http://127.0.0.1:8390/explore \
-H 'Content-Type: application/json' \
-d '{"resultType": {"facets": [{"field": "type"}, {"field": "tags", "limit": 5}]}}'
# 5. Inspect before running (invoke command + CLI arguments)
curl -s 'http://127.0.0.1:8390/inspect?identifier=urn:air:myhost.example.com:script:scripts:scan_subdomains'
# 6. Static manifest (for crawlers / federation)
curl -s http://127.0.0.1:8390/.well-known/ard.json
The discover → inspect → run flow mirrors commercial directories like
monid.ai, minus the marketplace: POST /search finds the capability,
GET /inspect?identifier=<urn> returns its invoke command and parsed
argparse flags (404 for unknown identifiers, 501 when the sidecar is
absent), and the run itself stays with your shell.
POST /search returns one page: pageSize is 1–100 (default 10) and the
response carries a pageToken cursor while more local rows remain — pass it
back unchanged for the next page. The cursor carries the query it was minted
for, so replaying it against a different text/filter is refused with 400
instead of quietly paging a different result set. Paging is local: send
"federation": "none" alongside a pageToken, because merged peer ordering
is not stable between requests. A pageSize outside 1–100 and an unknown
federation mode are 400s as well, not silent clamps.
GET /agents is the deterministic half of the same catalog (spec 5.3.4):
no relevance, no federation, stable order. It takes filter, orderBy,
pageSize (1–100, default 20) and the same pageToken cursor. The filter
grammar is the Appendix A subset this catalog can honour — field=value
clauses joined by AND, comma-separated values inside one field meaning
OR — over displayName (case-insensitive) and type. publisherId,
createdAfter and updatedAfter are in the spec, but a catalogized script
carries no publisher and no timestamps, so they are refused with 400
instead of matching everything:
curl -sG http://127.0.0.1:8390/agents \
--data-urlencode 'filter=type=application/vnd.ard-kit.skill+json' \
--data-urlencode 'orderBy=displayName DESC' \
--data-urlencode 'pageSize=5'
POST /explore answers the other question — what the catalog contains.
It returns facet counts over the whole matched set instead of ranked
entries: ask for resultType.facets (per facet limit and minCount
optional, otherCount reports the tail beyond limit), and narrow with the
same query.text/query.filter search takes, or neither to aggregate the
whole registry. Explore is scoped to this registry — it never federates —
and a filter key the registry cannot honour returns 400 rather than being
silently ignored, because over-counted buckets look identical to correct
ones on the client side.
For clients: the registry is stateless and nothing attaches to your
session. POST /search returns ranked summaries only; pull a full invoke
schema via /inspect for the single entry you actually run. Discard both
afterwards — keep the URN if you might reuse the tool, not the payload.
Discovery costs a query, not a mounting: there is nothing to unload because
nothing was loaded.
Require a bearer token on every endpoint except /health when the registry
leaves loopback. For service deployments prefer the ARD_REGISTRY_TOKEN
environment variable over --token — the command line is world-readable:
python3 -m ard_kit.registry --catalog ai-catalog.json --port 8390 --token "$(openssl rand -hex 16)"
# or: ARD_REGISTRY_TOKEN=<token> python3 -m ard_kit.registry --catalog ai-catalog.json --port 8390
A token that is set but empty is rejected at startup (fail closed).
Federate with peer registries — with "federation": "auto" (default) queries
fan out to --upstreams and results merge (every result names its source
registry and carries a clamped score); "federation": "referrals" returns the peers in
a referrals array instead; "federation": "none" stays local:
python3 -m ard_kit.registry --catalog ai-catalog.json --port 8390 \
--upstreams https://peer.example.com --public-url https://me.example.com
Self-check the whole pipeline:
python3 selfcheck.py
MCP front
The registry is HTTP, which means something has to hand the agent a URL first. Editors that speak MCP mount a command instead, so the same catalog is also served over stdio — no server to start, no port, no token:
{
"mcpServers": {
"ard": {
"command": "python3",
"args": ["-m", "ard_kit.mcp_server",
"--catalog", "/path/to/ai-catalog.json"],
"cwd": "/path/to/ard-kit"
}
}
}
Installed through pipx, the same front is "command": "ard-mcp" with no
path to keep in sync.
Two tools, the same discover → inspect → run contract:
| Tool | Returns |
|---|---|
ard_search |
Ranked summaries (identifier, displayName, description, type, score) for an intent, optionally filtered by media type. |
ard_inspect |
The invoke command and parsed CLI arguments for one urn:air: identifier. |
Search deliberately returns summaries only: the invoke schema arrives once,
for the single entry the agent actually runs. The catalog is re-read per
call, so re-running ard-catalogize is picked up without a restart, and the
host owns the process lifetime — the server exits when stdin closes.
Wiring agent harnesses
Two lanes; pick per harness, they serve the same catalog:
- MCP lane — native tools, present in every session's tool list. Any
MCP client runs the stdio front (
ard-mcp --catalog <absolute path to catalog.json>); the JSON block above is the generic mount, and harnesses with a CLI write the same config for you (Hermes:hermes mcp add <name> --command ard-mcp --args ...— the args option must come last). The front reads the catalog file directly, so it works while the HTTP registry is down; refresh is just re-runningard-catalogize. - Skill lane — a
SKILL.mddocumenting the HTTP registry (/search,/inspect, bearer token sourced out-of-band) loaded by the harness like any other skill. Zero prompt cost until triggered, and paging/filtering live server-side; the trade is that the agent must follow curl instructions instead of calling a tool.
Running one catalog over both lanes is normal: skills for the agents that read docs, MCP for the agents that call tools.
Entry shape
Each catalog entry carries the fields ARD consumers key on:
{
"@context": "https://agenticresourcediscovery.org/context/v1",
"identifier": "urn:air:myhost.example.com:script:scripts:scan_subdomains",
"displayName": "scan_subdomains",
"description": "Enumerate subdomains via passive sources",
"type": "application/vnd.ard-kit.script+json",
"url": "file:///path/to/scripts/scan_subdomains.py",
"tags": ["scan", "subdomains", "recon"],
"aliases": ["scan_subdomains"],
"representativeQueries": [
"Enumerate subdomains via passive sources",
"scan", "subdomains", "recon"
],
"metadata": { "invoke": "python3 scripts/scan_subdomains.py", ... }
}
representativeQueries lead with the natural description phrase and pad
with keywords, so both sentence-style and token-style agent queries hit.
Every entry carries the field: when a resource offers no keywords, the file
or skill name is split to pad the list, which is always 2–5 phrases.
Entries keep exactly one of url/data (spec §3.4) and scalar-only
metadata values, and the manifest envelope carries only
specVersion/host/entries — the shape the official
ai-catalog.schema.json validates.
The type media type is free-form per the spec, but entries use a standard
name wherever one exists — application/mcp-server-card+json for MCP servers,
text/markdown; profile="urn:air:agent-skills" for agent skills — so any
conformant registry routes them without a local mapping. Plain local scripts
have no standard type yet and keep the vendor one.
Identifiers follow the spec's Appendix C form
(urn:air:<publisher>:<namespace>:<name>), and the served manifest prepends
a self-advert entry of type application/ai-registry+json so peers can
discover this registry's search base URL. Search results carry a score in
the spec's 0–100 relevance band (relevance only — ARD decouples trust into
the trust manifest, §5).
Prior art / positioning
- HF Discover — reference implementation over the Hugging Face Hub; federated, semantic search.
- ARD spec — the standard itself (Apache-2.0).
- monid.ai — commercial take
on the same pattern: a CLI/Skill teaching an agent to
discover → inspect → runendpoints by intent. The difference: monid is a paid marketplace of third-party data endpoints behind an API key; ard-kit is the self-hosted, no-network, no-account version for resources you already own.
Known limitation: search here is lexical token overlap, not semantic ranking. That is deliberate — zero dependencies, and good enough for a few thousand private entries. The upgrade path is embeddings behind the same endpoint.
License
MIT.
Acknowledgements
Developed and hardened in Qoder — from the ARD v0.91 conformance rebuild through the ultra-review cycle.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agentic_ard_kit-0.14.0.tar.gz.
File metadata
- Download URL: agentic_ard_kit-0.14.0.tar.gz
- Upload date:
- Size: 30.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6e391db5ec9ecb68545317a4759f2a22521e1c937a0c8b5404da2fdea85baba5
|
|
| MD5 |
8f03b33575a9e16ea65d4d9a233d5ffa
|
|
| BLAKE2b-256 |
969ca928cba930a770be2054fcf3b05ed2adf8d0ca4fdb9bc88b2637c7328f2b
|
File details
Details for the file agentic_ard_kit-0.14.0-py3-none-any.whl.
File metadata
- Download URL: agentic_ard_kit-0.14.0-py3-none-any.whl
- Upload date:
- Size: 32.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
79749bac3b6c83fa7afe5c5b9f4b9ebc90acd940545cf403df7b3d8f9e66c67e
|
|
| MD5 |
c8faf6b095f640539e43e96b27286db9
|
|
| BLAKE2b-256 |
a7a5c7f01f87353999988ea1087f94197c1a90ed339b8032ebea8f973e38255a
|