webmcpify — make any web app agent-ready, verifiably
You are running the webmcpify pipeline. It takes an existing web application and
exposes its user-facing functionality as WebMCP
tools (document.modelContext — a proposed web standard incubated in the W3C Web
Machine Learning Community Group, currently a Chrome origin trial), so browser AI
agents can operate the app through structured tool calls instead of guessing at the DOM.
DETECT ──▶ INVENTORY ──▶ [HUMAN GATE: manifest approval] ──▶ INTEGRATE ──▶ VERIFY ──▶ HEAL ──▶ AUDIT
▲ loop per-area batches on big apps ▲ loop ▲ loop ▲ loop
└── per area └── per manifest entry ──┘
Everything you need ships inside this skill directory: phase guides in
references/, and vendorable code in templates/ (runtime, ambient types,
JS variant, React JSX typings, verification spec + compat helper). Never assume files exist
outside the skill dir.
Out of scope (stop and say so): backend-only MCP servers (that's classic MCP, not WebMCP), automating third-party sites you don't control, and generic SEO work.
Invocation modes
The user may pass an argument (/webmcpify <mode> or plain words):
| Argument | Run | Stop at |
|---|---|---|
(none) or full |
all phases, resuming from current manifest state | done |
inventory / map |
DETECT + INVENTORY loops only — zero code changes | present the manifest table for review |
integrate |
INTEGRATE loop only (requires approved tools in the manifest) | integrated + built |
verify |
VERIFY + HEAL loops on integrated/verified tools | green/skipped report |
status |
read .webmcpify/manifest.json — read-only |
report phase, per-status tool counts, and the recommended next command |
Any other text is scoping guidance (e.g. "only the checkout area", "read-only tools only").
Ground rules (non-negotiable, enforce in every phase)
- Zero unrelated changes. Every diff hunk you produce must trace to a manifest entry or the recorded one-time setup. Never refactor, reformat, rename, or "improve" anything else — note problems in the report instead. Files that were already dirty at baseline (recorded in the manifest) are untouchable: never modify or revert them.
- Read-only tools first. Mutations are tri-state:
mutating: false,"client"(browser-local only: prefs, localStorage), or"server"(data leaves the browser). Server-mutating tools require explicit per-tool human approval recorded in the manifest; client-mutating tools may be approved as a batch at the gate. The scope floor is precise: no auth/login/session/password/ MFA/SSO; no signup/registration/payment/billing/subscription; no tool returning a credential, token, key, JWT, signed URL, or cookie; and no irreversible delete except by opening the app's own confirmation UI. Creating or changing ordinary product objects is in scope and must not be mistaken for account creation. - The server is the only trust boundary. A tool's
execute()may only call code paths the UI already uses (same endpoints, same validation, same auth). Never create new endpoints, never bypass existing checks, never put secrets in tools. - Spec-shaped and dependency-free. Register via
document.modelContext.registerTool()with AbortSignal lifecycle (feature-detect the deprecatednavigator.modelContextfallback). No third-party WebMCP runtime dependencies. Everything feature-detected: the app behaves identically in browsers without WebMCP. - Never
toolautosubmiton state-changing forms — neithermutating: "client"nor"server". Only on pure read forms (search, filter, availability). - State lives in files, not in your context. Read/write
.webmcpify/constantly; assume your context can be wiped between any two steps. Write the manifest atomically (writemanifest.json.tmp, then rename overmanifest.json). - Commits are opt-in. Never commit unless the human chose a commit policy at the gate (see below). Without git or without permission, leave changes in the working tree and record progress in the manifest only.
Fresh, authoritative guidance
WebMCP is an evolving origin-trial API — the surface has already changed during the
trial (testing API removed 2026-07; navigator → document). Before Phase 2, if
network is available, pull Google's current official guides rather than relying on
memory:
npx -y modern-web-guidance@latest retrieve "webmcp,agentic-forms,agentic-javascript-tools"
If offline, use references/integrate.md — but prefer the live guides when they conflict.
The state protocol — .webmcpify/ in the target repo
| File | Purpose |
|---|---|
manifest.json |
Single source of truth (schema below; atomic writes) |
areas/<id>.tools.json |
Sub-agent shard output during inventory fan-out (merged, then deleted) |
report.md |
Human-facing running report; finalized at the end |
Resume rule: if manifest.json exists, resume — recompute nothing already
recorded. Merge leftover shards FIRST: any existing areas/<id>.tools.json
files are merged into the manifest (mark those areas inventoried, delete the
shards) before redispatching any sub-agents. Then continue at pipeline.phase,
the first pending area, or the first tool whose status is not terminal.
Terminal statuses: verified, skipped, rejected.
An inventory verdict is reusable only under the policy that produced it. Before
honouring an inventoried area, compare its policyFingerprint with
pipeline.inventoryPolicy.fingerprint. When a named gate is widened or removed,
mechanically reset every area whose exclusions cite that gate to pending, clear
its derived route-coverage entries and unapproved discovered tools, and record the
invalidation in log. A zero-candidate area without usable exclusion provenance is
also reset. Editing status alone is never an invalidation. Approved or integrated
tools affected by a later policy change return to the human gate.
Phase transitions (make the atomic manifest write the moment the condition holds):
detect → inventory:apprecorded,app.secureContext === trueat the recordedapp.verificationOrigin, backend/CORS assumptions captured,coverageTargetand inventory policy recorded, andbaselineSha/baselineDirtycaptured.inventory → gate: no areapending, route coverage is recorded, and the target-specific completeness pass has run.gate → integrate: everydiscoveredtool isapproved/rejected, andcommitPolicy+commitWebmcpifyDirare set.integrate → verify: noapprovedtools remain (eachintegratedor terminal), build green, andpipeline.discoveryisnullorcomplete: true.verify → heal: verify loop visited everyintegratedtool and ≥1 isfailed(none failed → straight toaudit).heal → audit: no toolfailedand post-heal full re-verify passed.audit → done: every hunk mapped-or-flagged,report.mdfinalized.
Manifest schema (Webmcpify Manifest v4):
{
"webmcpify": 4,
"app": { "stack": "react-vite", "typescript": true, "entry": "src/main.tsx",
"baseUrl": "https://app.example.test", "startCommand": "npm run dev",
"verificationOrigin": "https://app.example.test",
"secureContext": true,
"backendOrigins": ["http://localhost:3000"],
"corsAllowlist": ["https://app.example.test"],
"authFixtures": { // how verify OBTAINS each session
"member": { "obtain": "npm run seed:test-user, then sign in at /login",
"account": "member@example.test",
"env": ["TEST_MEMBER_PASSWORD"] } // env var NAMES only — never secret values
} },
"pipeline": {
"phase": "inventory", // detect|inventory|gate|integrate|verify|heal|audit|done — transition rules above
"coverageTarget": "parity", // REQUIRED before inventory: "curated" | "parity"; never silently default
"inventoryPolicy": {
"revision": 1,
"fingerprint": "sha256:<normalized-gates>",
"gates": {
"identity": "exclude auth/login/session/password/MFA/SSO",
"tenancy_billing": "exclude signup/registration/payment/billing/subscription",
"credentials": "exclude tools returning credentials/tokens/keys/JWTs/signed URLs/cookies",
"irreversible_delete": "only open the app's own confirmation UI"
}
},
"setup": { // PATHS created/modified per one-time setup step ([] = not done yet)
"runtimeVendored": ["src/webmcp/webmcpify.ts", "src/webmcp/webmcp.d.ts"],
"harnessInstalled": [".webmcpify/webmcp.spec.ts", ".webmcpify/webmcp-compat.js"],
"originTrialNoted": ["README.md"]
},
"discovery": null, // optional off-page layer (references/discovery.md). Stays null unless
// the human approves publishing; then, written BEFORE the first file:
// { "at": "2026-08-06",
// "publishedTools": ["get_faq"], // ids cleared for PUBLIC listing
// "paths": [], // artifacts, appended AS each is written ([] = none yet)
// "complete": false } // true only when every artifact exists and the drift test passes
// Absent field = null. A record written before this key existed has no
// `complete`: read that as false, re-check the artifacts, persist the flag.
"baselineSha": "abc1234", // HEAD at pipeline start; null if no git
"baselineDirty": ["src/wip.ts"], // paths dirty at start — untouchable (ground rule 1)
"commitPolicy": null, // set at the gate: "commit-per-batch" | "no-commit"
"commitWebmcpifyDir": null, // set at the gate: commit .webmcpify/ itself? true | false
"blockers": [] // e.g. "app won't start locally: needs $API_KEY" — surfaced at the gate
},
"areas": [
{ "id": "tickets", "paths": ["src/features/tickets/"], "routes": ["/projects/:id/tickets"],
"status": "inventoried", // pending|inventoried
"policyFingerprint": "sha256:<normalized-gates>",
"exclusions": [{ "gate": "identity", "reason": "login form" }] }
],
"routeCoverage": [
{ "route": "/projects/:id/tickets", "area": "tickets", "auth": ["role:member"],
"interactions": [
{ "element": "New ticket button", "source": "src/features/tickets/List.tsx:42",
"tool": "create_ticket", "reason": null },
{ "element": "Account menu", "source": "src/layout/AccountMenu.tsx:18",
"tool": null, "reason": "identity gate: logout/session action" }
] }
],
"tools": [
{
"id": "create_ticket",
"area": "tickets",
"kind": "imperative", // imperative | declarative
"mutating": "server", // false | "client" (browser-local only: prefs, localStorage) | "server" (data leaves the browser)
"priority": 1, // 1 = expose first; 2/3 = later waves
"description": "Creates a new ticket in the currently open project.",
"inputSchema": { /* JSON Schema */ },
"annotations": { "readOnlyHint": false, "untrustedContentHint": false }, // verify asserts these on the enumerated tool
"source": ["src/features/tickets/NewTicket.tsx:42"], // the UI code path it wraps
"route": "/projects/demo/tickets", // where verify navigates
"auth": ["role:member"], // "none" | "session" | ["role:<name>", ...] — keys into app.authFixtures; verify runs once per listed role
"examples": { "valid": { "title": "Test ticket" }, "invalid": {} },
// invalid: null ONLY for readOnlyHint tools with no/empty params —
// verify then asserts dual-outcome: rejects OR resolves with no side effect
"expect": { "result": "created", "navigation": null, "ui": "new row appears in the ticket list" },
// result = substring of the serialized structured result;
// navigation = destination URL/pattern after a declarative submit or deferred imperative route action
"cleanup": "delete the created ticket via the UI's own delete path (test data only)", // required for mutating:"server", recommended for "client"
"status": "discovered", // discovered|approved|rejected*|integrated|verified*|failed|skipped* (* = terminal)
"approval": null, // server-mutating tools, once approved: { "note": "...", "at": "2026-07-12",
// "productionSideEffect": null } — set only when verification unavoidably
// causes a real production effect (see VERIFY: production side-effect policy)
"contractRevision": 1,
"failure": null, // on failure: { "class": "contract|implementation|environment|external-policy|flaky|client-capacity", "signature": "...", "contractRevision": 1 }
"attempts": 0, // independent retries of this failure signature under this contract revision
"batchCommit": null, // sha under commit-per-batch — lands in the manifest one commit LATER
"notes": ""
}
],
"log": [ "2026-07-12 inventory: area checkout done, 4 candidates" ]
}
v2/v3→v4 migration: first perform the existing v2→v3 conversions (auth
string → array; setup booleans → path arrays; mutating: true → "server";
annotations/blockers/commitWebmcpifyDir/navigation defaults). Then require a
coverageTarget choice, capture the current inventory policy, add the origin/CORS
fields, routeCoverage: [], and tool contractRevision/failure fields. Existing
inventoried areas get policyFingerprint: null and are reset to pending; their
old zero-candidate verdicts are not trusted. Bump to 4 only after persisting that
invalidation.
Phase 0 — DETECT
The first browser gate is secure context. Read only enough startup config to
boot the app at a candidate verification origin, open it in headed Chrome, and
evaluate window.isSecureContext. Record the exact origin and result. HTTPS and
loopback origins can qualify; a plain-HTTP non-loopback origin does not. The
WebMCPTesting feature flag does not waive this gate. If false, record the blocker
and refuse to enter INVENTORY.
Capture the app's absolute backend-origin assumptions and exact CORS allow-list, then choose a verification origin compatible with both. If the page never boots, save console errors and failed requests before changing any tool code: an absolute localhost backend URL or port-pinned CORS rule is an environment failure, not a WebMCP integration failure.
Choose pipeline.coverageTarget explicitly before inventory: curated maps a
reviewed set of high-value actions; parity performs an exhaustive interactive-
element census per authenticated route. There is no measured universal client
tool-count ceiling, so do not promise that a large parity toolset is safe merely
because registration is route-scoped; verification must enumerate each route in
the target clients actually available.
Identify stack, build + dev-server commands, TypeScript or not, auth model
(including how verify obtains each test session → app.authFixtures), test
setup, and how the app starts locally; record under app. Record the git baseline:
pipeline.baselineSha = current HEAD and pipeline.baselineDirty = git status --porcelain paths (both null/[] without git). If the app cannot be started
locally, append the blocker to pipeline.blockers — integration may proceed, but
verification will be blocked and this must be surfaced at the gate. Details:
references/inventory.md.
Phase 1 — INVENTORY (loop; scales to any size)
Never map a large codebase in one pass.
- Area map first (cheap, structural): enumerate routes/views/feature modules
from the router config, pages directory, or navigation — without reading
implementation files. Write every area to
areaswith"pending". - Inventory loop — one area per iteration: deep-read only that area's files;
draft a candidate tool per user action (conventions, tool-count budget, and
overlap rules:
references/inventory.md) with ALL manifest fields filled, includingroute,auth,annotations,examples,expect, andcleanup(required formutating: "server", recommended for"client") — the verify phase runs from these fields alone. Append as"discovered", mark the area"inventoried", write the manifest, repeat.- Sub-agent fan-out: sub-agents never write
manifest.json. Each writes only its ownareas/<id>.tools.jsonshard — schema{ "webmcpifyShard": 4, "area": "<id>", "tools": [ /* full v4 tool entries */ ] }, written atomically (tmp + rename). You (the coordinator) merge shards into the manifest sequentially, then delete them; on resume, merge existing shards FIRST before redispatching (Resume rule).
- Sub-agent fan-out: sub-agents never write
- Coverage output: populate
routeCoveragein both modes.curatedmaps the selected tools and records why deliberately omitted interaction classes were left out.parityinventories every interactive element on every authenticated route and maps each to a tool or a written reason. Pay explicit attention to deletes, drag/drop ordering, bulk and multi-select, table sort/columns/pagination/ saved filters, invitations, membership and permission writes, settings toggles, and canvas/viewer controls. - Exit: no
pendingareas remain, plus the coverage-target completeness pass.
GATE — manifest approval (the one main checkpoint)
Present the manifest compactly (id, area, kind, mutating, priority, one-line description) — per-area batches on large apps. Ask the human to decide, in one exchange where possible:
- Which tools are
approvedvsrejected(rejectedis terminal — rejected tools are excluded from every later phase and from exit conditions).mutating: "server"tools need individual acknowledgment → record inapproval;mutating: "client"tools may be approved as a batch. - Commit policy:
commit-per-batch(each integration batch committed, revertable — recommended on a clean baseline) orno-commit(leave changes uncommitted for the human to review/commit) →pipeline.commitPolicy. Also whether.webmcpify/itself should be committed (recommended: yes — it documents the integration) →pipeline.commitWebmcpifyDir. - Every entry in
pipeline.blockers(e.g. app won't start). If verifying a tool will unavoidably cause a real production side effect (e.g. a mailer with an Origin-allow-listed endpoint), get that approved HERE and record it in the tool'sapproval.productionSideEffect— see VERIFY.
If the human changes a scope gate, update pipeline.inventoryPolicy, compute its
new fingerprint, apply the mandatory invalidation rule before presenting the
manifest again, and show which areas were reopened. Never carry forward a verdict
without its producing policy.
Apply references/security.md to every mutating tool before presenting.
Phase 2 — INTEGRATE (loop)
One-time setup first — record the created/modified file paths in
pipeline.setup (e.g. runtimeVendored: ["src/webmcp/webmcpify.ts", ...]):
vendor the runtime from this skill's templates/ (webmcpify.ts, or
webmcpify.js for non-TS projects, plus webmcp.d.ts for TS and
webmcp-jsx.d.ts for React TSX — keep the full MIT header; see
references/runtime.md) and note the origin-trial/flag requirement in the target
README (originTrialNoted). Then loop:
- Pick the next batch of
approvedtools — one area or ≤5 tools. - Implement per
references/integrate.md: declarative attributes for standard HTML forms (including framework-rendered and fetch-intercepted ones); imperative registration via the vendored runtime for non-form or controlled-state actions. - Build + typecheck; fix only what the batch broke.
- Mark tools
"integrated", write the manifest. Undercommit-per-batch: require a clean index before staging (unrelated staged changes → stop and surface); stage only the batch's files by path — nevergit add -A,-u,., orcommit -a; commitfeat(webmcp): expose <ids> (webmcpify). The commit sha lands inbatchCommiton the next manifest write — one commit later (the manifest can't contain its own commit's sha). Never amend a previous batch commit. - Repeat until no
approvedtools remain.
Optional discovery layer — a /.well-known/webmcp manifest, rel="webmcp"
links, llms.txt. Off by default: it publishes tool metadata to the open web, so
it needs its own human approval and only ever lists public, unauthenticated
tools. Offer it once tools are integrated; build it per references/discovery.md.
Record the approval before writing any file in pipeline.discovery (at,
publishedTools, paths: [], complete: false) — that's what survives a context
reset and what lets AUDIT map these hunks. Append each artifact to paths as you
write it and set complete: true only once every artifact exists and its drift
test passes. Approved but complete: false is unfinished work: finish it before
leaving INTEGRATE — otherwise a reset mid-publication looks exactly like a
finished one. pipeline.discovery: null means not approved: never create or update
a published manifest, and flag one the pipeline created or modified since
baselineSha as an unmapped hunk (a pre-existing, untouched manifest is not your
hunk — leave it alone).
Phase 3 — VERIFY (loop)
Set up once from templates/webmcp.spec.ts per references/verify.md (real headed
Chrome; current production document.modelContext.getTools()/executeTool() surface).
Then loop over every integrated tool, using its manifest route, auth,
examples, expect, and annotations fields:
- assert the tool is registered with the expected schema (enumerated
inputSchemais a stringified JSON Schema — parse before comparing) and the manifestannotations; - execute the valid example (mutating tools: dev/test data only, then run
cleanup) and one invalid example (invalid: nullzero-param read tools: dual-outcome assertion — seereferences/verify.md); - assert on the returned structured result and the resulting UI state per
expect; for route actions, assertexpect.navigationafter the deferred app navigation. A bare imperativenull/undefinedresult is a failure.
Pass → "verified". Fail → "failed" + failure note. Role-scoped tools: run the
loop once per role listed in auth, signing in via the matching
app.authFixtures entry.
Production side-effect policy — when a tool's verification unavoidably causes
a real production effect (e.g. an email actually sent), ALL THREE are required:
(1) the human approved it at the gate, recorded in approval.productionSideEffect;
(2) every test payload is marked [webmcpify verification]; (3) the effect is
listed in report.md. Without the recorded approval, don't execute the live
path — mark the tool skipped with a blocker note.
Phase 4 — HEAL (loop)
While any tool is "failed": diagnose via references/heal.md, fix only that
tool's integration — implementation-only fixes; if the fix would change the
approved contract (schema, description, mutating class, annotations,
expect), go back to the gate for re-approval instead of silently changing the
manifest. Classify the failure before counting it. attempts counts only
independent retries of the same failure signature under the same contract revision;
environment and shared contract failures do not burn one attempt on every affected
tool. When the contract changes after re-approval, increment contractRevision,
clear failure, and reset attempts. At three independent failed retries of an
unchanged implementation/flaky signature, "skipped" requires the failure class,
signature, and evidence that makes the tool impossible under the current contract.
Never widen the diff or fake a pass. After healing, re-run verification once for all tools
with status integrated or verified (healing one tool can break another —
scope collisions).
Exit: every tool is verified, skipped, or rejected; build green.
Final — AUDIT + report
- Diff audit (flag-only, never auto-revert): collect the pipeline's changes —
git diff <baselineSha>..HEADplus the index and untracked files undercommit-per-batch, or the working tree + index + untracked underno-commit. Every hunk must map to a manifest entry, a recordedpipeline.setuppath, or apipeline.discovery.pathsentry. An unmapped hunk → flag it in the report with file/line and a suggested disposition; never revert anything yourself. A hunk in abaselineDirtyfile → untouchable, flag only. Without abaselineSha, audit the files named in manifestsourcefields,pipeline.setuppaths, andpipeline.discovery.paths(setup entries recorded asnullby the v2→v3 migration: fall back to flag-only for those files). - Finalize
.webmcpify/report.md: the enumerated route→tool map, coverage target, tool coverage per area, skipped/rejected tools with reasons, security notes (which mutating tools exist, what guards them, any recorded production side effects), how to test manually (flag, DevTools WebMCP pane, inspector extension), and every blocker that needs a human. - Tell the human: what's exposed, what's skipped and why, and how to try it.
References (read on demand, not upfront)
references/inventory.md— area mapping, naming/schema conventions, budgets/overlapreferences/integrate.md— declarative + imperative patterns per stackreferences/runtime.md— vendoring + wiring thetemplates/runtimereferences/verify.md— harness setup: flags, surfaces, Playwright/Puppeteer, evalsreferences/heal.md— failure taxonomy → fixesreferences/discovery.md— optional off-page discovery (manifest,rel="webmcp",llms.txt) + how to read third-party audit scoresreferences/security.md— the security checklist (apply before the gate and at audit)references/client.md— dated ChatGPT Site tools availability and troubleshooting