Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"name": "webmcpify",
"source": "./",
"description": "WebMCP agent skill for curated core coverage or route-by-route parity — inventory an existing web app, integrate approved tools, then verify and heal them in a real browser.",
"version": "0.5.0"
"version": "0.5.1"
}
]
}
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "webmcpify",
"description": "WebMCP agent skill for curated core coverage or route-by-route parity — inventory an existing web app, integrate approved tools, then verify and heal them in a real browser.",
"version": "0.5.0",
"version": "0.5.1",
"author": {
"name": "Jonas Tüchler"
}
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "webmcpify",
"description": "WebMCP agent skill for curated core coverage or route-by-route parity — inventory an existing web app, integrate approved tools, then verify and heal them in a real browser.",
"version": "0.5.0",
"version": "0.5.1",
"author": {
"name": "Jonas Tüchler"
}
Expand Down
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ reconstruct them from git history.

## [Unreleased]

## [0.5.1] — 2026-09-14

- Updated verification for the CG draft and Chrome's 2026-09-11 execution
contract: `executeTool` now receives a JavaScript object, while Chrome 150's
deprecated JSON-string input remains supported through a temporary,
side-effect-free capability probe. Real application tools are invoked exactly
once, so a post-mutation handler failure cannot trigger a compatibility retry.
- Kept current and older browser evidence comparable: the harness and visual
Workbench accept object or stringified enumerated schemas, and the native proof
records whether the browser exposes `consequentialHint` instead of pinning the
Chrome 150 omission.

## [0.5.0] — 2026-09-09

- Added an agent-launched, dependency-free visual WebMCP Workbench with the
Expand Down Expand Up @@ -58,4 +70,5 @@ reconstruct them from git history.

[0.4.0]: https://github.com/TueJon/webmcpify/releases/tag/v0.4.0
[0.5.0]: https://github.com/TueJon/webmcpify/compare/v0.4.0...v0.5.0
[Unreleased]: https://github.com/TueJon/webmcpify/compare/v0.5.0...HEAD
[0.5.1]: https://github.com/TueJon/webmcpify/compare/v0.5.0...v0.5.1
[Unreleased]: https://github.com/TueJon/webmcpify/compare/v0.5.1...HEAD
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ 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 changed twice during the trial), learning tool-design
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.

