---
title: "Publish an Agentic Resource Discovery Manifest with Zuplo"
description: "Agentic Resource Discovery gives AI agents a standard way to find your MCP servers and APIs. Here's what the discovery manifest holds and how to serve it from your Zuplo gateway."
canonicalUrl: "https://zuplo.com/blog/2026/06/30/agentic-resource-discovery"
pageType: "blog"
date: "2026-06-30"
authors: "martyn"
tags: "Model Context Protocol, ai-agents, API Best Practices"
image: "https://zuplo.com/og?text=Publish%20an%20Agentic%20Resource%20Discovery%20Manifest%20with%20Zuplo"
---
Oh no, there's another spec to keep up with. My first reaction to Agentic
Resource Discovery (ARD), the standard
[Google announced on June 17](https://developers.googleblog.com/announcing-the-agentic-resource-discovery-specification/),
was to brace for yet another thing to implement and then keep updated.
Fortunately ARD is light, and it feels like one of the more inevitable steps in
agentic discovery: agents already call your APIs and MCP servers, and ARD gives
them a standard way to learn those servers exist in the first place.

If you're familiar with the `.well-known` pattern and its usage, you're 50% of
the way there already. Let's dive in.

<CalloutAudience
  variant="bestFor"
  items={[
    "Teams that already host an MCP server or API and want agents to discover it",
    "Platform owners deciding where a machine-readable capability catalog should live",
    "Anyone who runs a Zuplo gateway and wants a .well-known endpoint without standing up new infrastructure",
  ]}
/>

## What ARD is

ARD is an open standard, licensed under Apache 2.0, for publishing, discovering,
and verifying AI capabilities across the web. The announcement frames it as
three questions an agent has to answer before it can use anything: "Where does
the right capability live? Which capability should I actually use? And how do I
verify it's safe to connect to?"

There is no standard answer today. An agent can speak MCP fluently and still
have no idea your MCP server exists unless someone hands it a URL. ARD fills
that gap with two pieces:

| Piece    | What it is                                                                                                                                              | Who runs it   |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| Catalog  | A file describing your capabilities, published under your own domain. Domain ownership is the cryptographic root of trust.                              | You           |
| Registry | A search engine that crawls published catalogs, indexes them, and attaches verifiable trust metadata so an agent can decide what is safe to connect to. | A third party |

The catalog is the part you own and publish. In the words of the spec, it
describes capabilities that "can include things like MCP servers, A2A agents,
OpenAPI tools, or even other nested catalogs" (A2A is the agent-to-agent
protocol). If you have read about
[WebMCP and how websites expose tools to agents](/blog/2026/03/13/what-is-webmcp),
this is the discovery layer that sits a step earlier: WebMCP is how an agent
calls your tools, ARD is how it finds out you have any.

## Inside the manifest

