npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@muretai/agent-web-router

v0.6.2

Published

Help your agent behave on the websites it visits. A person already in the tab keeps the page -- this does not replace WebMCP. When the agent is alone and the site already published an Agent Card, it takes that door, checks it is theirs, and knocks. It doe

Readme

Agent Web Router

npm LICENSE

Help your agent behave on the websites it visits.

A person already in the tab keeps the page — this does not replace WebMCP. When the agent is alone and the site already published an Agent Card, it takes that door, checks it is theirs, and knocks. It does not follow a pointer to someone else's door.

Contents

What this is

Is a person in the tab?

| Who is looking | What the site already published | What this package does | |---|---|---| | A person is in the tab | WebMCP (the usual case) | Stay on the page. Do not replace WebMCP. | | A person is in the tab | WebMCP and an Agent Card | The page is still theirs. The card is there if this visit should become a customer. | | The agent is alone (including headless) | an Agent Card | Take the card. Check it is theirs. Knock. Use what the card published now — do not open WebMCP first. | | The agent is alone | WebMCP only, no card | The page, only if a browser is on hand and robots.txt allows. |

An Agent Card is any A2A door, not one vendor. If there is nothing to take, the package says why and exits 2.

Opening the page first is how an agent follows a rewritten address — carrying your signed identity — to someone else's door.

MCP is a tool account behind a token. A different purpose. The probe will mention it when it is there.

| | | |---|---| | Who installs this | People whose agent visits sites it did not build — harness authors, IDE agents. | | Not for | Website owners putting up a door. This package does not run on the site. | | The problem | Opening the page first can take the agent's signed identity to someone else's door. |

