Skip to content
KismetKismetDevelopers
Beta accessllms.txt

Agent discoverability

View .md

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.

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.

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.

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

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.

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.

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

app/.well-known/mcp.json/route.ts
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.

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:

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:

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

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

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:

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.

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