From 73837ffb720f1aa3c3f856349e3aebdc9022d35a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20T=C3=BCchler?= Date: Sat, 19 Sep 2026 18:09:14 +0200 Subject: [PATCH 1/2] docs: redesign the README around the pipeline Add a themed banner and pipeline diagram (light and dark, generated from one source), a demo poster, the manifest entry and registration from the proof fixture, and scannable install, command, coverage and guarantee tables. A test keeps the generated artwork in sync with its generator. --- README.md | 347 +++++++++++++++++-------------- assets/readme/banner-dark.svg | 41 ++++ assets/readme/banner-light.svg | 41 ++++ assets/readme/build.mjs | 155 ++++++++++++++ assets/readme/demo-poster.webp | Bin 0 -> 39358 bytes assets/readme/pipeline-dark.svg | 71 +++++++ assets/readme/pipeline-light.svg | 71 +++++++ tests/readme-art.test.mjs | 20 ++ 8 files changed, 588 insertions(+), 158 deletions(-) create mode 100644 assets/readme/banner-dark.svg create mode 100644 assets/readme/banner-light.svg create mode 100644 assets/readme/build.mjs create mode 100644 assets/readme/demo-poster.webp create mode 100644 assets/readme/pipeline-dark.svg create mode 100644 assets/readme/pipeline-light.svg create mode 100644 tests/readme-art.test.mjs diff --git a/README.md b/README.md index 961ca33..677f057 100644 --- a/README.md +++ b/README.md @@ -1,194 +1,225 @@ -# webmcpify — the WebMCP agent skill +

+ + + webmcpify — make any web app agent-ready, verifiably. The WebMCP agent skill for existing web apps. + +

+ +

+ Latest release + Checks + MIT license + WebMCP: document.modelContext +

+ +

+ Website · + Docs · + Install · + Demo · + Changelog +