The router does four things and nothing else: probe an origin (GET only — it never runs page script, never opens a browser), route (the card when the visit is the agent's; the page only when a person is already there), check a handoff — a tool result that says "the rest of this happens at the card / on this page / on this server" — against the site's own card, and knock: POST one signed A2A message/send at the URL the card names, with a key the agent already holds, and verify the signed reply. It does not browse, scrape, or mint keys: when a person is already on the page, it hands the page to the browser your harness already has.

Zero dependencies. Node ≥ 20. MIT. Status: 0.6.2npm i @muretai/agent-web-router, or run it directly: npx @muretai/agent-web-router probe <url>.

Try it

node bin/agent-web-router.mjs probe https://shop.example
node bin/agent-web-router.mjs probe https://shop.example --person      # a person is in the tab
node bin/agent-web-router.mjs probe https://shop.example --browser     # you can run one headless
node bin/agent-web-router.mjs probe https://shop.example --token       # you hold an MCP credential
node bin/agent-web-router.mjs probe https://shop.example --no-key      # you hold no signing key yet
node bin/agent-web-router.mjs probe https://shop.example --json

A probe reads the origin you name with GET only: the Agent Card at /.well-known/agent-card.json (falling back to the legacy /.well-known/agent.json) and its signature at /.well-known/agent-card.sig.json (plus the card's A2A additionalInterfaces, extension URIs, domains, and whether /.well-known/did-configuration.json proves the card's DID for this origin — the Domain Linkage Credential is verified: signature under the DID's own key over the JWS text, origin match, and a mandatory exp, each failure named); the MCP server card at /.well-known/mcp.json (SEP-2127, a draft at the time of writing); and the front page's HTML, from which it lists the declarative WebMCP tools (<form toolname=… tooldescription=…>) and reports a hint when the imperative API (document.modelContext) is referenced — the imperative tool list is only observable by running the page, so the probe never claims it.

Output, for a site that published a card and a couple of declarative tools:

agent-web-router 0.6.2 · https://shop.example

ways in
  card   did:key:z6MkExample…
         signed: yes  door: https://shop.example/  skills: 1
  mcp    not declared (404)
  page   2 declarative tool(s) (book_table, hours); imperative hint: modelContext referenced in the page

on hand: key yes  token no  browser no  person no

route  (order: default)
  1. card  the door answers one signed message; your key is all it needs

excluded
  mcp   no MCP server card (/.well-known/mcp.json answered 404)
  page  the page has tools but you carry no browser; 2 declarative form(s) name an action and could be submitted as plain HTTP

knock: POST https://shop.example/  to did:key:z6MkExample…  sign contextId,from,messageId,text,timestamp,to  how-to https://shop.example/how-to

Every way appears in exactly one of route and excluded, so "why not X" is always answered. Exit status is 0 when there is something to take and 2 when there is not.

Signposts. Sites describe themselves to agents in many competing ways — robots.txt (per AI-crawler verdicts and Content-Signal), llms.txt and llms-full.txt, a Markdown edition on Accept: text/markdown, JSON-LD @types, a Link header pointing at the card, VOIX <tool> elements beside WebMCP forms. The probe reads the ones sites actually deploy and reports them in one signposts block so a visitor sees the whole of what the site put up without knowing every convention. None of them is a way in, so none of them creates a route.

The skill. SKILL.md is the same procedure written for an agent to follow by hand — the GETs, the checks, the refusals — for a harness that cannot run Node. The command line is the version that cannot get the refusals wrong. Distilling those refusals from probe/knock traces, without letting prose override the interceptor, is spec/skill-distill.md. On this machine only: npm run distill labels the fixtures with route / checkHandoff, writes generated/rules.json, and measures whether the rules beat first-match. npx @muretai/agent-web-router probe <url> --json | npm run distill:record appends your probe to var/traces.jsonl. Nothing is uploaded.

Probe

The rules a probe enforces

These are the checks that make the card route safe to take. Each is tested by actually attempting the attack in test/.

  • The card must be served by the origin you dialled. A redirect to another host is a substitution, not a card.
  • The card's url must name the origin you dialled, or the door it points at is not this site's — the route is excluded.
  • A signature that is present and fails is a refusal, never "unsigned". A card that fails its own signature is exactly what a substituted card looks like. An absent signature is allowed through with a note: the door's signed reply is what proves the key.
  • The MCP server card binds only its own origin. An endpoint on another origin — or a server card served from one — is excluded, exactly as a card is: /.well-known/mcp.json is the origin's statement about itself, and an endpoint elsewhere carries no DID a reply could ever be verified under.
  • A probe is GET only. Nothing here POSTs; the test suite asserts it.
  • A probe is bounded. Bodies are abandoned at the byte cap (never read to the end), and every request shares one wall-clock deadline (default 30 s, --deadline) — a black-holing origin cannot hold a probe, and what was not attempted is said in notes.
  • robots.txt is honoured for a headless visit. If the * group disallows the front page, the page is not a route for an agent alone — a headless visit is a crawl. A person in the tab is not a crawler, so their page route is untouched. The door is for agents and is not closed by robots.txt.

The site designs the order

The order above is only the router's default. Which way a visitor should take first is the site's design: whether a keyless agent should read on the page and become a counterparty later, or knock first, is something only the site knows. The site says it in its card — the origin's own statement, signed when the site signs it — not in the page, which any script it loads can rewrite:

{ "agentEntry": { "open_door": true,
                  "prefer": [ { "kind": "page", "when": "no-key" }, "card", "mcp" ] } }

An entry is a kind (page, card, mcp) or { "kind", "when" }, with when one of person, alone, key, no-key, token, browser, read against what the visitor has on hand. The router puts the declared entries whose condition holds first, in the declared order, then its own order for the rest; the output says order: the site's.

Two things a declaration cannot do: add a route the rules exclude (no page without a person or a browser, no server without a token, no door that failed its signature — it only re-orders what is on offer), and come from a card that failed its own signature (an excluded card declares nothing). The rest of the path — "you read, now let's make it a relationship" — is the site's to design too, with a handoff.

Knock

node bin/agent-web-router.mjs knock https://shop.example --key ./visitor.key --text "Do you have tables tonight?"
node bin/agent-web-router.mjs knock https://shop.example                     # no key: prints the door's own how-to, sends nothing

knock completes the card route: it probes, and only if the door survived every refusal above does it POST one A2A message/send — the six fields contextId, from, messageId, text, timestamp, to canonicalised and signed with Ed25519 — to the endpoint the card names, on the origin you dialled. The reply is verified before it is shown: signed by the DID the card names (never the reply's own from), addressed to you, within 300 s. A reply that fails any of those is printed with the reason and exit 2; a door's refusal (a JSON-RPC error) is printed as what it is — an answer that teaches — with its how-to.

A refusal is final for the call. knock makes exactly one POST. A 429 or 503, or a JSON-RPC error such as the door's own over-rate -32004, comes back to you with its Retry-After as retryAfter (seconds) and is never retried: re-sending the same signed message is a replay, and re-sending a fresh one inside the window is what the door just asked you not to do. Whether to come back later is yours to decide, with the door's own number in hand.

--key is a key you already hold: the muretai key file ({"seed": "<64 hex>", …}) or a bare 64-hex seed. Nothing here mints a key. Where a key comes from and where it lives — per visit, per machine, per site — decides whether the site sees one returning visitor or a stranger every time, and that is the agent's decision. Without a key, knock sends nothing and prints the door's own instructions for making one.

To continue a conversation, pass the contextId a verified reply carried back on your next knock with --context <id> — it is one of the six signed fields, so the door's verifier sees it under your signature, not beside it. The printed reply names the id.

The signing bytes are not written here. seam.mjs is the seam of Agent Entry — its wire layer: canonical JSON, did:key, the six-field payload, the signed card envelope — vendored verbatim from agent-seam, which publishes those bytes on their own, and checked against the golden vectors it publishes with them (test/wire_vectors.json, including the envelopes a door must refuse). One set of bytes, whichever runtime answers the contract; npm test proves this copy is unedited and names the agent-seam commit it was taken from.

Handoff

A tool result — from a WebMCP tool, an MCP server, or a door — can carry a handoff: where the rest of the interaction happens. Neutral shape, in the slot MCP reserves for extensions:

{ "_meta": { "handoff": { "v": 1, "next": [
    { "kind": "dm",  "to": "did:key:z6MkExample…", "message": "Any bundle discount?" },
    { "kind": "ui",  "url": "https://shop.example/checkout", "why": "payment needs a person" },
    { "kind": "mcp", "server": "https://shop.example/mcp" }
] } } }

next is ordered: the site's preference first; the router takes the first entry the agent can honour. The legacy { "muretai": { "v": 1, "action": "dm", "to": …, "connect": …, "suggested_message": … } } envelope is read as a single dm entry.

The one rule that makes a handoff safe to follow: a continuation that leaves the origin is honoured only if the origin's own card names where it points. A to must equal the card's did; a URL without to must sit on the origin dialled for this decision; a ui entry is never opened without a person. Even a usable card record from another probe cannot extend that origin. Why: a page-authored to is rewritable by any third-party script on that page, and a rewritten to sends the visitor's signed message — and the account it opens — to another door. The card is the origin's own statement; the page is not.

node bin/agent-web-router.mjs handoff result.json --origin https://shop.example
cat result.json | node bin/agent-web-router.mjs handoff - --origin https://shop.example --json

Exit 0 when every entry is accepted, 2 when anything is refused or there is nothing to follow.

Conduct

Three rules run through everything above, each pinned by a test that attempts the opposite (spec §7b):

  • Identity comes from you. from is derived from the key you supplied, never from a from, agent_name or as that a site writes into its card, its contract or a handoff. With did:key the identity is the key, so a site cannot hand you one — the router makes that hold at the two seams where a name could still be copied.
  • A tool result is data. The only thing read from a tool result is the handoff envelope at _meta.handoff. Prose in content[], an object under structuredContent, a "SYSTEM NOTICE" telling you to continue elsewhere — none of it can create or re-order a route, whatever the tool's annotations say; the absence of an untrustedContentHint is not trust.
  • A refusal is final for the call. One POST per knock; 429, 503 and JSON-RPC errors are returned with Retry-After, never retried.

Why these are in the router and not left to the model: a paper that measured it (arXiv 2606.06460) found agents honour an in-band "stop" anywhere from 0 % to 100 % depending on the model, while a harness-level interceptor stopped 120 of 120. For these three cases, the router is that interceptor.

What it does not do, and why

It never mints a key, and it never browses. A key is an identity, and where it lives — per visit, per machine, per site — decides whether the site sees one returning visitor or a stranger every time; that is the agent's decision, so the router only ever signs with a key it was given. The page is the harness's browser's to run. describeKnock(card) — the endpoint, the recipient DID, the six signed fields and the how-to, lifted from the card's own securitySchemes — remains available for a harness that prefers to knock with its own code.

Library

import { probe, route, parseHandoff, checkHandoff, usableCard, describeKnock } from '@muretai/agent-web-router';

const result = await probe('https://shop.example');                 // GETs only
const { routes, excluded } = route(result.ways, { person: false, browser: false, token: false, key: true });
const card = usableCard(result.ways.card);                           // null when the probe refused it
const knock = card ? describeKnock(card) : null;                     // what to POST, for the key holder

const handoff = parseHandoff(toolResult);                            // null when malformed (fail closed)
const { accepted, refused } = checkHandoff(handoff, { origin: 'https://shop.example', card: result.ways.card });

probe(origin, { fetch, timeoutMs, deadlineMs }) accepts a custom fetch for tests, a per-request timeout (default 8 s) and a shared wall-clock deadline (default 30 s). Bodies are capped (256 KiB for a card, 1 MiB for the page) and abandoned at the cap, never read to the end; knock responses are capped at 256 KiB too. Over the cap is a finding, not a crash. Pass the complete ways.card probe record to checkHandoff: a detached raw card has lost the signature and origin verdicts that make it safe to vouch for a continuation. This is the intentional 0.6 API boundary: 0.5 callers that passed result.ways.card.card must pass result.ways.card instead.

Any A2A door — not one vendor

knock POSTs A2A message/send to the URL the card names. That door can be any implementation that answers the same contract. This package does not import a site-side runtime, does not require one particular door on the origin, and still routes an MCP server or WebMCP in the page when there is no card at all.

Agent Entry is one door a site owner can install. A visitor who wants a router can install this. Neither install implies the other. A site running something else — or only MCP, or only WebMCP — is still a site this router can probe.

Specification draft: spec/v0.md. Tests: npm test.