# Poppy website discovery

> Serve collection-scoped Personal Agent Protocol discovery through the shared website agent entry.


Install one server-side AI service for your site. Use the same resolved entry for llms.txt, public page Markdown, the guest MCP card and Poppy discovery. Visible [agent marks](https://developers.kismet.travel/sdk/agent-marks.md) are presentation, not the installation or an access grant.

## Install the compatible SDK

Use the compatible `poppy02` release for both draft 0.1 and draft 0.2 discovery.
Verify the exact version is available in npm before installing:

```sh
npm install --save-exact @kismet-tech/sdk@0.2.0-beta.1-poppy02.20261019.2
npx kismet init --update
```

The existing `beta` tag is a separate Forms evaluation release; do not assume it
contains these helpers. Package installation does not activate Poppy, verify
ownership, grant API permissions, or establish authenticated-agent acceptance.

## Resolve and serve together

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

const site = defineSite({
  name: 'Example Stays',
  origin: 'https://www.example-stays.com',
  collectionSlug: 'example-stays',
  pages, // your existing public/private typed page definitions
});

// Run in server request/data loading, not once in a permanent static build.
const agentEntry = await resolveAgentEntry({
  host: new URL(site.origin).hostname,
  collectionSlug: site.collectionSlug,
});
const adapter = createKismetNextPageAdapter(site, { agentEntry });
```

Mount these handlers in your framework:

| Route | Handler | Purpose |
| --- | --- | --- |
| `/llms.txt` | `adapter.llmsTxt()` | Public page index, existing connector briefs and authorized Poppy link |
| Public page Markdown | `adapter.markdown(request)` or `adapter.html(request, render)` for `Accept: text/markdown` | Existing public content plus the same authorized Poppy link |
| `/.well-known/mcp.json` | `adapter.mcpServerCard()` | Existing signed-out collection MCP card |
| `/.well-known/poppy.json` | `adapter.poppyDiscovery()` | Website discovery or the centrally authorized collection manifest |
| `/robots.txt` | `adapter.robots({ environment: 'production' })` | Existing crawler signposts; not an access grant |

Private pages remain private. Poppy does not replace the existing guest MCP card or turn the brand MCP relay into a personal-agent token relay. Its manifest names the canonical issuer and broader MCP resource separately.

`resolveAgentEntry` opts into the shared anonymous authority at `GET /v1/public/agent-surface/connector?host=<exact-host>&surface=poppy` (with an `agent` parameter for each existing brief). The response adds `poppy`, `poppyMarkdown` and `poppyReadiness`. The SDK validates the manifest against the requested hostname; it does not infer permission from a collection slug, installed script or local configuration. It uses its own validated manifest to render the link.

The platform decides eligibility from stored verified **production** ownership and a current public collection. Apex and `www` are distinct for collection authorization; do not substitute one for the other. Staging registration alone cannot authorize collection Poppy access. Cloudflare and WordPress consume the same authority.

Refresh the resolved entry at least every five minutes, the SDK cache lifetime. Use `cache: false` for a fresh diagnostic lookup. Recreate the adapter with the refreshed entry; retaining an adapter forever retains its old answer. Collection manifests are served with `Cache-Control: no-store`; the bounded SDK lookup cache is separate. Final token/tool authorization remains server-side, including after ownership withdrawal.

When the authority is unavailable or returns no eligible manifest, llms.txt, Markdown and the guest MCP card keep working, without a collection Poppy link. The manifest falls back to website-only discovery without `auth` or `apis`. A mismatched host or collection manifest is refused, not relabelled as the current site's authority.

For a Node server, use `poppyDiscoveryResponse({ name, origin }, agentEntry?.poppy)` from `@kismet-tech/sdk/server`.

### Website-only draft selection

When the installed SDK exposes `poppyWebsiteProtocolVersion`, choose draft 0.2 for
a website-only staging installation explicitly:

```ts
const adapter = createKismetNextPageAdapter(site, {
  agentEntry,
  poppyWebsiteProtocolVersion: '0.2',
});
```

For a Node handler, pass `protocolVersion: '0.2'` to
`poppyDiscoveryResponse({ name, origin, protocolVersion: '0.2' })`. Existing
callers retain the 0.1 website-only default. This choice changes only the
website fallback: a resolved Company manifest passes through unchanged, and a
foreign authority is still refused. It does not enable Sessions or promote a
staging domain into production authority.

### Validate with the official Company kit

Use the [official Poppy Company skill](https://personalagentprotocol.org/kit/company/SKILL.md)
and its [downloadable kit](https://personalagentprotocol.org/kit/implement-personal-agent-protocol.zip).
Download each new audit's kit outside your application repository. Record its
retrieval time and SHA-256 before reviewing and running it; retain that exact
copy with the report. Fetching a newer kit starts a new evidence run rather than
rewriting a previous pass. Do not modify the validator to pass a check.

Start with `node <kit-dir>/validate/index.ts survey https://<exact-host>`.
The kit's live Session checks create real Sessions, so follow its test-identity
and production-approval instructions. Discovery-only or website-only success
does not demonstrate authenticated MCP, sign-in or checkout. Record skips and
manual checklist results alongside passes.


## Ownership proof during authorized installation

The SDK can deliver an existing domain challenge; it cannot issue a challenge, register ownership or mark a domain verified. An authorized installation operator uses the existing collection-domain tools:

1. Preview and confirm `prepare_collection_domain` for the registered exact hostname and authorized collection. The confirmed response supplies `hostname`, `proof` and `expires_at`.
2. Put that response into trusted server/deployment configuration, mapping `expires_at` to `expiresAt`. Do not accept proofs from query strings, cookies, browser forms or an anonymous lookup.
3. Publish the fixed HTTPS proof route below, then preview and confirm `verify_collection_domain`. The platform independently fetches the exact bytes, consumes the challenge and updates the stored domain.
4. Remove the proof after verification; expired proofs are refused automatically. If deployment takes longer than the challenge lifetime, prepare and publish a new challenge. Configuration alone never grants access.

```ts
const adapter = createKismetNextPageAdapter(site, {
  agentEntry,
  domainVerification: {
    hostname: prepared.hostname,
    proof: prepared.proof,
    expiresAt: prepared.expires_at,
  }, // prepared is the authorized operator response, never request input
});

// app/.well-known/kismet-domain-verification/route.ts
export const dynamic = 'force-dynamic'; // check expiry on every request
export const GET = (request: Request) => adapter.domainVerification(request);
export const HEAD = GET;
```

For a non-Next server, `domainVerificationResponse(request, { origin, proof })` is exported from `@kismet-tech/sdk/server`. GET returns the exact proof with no trailing newline; HEAD has no body. Responses use `text/plain`, `no-store` and `nosniff`. Wrong host, path, HTTP origin, missing or expired configuration return 404; writes return 405 for an otherwise valid binding. The helper never fetches a challenge or verifies ownership itself. Do not redirect this route from apex to `www`.

The Next.js SDK starter includes both well-known routes. Store the authorized
prepare result as server-only `KISMET_DOMAIN_VERIFICATION_PROOF` JSON with
`hostname`, `proof` and `expiresAt` (mapped from `expires_at`), and configure
`NEXT_PUBLIC_SITE_URL` as that exact HTTPS origin. The proof handler does not load
the catalog or call the anonymous connector. Remove the binding after successful
verification or expiry; never populate it from browser input.

Run `npx kismet check <url> --env production`. The opt-in
`site/poppy-discovery` warning checks the exact-host manifest against `llms.txt`,
public Markdown and the guest MCP card's collection. Sites that do not advertise
Poppy are unchanged. Website-only discovery is valid without collection auth.
These are consistency checks, not ownership verification or session acceptance;
use `checkPoppyReadiness` and the acceptance sequence below for those boundaries.

Automatic prepare, publish and verify during managed installation is a separate onboarding integration. Do not assume an installed worker/plugin or this SDK helper has completed it: check the stored ownership result, then discovery and registered-agent acceptance. There is no additional public onboarding API implied by this guide.

## Capture agent fetches before the response cache

Publishing or updating the SDK does not instrument an existing application. Adopt
one server-side capture owner for each route: middleware/edge **before the cache**
for static or CDN-served documents, or the dynamic adapter when every request
reaches its handler. Do not enable both for the same fetch. The helper's
per-Request deduplication is not a distributed deduplication guarantee.

Canonical public `/markdown` and `…/markdown` routes are document fetches for
both GET and HEAD, including requests without an `Accept` header. Capture keeps
the original URL, query string and User-Agent; do not rewrite request headers to
make an otherwise untracked request appear to negotiate Markdown. Keep private
routes and ownership proofs outside both machine and legacy page-view capture.

Use the collection tracking key supplied through [telemetry onboarding](https://developers.kismet.travel/telemetry.md),
stored as server-only `KISMET_TRACKING_KEY`. It is not your Developer API server
key. If a managed worker, plugin or existing middleware already owns these fetch
events, verify that capture instead of adding a second SDK emitter.

```ts
import { recordAgentFetch } from '@kismet-tech/sdk/edge';

// In your host's middleware/edge request hook, before returning a cached response:
recordAgentFetch(request, {
  origin: 'https://www.example-stays.com',
  collectionSlug: 'example-stays',
  trackingKey: serverTrackingKey, // trusted installation setting; never browser code
  waitUntil: (pending) => context.waitUntil(pending),
  environment: 'production',
});
// Continue normal serving. Omit responseStatus unless this plane knows it.
```

The host supplies the incoming `Request` and its request-lifetime scheduler. Do
not replace the incoming User-Agent with the SDK's identity or truncate it.
`recordAgentFetch` retains the exact URL, installed collection, serving domain,
original User-Agent (including unknown or missing values), and marks the event as
`actionType: 'fetch'` without a human session. It records GET and HEAD; it does not
issue agent sessions or confer access. Static files still need this serving-plane
hook even when the app's dynamic routes are instrumented.

For dynamic-only routes, use the alternative adapter-owned capture:

```ts
const adapter = createKismetNextPageAdapter(site, {
  agentEntry,
  fetchTracking: {
    trackingKey: serverTrackingKey,
    waitUntil: (pending) => context.waitUntil(pending),
    environment: 'production',
  },
});

export const GET = (request: Request) => adapter.poppyDiscovery(request);
export const HEAD = GET;
```

With `fetchTracking` enabled, pass the real request to `llmsTxt`, `mcpServerCard`,
`poppyDiscovery`, and the other mounted discovery handlers. HEAD responses have
no body. A PHP-only WordPress hook cannot see a file or page served directly by a
static server/CDN: add edge/host capture or explicit cache exclusions, then prove
the deployed behavior. Cloudflare workers likewise must capture early discovery
returns, not only the normal page-render path.

Before claiming tracking coverage, request every installed surface (`llms.txt`,
`llms-full.txt` when present, public Markdown including negotiated Markdown,
Poppy and MCP discovery). Use uniquely marked GET and HEAD requests with a known
agent UA, an unknown UA, and no UA. Repeat the **same URL** to exercise warm-cache
hits. Verify matching persisted `content_events`: full original UA, exact URL,
installed collection/domain, `action_type = fetch`, and no human session. Check
the request count for duplicate capture. A successful manifest response, a
tracking-key health response, or a passing source test is not this evidence.

## Versioned issuer and token contracts

Compatible adapters accept both draft 0.1 and draft 0.2 central collection
manifests. They preserve the authority's version; they do not relabel an older
document or infer a company from a hostname or configured collection slug.

For **draft 0.1 compatibility**, the issuer is `https://kismet.travel` and the
legacy endpoint is `https://kismet.travel/api/oauth/token`. Send the manifest's
`kismet.token_parameters` on issuance and renewal; its required `kismet_domain`
extension selects the exact verified website scope. Omitting that parameter is
not a collection-scoped request. Its existing MCP resource remains
`https://mcp.kismet.travel/mcp`; ordinary anonymous/guest OAuth clients keep that
existing entry point.

For **draft 0.2**, each company has a stable issuer:
`https://kismet.travel/poppy/collections/{collection_id}`. The `collection_id`
is the stored lowercase UUID in the central manifest, not a slug or a value to
construct locally. Kismet's separate network issuer is
`https://kismet.travel/poppy`, with `collection_id: null`; it does not grant a
partner collection's authority.

Fetch RFC 8414 metadata by inserting `/.well-known/oauth-authorization-server`
before the issuer path, for example
`https://kismet.travel/.well-known/oauth-authorization-server/poppy/collections/{collection_id}`.
Require exact issuer agreement and the website in `poppy_domains` before using
the metadata's issuer-specific `token_endpoint` and `revocation_endpoint`.
Both client and session assertion `aud` values are that exact issuer, not the
token endpoint, website hostname or MCP URL. A company must not renew through
another company's issuer or the network issuer.

Draft 0.2 `kismet.token_parameters` may be `{}` or contain a matching
`kismet_domain` legacy adapter hint. Standard 0.2 clients do not need that hint;
it never selects or changes the UUID-bound company scope. Do not carry the 0.1
parameter-selection behavior into the new endpoints.

The draft 0.2 MCP URL and resource are `https://kismet.travel/poppy/mcp`, shared by
the eligible company issuers. This entry requires a verified 0.2 Poppy token for
every POST; it does not replace the older public/guest MCP entry point. Its
protected-resource metadata is
`https://kismet.travel/.well-known/oauth-protected-resource/poppy/mcp` and must
include the company's exact issuer.

General 0.2 session creation requires an ES256 DPoP proof. This MCP resource's
protected-resource metadata does **not** advertise DPoP: request its Bearer token
without a proof and explicitly set `resource` to that exact URL for issuance and
renewal. This exception does not authorize Bearer tokens on other APIs or
downgrading a DPoP-bound token. See the [pinned draft 0.2 specification](https://github.com/personal-agent-protocol/personal-agent-protocol/blob/dc5730d3150037ac63c385de68a4c48347ee4adc/content/versions/0.2/spec.mdx).

These are signed-out discovery and published-inventory contracts. No guest
sign-in, account, booking or payment permissions are advertised. Use the
registered personal-agent protocol client, not guest OAuth helpers, for these
assertions and tokens.

## Discovery is not complete acceptance

Run `checkPoppyReadiness({ origin })` from `@kismet-tech/sdk/server` after installation. It checks the served manifest and version-specific issuer-domain agreement. For 0.2 it also checks issuer-specific token/revocation metadata, JWT-bearer/private-key/ES256 capabilities and the declared MCP resource's issuer/Bearer metadata. Its result deliberately keeps `session: 'unverified'` and `ready: false`: public discovery alone cannot prove session issuance, cross-collection denial or usage attribution. A registered-agent acceptance run must verify those separately before advertising full support.

The website-only helper retains its draft 0.1 output for compatibility and grants
no authentication authority. A centrally authorized 0.2 manifest passes through
the compatible adapter as 0.2. Do not enable 0.2 discovery for an old installed
parser: publish and adopt compatible adapters first, or continue serving a
negotiated compatible version. A source change does not upgrade pinned packages
or static files, and passing these checks is not a deployment/conformance claim.

Before calling a deployment fully supported, verify the exact production host's GET/HEAD manifest, llms.txt and a real public Markdown page. Then use a registered agent to prove current published inventory access, denial of another collection and its properties, same-host renewal, refusal of omitted/cross-host renewal, and durable collection/domain usage attribution. Repeat with a second collection. Agent authentication is not guest sign-in and does not confer payment authority.

For 0.2, also prove that unauthenticated `initialize` on the strict MCP URL returns
HTTP 401 and that a correctly bound registered-agent token succeeds. Anonymous
access on the older public MCP entry is not evidence for this check.
`checkPoppyReadiness` reads public metadata; it does not issue tokens or perform
this authenticated MCP acceptance run.

Keep source tests, published SDK/plugin version, deployed server/worker revision and live acceptance evidence separate. A merged change, HTTP 200, website-only manifest or `discovery: 'verified'` is not complete acceptance. Confirm your installed release exports these helpers before using them; a source change does not update already pinned packages. Install the compatible server routes once, then refresh central configuration for supported changes. A script embed can render marks but cannot create well-known HTTP routes or establish ownership.