+ +**webmcpify** is an agent skill that makes an **existing** web app callable by browser +AI agents through [WebMCP](https://webmachinelearning.github.io/webmcp/) — +`document.modelContext`, the proposed standard incubated in the W3C Web Machine +Learning Community Group and in Chrome origin trial. Your coding agent inventories +the app, proposes a tool manifest for your approval, integrates the tools with a +tiny vendored runtime, and **proves each one works in real Chrome**. Unrelated code +stays untouched — from a static landing page to a multi-tenant SaaS. -**Make any web app agent-ready — verifiably.** - -🌐 **[webmcpify.at](https://webmcpify.at)** — the site itself is webmcpified: open it with a WebMCP-enabled agent and call its tools. - -webmcpify is a WebMCP agent skill for **curated core coverage or route-by-route -parity**. It integrates [WebMCP](https://webmachinelearning.github.io/webmcp/) -(`document.modelContext` — a proposed web standard incubated in the W3C Web Machine -Learning Community Group, currently in Chrome origin trial) into an **existing** -web application — from a static landing page to a large multi-tenant SaaS — end to end: - -``` -DETECT ─▶ INVENTORY ─▶ [you approve the tool manifest] ─▶ INTEGRATE ─▶ VERIFY ─▶ HEAL ─▶ AUDIT - loop loop loop loop +```sh +npx skills add TueJon/webmcpify # once +/webmcpify # in your app's repo, inside your coding agent ``` -Your coding agent investigates the codebase, asks whether you want a curated set of -high-value actions or an auditable per-route interaction census, then proposes a -**tool manifest** with names, schemas, examples, coverage reasons, and a read-only/ -mutating classification. After your approval it integrates the tools, **exercises -each one in a real browser**, and heals failures—while keeping unrelated logic and -UI untouched. - -## See native Chrome verification - -**[Watch the uncut 63-second runtime demo](proof/artifacts/webmcpify-proof-480p.mp4)** — -a prepared local fixture passes a real approval click, registers one client-only -tool, then exercises native `document.modelContext.getTools()` / `executeTool()` -verification, a visible UI change, invalid-input handling, and cleanup. - -The runtime registration and browser assertions are real. The phase labels are -advanced by a deterministic script for legibility; the recording does not execute -the skill's inventory, integration, or audit phases. The [`proof/`](proof/README.md) -pack includes the runnable fixture, prepared before/after example manifests, an -illustrative integration patch, and artifact checksums. Reproduce the native-browser -checks with `npm run proof:verify`. - -## Why - -Browser AI agents (Gemini in Chrome, extensions, assistive tech) are learning to -call structured page tools instead of scraping the DOM. WebMCP is the emerging -standard for that, co-authored by Google and Microsoft engineers, in origin trial -since Chrome 149. Making an app agent-ready by hand means reading a spec that is -still moving (the API surface has changed repeatedly during the trial), learning tool-design -conventions, and building a verification setup — webmcpify packages all of that -into one command for your coding agent. +> [!TIP] +> **New in [v0.6.0](https://github.com/TueJon/webmcpify/releases/tag/v0.6.0):** +> evidence-aware resume — changed app files, contracts or browsers invalidate the +> verification they affect — plus scoped browser access and independent checks for +> every mutation. + +## How it works + +

+ + + Pipeline: detect, inventory, you approve the tool manifest, integrate, verify in real Chrome, heal failures with capped retries, audit. Every phase reads and writes .webmcpify/manifest.json. + +

+ +- **One human checkpoint.** You approve the tool manifest: names, schemas, examples, + coverage reasons, and a read-only or mutating class per tool. After that the agent + only comes back for what it genuinely can't resolve — an app that won't start, or a + tool that still fails after its capped heal attempts. +- **Loops over persisted state.** Every phase reads and writes + `.webmcpify/manifest.json`, so a run resumes across sessions, context windows and + even different agents. Recorded evidence tells it which checks are still valid. +- **Proof, not promises.** Each tool is enumerated and executed through Chrome's + native `getTools()` / `executeTool()`, asserting on the tool result **and** the + resulting UI state. + +## What your agent adds to your app + +From the [reproducible proof fixture](proof/README.md). First, the manifest entry +you approve at the gate (abridged): + +```jsonc +{ + "id": "set_release_filter", + "mutating": "client", // browser state only — no server write + "inputSchema": { + "type": "object", + "properties": { "category": { "type": "string", "enum": ["all", "feature", "fix"] } }, + "required": ["category"], + "additionalProperties": false + }, + "source": ["proof/demo/app.js:applyFilter"], // the UI's existing code path + "examples": { "valid": { "category": "fix" }, "invalid": { "category": "private" } }, + "expect": { "result": "2 release notes visible", + "ui": "only the two synthetic fix notes remain visible" }, + "cleanup": "execute the same UI path with category=all" +} +``` -## Install (one command) +Then the integration: a registration that calls the code path the UI already uses, +through the vendored runtime. + +```js +import { createToolScope } from './webmcpify.js'; // vendored, MIT, ~290 lines +import { applyFilter } from './app.js'; // existing UI logic, unchanged + +createToolScope('proof-release-notes', [{ + name: 'set_release_filter', + description: 'Filters the visible synthetic release notes by category ' + + "using the page's existing filter path.", + inputSchema: schema, // the approved schema above + annotations: { readOnlyHint: false, untrustedContentHint: false, consequentialHint: false }, + execute: ({ category }) => { + if (!schema.properties.category.enum.includes(category)) { + return 'ERROR: category must be one of all, feature, or fix.'; + } + return `${applyFilter(category)} release notes visible for ${category}.`; + }, +}]); // feature-detected: a safe no-op in browsers without WebMCP +``` -**Any agent** — Claude Code, Codex, Cursor, opencode, Copilot, and [70+ more](https://github.com/vercel-labs/skills): +## See it run -```sh -npx skills add TueJon/webmcpify -``` +

+ Watch the uncut 63-second runtime demo: approval click, native getTools and executeTool, UI change, invalid input, cleanup +

