Metadata-Version: 2.4
Name: agentic-ard-kit
Version: 0.15.3
Summary: Self-hosted Agentic Resource Discovery over local scripts, agent skills and MCP servers.
Author: ameobius
License-Expression: MIT
Project-URL: Homepage, https://gitlab.com/ameobius-ai/ard-kit
Project-URL: Source, https://gitlab.com/ameobius-ai/ard-kit
Project-URL: Issues, https://gitlab.com/ameobius-ai/ard-kit/-/issues
Keywords: ard,agentic-resource-discovery,mcp,agent-skills,registry,discovery
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# ard-kit

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)](https://agenticresourcediscovery.org/)
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 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.

## Publish what you already have

Three shapes, all zero-config:

```bash
# 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.

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.

## 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](https://huggingface.co/blog/agentic-resource-discovery-launch)),
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). 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, `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 over a temp workspace: catalogize, ranking, schema conformance, the invoke sidecar, bearer auth, the MCP front, two-registry federation 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

```bash
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](https://qoder.com) 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). |

```bash
qoder plugins install ./ard-kit   # then /plugins reload
qoder plugins validate ./ard-kit  # manifest + component check
```

## Quickstart

```bash
# 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:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```json
{
  "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:

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:

```json
{
  "@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](https://github.com/huggingface/hf-discover) — reference
  implementation over the Hugging Face Hub; federated, semantic search.
- [ARD spec](https://github.com/ards-project/ard-spec) — the standard itself
  (Apache-2.0).
- [monid.ai](https://monid.ai/docs/guide/quickstart-skill) — 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](https://qoder.com) — from the ARD v0.91
conformance rebuild through the ultra-review cycle.
