Skip to main content

ard-kit

pipeline

Turn what you already have into a discoverable catalog. Point it at a folder of scripts or at a packaged repo and it writes a valid ai-catalog.json: docstrings, argparse flags, SKILL.md frontmatter, .mcp.json servers and declared console commands become Agentic Resource Discovery (ARD) entries. Nothing to annotate, no account to create, no dependencies to add.

Serving is included, but it is not the point: the same catalog goes out as a static manifest at /.well-known/ard.json, a dynamic POST /search, GET /agents and POST /explore facet aggregation, or over stdio as an MCP front. 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.

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.

Publish what you already have

Three shapes, all zero-config:

# A folder of scripts: docstrings, argparse flags, SKILL.md, .mcp.json
ard-catalogize --dir scripts

# A packaged Python repo: console commands it already declares
cd my-project && ard-catalogize

# An npm package: the bin field it already declares
cd my-node-project && ard-catalogize

The first walks the directory — *.py and *.sh files (first docstring or header comment as the description, argparse flags as the invoke schema), SKILL.md agent skills, .mcp.json client configs. The second reads [project.scripts] from the workspace manifest and catalogs the commands your package already ships: urn:air:<host>:cli:<command>, arguments mined from the module behind the entry point (flat and src layouts), distribution and version from [project]. A packaged repo with no scripts/ directory is a normal case, not an error.

The third reads bin from package.json the way npm resolves it itself: the string form is the package name without its scope (@org/tool installs as .bin/tool, so it is cataloged as urn:air:<host>:cli:tool), and the object form follows npm's .bin/ resolution rather than the raw key: the directory part of a key is dropped (@org/tool links as tool, deep/sub/dir as dir), a later key wins a basename collision, and the commands are cataloged sorted instead of in declaration order. JavaScript has no argparse to mine, so arguments stays empty — a declared command is cataloged, not validated, and a bin path that does not exist yet is the publisher's problem rather than the catalogizer's. A workspace shipping both manifests is read as Python: pyproject.toml wins and package.json is not read.

All three shapes also read a .mcp.json at the workspace root, not only the ones found under a scanned directory: a client config sits above --dir scripts by definition, and without it the servers a workspace declares stay invisible to the catalog an agent asks "which of these can do X". They become urn:air:<host>:mcp:<server>command + args shell-quoted into invoke for a stdio server, the url for an http one — appended after the scanned entries, so re-catalogizing an existing catalog stays a pure append. env is never copied: that is where client configs keep their secrets. A disabled: true server is still cataloged — under JIT mounting a switched-off server is exactly what discovery should surface — and carries metadata.disabled so its card does not read as callable right now; the flag is kept out of description, which is search text. A workspace holding nothing but a root client config catalogs on its own.

A manifest says a server exists and how to start it, never what it does, so a card's description is generated — MCP server <name> (stdio). — and the card is found by the server's name. To make it findable by capability, declare prose in a sibling file: .mcp.json.mcp.ard.json, keyed by the exact mcpServers key, each value {"description": ..., "tags": [...], "aliases": [...]}, every field optional and every value a plain string or list of strings. Declared prose replaces the generated description and feeds tags, aliases and representative queries; the name is always kept as an alias and mcp as a tag. A wrongly-typed field is dropped, and a missing, unreadable, misshapen or orphan-keyed sidecar costs one line on stderr and never the scan — prose is optional, so it cannot be allowed to abort the thing carrying it. Write it in a language the tokenizer reads ([a-z0-9]+).

What you do not do: write metadata files, decorate your code, register an account, or install anything beyond the Python 3.10+ stdlib. If a tool is already documented for humans, it is already documented for agents. The card prose above is the one exception, and it exists because a client config is the one source that documents nothing for humans: it holds a name and a command, so there is no prose to lift and the alternative is a card nothing can find.

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; an optional <manifest>.ard.json sibling declares per-server card prose). Also reads the commands a package already declares straight from the workspace manifest, with no file scan at all — [project.scripts] in pyproject.toml, bin in package.json (string form unscoped, the way npm links it into .bin/; object form by the name npm links each key as, sorted) — cataloged as cli entries. A publisher-declared name — a bin key, an mcpServers key — is cataloged as the single segment it is, never as URN path structure. A manifest, client config or skill file it cannot read or parse is skipped with one line on stderr, never a traceback: one bad workspace never costs you the scripts already collected, and when two sources collide on one identifier the dropped one is named on stderr instead of vanishing. 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 — every peer row screened, a SUSPECT one dropped and named on stderr — 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), ard_inspect (invoke command + CLI arguments) and ard_verify (trust verdict) as MCP tools, so an editor mounts a command instead of being handed a URL. Stdlib only.
ard_kit/trust.py The verdict engine and the ard-verify CLI: screens an entry for injection markers, invisible characters and padded fields, checks a trustManifest.identity against the domain its URN claims, verifies a detached Ed25519 JWS attestation against a JWKS, and reports VERIFIED / UNVERIFIED / SUSPECT / UNSUPPORTED. Stdlib only — JWS verification uses cryptography when it is importable and degrades to UNSUPPORTED when it is not.
selfcheck.py End-to-end pipeline check over a temp workspace: catalogize, ranking, schema conformance, the invoke sidecar, bearer auth, the MCP front, two-registry federation, trust verdicts and the federation screen, and declared entry points. New extraction contracts are pre-registered here as expected failures before the code that satisfies them exists.
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, four commands on PATH
pipx install .         # same, from a checkout

