Zuplo
Model Context Protocol

Publish an Agentic Resource Discovery Manifest with Zuplo

Martyn DaviesMartyn Davies
June 30, 2026
7 min read

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.

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, 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.

Best for:
  • 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, 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. The shape follows the spec’s publishing guide:

JSONjson
{
  "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 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:

plaintext
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.

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

TypeScriptts
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.

Custom function handlers in Zuplo

The full handler API, including how to read params and query strings and return a Response with custom headers.

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.

Common 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.

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

JSONjson
{
  "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, 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.