-**Claude Code** (as a plugin): +A prepared local fixture passes a real approval click, registers one client-only tool, +then exercises native `document.modelContext.getTools()` / `executeTool()`, a visible +UI change, invalid-input handling and cleanup. The runtime registration and browser +assertions are real; the phase labels are advanced by a script for legibility, so the +recording does not run the skill's inventory, integration or audit phases. Reproduce +the native checks with `npm run proof:verify`; the [`proof/`](proof/README.md) pack +holds the fixture, before/after manifests, an illustrative patch and checksums. -``` -/plugin marketplace add TueJon/webmcpify -/plugin install webmcpify@webmcpify -``` +## Install -**Manual**: copy [`skills/webmcpify/`](skills/webmcpify/) into your agent's skills -directory, or just tell your agent to follow -[`skills/webmcpify/SKILL.md`](skills/webmcpify/SKILL.md). +| Where | How | +|---|---| +| **Any agent** — Claude Code, Codex, Cursor, opencode, Copilot and [70+ more](https://github.com/vercel-labs/skills) | `npx skills add TueJon/webmcpify` | +| **Claude Code** plugin | `/plugin marketplace add TueJon/webmcpify` then `/plugin install webmcpify@webmcpify` | +| **Manual** | Copy [`skills/webmcpify/`](skills/webmcpify/) into your agent's skills directory, or tell your agent to follow [`SKILL.md`](skills/webmcpify/SKILL.md) | -The skill directory is self-contained — pipeline, phase guides, vendorable runtime, -and the verification template all ship inside it. +The skill directory is self-contained: pipeline, phase guides, vendorable runtime and +the verification template all ship inside it. ## Use -Open your agent in the target repo and pick your scope: - -``` -/webmcpify # full pipeline -/webmcpify inventory # just investigate + propose the tool manifest (zero code changes) -/webmcpify integrate # integrate the approved manifest -/webmcpify workbench # agent launches the temporary visual tool inspector -/webmcpify verify # verify + heal what's integrated -/webmcpify status # where are we? what's next? -/webmcpify full parity # census every interactive element on every authenticated route -``` +Open your agent in the target repo and pick a scope — or just say *"webmcpify this app"*. -(or in plain words: *"webmcpify this app"*, *"map what tools this app could expose"*) +| Command | What happens | Changes your code | +|---|---|---| +| `/webmcpify` | Full pipeline, resuming wherever the manifest says | After your approval | +| `/webmcpify inventory` | Investigate and propose the tool manifest | Never | +| `/webmcpify integrate` | Integrate the approved manifest in small batches | Yes | +| `/webmcpify workbench` | Agent launches a temporary visual tool inspector | No — development aid only | +| `/webmcpify verify` | Verify and heal what is integrated | Only to fix a failing tool | +| `/webmcpify status` | Where are we, what's next | Never (read-only) | +| `/webmcpify full parity` | Census every interactive element on every authenticated route | After your approval | -The pipeline has **one main checkpoint** — you approve the tool manifest (and, for -apps under git, choose whether integration batches are committed). Beyond that it -only comes back for things it genuinely can't resolve: an app that won't start, or -a tool that still fails after capped heal attempts. All state persists in -`.webmcpify/manifest.json`, so runs are **resumable** across sessions, context -windows, and even different agents. On resume, recorded app files, tool contracts, -runtime and browser inputs determine which verification evidence remains valid. -Changed or unknown dependencies require fresh checks; uncertain interrupted -mutations must be reconciled through a read path before retrying. These records -are skill-managed evidence, not an automatic dependency tracker or a WebMCP field. -See [re-verification](skills/webmcpify/references/reverify.md). +### Curated core or route-by-route parity -Verification uses a dedicated test context and approved origins, accounts and -fixtures. Official guidance is read directly; running an optional guidance package -requires an exact reviewed version and separate authorization. Mutation checks -compare the intended effect with an independent read path and an unchanged -neighbor or invariant. Optional model evals need approved data and explicit -run, time and spend limits; a smoke pass does not establish journey quality. +| | **Curated** | **Parity** | +|---|---|---| +| Goal | A usable toolset for the actions that matter | Auditable completeness | +| Output | Reviewed route → tool map for core actions | Per-route element census: every interaction maps to a tool or a written reason | +| Keeps it usable by | Priority waves, an overlap rule (no two tools match the same request), role/tenant coverage | Route-scoped registration; client-capacity gaps are reported, never guessed | -## Built to scale to large codebases +The agent asks you to choose before inventory — there is no silent default — and a +tool count alone is never called 100%. -Every phase is a **loop over persistent state**, not a one-shot pass: +## Guarantees -- **Inventory** maps the codebase into areas (routes/views/modules) first, then +| | Guarantee | How it's enforced | +|---|---|---| +| 🧩 | **Unrelated code stays untouched** | Every diff hunk traces to a manifest entry; a final audit checks against the recorded baseline commit; files already dirty at the start are never modified or reverted | +| 🔒 | **Read-only first** | Server mutations need your explicit per-tool approval; auth, signup, billing, payment and credential-returning tools stay excluded; irreversible deletes can only open the app's own confirmation UI | +| 🛡️ | **Your server stays the trust boundary** | Tools only call code paths your UI already uses — no new endpoints, no bypasses | +| 📦 | **Zero dependencies** | A small MIT runtime is vendored and feature-detected; the app behaves the same in browsers without WebMCP | +| 🧪 | **Exercised, not assumed** | Every tool runs in real Chrome against the result and the UI state; mutations are confirmed through an independent read path with an unchanged neighbor; declarative forms get the real submit click | +| ♻️ | **Honest resume** | Changed files, contracts, runtimes or browsers invalidate the evidence they affect, unknown dependencies mean a full re-check, and interrupted mutations are reconciled before any retry ([re-verification](skills/webmcpify/references/reverify.md)) | +| 🔐 | **Scoped access** | A dedicated test context with approved origins, accounts and fixtures; official guidance is read directly, never executed as an unreviewed package | +| 🧭 | **Spec over scoreboard** | Checker findings are classified, not chased; the public discovery layer (`/.well-known/webmcp`) is a separate approval | + +
+Built to scale to large codebases + +- **Inventory** maps the codebase into areas (routes, views, modules) first, then deep-reads one area per iteration — a 500-file SaaS is processed area by area, - never in one context-busting sweep. Sub-agent fan-out writes per-area shard - files; a single coordinator merges them (no write races). -- **Coverage is explicit:** `curated` produces a reviewed route→tool map for core - actions; `parity` produces a per-route element census where every interaction is - mapped to a tool or a written reason. A tool count alone is never called 100%. -- **Tool budgets** keep curated SaaS toolsets usable: priority waves, an overlap rule - (no two tools matching the same request), and role/tenant coverage tracking. - Parity uses route-scoped registration and reports client-capacity gaps instead of - claiming an unmeasured universal per-page limit. -- **Integrate** works in small batches (one area or ≤5 tools), each independently - built and typechecked — committed per batch only if you opted in. -- **Verify/Heal** iterate per tool with attempt caps and honest escalation - instead of infinite loops; mutating tools get cleanup steps between retries. -- Interrupt at any point; the next run resumes from the manifest. Verification - evidence records app, contract and browser inputs; changed inputs trigger - bounded re-verification, while `status` remains read-only. - -## Guarantees + never in one context-busting sweep. Sub-agent fan-out writes per-area shard files; + a single coordinator merges them. +- **Integrate** works in batches of one area or at most five tools, each built and + typechecked — committed per batch only if you opted in. +- **Verify / Heal** iterate per tool with attempt caps and honest escalation; + mutating tools get cleanup steps between retries. +- **Interrupt anywhere.** The next run resumes from the manifest; `status` stays + read-only. + +
+ +
+Platform status and compatibility + +WebMCP is an **origin trial** (Chrome 149 onward; the stable milestone is an +estimate, not a commitment). Production exposure needs an +[origin-trial token](https://developer.chrome.com/origintrials/); local development +needs `chrome://flags/#enable-webmcp-testing`. The API has already changed during +the trial (testing API removed 2026-07; `navigator` → `document`) — webmcpify +isolates that churn in one vendored file, and its verification probes whether the +browser takes current object input or Chrome 150's legacy JSON-string input without +retrying real tools. Integration reads the official Chrome guides and the CG draft +directly. + +ChatGPT's separate, model- and account-gated client surface is documented as +[Site tools](skills/webmcpify/references/client.md), with dated availability facts +and a troubleshooting order. Release-by-release spec adaptations are in the +[changelog](CHANGELOG.md). -- **Unrelated logic and UI stay untouched** — every diff hunk traces to a manifest - entry; a final audit against the recorded baseline commit enforces it, and files - that were already dirty when the run started are never modified or reverted. -- **Read-only first** — server mutations require your explicit per-tool approval. - Auth, signup, billing, payment and credential-returning tools stay excluded; - irreversible delete actions can only open the app's existing confirmation UI. -- **Server stays the trust boundary** — tools only call code paths your UI already - uses; no new endpoints, no bypasses. -- **Spec-shaped, zero dependencies** — a small MIT runtime is vendored into your - repo (no npm dependency), everything feature-detected: your app is - **behaviorally unchanged** in browsers without WebMCP. -- **No ambiguous imperative results** — the runtime guards accidental bare - `null`/`undefined`, and route-changing tools return a structured result before - deferring navigation and route-scope disposal. -- **Exercised, not assumed** — every tool is enumerated and executed in real - Chrome, asserting on both the tool result and the resulting UI state, from - examples recorded in the manifest. That includes mutating declarative forms, - where Chrome pauses the execution until a real submit interaction — the - harness performs that submit click mid-execution instead of faking the pass. -- **Spec over scoreboard** — WebMCP checkers and inspector extensions grade pages - against a mix of spec features, conventions, and invented checks. webmcpify - classifies their findings instead of chasing them: it never emits - non-existent attributes or adds markup an app doesn't need to raise a score. - Optional off-page discovery (a `/.well-known/webmcp` manifest, `rel="webmcp"` - links) is a separately approved layer, because it publishes tool metadata - publicly — never part of a default integration. +
## What's in this repo | Path | Purpose | |---|---| -| [`skills/webmcpify/SKILL.md`](skills/webmcpify/SKILL.md) | The pipeline (what your agent follows) | -| [`skills/webmcpify/references/`](skills/webmcpify/references/) | Phase guides: inventory, integrate, Workbench, runtime, verify, heal, security, discovery | +| [`skills/webmcpify/SKILL.md`](skills/webmcpify/SKILL.md) | The pipeline your agent follows | +| [`skills/webmcpify/references/`](skills/webmcpify/references/) | Phase guides: inventory, integrate, Workbench, runtime, verify, re-verify, heal, security, discovery, client surfaces | | [`skills/webmcpify/templates/`](skills/webmcpify/templates/) | Vendorable runtime (TS + JS), temporary visual Workbench, ambient types, Playwright verification template, discovery manifest | - -## Status - -WebMCP itself is an **origin trial** (Chrome 149 →; the stable milestone is an -estimate, not a commitment): production exposure needs an -[origin-trial token](https://developer.chrome.com/origintrials/), local development -needs `chrome://flags/#enable-webmcp-testing`. The API surface has already changed -during the trial (testing API removed 2026-07; `navigator` → `document`) — webmcpify -isolates that churn in one vendored file, and its verification surfaces probe -whether the browser uses current object input or Chrome 150's legacy JSON-string -input without retrying real tools. It reads the official Chrome guides and CG draft directly at integration time. -The optional modern-web-guidance CLI requires a reviewed exact version and -separate approval before execution. - -Release-by-release spec adaptations are recorded in the [changelog](CHANGELOG.md). -ChatGPT's separate, model/account-gated client surface is documented as -[Site tools](skills/webmcpify/references/client.md), with dated availability facts -and a troubleshooting order. +| [`proof/`](proof/README.md) | Reproducible native-Chrome proof: fixture, manifests, recording, checksums | ## Related projects -- [webmcpify.at](https://webmcpify.at) — project website (itself agent-ready, in all three layers: imperative tools via the vendored runtime, a declarative install form, and a published `/.well-known/webmcp` manifest) +- [webmcpify.at](https://webmcpify.at) — the project website, itself agent-ready in all three layers: imperative tools via the vendored runtime, a declarative install form, and a published `/.well-known/webmcp` manifest - [webmachinelearning/webmcp](https://github.com/webmachinelearning/webmcp) — the spec draft (W3C WebML CG) -- [GoogleChromeLabs/webmcp-tools](https://github.com/GoogleChromeLabs/webmcp-tools) — Google's demos, types, and evals CLI (webmcpify follows these patterns) +- [GoogleChromeLabs/webmcp-tools](https://github.com/GoogleChromeLabs/webmcp-tools) — Google's demos, types and evals CLI (webmcpify follows these patterns) - [GoogleChrome/modern-web-guidance](https://github.com/GoogleChrome/modern-web-guidance) — official best-practice guides (optional CLI; exact version and execution approval required) - [Puppeteer WebMCP](https://pptr.dev/guides/webmcp) — experimental first-class WebMCP automation API (Chrome 151+ as documented 2026-08-29; alternative verify harness) -- [MCP-B / WebMCP-org](https://github.com/WebMCP-org/npm-packages) — WebMCP ecosystem: polyfill, extension, transports, and dev tooling (webmcpify vendors a minimal runtime instead of adding dependencies) +- [MCP-B / WebMCP-org](https://github.com/WebMCP-org/npm-packages) — polyfill, extension, transports and dev tooling (webmcpify vendors a minimal runtime instead of adding dependencies) ## License diff --git a/assets/readme/banner-dark.svg b/assets/readme/banner-dark.svg new file mode 100644 index 0000000..8cae808 --- /dev/null +++ b/assets/readme/banner-dark.svg @@ -0,0 +1,41 @@ + + webmcpify — the WebMCP agent skill + Make any web app agent-ready, verifiably: an agent calls typed tools that webmcpify registers in your existing app. + + + + + + + webmcpify + Make any web app agent-ready — verifiably. + The WebMCP agent skill for existing web apps: inventory, + approve, integrate, then verify every tool in real Chrome. + document.modelContextzero dependenciesMIT + + + + your-app.example + + search_products() + add_to_cart() + track_order() + + + + agent + tool calls + + + + + + VERIFIED ✓ + + diff --git a/assets/readme/banner-light.svg b/assets/readme/banner-light.svg new file mode 100644 index 0000000..9944e56 --- /dev/null +++ b/assets/readme/banner-light.svg @@ -0,0 +1,41 @@ + + webmcpify — the WebMCP agent skill + Make any web app agent-ready, verifiably: an agent calls typed tools that webmcpify registers in your existing app. + + + + + + + webmcpify + Make any web app agent-ready — verifiably. + The WebMCP agent skill for existing web apps: inventory, + approve, integrate, then verify every tool in real Chrome. + document.modelContextzero dependenciesMIT + + + + your-app.example + + search_products() + add_to_cart() + track_order() + + + + agent + tool calls + + + + + + VERIFIED ✓ + + diff --git a/assets/readme/build.mjs b/assets/readme/build.mjs new file mode 100644 index 0000000..f72020a --- /dev/null +++ b/assets/readme/build.mjs @@ -0,0 +1,155 @@ +// Generates the README artwork (banner + pipeline) in light and dark variants. +// Both variants share one layout; only the color tokens differ, so they cannot drift. +// Run: node assets/readme/build.mjs (tests/readme-art.test.mjs fails on stale output) +import { writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = dirname(fileURLToPath(import.meta.url)); + +export const themes = { + light: { + panel: '#f8f7fd', panelStroke: '#e4e2ee', ink: '#14121c', muted: '#555365', + hairline: '#d9d6e6', primary: '#653ec7', primaryInk: '#4c279f', tint: '#efebfd', + node: '#ffffff', gate: '#653ec7', onGate: '#ffffff', logoFill: '#f5f3fb', skeleton: '#c9c6d6', + }, + dark: { + panel: '#12111c', panelStroke: '#2c2a3b', ink: '#eeedf5', muted: '#a4a2b7', + hairline: '#3a3850', primary: '#a492fb', primaryInk: '#c3b6ff', tint: '#1d1930', + node: '#0d0c15', gate: '#7a58ea', onGate: '#ffffff', logoFill: '#1d1930', skeleton: '#3c3a4e', + }, +}; + +const SANS = "'Familjen Grotesk','Segoe UI',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif"; +const MONO = "'Spline Sans Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,'Liberation Mono',monospace"; + +const esc = (s) => s.replace(/&/g, '&').replace(//g, '>'); + +function styles(t) { + return ``; +} + +const arrowHead = (x, y, dir) => { + // Small filled triangle whose tip sits at (x, y). + const d = { right: `M${x} ${y} l-9 -4.5 v9 z`, down: `M${x} ${y} l-4.5 -9 h9 z`, left: `M${x} ${y} l9 -4.5 v9 z` }; + return ``; +}; + +export function banner(t) { + const chips = ['document.modelContext', 'zero dependencies', 'MIT']; + let x = 44; + const chipSvg = chips.map((label) => { + const w = Math.round(label.length * 7.7 + 26); + const out = `` + + `${esc(label)}`; + x += w + 10; + return out; + }).join(''); + const rows = [110, 156, 202]; + const tools = ['search_products()', 'add_to_cart()', 'track_order()']; + return ` + webmcpify — the WebMCP agent skill + Make any web app agent-ready, verifiably: an agent calls typed tools that webmcpify registers in your existing app. + ${styles(t)} + + + + + + webmcpify + Make any web app agent-ready — verifiably. + The WebMCP agent skill for existing web apps: inventory, + approve, integrate, then verify every tool in real Chrome. + ${chipSvg} + + + + your-app.example + + ${rows.map((y, i) => `` + + `${tools[i]}`).join('\n ')} + + + + agent + tool calls + ${arrowHead(836, 128, 'left')} + ${arrowHead(836, 174, 'left')} + ${arrowHead(836, 220, 'left')} + + + VERIFIED ✓ + + +`; +} + +const stages = [ + { name: 'DETECT', lines: ['stack, routes,', 'auth and state'] }, + { name: 'INVENTORY', lines: ['reads area by area,', 'proposes a tool', 'manifest'], loop: '↻ per area' }, + { name: 'YOU APPROVE', lines: ['names, schemas,', 'read-only or', 'mutating — per tool'], gate: true }, + { name: 'INTEGRATE', lines: ['vendored runtime,', 'built + typechecked'], loop: '↻ per batch' }, + { name: 'VERIFY', lines: ['real Chrome:', 'result + UI state'] }, + { name: 'HEAL', lines: ['fixes only that', 'tool, capped'] }, + { name: 'AUDIT', lines: ['every diff hunk', 'maps to the manifest'] }, +]; + +export function pipeline(t) { + const cx = (i) => 86 + i * 138; + const half = 58; + const top = 158; + const parts = []; + stages.forEach((s, i) => { + const x = cx(i); + parts.push(``); + parts.push(`${s.name}`); + s.lines.forEach((line, j) => parts.push(`${esc(line)}`)); + parts.push(``); + if (i < stages.length - 1) { + parts.push(`${arrowHead(cx(i + 1) - half - 2, 182, 'right')}`); + } + if (s.loop) { + parts.push(`${arrowHead(x - 34, 156, 'down')}`); + parts.push(`${s.loop}`); + } + if (s.gate) parts.push(`◆ human gate`); + }); + // VERIFY ⇄ HEAL: failed tools go to HEAL, then back to VERIFY until they pass or hit the cap. + const v = cx(4); const h = cx(5); + parts.push(`${arrowHead(v + 8, 156, 'down')}`); + parts.push(`↻ re-verify, capped`); + return ` + The webmcpify pipeline + Detect, inventory, human approval of the tool manifest, integrate, verify in real Chrome, heal failures with capped retries, audit. Every phase reads and writes .webmcpify/manifest.json. + ${styles(t)} + + FIG. 2 — THE PIPELINE + ↻ loops over persisted state + + ${parts.join('\n ')} + + + .webmcpify/manifest.json + single source of truth — every phase reads and writes it + ✓ resumes across sessions, context windows and agents + ✓ keeps verification evidence per tool + +`; +} + +export const outputs = { + 'banner-light.svg': () => banner(themes.light), + 'banner-dark.svg': () => banner(themes.dark), + 'pipeline-light.svg': () => pipeline(themes.light), + 'pipeline-dark.svg': () => pipeline(themes.dark), +}; + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + for (const [name, render] of Object.entries(outputs)) writeFileSync(join(here, name), render()); +} diff --git a/assets/readme/demo-poster.webp b/assets/readme/demo-poster.webp new file mode 100644 index 0000000000000000000000000000000000000000..ec90edb459ba475db6b160ef9a8ef7463b03b304 GIT binary patch literal 39358 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 literal 0 HcmV?d00001 diff --git a/assets/readme/pipeline-dark.svg b/assets/readme/pipeline-dark.svg new file mode 100644 index 0000000..c1e084c --- /dev/null +++ b/assets/readme/pipeline-dark.svg @@ -0,0 +1,71 @@ + + The webmcpify pipeline + Detect, inventory, human approval of the tool manifest, integrate, verify in real Chrome, heal failures with capped retries, audit. Every phase reads and writes .webmcpify/manifest.json. + + + FIG. 2 — THE PIPELINE + ↻ loops over persisted state + + + DETECT + stack, routes, + auth and state + + + + INVENTORY + reads area by area, + proposes a tool + manifest + + + + ↻ per area + + YOU APPROVE + names, schemas, + read-only or + mutating — per tool + + + ◆ human gate + + INTEGRATE + vendored runtime, + built + typechecked + + + + ↻ per batch + + VERIFY + real Chrome: + result + UI state + + + + HEAL + fixes only that + tool, capped + + + + AUDIT + every diff hunk + maps to the manifest + + + ↻ re-verify, capped + + + .webmcpify/manifest.json + single source of truth — every phase reads and writes it + ✓ resumes across sessions, context windows and agents + ✓ keeps verification evidence per tool + diff --git a/assets/readme/pipeline-light.svg b/assets/readme/pipeline-light.svg new file mode 100644 index 0000000..86f42f1 --- /dev/null +++ b/assets/readme/pipeline-light.svg @@ -0,0 +1,71 @@ + + The webmcpify pipeline + Detect, inventory, human approval of the tool manifest, integrate, verify in real Chrome, heal failures with capped retries, audit. Every phase reads and writes .webmcpify/manifest.json. + + + FIG. 2 — THE PIPELINE + ↻ loops over persisted state + + + DETECT + stack, routes, + auth and state + + + + INVENTORY + reads area by area, + proposes a tool + manifest + + + + ↻ per area + + YOU APPROVE + names, schemas, + read-only or + mutating — per tool + + + ◆ human gate + + INTEGRATE + vendored runtime, + built + typechecked + + + + ↻ per batch + + VERIFY + real Chrome: + result + UI state + + + + HEAL + fixes only that + tool, capped + + + + AUDIT + every diff hunk + maps to the manifest + + + ↻ re-verify, capped + + + .webmcpify/manifest.json + single source of truth — every phase reads and writes it + ✓ resumes across sessions, context windows and agents + ✓ keeps verification evidence per tool + diff --git a/tests/readme-art.test.mjs b/tests/readme-art.test.mjs new file mode 100644 index 0000000..f79ea24 --- /dev/null +++ b/tests/readme-art.test.mjs @@ -0,0 +1,20 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; +import { outputs } from '../assets/readme/build.mjs'; + +test('README artwork matches its generator (run node assets/readme/build.mjs)', () => { + for (const [name, render] of Object.entries(outputs)) { + const committed = readFileSync(new URL(`../assets/readme/${name}`, import.meta.url), 'utf8'); + assert.equal(committed, render(), `${name} is stale`); + } +}); + +test('README references only existing artwork and both theme variants', () => { + const readme = readFileSync(new URL('../README.md', import.meta.url), 'utf8'); + for (const base of ['banner', 'pipeline']) { + assert.match(readme, new RegExp(`assets/readme/${base}-dark\\.svg`)); + assert.match(readme, new RegExp(`assets/readme/${base}-light\\.svg`)); + } + readFileSync(new URL('../assets/readme/demo-poster.webp', import.meta.url)); +}); From 7c558b535848f4c9adb76cddab6fc4e1e2970e16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20T=C3=BCchler?= Date: Sat, 19 Sep 2026 16:40:38 +0000 Subject: [PATCH 2/2] docs: restore semantic README heading --- README.md | 2 ++ tests/readme-art.test.mjs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 677f057..f3def35 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +

webmcpify — the WebMCP agent skill

+

diff --git a/tests/readme-art.test.mjs b/tests/readme-art.test.mjs index f79ea24..972ad7c 100644 --- a/tests/readme-art.test.mjs +++ b/tests/readme-art.test.mjs @@ -12,6 +12,8 @@ test('README artwork matches its generator (run node assets/readme/build.mjs)', test('README references only existing artwork and both theme variants', () => { const readme = readFileSync(new URL('../README.md', import.meta.url), 'utf8'); + assert.match(readme, /]*>[^<]*WebMCP agent skill[^<]*<\/h1>/, + 'README must retain a semantic h1 containing the discoverability phrase'); for (const base of ['banner', 'pipeline']) { assert.match(readme, new RegExp(`assets/readme/${base}-dark\\.svg`)); assert.match(readme, new RegExp(`assets/readme/${base}-light\\.svg`));