This installs ard-catalogize, ard-registry, ard-mcp and ard-verify — the same entry points as python3 -m ard_kit.catalogize, python3 -m ard_kit.registry, python3 -m ard_kit.mcp_server and python3 -m ard_kit.trust. 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, bin/ard-verify 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

# 7. Verdict its trust (exit 1 if anything came back SUSPECT or UNSUPPORTED)
python3 -m ard_kit.trust --catalog ai-catalog.json --quiet

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. Rows are ranked by (score, identifier), so a score tie cannot fall back to catalog order and move a page boundary the next time the catalog is regenerated. The cursor names the last row of the page it came from rather than a position in the result set: entries cataloged or removed mid-walk shift nothing that was already handed out, and a row added ahead of the cursor simply belongs to the next walk. The cursor also 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. Cursors are versioned; one minted by an older release is refused the same way rather than reinterpreted. 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. Rows are ordered by the orderBy field with identifier as the tiebreak: a displayName shared by several scripts would otherwise fall back to catalog order and move a page boundary the next time the catalog is regenerated. It takes filter, orderBy, pageSize (1–100, default 20) and the same pageToken cursor, with the same guarantee: every row present when the walk started is delivered exactly once, whether or not the catalog is regenerated in between. orderBy=... DESC reads the same order backwards, and a cursor minted by one direction or ordering is refused by the other. 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. Under auto, totalResults counts each peer's own total for the query rather than the rows its page happened to return, and a peer still holding rows beyond that page comes back as a referrals entry — a merged page has no cursor, so the remainder gets an address instead of only a number:

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.

Mounted as a plugin (Hermes reads mcp.json from the plugin root), command must be a bare executable or ./-relative: ${PLUGIN_ROOT} expands in args/cwd/env, never in command. The form that resolves from the plugin root is ./bin/ard-mcp.

Cross-platform. The Python is stdlib-only and runs on Linux, macOS and Windows unchanged — only the launch form is platform-flavoured. python3 is the interpreter name on Linux/macOS; on Windows it is python (or py), and the -m ard_kit.mcp_server module form is identical. The bin/ard-* wrappers are #!/bin/sh scripts, so they run on Linux and macOS but Windows cannot exec them: on Windows mount either the pipx console script ("command": "ard-mcp" — pip installs a real ard-mcp.exe) or the interpreter form ("command": "python", "args": ["-m", "ard_kit.mcp_server", ...], "cwd" at the checkout). macOS is POSIX and needs none of this; its one caveat is that a source ZIP (unlike git clone) drops the executable bit, so ./bin/ard-mcp may want a chmod +x bin/* first — or run it as sh bin/ard-mcp.

Three 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.
ard_verify The trust verdict and its findings for one identifier, over the same catalog. It takes no URL: the front never fetches on model-supplied input.

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:

  1. 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-running ard-catalogize.
  2. Skill lane — a SKILL.md documenting 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).

Trust verdicts

A relevance score says nothing about whether an entry is telling the truth, and a search row is echoed into an agent's context verbatim — description, displayName, url and metadata.invoke, all of it. ard_kit/trust.py answers the other question, per entry:

Verdict Meaning
VERIFIED Evidence beyond the entry's own text: the manifest was fetched from the domain its URNs claim, and its trustManifest.identity binds to that same domain.
UNSUPPORTED A check was asked for that this install cannot perform — an identity the spec allows but nothing here can resolve (did:key, SPIFFE, ANS), or a detached JWS with no cryptography importable. Not an accusation: SUSPECT is.
UNVERIFIED Nothing wrong, nothing proven. The normal verdict for a local catalog, where the entry is the only witness to itself.
SUSPECT A concrete reason not to believe it, and the findings name it: an injection marker, an invisible or control character, a field padded past its ceiling, shell chaining inside metadata.invoke, a URL scheme the catalog has no business carrying, or an identity that does not match the URN authority.