The catalog is a static JSON file named `ai-catalog.json`. Here's one for
Changeloggle, a demo application whose MCP server I run on Zuplo using the
awesome
[dynamic MCP server functionality](https://zuplo.com/docs/mcp-server/introduction).
The shape follows the
[spec's publishing guide](https://agenticresourcediscovery.org/how_to_publish/):

```json
{
  "specVersion": "1.0",
  "host": {
    "displayName": "Changeloggle",
    "identifier": "did:web:changeloggle-main-fbddb8e.d2.zuplo.dev"
  },
  "entries": [
    {
      "identifier": "urn:air:changeloggle-main-fbddb8e.d2.zuplo.dev:server:changeloggle",
      "displayName": "Changeloggle MCP Server",
      "type": "application/mcp-server+json",
      "url": "https://changeloggle-main-fbddb8e.d2.zuplo.dev/mcp",
      "capabilities": [
        "listProjects",
        "createProject",
        "getProject",
        "updateProject",
        "deleteProject",
        "listChangelogs",
        "createChangelog",
        "getChangelog",
        "getLatestChangelog",
        "updateChangelog",
        "deleteChangelog",
        "getLatestChangelogGlobal"
      ],
      "description": "Manage changelog projects and entries for Changeloggle, a changelog management app.",
      "representativeQueries": [
        "show the latest changelog for the payments project",
        "publish a changelog entry for version 2.0"
      ]
    }
  ]
}
```

Reading it top to bottom:

| Field                             | What it holds                                                                                                                                                                          |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `specVersion`                     | Manifest format version, currently `"1.0"`.                                                                                                                                            |
| `host.displayName`                | Human-readable name for the publisher.                                                                                                                                                 |
| `host.identifier`                 | A `did:web` value tied to your domain. Only the domain owner can serve content from it over HTTPS, so controlling the domain proves the catalog is yours. That is ARD's root of trust. |
| `entries[].identifier`            | ARD's `urn:air:<your-domain>:<namespace>:<agent-name>` pattern. Here the host is the domain, `server` the namespace, `changeloggle` the name.                                          |
| `entries[].displayName`           | Human-readable name for the capability.                                                                                                                                                |
| `entries[].type`                  | The resource media type, here `application/mcp-server+json`.                                                                                                                           |
| `entries[].url`                   | Where the resource lives. For Changeloggle's MCP server, that's its `/mcp` endpoint, the address a client connects to.                                                                 |
| `entries[].capabilities`          | Array of capability names the resource exposes.                                                                                                                                        |
| `entries[].description`           | Plain-language summary of what the resource does.                                                                                                                                      |
| `entries[].representativeQueries` | The quiet heavy lifting: two to five natural language examples that let a registry match your server to a user's intent by semantic search, not by someone already knowing its name.   |

Swap the host (`changeloggle-main-fbddb8e.d2.zuplo.dev`) for your own domain in
the `did:web` value and in each `urn:air` identifier. For testing you can use
the working-copy domain Zuplo hands you (the one ending in `zuplo.dev`), as I've
done here. For production, add a
[custom domain](https://zuplo.com/docs/articles/custom-domains) for your API and
update both identifiers to that, since the domain is ARD's root of trust and
your catalog should be served from the domain you actually own.

The spec is strict about how you serve the file:

- Over HTTPS.
- With `Content-Type: application/json`.
- With `Access-Control-Allow-Origin: *`, so any agent can read it cross-origin.

## Build the manifest

You could drop a static file on a bucket and call it done. Serve it from your
gateway instead because the gateway is already the front door for the MCP
servers and APIs the catalog points at. The thing doing discovery and the thing
being discovered live in one place, so the catalog can't drift from what you
expose.

Keep the catalog in a module and return it from a function handler. Zuplo
projects hold this code in the `modules/` folder, alongside `config/` where
`routes.oas.json` lives:

```
your-project/
├── config/
│   ├── routes.oas.json
│   └── policies.json
└── modules/
    └── well-known.ts   ← the handler you'll add
```

Create `modules/well-known.ts` by clicking the three-dot menu next to `modules`
and choosing **New Empty Module**:

![The Files panel three-dot menu next to the modules folder open, with New Empty Module highlighted.](/blog-images/2026-06-30-agentic-resource-discovery/new-empty-module.png)

Name it `well-known.ts` and paste in the handler:

```ts
import { ZuploRequest, ZuploContext } from "@zuplo/runtime";

const catalog = {
  specVersion: "1.0",
  host: {
    displayName: "Changeloggle",
    // This did:web has to match the domain serving the file. That match is what proves the catalog is yours.
    identifier: "did:web:changeloggle-main-fbddb8e.d2.zuplo.dev",
  },
  entries: [
    {
      identifier:
        "urn:air:changeloggle-main-fbddb8e.d2.zuplo.dev:server:changeloggle",
      displayName: "Changeloggle MCP Server",
      type: "application/mcp-server+json",
      // The /mcp endpoint our Zuplo project already exposes.
      url: "https://changeloggle-main-fbddb8e.d2.zuplo.dev/mcp",
      capabilities: [
        "listProjects",
        "createProject",
        "getProject",
        "updateProject",
        "deleteProject",
        "listChangelogs",
        "createChangelog",
        "getChangelog",
        "getLatestChangelog",
        "updateChangelog",
        "deleteChangelog",
        "getLatestChangelogGlobal",
      ],
      description:
        "Manage changelog projects and entries for Changeloggle, a changelog management app.",
      representativeQueries: [
        "show the latest changelog for the payments project",
        "publish a changelog entry for version 2.0",
      ],
    },
  ],
};

export async function handleWellKnown(
  _request: ZuploRequest,
  _context: ZuploContext,
): Promise<Response> {
  // Pretty-print it so the catalog is readable if someone fetches it in a browser.
  return new Response(JSON.stringify(catalog, null, 2), {
    status: 200,
    headers: {
      "content-type": "application/json",
    },
  });
}
```

That is the whole content side. The handler returns the manifest as JSON, and
the route handles CORS in the next step.

<CalloutDoc
  title="Custom function handlers in Zuplo"
  description="The full handler API, including how to read params and query strings and return a Response with custom headers."
  href="https://zuplo.com/docs/handlers/custom-handler"
  icon="book"
/>

## Add the route

Now wire the handler to the path ARD expects, from the Route Designer without
touching the config files by hand:

1. Open your project in the Zuplo portal and go to the **Route Designer**
   (`routes.oas.json`).
2. Click **+ Add** and choose **REST Operation**, then give the route a summary
   like `Expose ARD Catalog`.
3. Set the method to **GET** and the **Path** to `/.well-known/ai-catalog.json`.
4. Under **Request Handling**, set the handler to **Function**, then point it at
   the `./modules/well-known` module and the `handleWellKnown` export.
5. Turn on **Allow All Origins (CORS)** so any agent or registry can read the
   catalog cross-origin. That is why the handler above doesn't set the header
   itself.
6. Leave the inbound policies empty. A Zuplo route is public unless you attach
   an authentication policy, and this endpoint needs to stay public so
   registries and agents can fetch it.

![Zuplo Route Designer with the Expose ARD Catalog route (GET /.well-known/ai-catalog.json) wired to the well-known.ts handler and Allow All Origins (CORS) enabled.](/blog-images/2026-06-30-agentic-resource-discovery/ard-route-setup.png)

<CalloutTip variant="mistake">
  Leaving the inbound policies empty is intentional here, not an oversight. The
  catalog has to be world-readable, so resist the reflex to bolt auth onto it.
  Save auth for the MCP servers and APIs the catalog points at, not the catalog
  itself.
</CalloutTip>

Saving the route writes this into `routes.oas.json`, which is what you would
edit directly if you prefer the IDE:

```json
{
  "paths": {
    "/.well-known/ai-catalog.json": {
      "get": {
        "x-zuplo-route": {
          "handler": {
            "export": "handleWellKnown",
            "module": "$import(./modules/well-known)"
          },
          "policies": {
            "inbound": []
          }
        }
      }
    }
  }
}
```

Deploy, then fetch `https://your-gateway/.well-known/ai-catalog.json`. You
should get the catalog back as `application/json`, ready for any agent to read.

## Point at your MCP server

We already front MCP servers for teams whose agents reach those tools by
hardcoded URL today, and ARD is the piece that turns that into crawlable
discovery. The catalog is only as useful as what it points at.

If you are running the
[Zuplo MCP server](/blog/2026/05/20/introducing-the-zuplo-mcp-server), the `url`
field points at the MCP endpoint your gateway already hosts, and the
`capabilities` array is the set of tools you have chosen to surface. The gateway
fronts the server, decides which tools are visible, and now advertises it
through ARD, all from the same project.

That is what makes this more than a metadata exercise. An agent reads your
catalog, sees the Changeloggle MCP server with a `createChangelog` capability
and a representative query that matches what its user asked for, and connects,
while your gateway still enforces auth, rate limits, and tool curation on every
call.

ARD is young and the registry side is still filling in. Registries crawl and
index published catalogs rather than asking you to push to them, and the spec is
explicit that hosting alone doesn't guarantee inclusion. It also defines
optional DNS records for the case where you can't host at the standard
`.well-known` path and need to point discovery at an alternate location.

Either way your job ends at serving the file, which costs almost nothing if your
gateway is already in place: one module, one route, and the agents crawling the
web can find what you have built.