{"_id":"@7h3/protocol-webmcp","_rev":"2-0a81f7f9278d0d332efb2c79fa6ce10d","name":"@7h3/protocol-webmcp","dist-tags":{"latest":"0.6.1"},"versions":{"0.6.0":{"name":"@7h3/protocol-webmcp","version":"0.6.0","keywords":["7h3","webmcp","modelcontext","agent","capability","ed25519","audit","provenance","mcp"],"license":"Apache-2.0","_id":"@7h3/protocol-webmcp@0.6.0","maintainers":[{"name":"7h3.agency","email":"ice@7h3.agency"}],"homepage":"https://github.com/IceMasterT/7h3-protocol#readme","bugs":{"url":"https://github.com/IceMasterT/7h3-protocol/issues"},"dist":{"shasum":"7c44eca290a02c3a98f693ed7234d4ad613d5b31","tarball":"https://registry.npmjs.org/@7h3/protocol-webmcp/-/protocol-webmcp-0.6.0.tgz","fileCount":16,"integrity":"sha512-UYfKjtRpJ4EPZFUotxqCYyjoBsrVCn59L3CXyVN5uZ58TOIfgEWZRDPWTNKR8ltBG7KW3Q5ff+X4XxQ5OJPmqg==","signatures":[{"sig":"MEYCIQCz7rIS0R9EcKQQr6dvmBwPwZEkY5WtetoqGiqRbG2w6wIhAMXDVbq1v7tEEMVAm4SjIV171bkfGoSDry8jWcgWI3ZC","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@7h3%2fprotocol-webmcp@0.6.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":77371},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"e0af87e5defd49975b35980b9f9022dd511ee308","scripts":{"test":"vitest run","build":"tsc","prepublishOnly":"npm run build"},"_npmUser":{"name":"7h3.agency","email":"ice@7h3.agency"},"repository":{"url":"git+https://github.com/IceMasterT/7h3-protocol.git","type":"git"},"_npmVersion":"11.17.0","description":"7h3 Protocol — signed, capability-scoped, receipted WebMCP tools. Deterministic authorization for browser agents.","directories":{},"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.1.8","typescript":"~6.0.3","@types/node":"^26.1.0"},"peerDependencies":{"@7h3/protocol":">=0.5.0 <1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/protocol-webmcp_0.6.0_1788378736003_0.6968032865619764","host":"s3://npm-registry-packages-npm-production"}},"0.6.1":{"name":"@7h3/protocol-webmcp","version":"0.6.1","description":"7h3 Protocol — signed, capability-scoped, receipted WebMCP tools. Deterministic authorization for browser agents.","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"peerDependencies":{"@7h3/protocol":">=0.5.0 <1.0.0"},"devDependencies":{"@types/node":"^26.1.0","typescript":"~6.0.3","vitest":"^4.1.8"},"scripts":{"test":"vitest run","build":"tsc","prepublishOnly":"npm run build","smoke":"node scripts/smoke-test-package.mjs"},"license":"Apache-2.0","keywords":["7h3","webmcp","modelcontext","agent","capability","ed25519","audit","provenance","mcp"],"repository":{"type":"git","url":"git+https://github.com/IceMasterT/7h3-protocol.git"},"gitHead":"febb258393a1b1adc8002fb8a8f5385f40bf6db3","_id":"@7h3/protocol-webmcp@0.6.1","bugs":{"url":"https://github.com/IceMasterT/7h3-protocol/issues"},"homepage":"https://github.com/IceMasterT/7h3-protocol#readme","_nodeVersion":"24.19.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-itbQJFgzgpsU3zdXk6yKCz0s57Q7OpxV4VU+ghg1zX1jw2KRZycsZmHLz9vvWj9W3tFHj8/R2hv6XrRhH2c+dw==","shasum":"e3c7e5da6f648ccff2021b9046b565eb495a7f09","tarball":"https://registry.npmjs.org/@7h3/protocol-webmcp/-/protocol-webmcp-0.6.1.tgz","fileCount":16,"unpackedSize":77477,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@7h3%2fprotocol-webmcp@0.6.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDVguXUd4yeJmMuXNAw3kaU519aKvzWBU3h44YpOQzyKQIhAOFLRpfHYxXpR4wJfEU9fwrrO6mV+hWqJqzyI+IaFAs3"}]},"_npmUser":{"name":"7h3.agency","email":"ice@7h3.agency"},"directories":{},"maintainers":[{"name":"7h3.agency","email":"ice@7h3.agency"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/protocol-webmcp_0.6.1_1788379367211_0.5243268844992441"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-02T19:52:15.752Z","modified":"2026-09-02T20:02:47.682Z","0.6.0":"2026-09-02T19:52:16.156Z","0.6.1":"2026-09-02T20:02:47.341Z"},"bugs":{"url":"https://github.com/IceMasterT/7h3-protocol/issues"},"license":"Apache-2.0","homepage":"https://github.com/IceMasterT/7h3-protocol#readme","keywords":["7h3","webmcp","modelcontext","agent","capability","ed25519","audit","provenance","mcp"],"repository":{"type":"git","url":"git+https://github.com/IceMasterT/7h3-protocol.git"},"description":"7h3 Protocol — signed, capability-scoped, receipted WebMCP tools. Deterministic authorization for browser agents.","maintainers":[{"name":"7h3.agency","email":"ice@7h3.agency"}],"readme":"# @7h3/protocol-webmcp\n\n**WebMCP gives agents hands. This gives those hands a signature, a scope, and a receipt.**\n\nA deterministic authorization layer for [WebMCP](https://webmachinelearning.github.io/webmcp/)\n(`document.modelContext`) tool calls, built on the [7h3 Protocol](https://github.com/IceMasterT/7h3-protocol)\nsigning, capability and replay primitives. Pure Web Crypto, zero dependencies,\nruns unchanged in a page, a Worker, or a test process.\n\n---\n\n## The gap this fills\n\nWebMCP lets a page hand an agent real, authenticated, signed-in capability. The\nguidance around it is candid about what that costs — and about what it does not\nsolve.\n\nChrome's [agent security guidance](https://developer.chrome.com/docs/agents/security)\nis entirely **probabilistic**: prompt-injection classifiers, \"spotlighting\"\nuntrusted content, critic LLMs. It is explicitly silent on authentication,\nauthorization and provenance.\n\nOpenAI's site-tools guidance is blunter:\n\n> Website-provided tool definitions and results are untrusted content.\n> **A tool's name or claim that it only reads data isn't proof of what it does.**\n\n> Use your application's **existing** authentication, authorization, and input validation.\n\nBut no site has an existing authorization model for *delegated agent action* —\n\"this agent, these tools, this ceiling, for the next ten minutes\". So the advice\nbottoms out on something that mostly doesn't exist yet.\n\nThis package is that missing layer, and it is deterministic. **A refusal here is\na failed signature or an uncovered scope, not a judgement call.** You cannot\nprompt-inject your way past a signature check.\n\nIt complements the probabilistic defenses rather than replacing them: classifiers\ndecide what an agent *should* do; this bounds what it *can* do.\n\n---\n\n## Install\n\n```bash\nnpm install @7h3/protocol-webmcp @7h3/protocol\n```\n\n## Use\n\n`registerTool` keeps the exact WebMCP signature and adds three optional fields —\n`scope`, `limit`, `confirm`. Existing tools need one import and one wrapper.\n\n```js\nimport { guard } from '@7h3/protocol-webmcp'\n\nconst g = guard({ origin: 'ledger.example', privateKey, publicKey })\n\nawait g.registerTool({\n  name: 'pay_invoice',\n  description: 'Pay an open invoice from the operating account.',\n  inputSchema: {\n    type: 'object',\n    properties: { id: { type: 'string' }, amountCents: { type: 'number' } },\n    required: ['id', 'amountCents'],\n    additionalProperties: false,\n  },\n  annotations: { destructiveHint: true },\n  scope: 'money/pay_invoice',              // ← capability required\n  limit: { field: 'amountCents', max: 2_000_00 }, // ← ceiling the site never exceeds\n  execute: async ({ id }) => ledger.payInvoice(id),\n})\n\n// The human consents, in the page, to a scoped and expiring grant:\nawait g.grant({\n  subject: 'chatgpt-agent',\n  scopes: ['money/pay_invoice'],\n  caps: { amountCents: 50_00 },  // bound *inside* the signed token\n  ttlMs: 10 * 60_000,\n})\n```\n\nFor reference, the underlying WebMCP call this wraps:\n\n```javascript\ndocument.modelContext.registerTool({\n  name: \"search_products\",\n  description: \"Search the product catalog\",\n  inputSchema: { /* ... */ },\n  execute: async (input) => { /* ... */ }\n});\n```\n\n---\n\n## Adopting it in an app that already has WebMCP tools\n\nA guarded tool is shape-compatible with an unguarded one, so adoption is an\nimport, a constructor, and one field per tool you want to protect:\n\n```diff\n+import { guard } from '@7h3/protocol-webmcp'\n+\n+const g = guard({ origin: 'shop.example', privateKey, publicKey })\n\n-await document.modelContext.registerTool({\n+await g.registerTool({\n   name: 'place_order',\n   description: 'Place an order for the current cart',\n   inputSchema: { /* unchanged */ },\n   annotations: { destructiveHint: true },\n+  scope: 'orders/place',\n+  limit: { field: 'amountCents', max: 500_00 },\n   execute: async ({ cartId }) => placeOrder(cartId),   // unchanged\n })\n```\n\nYour handler does not change, and neither does the schema an agent sees. Tools\nyou leave on `document.modelContext` keep working exactly as before, so you can\nadopt one tool at a time.\n\nOr generate it. The repo's MCP server ships a WebMCP scaffold target:\n\n```\n7h3_scaffold framework=\"webmcp\" sender=\"shop.example\"\n```\n\n```bash\nclaude mcp add 7h3-protocol -- npx -y @7h3/protocol-mcp\n```\n\n---\n\n## Three primitives\n\n### 1. Signed tool manifests — provenance\n\nThe page signs its own tool surface: name, description, input schema and\nannotations of every tool. Serve it at a well-known path and anyone can check\nthat the tools an agent sees are the tools the origin published.\n\n```js\nconst manifest = await g.manifest()          // Ed25519-signed, with a surface digest\nawait verifyManifest(manifest, originPubKey) // { ok: true }\n\n// Catches the tool-surface poisoning class of attack:\nawait diffAgainstManifest(liveTools, manifest)\n// → { ok: false, added: ['list_invoices_v2'], removed: [], modified: [] }\n```\n\nAn injected lookalike tool, a silently reworded description, or a removed tool\nall change the surface. This turns \"a tool's claim\" into something checkable.\n\n**Two keys, deliberately.** The *origin identity key* is long-lived, lives on the\ndeploy machine, and signs the manifest — only its public half is served. The\n*session key* is generated in the browser per visitor and signs that visitor's\ngrants and receipts. Conflating them would mean shipping a private key in the\nbundle, which is exactly the mistake a signing layer must not make.\n\nSign at deploy time from a declarative tool table, with no handlers in scope —\n`manifestEntry` accepts a `ToolSurface`, which is a tool minus its `execute`:\n\n```js\nconst entries = await Promise.all(TOOL_DEFS.map(manifestEntry))\nconst manifest = await signManifest({ origin, entries, privateKey, keyId })\n// → serve at /.well-known/7h3-webmcp-manifest.json, public key at /.well-known/7h3-keys.json\n```\n\nThe page then fetches both, verifies the manifest under the published key, and\ndiffs it against the tools actually registered. Anyone can run the same check\nfrom outside the page.\n\n### 2. Capability-scoped execution — authorization\n\nGrants are **page-held by default**: the token never passes through the agent, so\na prompt-injected agent cannot exfiltrate it. Serializing a chain into the\nreserved `__7h3_grant` input supports deliberate cross-agent delegation.\n\nNumeric ceilings are encoded as reserved `caps/<field>/<max>` scopes, so a spend\ncap is **bound inside the signed token** rather than trusted from page state. A\ngrant can tighten a tool's ceiling; it can never loosen it.\n\nCeilings **fail closed**: if a tool declares `limit: { field: 'amountCents', … }`\nand a call omits `amountCents`, the call is refused rather than allowed\nunchecked. Schema `required` is not a defense — the guard does not trust a\ncaller to honour it.\n\nGrant selection is **permissive across grants**: a call is allowed if *any*\nactive grant authorizes it. A narrow grant cannot veto a broader one that covers\nthe call, and one corrupt grant cannot disable the whole tool surface. When\nnothing authorizes, the most specific refusal is reported — `limit-exceeded`\ntells an agent more than `scope-not-covered`.\n\nRefusals are structured, not thrown, so an agent can read *why* and ask the human\nfor authority:\n\n| Reason | Meaning |\n|---|---|\n| `no-active-grant` | nothing authorizes this scope |\n| `scope-not-covered` | the active grant does not reach this tool |\n| `grant-expired` / `grant-revoked` | authority lapsed or was withdrawn |\n| `grant-invalid-signature` | the grant does not verify |\n| `limit-exceeded` | the value exceeds the authorized ceiling |\n| `replayed-call` | this nonce was already used |\n| `confirmation-denied` | a human declined |\n\n### 3. Hash-chained receipts — audit\n\nEvery call is recorded, **allowed and refused**. Each receipt carries the hash of\nits predecessor, so the log is tamper-evident as a whole rather than entry by\nentry — deletion and reordering are detectable, which independently-signed\nentries cannot catch. Inputs are hashed, not stored, so a receipt proves *what*\nhappened without disclosing the payload.\n\n```js\nconst result = await verifyChain(g.receipts.all(), publicKey)\n// → { ok: false, brokenAt: 3, reason: 'bad-signature' }\n```\n\n---\n\n## Honest threat model\n\nThis is worth stating plainly, because overselling a security boundary is worse\nthan not having one.\n\n- **What it does.** Bounds what an agent can do to what a human explicitly, and\n  verifiably, authorized — and makes every attempt provable after the fact.\n  Enforcement is cryptographic and runs before your handler.\n- **What it does not do.** It cannot stop a fully compromised agent acting\n  *inside* a scope it was legitimately granted. If you grant `money/**` with a\n  $10,000 cap, a hijacked agent can spend $10,000. Grant narrowly and briefly.\n- **It is not an anti-prompt-injection system.** It is the layer that makes a\n  successful prompt injection *bounded and auditable* rather than unbounded and\n  invisible. Keep the probabilistic defenses too.\n- **Page-held grants are not bearer tokens** — that is the default and the safer\n  mode. Explicitly delegated `__7h3_grant` chains *are* bearer credentials within\n  their scope and TTL, in the same sense as OAuth scopes or macaroons.\n- **A compromised page is out of scope.** Script running in your origin can\n  register its own tools; that is what the signed manifest is for — it makes the\n  tampering *detectable*, not impossible.\n\n---\n\n## API\n\n| Export | Purpose |\n|---|---|\n| `guard(options)` | Create a `ToolGuard` |\n| `.registerTool(tool, opts?)` | WebMCP registration, wrapped |\n| `.grant(request)` | Issue a scoped, expiring capability |\n| `.revoke(grantId)` | Withdraw authority; effective on the next call |\n| `.activeGrants()` | Unexpired, unrevoked grants |\n| `.manifest()` | Sign the current tool surface |\n| `.invoke(name, input)` | Run a tool through the identical wrapper (tests, non-WebMCP browsers) |\n| `.receipts` | The hash-chained `ReceiptLog` |\n| `.on(listener)` | Subscribe to registrations, grants, and calls |\n| `verifyChain(entries, key)` | Verify a receipt chain, reporting the first break |\n| `verifyManifest(m, key)` | Verify a signed manifest |\n| `diffAgainstManifest(live, m)` | Detect injected, modified or removed tools |\n| `isWebMcpSupported()` | Feature-detect `document.modelContext` |\n\n## Testing\n\n```bash\nnpm test   # from the repo root; 46 tests cover this package\n```\n\nCovering refusal on every path, expiry, revocation, ceiling tightening, replay,\nreceipt tamper detection (edit / delete / reorder / forge) and surface poisoning.\n\n## License\n\nApache-2.0\n","readmeFilename":"README.md"}