Expand Down Expand Up @@ -156,8 +156,9 @@ 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, probes for the current
enumeration/execution surface, and treats Google's live
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 treats Google's live
[modern-web-guidance](https://github.com/GoogleChrome/modern-web-guidance) as the
source of current best practices at integration time.

Expand Down
2 changes: 1 addition & 1 deletion gemini-extension.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "webmcpify",
"description": "WebMCP agent skill for curated core coverage or route-by-route parity — inventory an existing web app, integrate approved tools, then verify and heal them in a real browser.",
"version": "0.5.0",
"version": "0.5.1",
"author": {
"name": "Jonas Tüchler"
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "WebMCP agent skill for curated core coverage or route-by-route parity — inventory an existing web app, integrate approved tools, then verify and heal them in a real browser.",
"type": "module",
"license": "MIT",
"version": "0.5.0",
"version": "0.5.1",
"publisher": "TueJon",
"repository": {
"url": "https://github.com/TueJon/webmcpify"
Expand Down
65 changes: 49 additions & 16 deletions proof/demo/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,36 @@ const artifacts = join(repo, 'proof', 'artifacts');
const sourceVideo = join(artifacts, 'webmcpify-proof-source.webm');
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, mode === 'record' ? ms : 20));

async function detectExecuteInputMode(page) {
return page.evaluate(async () => {
const controller = new AbortController();
const name = `webmcpify_input_probe_${crypto.randomUUID().replaceAll('-', '')}`;
let calls = 0;
await document.modelContext.registerTool({
name,
description: 'Side-effect-free verification of the browser executeTool input contract.',
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
async execute() { calls += 1; return 'webmcpify-input-probe'; },
}, { signal: controller.signal });
try {
const tool = (await document.modelContext.getTools()).find((candidate) => candidate.name === name);
if (!tool) throw new Error('WebMCP input-contract probe did not register');
try {
await document.modelContext.executeTool(tool, {});
if (calls !== 1) throw new Error('Object-input probe did not execute exactly once');
return 'object';
} catch (error) {
if (calls !== 0) throw error;
await document.modelContext.executeTool(tool, '{}');
if (calls !== 1) throw new Error('JSON-string input probe did not execute exactly once');
return 'json-string';
}
} finally {
controller.abort();
}
});
}

const types = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8' };
const server = createServer(async (request, response) => {
try {
Expand Down Expand Up @@ -59,6 +89,7 @@ try {
}));
assert.deepEqual(nativeSurface, { context: 'object', enumerate: 'function', execute: 'function' });
assert.equal((await page.evaluate(() => document.modelContext.getTools())).length, 0);
const executeInputMode = await detectExecuteInputMode(page);

await page.evaluate(() => {
window.proof.phase('inventory', 'Inventory the existing app');
Expand Down Expand Up @@ -110,12 +141,10 @@ try {
assert(tool);
assert.equal(tool.annotations.readOnlyHint, false);
assert.equal(tool.annotations.untrustedContentHint, false);
assert.equal(
tool.annotations.consequentialHint,
undefined,
'Chrome compatibility changed: update the dated consequentialHint evidence and harness expectation',
);
assert.deepEqual(JSON.parse(tool.inputSchema), {
if (tool.annotations.consequentialHint !== undefined) {
assert.equal(tool.annotations.consequentialHint, false);
}
assert.deepEqual(typeof tool.inputSchema === 'string' ? JSON.parse(tool.inputSchema) : tool.inputSchema, {
type: 'object',
properties: { category: { type: 'string', enum: ['all', 'feature', 'fix'] } },
required: ['category'],
Expand All @@ -127,29 +156,32 @@ try {
await delay(2800);

const before = await page.locator('article:visible').count();
const result = await page.evaluate(async () => {
const result = await page.evaluate(async (inputMode) => {
const registered = (await document.modelContext.getTools()).find((item) => item.name === 'set_release_filter');
return document.modelContext.executeTool(registered, JSON.stringify({ category: 'fix' }));
});
const args = { category: 'fix' };
return document.modelContext.executeTool(registered, inputMode === 'object' ? args : JSON.stringify(args));
}, executeInputMode);
const after = await page.locator('article:visible').count();
assert.equal(before, 4);
assert.equal(after, 2);
assert.match(result, /2 release notes visible/);
await page.evaluate(() => { window.proof.check('valid call changed visible UI: 4 → 2'); window.proof.line('EXECUTE category=fix → 2 release notes visible'); });
await delay(3300);

const invalidResult = await page.evaluate(async () => {
const invalidResult = await page.evaluate(async (inputMode) => {
const registered = (await document.modelContext.getTools()).find((item) => item.name === 'set_release_filter');
return document.modelContext.executeTool(registered, JSON.stringify({ category: 'private' }));
});
const args = { category: 'private' };
return document.modelContext.executeTool(registered, inputMode === 'object' ? args : JSON.stringify(args));
}, executeInputMode);
assert.match(invalidResult, /^ERROR:/);
assert.equal(await page.locator('article:visible').count(), 2);
await page.evaluate(() => { window.proof.check('invalid enum returned bounded error; UI unchanged'); window.proof.line('INVALID category=private → ERROR (no UI side effect)'); });
await delay(3000);
await page.evaluate(async () => {
await page.evaluate(async (inputMode) => {
const registered = (await document.modelContext.getTools()).find((item) => item.name === 'set_release_filter');
return document.modelContext.executeTool(registered, JSON.stringify({ category: 'all' }));
});
const args = { category: 'all' };
return document.modelContext.executeTool(registered, inputMode === 'object' ? args : JSON.stringify(args));
}, executeInputMode);
assert.equal(await page.locator('article:visible').count(), 4);
await page.evaluate(() => { window.proof.check('cleanup restored all notes'); window.proof.line('CLEANUP category=all → fixture restored'); });
await delay(3000);
Expand Down Expand Up @@ -177,7 +209,8 @@ try {
await rename(generated, sourceVideo);
console.log(`recorded ${sourceVideo}`);
}
console.log(`proof verified in Chrome ${chromeVersion}: native getTools/executeTool, schema, annotations (consequentialHint omitted by this build), UI delta, bounded invalid input, cleanup`);
const consequentialState = tool.annotations.consequentialHint === undefined ? 'consequentialHint omitted' : 'consequentialHint exposed';
console.log(`proof verified in Chrome ${chromeVersion}: native getTools/executeTool (${executeInputMode} input), schema, annotations (${consequentialState}), UI delta, bounded invalid input, cleanup`);
} finally {
await browser?.close().catch(() => {});
await new Promise((resolve) => server.close(resolve));
Expand Down
22 changes: 22 additions & 0 deletions release/v0.5.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# webmcpify v0.5.1

This patch keeps native verification aligned with the WebMCP CG draft and
Chrome's September 11 execution update.

- Current browsers receive a JavaScript object in `executeTool()`; Chrome 150's
deprecated JSON-string input remains supported.
- A temporary, side-effect-free probe chooses the browser input mode before any
application tool runs. Real tools are never retried, preventing duplicate
mutations when a handler fails after changing state.
- The verification template, visual Workbench, ambient types, and reproducible
native proof accept both the current object-shaped and older stringified
`inputSchema` evidence.
- Native proof records whether `consequentialHint` is exposed instead of assuming
Chrome 150's omission remains universal.

Primary compatibility references:

- https://webmachinelearning.github.io/webmcp/
- https://developer.chrome.com/docs/ai/webmcp/imperative-api
- https://github.com/web-platform-tests/wpt/commit/1a21db90adf8a264370ad806ed761f39e1d435a0
- https://github.com/web-platform-tests/wpt/commit/c94abb33b8b0d162ab33f259c1dc44724d42f3d3
2 changes: 1 addition & 1 deletion skill.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "webmcpify",
"version": "0.5.0",
"version": "0.5.1",
"description": "WebMCP agent skill for curated core coverage or route-by-route parity — inventory an existing web app, integrate approved tools, then verify and heal them in a real browser.",
"license": "MIT",
"author": {
Expand Down
2 changes: 1 addition & 1 deletion skills/webmcpify/references/client.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ChatGPT client reality — Site tools

Checked 2026-08-31 against the official OpenAI documentation:
Checked 2026-09-14 against the official OpenAI documentation:
<https://learn.chatgpt.com/docs/webmcp>. Re-check that page before publishing or
relying on model/workspace availability; this UI is moving independently of the
WebMCP draft and Chrome implementation.
Expand Down
2 changes: 1 addition & 1 deletion skills/webmcpify/references/integrate.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export const searchTicketsTool = {
};
```

> **Native I/O compat** — `getTools()` returns `inputSchema` stringified on native Chrome but as object in stubs — handle both (`typeof === 'string' ? JSON.parse : id`). `executeTool` needs `JSON.stringify(args)` on native lag, object per spec. For `validate:true`, register with `inputSchema` only. Runner LLM envelope: `const raw=t.inputSchema; const schema=typeof raw==='string'?JSON.parse(raw):raw??{type:'object',properties:{}}; const llmTool={function:{parameters:schema}}` — never pass `parameters` through WebMCP.
> **Native I/O compat** — `getTools()` may return `inputSchema` as a string on older Chrome or an object on current implementations — handle both (`typeof === 'string' ? JSON.parse : id`). Current `executeTool` takes an object; Chrome 150 needs `JSON.stringify(args)`, so use the capability-probe adapter from the verification template rather than retrying a real tool. For `validate:true`, register with `inputSchema` only. Runner LLM envelope: `const raw=t.inputSchema; const schema=typeof raw==='string'?JSON.parse(raw):raw??{type:'object',properties:{}}; const llmTool={function:{parameters:schema}}` — never pass `parameters` through WebMCP.

Key rules:
- **Annotations describe risk; they do not enforce it.** Use
Expand Down
20 changes: 11 additions & 9 deletions skills/webmcpify/references/verify.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,23 @@ const tools = await mc.getTools();

Contract facts that generated assertions MUST respect:

- Enumerated `inputSchema` may be stringified (Chrome native lag) or object (spec/stub) — `typeof === "string" ? JSON.parse(s) : s ?? {type:'object',properties:{}}` before comparing.
- Enumerated `inputSchema` may be stringified (older Chrome) or object (current
implementations/spec stubs) — `typeof === "string" ? JSON.parse(s) : s ??
{type:'object',properties:{}}` before comparing.
- The CG draft and Chrome docs define `consequentialHint`, but Chrome 150 accepted
it at registration and omitted it from `getTools()`. Assert the expected value
when the enumerated property exists; otherwise record a dated browser-compatibility
annotation and prove the registration object in an app/unit test. Never claim
native propagation from an absent field.
- `executeTool(...)` resolves to a **JSON string result, or `null` when the execution
navigated** — stub may return object; normalize via `typeof` before `toMatch`.
- **Native `executeTool` needs JSON-string arguments even for tools with OMITTED
`inputSchema`** (zero-param): `executeTool(tool, '{}')`, not `executeTool(tool, {})`.
The harness uses an explicit adapter mode: stub `tool.execute(object)` or
spec-shaped `mc.executeTool(tool, object)` when `mc.__webmcpStubObjectMode` is set,
native `mc.executeTool(tool, JSON.stringify(args))` otherwise — preserved when
wrapped and for omitted schemas. No retry: a handler `TypeError` after mutation
must never trigger a second execution.
- The current CG draft and Chrome documentation use a JavaScript object for
`executeTool` input. Chrome 150 still requires a JSON string, including for
tools with omitted `inputSchema`, and Chrome documents string input as
deprecated from 155. The harness registers and executes one temporary,
side-effect-free probe tool to select `object` or `json-string`, then invokes
every application tool exactly once in that mode. Never retry a real tool after
an exception: its handler may already have mutated state.
- Execution and declarative-validation failures **reject the promise** — they do
not resolve to `"ERROR: ..."`. Only imperative tools following the runtime's
convention resolve with `"ERROR: ..."` strings. Assert accordingly per tool
Expand All @@ -71,7 +73,7 @@ Contract facts that generated assertions MUST respect:

When an LLM agent loop consumes `getTools` -> OpenAI-compatible `tools` -> `executeTool`:

- `inputSchema` is stringified on native — do `typeof s === "string" ? JSON.parse(s) : s ?? {type:'object',properties:{}}` before sending `parameters: <object>` to the LLM; otherwise `400 'tools.0.function.parameters must be object'` -> loop 502s.
- Native `inputSchema` may be stringified on older builds or object-shaped on current builds — do `typeof s === "string" ? JSON.parse(s) : s ?? {type:'object',properties:{}}` before sending `parameters: <object>` to the LLM; otherwise `400 'tools.0.function.parameters must be object'` -> loop 502s.
- Missing `tools` with `tool_choice:none` surfaces as `tool_use_failed` — don't force `tool_choice`; use `disable_tool_validation: true` only when needed.
- Result may be stringified JSON (`"{\"ok\":true}"`) on native or object on stub — `typeof r === "string" ? try{JSON.parse(r)}catch{ r } : r` and `resultOk` helpers must handle both.

Expand Down
18 changes: 9 additions & 9 deletions skills/webmcpify/templates/webmcp-compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,25 @@
*
* Full text: https://github.com/TueJon/webmcpify/blob/main/LICENSE
*
* Shared string/object compat helpers for the native-vs-stub I/O divergence.
* Shared string/object compat helpers for the native transition from JSON
* strings to JavaScript objects.
* Used by templates/webmcp.spec.ts (inlined inside page.evaluate for the
* browser-boundary parts) and tests/compat.test.mjs — single source of truth.
* Collapse the string branch when Chrome aligns with the spec (#278/#279).
* Remove the JSON-string branch after Chrome 154 is no longer supported.
*/

export function parseInputSchema(raw) {
return typeof raw === 'string' ? JSON.parse(raw) : raw ?? { type: 'object', properties: {} };
}

/**
* Explicit adapter mode — no heuristics, no retry:
* Explicit adapter mode — capability-probe first, no retry of a real tool:
* - stub via direct tool.execute(object) — headless-era stub
* - stub via mc.executeTool(tool, object) — spec-shaped stub (RegisteredTool
* has no .execute); distinguished by explicit mc.__webmcpStubObjectMode set
* by the stub harness
* - native mc.executeTool(tool, JSON string) — Chrome native (also when wrapped
* or with omitted inputSchema)
* A handler TypeError never double-executes; collapse when spec norms.
* - current native/spec mc.executeTool(tool, object)
* - legacy Chrome mc.executeTool(tool, JSON string)
* The harness determines the native mode with a temporary side-effect-free
* tool before invoking application tools. A handler failure never triggers a
* retry, so a mutation cannot execute twice.
*/
export function isStubTool(tool) {
return typeof tool?.execute === 'function';
Expand Down
Loading
Loading