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

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

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

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

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