---
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 manifest is a static JSON file named `ard.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).
This example follows ARD v0.91 and points to the OpenAPI document that contains
the server's `x-mcp` metadata. The live MCP transport stays in `metadata` rather
than pretending that the endpoint itself is a server card:

```json
{
  "entries": [
    {
      "@context": "https://agenticresourcediscovery.org/context/v1",
      "identifier": "urn:air:changeloggle-main-fbddb8e.d2.zuplo.dev:mcp:changeloggle",
      "displayName": "Changeloggle MCP Server",
      "type": "application/json",
      "url": "https://changeloggle-main-fbddb8e.d2.zuplo.dev/openapi.json",
      "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.",
      "metadata": {
        "resourceType": "mcp-server",
        "endpoint": "https://changeloggle-main-fbddb8e.d2.zuplo.dev/mcp"
      },
      "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                                                                                                                                                                      |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `entries[].@context`              | The ARD JSON-LD context for this entry.                                                                                                                                            |
| `entries[].identifier`            | ARD's `urn:air:<publisher>:<namespace>:<name>` pattern. Here the publisher is the domain, `mcp` the namespace, and `changeloggle` the name.                                        |
| `entries[].displayName`           | Human-readable name for the capability.                                                                                                                                            |
| `entries[].type`                  | The media type of the resource at `url`. This example uses JSON because the resource is an OpenAPI JSON document.                                                                  |
| `entries[].url`                   | The authoritative metadata artifact. Use a real OpenAPI document, MCP server card, or other conforming resource rather than labeling a live transport as a card.                   |
| `entries[].capabilities`          | Tool or capability names the resource actually declares. Omit the field when they are unknown instead of publishing an empty or invented list.                                     |
| `entries[].description`           | Plain-language summary of what the resource does.                                                                                                                                  |
| `entries[].metadata.endpoint`     | The live MCP endpoint associated with the authoritative artifact.                                                                                                                  |
| `entries[].representativeQueries` | Owner-authored natural-language examples that let a registry match the resource to user intent. Omit them if you cannot provide truthful examples; the ARD conformance tool warns. |

Swap the publisher (`changeloggle-main-fbddb8e.d2.zuplo.dev`) for your own
domain in each `urn:air` identifier and artifact URL. 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 the identifiers and URLs to it, since domain ownership anchors the
publisher identity and the manifest should be served from the origin you 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 manifest = {
  entries: [
    {
      "@context": "https://agenticresourcediscovery.org/context/v1",
      identifier:
        "urn:air:changeloggle-main-fbddb8e.d2.zuplo.dev:mcp:changeloggle",
      displayName: "Changeloggle MCP Server",
      type: "application/json",
      // This OpenAPI document contains the x-mcp declaration for the server.
      url: "https://changeloggle-main-fbddb8e.d2.zuplo.dev/openapi.json",
      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.",
      metadata: {
        resourceType: "mcp-server",
        endpoint: "https://changeloggle-main-fbddb8e.d2.zuplo.dev/mcp",
      },
      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(manifest, 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/ard.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.

<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/ard.json": {
      "get": {
        "x-zuplo-route": {
          "handler": {
            "export": "handleWellKnown",
            "module": "$import(./modules/well-known)"
          },
          "policies": {
            "inbound": []
          }
        }
      }
    }
  }
}
```

Deploy, then fetch `https://your-gateway/.well-known/ard.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), point
`url` at the authoritative OpenAPI artifact containing the server's `x-mcp`
metadata, put the actual transport endpoint in `metadata.endpoint`, and derive
`capabilities` from the tools you have chosen to surface. If you publish a real
conforming MCP server card, you can point the entry at that instead. The gateway
fronts the server, decides which tools are visible, and advertises that truthful
metadata through ARD 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.