# Agent discoverability

> Put the doors to your collection where an AI agent finds them. One resolved agent entry (the For AI agents page, the unmarked guest MCP endpoint, the brief each agent reads on your host) feeds the marks beside share and save, your footer, llms.txt and robots.txt, so your site says the same thing a Kismet-rendered page says. agentEntryForCollection and resolveAgentEntry from @kismet-tech/sdk/server.



An AI agent that reaches your site, or a person who plans with one, needs
to find the door to *your* collection: the page that tells an agent how to
connect and lists the homes open for a stay, the guest MCP endpoint scoped
to your inventory, and the short brief each agent reads to wire itself in.
Every surface Kismet renders itself (kismet.travel storefronts, the
WordPress fixtures, the edge worker on partner domains) prints those doors
from one authority. This guide is how a site built on the SDK prints the
same ones, so a page says the same thing whichever renderer printed it.

**The division of labor: Kismet decides the addresses, the site decides
where they appear.** Which brief your host names for each agent, and
whether it is your own brand brief or the shared Kismet one, is decided by
Kismet, never by your site. The SDK asks, caches the answer for the five
minutes Kismet advertises, and hands you one object to print from.

## Install one AI service

Keep llms.txt, public page Markdown, the MCP card and Poppy discovery on the same
server/edge installation and the same resolved agent entry. The visible marks
can be restyled or hidden without granting or removing collection access.
For the complete route setup, expiring ownership-proof responder and acceptance
boundaries, read [Poppy and the unified AI service](https://developers.kismet.travel/sdk/poppy-discovery.md).
Managed automatic ownership verification is a separate onboarding integration;
installation configuration alone never makes a domain eligible.

## The doors

For a collection with the slug `cascadia-getaways`:

| Door | Address | How it is printed |
|---|---|---|
| The For AI agents page | `https://kismet.travel/c/cascadia-getaways/for-agents` | a link, in the footer and in llms.txt |
| The guest MCP endpoint, unmarked | `https://kismet.travel/c/cascadia-getaways/mcp` | **text, never a link**: it answers POST only, and a link sends a fetching agent to a 405 |
| The Kismet app in ChatGPT | the plugin's address | a link, the ChatGPT mark |
| One brief per agent (Muse, Instinct) | `https://<your host>/connectors/muse.md` when your brand brief is on, else `https://mcp.kismet.travel/connectors/muse.md` | a link, the agent's mark; a line in llms.txt and a comment in robots.txt |

Only a brief hands out the marked form of the endpoint (`?src=muse`); a
page prints the unmarked one, or every agent that reads the page would be
labelled as that one.

## Step 1: resolve the entry where you already fetch

```ts
import { resolveAgentEntry } from '@kismet-tech/sdk/server';

// In your data layer, a route handler or a build step. Never inside a
// component's render: the answer is cached for 300 s, the marks still
// read Kismet's served list in the browser on first intent.
const entry = await resolveAgentEntry({
  host: 'www.cascadiagetaways.com',   // your site's own host, or its origin
  collectionSlug: 'cascadia-getaways', // the fallback for a host Kismet does not serve yet
  collectionName: 'Cascadia Getaways',
});
```

`resolveAgentEntry` asks `GET /v1/public/agent-surface/connector?host=&agent=`
once per agent, the same anonymous read kismet.travel's robots.txt and the
WordPress fixtures make. The answer:

```ts
{
  slug: 'cascadia-getaways',
  name: 'Cascadia Getaways',
  forAgentsUrl: 'https://kismet.travel/c/cascadia-getaways/for-agents',
  mcpUrl: 'https://kismet.travel/c/cascadia-getaways/mcp',
  chatgptUrl: 'https://chatgpt.com/plugins/...',
  marks: [
    { agent: 'muse', name: 'Muse', briefUrl: 'https://www.cascadiagetaways.com/connectors/muse.md', href: '...', pasteLine: 'Read ... and connect to Kismet, then find me a place to stay with Cascadia Getaways.' },
    { agent: 'instinct', name: 'Instinct', briefUrl: '...', href: '...', pasteLine: '...' },
  ],
  briefs: { muse: '...', instinct: '...' },
}
```

It fails closed, per agent: a brief Kismet did not name, or one that is
not an absolute https address at that agent's brief path, is left null, and
that mark's `href` falls back to the For AI agents page, which names the
briefs. It never throws. A host Kismet does not serve yet gets the shared
briefs and the doors of the `collectionSlug` you passed; with no slug from
either side the answer is `null`, so print nothing rather than a guess.

`agentEntryForCollection({ collectionSlug, collectionName, briefs })` is
the pure half, when you already hold the briefs (a build that read them
once, a test).

## Step 2: the marks carry the briefs from the first byte

Hand `entry.briefs` to the marks beside share and save, so the server HTML
links the exact briefs your host names and an agent that only fetches the
page sees them:

```tsx
import { AgentMarks } from '@kismet-tech/sdk/react';

<AgentMarks
  kind="property"
  collectionSlug={entry.slug}
  collectionName={entry.name}
  briefs={entry.briefs}
  homeName={home.name}
/>
```

Everything else about the marks (the four ways to put them in a site, the
served list, restyling) is in [The agent marks](https://developers.kismet.travel/sdk/agent-marks.md).

## Step 2b: the footer row

Under the place your footer names Kismet, print the row every Kismet-rendered
page prints: the ChatGPT mark, the Muse and Instinct marks (a click copies
the one line that connects that agent), and "For AI agents" linking the
collection's page. Stacked, in the footer's own muted colour, never a loud
block.

```tsx
import { AgentEntryRow } from '@kismet-tech/sdk/react';

<AgentEntryRow entry={entry} />                    // marks, then the link under them
<AgentEntryRow entry={entry} layout="inline" />    // one line
```

For a template that is not React, `renderAgentEntryRowHtml(entry)` from
`@kismet-tech/sdk/server` is the same row as a string, with no script: each
agent mark carries `data-paste`, so your own script can copy it on click,
or the marks stay plain links to the briefs. The classes
(`kismet-agent-entry`, `kismet-agent-entry-marks`, `kismet-agent-mark`,
`kismet-agent-entry-link`) are hooks for your CSS; the marks are
`currentColor`.

## Step 3: llms.txt names the doors

Hand the entry to `llmsTxt`, `llmsTxtForSite` or the Next page adapter and
the `## Agent connector` section at the top of your `llms.txt` prints the
same lines a Kismet-rendered WordPress site prints: the brief each agent
reads on your host, the collection's guest MCP endpoint (unmarked), and the
For AI agents page.

```ts
import { createKismetNextPageAdapter, defineSite } from '@kismet-tech/sdk';
import { llmsTxt } from '@kismet-tech/sdk/server';

// A site on the page adapter:
const adapter = createKismetNextPageAdapter(defineSite({ name, origin, pages }), { agentEntry: entry });
// app/llms.txt/route.ts: export const GET = () => adapter.llmsTxt();

// Or the renderer directly:
llmsTxt({ siteName: entry.name, sections, agentEntry: entry });
```

```
## Agent connector

- [Muse connector brief](https://www.cascadiagetaways.com/connectors/muse.md): the brief Muse reads to connect itself to Cascadia Getaways through the Kismet guest MCP.
- [Instinct connector brief](https://www.cascadiagetaways.com/connectors/instinct.md): the brief Instinct reads to connect itself to Cascadia Getaways through the Kismet guest MCP.
- [Guest MCP endpoint](https://kismet.travel/c/cascadia-getaways/mcp): this collection's inventory only (Streamable HTTP, JSON-RPC 2.0 over POST). No account or API key.
- [For AI agents](https://kismet.travel/c/cascadia-getaways/for-agents): how an agent connects and what it can do here.
```

An agent whose brief is not known is left out of the section (a gap, never
a guess); the two doors always print. With no entry, `llmsTxt` prints the
shared Kismet brief as before, or the one `connectorBriefUrl` you pass.

## Step 4: robots.txt names the brief

```ts
// app/robots.txt/route.ts
export const GET = () => adapter.robots();

// Or the renderer directly:
import { aiRobots } from '@kismet-tech/sdk/server';
const museBrief = entry.marks.find((m) => m.agent === 'muse')?.briefUrl ?? null;
aiRobots({ environment: 'production', sitemapUrl, llmsUrl, connectorBriefUrl: museBrief });
```

The adapter's `robots()` prints the sitemap and llms.txt signposts on your
origin and the brief your host names for Muse from the entry (none when the
entry has no brief for it, never a guess); `environment` and `sitemapUrl`
can be overridden. This SDK release keeps one `# Agent-Connector:`
comment in robots.txt, naming the Muse brief. The Poppy helpers do not change
that behavior; additional connector comments require a compatible SDK update
to the shared robots integration. Passing the brief from the entry, not by hand,
keeps robots.txt, llms.txt and the marks naming the same brief; the
`site/agent-connector` conformance check warns when they disagree.

## Step 5: the MCP server card

Tell the site which collection it is, and serve the card every Kismet
collection answers at `/.well-known/mcp.json`: the unmarked guest MCP
endpoint, its transport, and the GET routes an agent can use with no MCP
client (the For AI agents page, its dated search, a home's page and rates,
Agent Checkout).

```ts
const site = defineSite({ name, origin, pages, collectionSlug: 'cascadia-getaways' });
const adapter = createKismetNextPageAdapter(site, { agentEntry: entry });

// app/.well-known/mcp.json/route.ts
export const GET = () => adapter.mcpServerCard();
```

Point every page at it with the head tag `MCP_SERVER_LINK_TAG`
(`<link rel="mcp-server" href="/.well-known/mcp.json">`), or the `Link`
header `MCP_SERVER_LINK_HEADER`, the way the edge worker does on a partner
domain. `mcpServerCardForCollection(slug)` is the card as data and
`mcpServerCardResponse(slug)` the response, for a site not on the adapter.

## Step 5b: Poppy website discovery

Use a compatible SDK release that exports `poppyDiscovery`; confirm the installed
package, not just repository main. Existing installations do not gain a server
route merely because a new SDK release exists.

For Personal Agent Protocol (Poppy) draft 0.1, also mount a real server route:

```ts
// app/.well-known/poppy.json/route.ts
export const GET = () => adapter.poppyDiscovery();
```

The adapter uses your configured HTTPS site origin and organization name, not a
request's Host header. It returns a public JSON document cached for five minutes:

```json
{
  "protocol_version": "0.1",
  "organization": { "name": "Example Stays", "domain": "www.example-stays.com" },
  "web": {}
}
```

Without a verified collection manifest, this is **website discovery only**: agents browse as ordinary signed-out visitors.
It does not advertise Poppy sessions, account access, APIs, or a company agent.
Poppy requires a compatible session issuer before those interfaces can be declared;
Kismet's existing MCP discovery remains separate. See the
[Poppy discovery specification](https://personalagentprotocol.org/docs/spec).

For verified collection domains, pass the resolved agent entry to the adapter to
serve the centrally authorized MCP manifest and add its link to llms.txt and
Markdown. See [Collection Poppy discovery](https://developers.kismet.travel/sdk/poppy-discovery.md) for the
shared authority contract and readiness checks.

Without the Next adapter, call `poppyDiscoveryResponse({ name, origin })` from
`@kismet-tech/sdk/server`, or serialize `poppyDiscoveryForSite({ name, origin })`
at `POPPY_DISCOVERY_PATH`. Invalid inputs return null or HTTP 404. The adapter
also returns 404 when no collection is configured or the origin is not HTTPS.

A script embed cannot create a well-known HTTP route. Keep this route in the
one-time server/edge installation even when the visible marks use a script.
For collection authorization, its organization domain and token domain parameter
must match the exact requested hostname. Never infer apex/`www` equivalence;
use a separate configured response for a different staging host.

On a Kismet-managed edge install, the worker serves this route from the public
collection name and requested HTTPS hostname. Rollout requires the updated worker
and a domain configuration sync; existing configurations without an organization
name return 404. Turning off visible agent marks does not remove this discovery
file. No API credential or guest session is included.

## Step 6: the structured data names the door

With `collectionSlug` on the site, every page's Organization node carries
the For AI agents page as its `subjectOf`, inline, the same node a
Kismet-rendered page emits:

```json
{
  "@type": "Organization",
  "@id": "https://www.cascadiagetaways.com/#organization",
  "name": "Cascadia Getaways",
  "subjectOf": {
    "@type": "TechArticle",
    "@id": "https://kismet.travel/c/cascadia-getaways/for-agents",
    "name": "Cascadia Getaways for AI agents",
    "url": "https://kismet.travel/c/cascadia-getaways/for-agents"
  }
}
```

Nothing to call: `<Kismet.Page site={site}>` and `jsonLdForPage(page, site)`
read the slug from the site. A collection Organization built from the
manager entity carries it from the manager's own slug.

## Step 7: check it

```bash
npx kismet check https://www.your-site.com --env production
```

`site/agent-entry` (a warning, production only) reads your `llms.txt` and
your `/.well-known/mcp.json` and says when they name different collections,
when one names a door the other does not, or when the card names a marked
endpoint. It grades only a site that has started to name its doors, so a
site that never did stays clean. `site/agent-connector` still checks that
`robots.txt` and `llms.txt` name the same brief.

## Step 8: your own domain as the door

Which host a door is on is decided by Kismet, not by the site. The
authority names the brand host for the guest MCP endpoint only after a
stored check saw that host answer an MCP initialize and serve the brief;
until then every door is on kismet.travel. The entry carries the answer
(`mcpUrl`, `mcpHost`, `signInMcpUrl`, `federatedMcpUrl`, `forAgentsUrl`)
and the card, llms.txt and the marks print it; a site spells no host.

To pass that check, run the two relays on your domain:

```ts
// app/c/[slug]/mcp/route.ts
export const POST = (request: Request) => adapter.mcp(request);
// app/connectors/[brief]/route.ts
export const GET = (request: Request) => adapter.connectorBrief(request);
```

`adapter.mcp` relays `POST /c/<slug>/mcp` for the site's own collection to
kismet.travel, forwarding the body, the query, Content-Type, Accept,
Mcp-Session-Id and User-Agent and returning the upstream answer unchanged;
it never forwards Authorization (sign-in through a brand host is a separate
decision). `adapter.connectorBrief` relays `GET /connectors/<agent>.md`
from the platform renderer with its Content-Type. Register and verify your
domain on the collection and set the site origin to the canonical host your
routes are stored on; the next domain sync runs the check and the doors
move to your domain with no change to your code.

## The rules a page keeps

- Every mark is a real `<a href>`, with its name as the SVG `<title>` and as
  text inside the link.
- The guest MCP endpoint is printed as text, never linked.
- No address carries a session id, a quote token or a marked endpoint.
- No brief is written by hand: the entry comes from the authority.
- Subtle marks, never a loud block. A section inside the page's main content
  that tells a guest how to connect an assistant was tried on partner sites
  and removed; the marks beside share and save, the footer row and the
  machine-readable files are the surfaces that stayed.

## The data functions

- `agentEntryForCollection(input)`: the entry for a slug and the briefs you hold, or `null` for a slug that is not shaped like one.
- `resolveAgentEntry(options)`: the entry for a host, from the authority, cached.
- `collectionMcpUrl(slug)`, `collectionForAgentsUrl(slug)`: the two doors as addresses.
- `agentPasteLine(briefUrl, collectionName)`: the one line a person pastes.
- `agentEntryLlmsLines(entry, siteName)`: the `## Agent connector` lines, for a hand-rolled llms.txt.
- `renderAgentEntryRowHtml(entry, { layout })`: the footer row as an HTML string; `<AgentEntryRow>` from `@kismet-tech/sdk/react` is the same row with copy on click.
- `collectionForAgentsSubjectOf(slug, name)`: the inline TechArticle the Organization node carries.
- `relayMcp(request, { collectionSlug })` and `relayConnectorBrief(request, { host })`: the two relays, for a site not on the adapter; `KISMET_FEDERATED_MCP_URL`, `isCollectionMcpUrl`.
- `mcpServerCardForCollection(slug)`, `mcpServerCardResponse(slug)`, `agentGetRoutesForCollection(slug)`, `MCP_SERVER_CARD_PATH`, `MCP_SERVER_LINK_TAG`, `MCP_SERVER_LINK_HEADER`: the card and where it lives.
- `poppyDiscoveryForSite({ name, origin })`, `poppyDiscoveryResponse({ name, origin })`, `POPPY_DISCOVERY_PATH`: website-only Poppy discovery, from the root and server entries.
- `CONNECTOR_AGENTS`, `AGENT_DISPLAY_NAMES`, `CHATGPT_APP_URL`.

The pure functions are exported from `@kismet-tech/sdk`, `@kismet-tech/sdk/server`
and `@kismet-tech/sdk/react`; `resolveAgentEntry` from `@kismet-tech/sdk`
and `@kismet-tech/sdk/server`.
