Agent discoverability
View .mdAn 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
Section titled “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. Managed automatic ownership verification is a separate onboarding integration; installation configuration alone never makes a domain eligible.
The doors
Section titled “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
Section titled “Step 1: resolve the entry where you already fetch”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:
{ 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
Section titled “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:
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.
Step 2b: the footer row
Section titled “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.
import { AgentEntryRow } from '@kismet-tech/sdk/react';
<AgentEntryRow entry={entry} /> // marks, then the link under them<AgentEntryRow entry={entry} layout="inline" /> // one lineFor 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
Section titled “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.
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
Section titled “Step 4: robots.txt names the brief”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
Section titled “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).
const site = defineSite({ name, origin, pages, collectionSlug: 'cascadia-getaways' });const adapter = createKismetNextPageAdapter(site, { agentEntry: entry });
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
Section titled “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:
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:
{ "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.
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 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
Section titled “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:
{ "@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
Section titled “Step 7: check it”npx kismet check https://www.your-site.com --env productionsite/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
Section titled “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:
export const POST = (request: Request) => adapter.mcp(request);// app/connectors/[brief]/route.tsexport 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
Section titled “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
Section titled “The data functions”agentEntryForCollection(input): the entry for a slug and the briefs you hold, ornullfor 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 connectorlines, for a hand-rolled llms.txt.renderAgentEntryRowHtml(entry, { layout }): the footer row as an HTML string;<AgentEntryRow>from@kismet-tech/sdk/reactis the same row with copy on click.collectionForAgentsSubjectOf(slug, name): the inline TechArticle the Organization node carries.relayMcp(request, { collectionSlug })andrelayConnectorBrief(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.