Draft v0.1 — published for review. Not final until approved; may change incompatibly before 1.0.

Reference holder — connect your portal in an hour

On this page
  1. 1. Run it (5 minutes)
  2. 2. Put it on your domain (15 minutes)
  3. 3. Plug in your mailer and your search (30 minutes)
  4. 4. Get listed (10 minutes)
  5. 5. Optional: recognise the forwarder
  6. 6. Test calls
  7. Tests

A complete Isto Intents intent server in one file: holder.mjs (Node 20 or newer, no dependencies) with its tests, holder.test.mjs. It implements the consent-link profile exactly as the registry check and the forwarder expect it:

What it deliberately does not do: decide which of your listings fit a wish. That is your search, your ranking and your notifications. The server hands you the confirmed wishes and records what your search delivers.

1. Run it (5 minutes)#

HOLDER_HOST=www.your-portal.pt HOLDER_OPERATOR="Your Portal" HOLDER_CURRENCIES=EUR PORT=8787 node holder.mjs
variable meaning
HOLDER_HOST your public host — the one that will publish the descriptor
HOLDER_OPERATOR your name as people should see it
HOLDER_CURRENCIES currencies you accept for a hidden limit, e.g. EUR or PLN,EUR
HOLDER_DATA_FILE where the state is kept (default holder-data.json) — it contains e-mail addresses, so treat it as personal data
TRUST_IP_HEADER behind a proxy: the header with the client address, e.g. cf-connecting-ip
FORWARDER_KEY optional, see §5

2. Put it on your domain (15 minutes)#

Route these two paths of https://<your host> to the process (any reverse proxy):

3. Plug in your mailer and your search (30 minutes)#

Use it as a module instead of the command line:

import { createServer } from "node:http";
import { createHolder } from "./holder.mjs";

const holder = createHolder({
  host: "www.your-portal.pt",
  operator: "Your Portal",
  currencies: ["EUR"],
  dataFile: "/var/lib/holder/data.json",
  sendMail: ({ to, subject, text }) => yourMailer.send({ to, subject, text }),
  consentText: "I want to receive e-mails with listings that match this wish …", // the same text as your other channels
});
createServer((req, res) => holder.handle(req, res)).listen(8787);

// Your search, as often as you like (e.g. every hour):
for (const wish of holder.activeIntents()) {
  // wish: { id, statement, niceToHave, buyBy, hiddenLimit, activeSince, delivered }
  for (const listing of await yourSearch(wish.statement, { maxPrice: wish.hiddenLimit?.amount })) {
    await holder.deliver(wish.id, { url: listing.url, title: listing.title, price: listing.price });
  }
}

deliver() sends each listing once, e-mails it with a stop link and makes it visible to intent_list_matches. Its url must be on your host — a forwarder refuses anything else. Optionally give understand: (statement) => ({ understood: [...], notUnderstood: [...] }) so the assistant can tell the person what you will and will not watch for.

4. Get listed (10 minutes)#

  1. Publish https://<your host>/.well-known/isto-intents.json — copy the example descriptor, set your host, operator, countries, categories and languages, and point servers[].url at https://<your host>/mcp/intents with "profiles": ["consent-link"].
  2. On istointents.org/join check your host and tick trial run: the registry calls your four tools once, as a forwarder would, and tells you whether you are ready.
  3. Submit: POST https://istointents.org/registry/v0/submit with { "host": "<your host>" }.
  4. To receive wishes from people whose AI assistant does not know you yet, add "forwarding": { "accept": "requested", "terms": "https://istointents.org/forwarding/terms/" } to the descriptor. Demand by country shows what is being asked.

5. Optional: recognise the forwarder#

The forwarder at https://istointents.com/mcp reaches you from one shared address for all of its users, so a per-network limit would put them all in one bucket. It signs every call (forwarding §3 rule 6):

x-isto-forwarder: <sig>.<ts>      sig = base64url(HMAC-SHA256(your key, "<ts>|<your host>")), ts = Unix seconds

Your key is issued per host: it opens only your door, so nobody else who verifies the header can sign calls to you, and you cannot sign calls to anybody else. Ask the forwarder's operator for it through istointents.com and set it as FORWARDER_KEY. holder.mjs then checks the signature against your host from your own configuration (never the request's Host header), accepts ±300 seconds, compares in constant time and gives verified calls their own, larger limit. Without a key the server works the same; it just cannot tell forwarder calls apart.

6. Test calls#

A request with x-isto-test: 1 (the registry's trial run, or a forwarder relaying a test) is answered exactly like any other, but not counted in holder.stats and never returned by activeIntents() (MCP binding, profile rule 8).

Tests#

node --test holder.test.mjs

They cover the exact interface, the sealed limit, the refusal of undeclared arguments, the double opt-in (a link opened by a mail scanner confirms nothing), first-come consent, delivery on your host only, withdrawal, expiry, the forwarder proof (wrong host, expired, wrong key) and restarts.

Licence: CC BY 4.0, like the rest of the protocol's public part.