The worst verdict wins per entry, so a correctly bound identity cannot launder an injection marker. Every ceiling and marker list was calibrated against the station's live catalog (~1700 entries, regenerated hourly) before shipping: the one SUSPECT it turned up was a real defect — two raw control bytes a script's docstring carried into the catalog — since fixed at the source, so the catalog now comes back clean. Nothing verdicts UNSUPPORTED, which is what makes a SUSPECT actionable instead of noise. Two rules were loosened by that measurement — \bexec\b fires on the ordinary bash -lc 'exec $HOME/…/run.sh' launcher line, and a 200-char list cap fired on real representativeQueries — and selfcheck.py pins both loosenings with an honest entry that must come back clean.

# A catalog on disk: nothing is proven, but every defect is found.
ard-verify --catalog ai-catalog.json --quiet

# A live registry. Fetching the manifest from the claimed domain is itself
# the hosting evidence, so this is the only mode that can return VERIFIED
# (ora.ai: 5/5 VERIFIED, did:web:ora.ai over urn:air:ora.ai:...).
ard-verify --url https://ora.ai --quiet

# A detached Ed25519 JWS attestation, checked against the issuer's JWKS.
ard-verify --attestation attestation.json --jwks https://ora.ai/.well-known/jwks.json

--catalog, --url and --attestation are mutually exclusive and one is required. --max-age-days (default 90) rejects a stale attestation, --json emits {source, hostedFrom, counts, verdicts}, --quiet prints only the flagged rows (SUSPECT and UNSUPPORTED). Exit is 0 when every row came back VERIFIED or UNVERIFIED, 1 when anything did not, 2 when the source could not be loaded — so it drops into a CI step or a pre-run gate unchanged.

Federation enforces the same verdicts without being asked: --upstreams screens every peer row and drops the SUSPECT ones, naming each on stderr. Dropped rather than annotated, because the /search body is conformance-validated and has no room for a verdict field. A peer row is never given hosted_from: an aggregator relaying third-party entries is doing its job, and demanding hosting there would condemn every row it relays and break federation. ard_verify over MCP is the same engine over the same catalog file, for the agent that is about to run something — identifier only, because a fetch driven by model input is an SSRF surface. The operator CLI keeps --url.

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 → run endpoints 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

agentic_ard_kit-0.19.4.tar.gz (56.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agentic_ard_kit-0.19.4-py3-none-any.whl (58.3 kB view details)

Uploaded Python 3

File details

Details for the file agentic_ard_kit-0.19.4.tar.gz.

File metadata

  • Download URL: agentic_ard_kit-0.19.4.tar.gz
  • Upload date:
  • Size: 56.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.12

File hashes

Hashes for agentic_ard_kit-0.19.4.tar.gz
Algorithm Hash digest
SHA256 588e2d3ab8550f418d3be9e3087cb6760f3e90a70fc9a7d95a3c8920ab99d618
MD5 e09aa55d2d32bdc8a2af03cce9b30418
BLAKE2b-256 7d7c8415be7706a3ac23d097eb1ccc334ba05a08c3abe4456af9fd8532866b6a

See more details on using hashes here.

File details

Details for the file agentic_ard_kit-0.19.4-py3-none-any.whl.

File metadata

File hashes

Hashes for agentic_ard_kit-0.19.4-py3-none-any.whl
Algorithm Hash digest
SHA256 de9422b26f7b7e2c620c5d96df5d44f42f299f6191f54769fb4a7c9ae4490bde
MD5 0046a3ec686a18aca4923320985ed989
BLAKE2b-256 ed1a2eb806da3597e1f463683dbaa8d573e925dd0bf785ca3d4975bc1544600c

See more details on using hashes here.

Release history Release notifications | RSS feed

0.19.5

2 files

This release

0.19.4 This release

2 files

0.19.3

2 files

0.19.2

2 files

0.19.1

2 files

0.19.0

2 files

0.18.0

2 files

0.17.0

2 files

0.16.0

2 files

0.15.9

2 files

0.15.7

2 files

0.15.6

2 files

0.15.5

1 file

0.15.4

1 file

0.15.3

1 file

0.15.2

1 file

0.15.1

1 file

0.15.0

1 file

0.14.0

2 files

0.13.1

2 files

0.13.0

2 files

0.12.0

2 files

0.11.0

2 files

0.10.1

2 files

0.10.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page