webmcp-gauge 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -9
- package/action.yml +8 -3
- package/bin/webmcp-gauge.mjs +20 -3
- package/docs/concept.md +4 -4
- package/docs/getting-started.md +2 -0
- package/judges/anthropic-messages.mjs +97 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Measures whether an AI agent actually calls the tools your web page exposes through WebMCP.
|
|
4
4
|
|
|
5
|
-
**Status: measuring, and the metric discriminates.** The harness runs end to end and has produced real numbers against a live page. It launches its own browser per session — a cold profile seeded with the WebMCP flag, in headless Chrome — captures the settled tool manifest, asks a judge model which tool to call, classifies the choice, executes it, and classifies the result. The utterance set is frozen at `1.3.0` (140 utterances plus 20 negative controls), and
|
|
5
|
+
**Status: measuring, and the metric discriminates.** The harness runs end to end and has produced real numbers against a live page. It launches its own browser per session — a cold profile seeded with the WebMCP flag, in headless Chrome — captures the settled tool manifest, asks a judge model which tool to call, classifies the choice, executes it, and classifies the result. The utterance set is frozen at `1.3.0` (140 utterances plus 20 negative controls), and twenty-three runs totalling **5,220 trials** are published under [`reports/`](reports/) — the best isolated reference run being 960 trials across three separate processes, browsers and cold profiles, a second client joining them: the same page, set and judge through Edge `153.0.4234.13` reads **99.3% (139/140)** with every tool inside or above its Chrome interval, and a third client at a second judge family: Brave `154.1.96.59` reads **100% (140/140)** under `claude-opus-4-8`, identical to its paired Chrome arm.
|
|
6
6
|
|
|
7
7
|
The question that mattered most has an answer. Every early number came from a page chosen for being well described, so the same frozen utterance set was fired at a deliberately mis-described twin of that page: one implementation, one dataset, several manifests, **1,320 trials at three isolated sessions per arm**. Clean manifest **99.3%**; degraded manifest **83.1%** overall, with `sum_by_category` at **60.0%** [47.4, 71.4] and `top_expenses` at **26.7%** [17.1, 39.0] against a clean 95.0% and 100%. Between-session σ is at most **0.094** where the effects are 0.35 and larger, so the gap is not noise.
|
|
8
8
|
|
|
@@ -10,11 +10,11 @@ Four ablations, each the clean manifest plus exactly one defect, then answered *
|
|
|
10
10
|
|
|
11
11
|
Re-running one arm with its sessions **9 to 17 hours apart instead of minutes** then asked whether any of this survives a clock. The reproducibility figures do: between-session σ came back 0.085 worst-case against 0.062 back-to-back. Point estimates did not sit still — both mid-range tools declined monotonically across the 26-hour span, 8 to 12 points with overlapping intervals — so a single arm measured at one time is sound, while comparing a page against itself across days inherits a drift question. [`spacing-2026-09-01.md`](reports/spacing-2026-09-01.md).
|
|
12
12
|
|
|
13
|
-
What exists: `lint` (static manifest rules, no judge or key), `trial` (one utterance, one outcome), `run` (S isolated sessions × R repeats, Wilson intervals, control false-positive rate, stamped JSON and Markdown reports, plus a badge that refuses to report a rate it cannot stand behind), JSONL checkpointing with `--resume`, a CI gate with split exit codes where a crash is *"could not measure"* rather than a failing page, a [GitHub Action](action.yml) wrapping both modes, cohort-capture tooling that records what a builder shipped and what an agent can actually call as two separate numbers, private per-project scorecards, a served fixture page, a browser-side tool view so a client that drops a tool can be told from a page that never registered one, and a pre-push visibility gate with its own tested rules. **229 tests.** The findings live in the published report, [`reports/public-report-draft.md`](reports/public-report-draft.md): the invocation-rate result on
|
|
13
|
+
What exists: `lint` (static manifest rules, no judge or key), `trial` (one utterance, one outcome), `run` (S isolated sessions × R repeats, Wilson intervals, control false-positive rate, stamped JSON and Markdown reports, plus a badge that refuses to report a rate it cannot stand behind), JSONL checkpointing with `--resume`, a CI gate with split exit codes where a crash is *"could not measure"* rather than a failing page, a [GitHub Action](action.yml) wrapping both modes, cohort-capture tooling that records what a builder shipped and what an agent can actually call as two separate numbers, private per-project scorecards, a served fixture page, a browser-side tool view so a client that drops a tool can be told from a page that never registered one, and a pre-push visibility gate with its own tested rules. **229 tests.** The findings live in the published report, [`reports/public-report-draft.md`](reports/public-report-draft.md): the invocation-rate result on three builds, the compatibility matrix, and the complete WebMCP Challenge census — **80.8% of reachable submissions register WebMCP tools, and 43.9% of pages carry tools an agent can call that no page surface lists**.
|
|
14
14
|
|
|
15
15
|
What does not: **a measurement inside a real assistant.** The *browser* in the ChatGPT desktop app is CDP-drivable and its WebMCP surface is documented here — but that assistant reads pages through a Chrome extension bridge into the user's ordinary Chrome, not through the app's own browser, so no agent-side invocation has been observed. Every number below is a judge-model stand-in for a real client, and whether the stand-in predicts it is still an open assumption.
|
|
16
16
|
|
|
17
|
-
Findings have gone back to the spec: two comments on [`webmcp#227`](https://github.com/webmachinelearning/webmcp/issues/227#issuecomment-5499568493) establish what Chrome 152 actually does across a frame boundary — a same-origin embed's tools join the host's manifest, a cross-origin embed is blocked by a Permissions Policy feature named `tools`, and once that is delegated the browser offers an agent a tool **no page can list**. The full compatibility record, including the cross-site case where even a host-attached browser-side view loses the embed entirely, is [`reports/compatibility-matrix.md`](reports/compatibility-matrix.md): 22 measured behaviours across
|
|
17
|
+
Findings have gone back to the spec: two comments on [`webmcp#227`](https://github.com/webmachinelearning/webmcp/issues/227#issuecomment-5499568493) establish what Chrome 152 actually does across a frame boundary — a same-origin embed's tools join the host's manifest, a cross-origin embed is blocked by a Permissions Policy feature named `tools`, and once that is delegated the browser offers an agent a tool **no page can list**. The full compatibility record, including the cross-site case where even a host-attached browser-side view loses the embed entirely, is [`reports/compatibility-matrix.md`](reports/compatibility-matrix.md): 22 measured behaviours across five builds, each cell dated and traced to a re-runnable probe, with a standing Corrections section for the one finding that turned out to be this project's own bug.
|
|
18
18
|
|
|
19
19
|
## The problem
|
|
20
20
|
|
|
@@ -32,7 +32,7 @@ Every trial lands in exactly one bucket — `not_supported`, `not_registered`, `
|
|
|
32
32
|
|
|
33
33
|
- **A static linter** — no browser needed to reason, no model, no API key. Fourteen rules across four families: invalid or colliding tool names, missing, thin, duplicate or near-duplicate descriptions, **a pair of close-named tools whose descriptions neither of them distinguishes** (adopted 2026-09-05 on measurement, after five arms showed that pattern costs 45 points while no similarity threshold catches it), over-parameterised and under-documented schemas, and tool counts approaching a per-page budget that is still unmeasured (the reported 296-tool figure does **not** reproduce on Chrome 152, where 507 registered tools were all accepted and surfaced). Thresholds are calibrated so the reference page — the one measured at 100% over 960 trials — lints clean, because a default that flags a manifest known to work is a broken default. *Built.*
|
|
34
34
|
- **The harness** — drives real browsers over the Chrome DevTools Protocol, fires the utterance set at the page's registered tools, classifies every outcome, and emits a JSON report plus a CI gate. Every trial reads the tool set **twice** — the page's own `getTools()` and the browser's `WebMCP.toolsAdded` stream — because "the page never registered it" and "the client dropped it" are indistinguishable from inside the page. *Built.*
|
|
35
|
-
- **A public dataset** — the cross-client compatibility record and invocation-rate corpus, regenerated as browsers change, published with the code that produced every number. *
|
|
35
|
+
- **A public dataset** — the cross-client compatibility record and invocation-rate corpus, regenerated as browsers change, published with the code that produced every number. *Sixteen runs and the complete WebMCP Challenge census so far, in `reports/` — including the four-build compatibility matrix, a second client for the rate, and the census with 80.8% adoption across 2,183 captured pages.*
|
|
36
36
|
|
|
37
37
|
## How a number is reported
|
|
38
38
|
|
|
@@ -45,17 +45,54 @@ Conflating those two was a real defect in this project's first two sweeps: a too
|
|
|
45
45
|
|
|
46
46
|
## Usage
|
|
47
47
|
|
|
48
|
+
**Quick start, zero install, no API key:**
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx webmcp-gauge lint --url https://your-page.example
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Requirements:**
|
|
55
|
+
|
|
56
|
+
| Mode | Node | Browser | Judge key |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| `lint --manifest <file>` | ≥ 22 (24 tested) | not needed | **not needed** |
|
|
59
|
+
| `lint --url <page>` | ≥ 22 (24 tested) | yes — launches its own Chrome | **not needed** |
|
|
60
|
+
| `trial` / `run` | ≥ 22 (24 tested) | yes | **yes** — any OpenAI-compatible endpoint |
|
|
61
|
+
|
|
62
|
+
Install once with `npm i -g webmcp-gauge`, or from a clone use `npm run gauge …` / `node bin/webmcp-gauge.mjs …` — every form runs the same CLI:
|
|
63
|
+
|
|
48
64
|
```
|
|
49
65
|
webmcp-gauge lint --url https://example.com
|
|
66
|
+
webmcp-gauge lint --manifest page-tools.json
|
|
50
67
|
webmcp-gauge trial --utterance sum_by_category-05 --judge <model> --base-url <endpoint>
|
|
51
68
|
webmcp-gauge run --sessions 3 --repeats 2 --out artifacts/run --judge <model> --base-url <endpoint>
|
|
52
69
|
webmcp-gauge run --sessions 1 --fail-under 0.9 --judge <model> --base-url <endpoint>
|
|
53
70
|
webmcp-gauge run --serve fixtures/broken --url "twin.html?variant=degraded" --subject "twin" ...
|
|
54
71
|
```
|
|
55
72
|
|
|
56
|
-
`
|
|
73
|
+
Every flag is documented in `--help`. A `run` writes four files into `--out`: `report.md` for reading, `report.json` (the machine record with per-tool outcome counts and coverage), `badge.json` + `badge.svg`, and a JSONL checkpoint that `--resume` continues. The cohort capture and harvest tools live in [`probes/`](probes/) in this repository, not in the npm package — they are one-day workflows, documented in their own READMEs.
|
|
57
74
|
|
|
58
|
-
|
|
75
|
+
**The API key, only where it is actually needed.** `lint` reads the manifest and applies static rules — no model anywhere in the path, so it runs on every push with no secrets. `trial` and `run` measure whether an agent-model *chooses* the tool, so they need a judge: any OpenAI-compatible endpoint (hosted or local — Ollama and LM Studio both speak the protocol), costing one model call per trial. An **anthropic-messages endpoint works too** — pass `--judge-shape anthropic` (or `WEBMCP_GAUGE_JUDGE_SHAPE=anthropic`); the shape is the endpoint's and the CLI will not guess it from the hostname. The CLI refuses to run if the judge is the model that authored the frozen utterance set; the set records which one that was.
|
|
76
|
+
|
|
77
|
+
Credentials come from the environment:
|
|
78
|
+
|
|
79
|
+
| Variable | Purpose |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `WEBMCP_GAUGE_JUDGE_API_KEY` | judge key (run/trial only) |
|
|
82
|
+
| `WEBMCP_GAUGE_JUDGE_BASE_URL` | judge endpoint (OpenAI-compatible by default) |
|
|
83
|
+
| `WEBMCP_GAUGE_JUDGE_MODEL` | judge model id, stamped into every report |
|
|
84
|
+
| `WEBMCP_GAUGE_JUDGE_SHAPE` | `anthropic` for an anthropic-messages endpoint (default: `openai`) |
|
|
85
|
+
| `WEBMCP_GAUGE_CHROME` | browser override (default: find Chrome; any Chromium works) |
|
|
86
|
+
| `CDP_PORT` | attach to a browser you started instead of launching one |
|
|
87
|
+
| `WEBMCP_GAUGE_CHROME_LOG` | `1` passes Chrome's own stderr through, for launch debugging |
|
|
88
|
+
|
|
89
|
+
**Nothing loads `.env` automatically** — the CLI reads the environment. Export the variables, or run through the env file directly:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
node --env-file=.env bin/webmcp-gauge.mjs run --sessions 1 --judge "$WEBMCP_GAUGE_JUDGE_MODEL" ...
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
See [`.env.example`](.env.example) for the annotated list.
|
|
59
96
|
|
|
60
97
|
`--serve <dir>` publishes a directory on 127.0.0.1 and resolves `--url` against it, which is how the deliberately mis-described fixture page in [`fixtures/broken/`](fixtures/broken/) gets measured with the same frozen utterance set as the reference page.
|
|
61
98
|
|
|
@@ -69,6 +106,8 @@ A gate is only useful if `1` means one thing, so the three cases are separated:
|
|
|
69
106
|
| `1` | Every planned trial was measured, and a rate is below `--fail-under` — the page regressed |
|
|
70
107
|
| `2` | The run cannot answer: planned trials have no measurement (re-run with `--resume`), or the arguments were unusable |
|
|
71
108
|
|
|
109
|
+
In `lint` mode the same codes mean: `0` no findings at or above `--fail-on`, `1` findings at that level, `2` the manifest could not be read.
|
|
110
|
+
|
|
72
111
|
Incomplete outranks a breach on purpose. Gaps are not random — a judge outage or a page that never loaded can take out one tool's utterances and nothing else — so a rate over a run with holes is a rate over a denominator the run did not choose, and reporting that as a regression would be a lie with a plausible number. Completeness is derived from the plan against the checkpoint, not from the failure log, because a session killed mid-plan logs nothing.
|
|
73
112
|
|
|
74
113
|
The threshold is compared against the **point rate**, not the Wilson lower bound: 20 of 20 has a lower bound of 83.9%, so gating on the bound would fail a flawless page on sample size alone. The interval is printed beside the rate instead, and the verdict says so when a breach sits inside it.
|
|
@@ -87,22 +126,23 @@ A badge is a bare number in a coloured pill — the exact thing this project ref
|
|
|
87
126
|
|
|
88
127
|
### GitHub Action
|
|
89
128
|
|
|
90
|
-
[`action.yml`](action.yml) wraps both modes. `lint` needs no browser flag, no judge and no key, so it can run on every push; `run` spends a model call per trial and belongs on a schedule or a manual dispatch.
|
|
129
|
+
[`action.yml`](action.yml) wraps both modes and is [on the GitHub Marketplace](https://github.com/marketplace/actions/webmcp-gauge) — pin `@v1` for a stable interface. `lint` needs no browser flag, no judge and no key, so it can run on every push; `run` spends a model call per trial and belongs on a schedule or a manual dispatch.
|
|
91
130
|
|
|
92
131
|
```yaml
|
|
93
|
-
- uses: Svishwa2004/webmcp-gauge@
|
|
132
|
+
- uses: Svishwa2004/webmcp-gauge@v1
|
|
94
133
|
with:
|
|
95
134
|
mode: lint
|
|
96
135
|
url: https://your-page.example
|
|
97
136
|
fail-on: error
|
|
98
137
|
|
|
99
|
-
- uses: Svishwa2004/webmcp-gauge@
|
|
138
|
+
- uses: Svishwa2004/webmcp-gauge@v1
|
|
100
139
|
with:
|
|
101
140
|
mode: run
|
|
102
141
|
url: https://your-page.example
|
|
103
142
|
fail-under: '0.9'
|
|
104
143
|
judge: ${{ vars.JUDGE_MODEL }}
|
|
105
144
|
base-url: ${{ vars.JUDGE_BASE_URL }}
|
|
145
|
+
# judge-shape: anthropic # only for an anthropic-messages endpoint; default openai
|
|
106
146
|
env:
|
|
107
147
|
WEBMCP_GAUGE_JUDGE_API_KEY: ${{ secrets.JUDGE_API_KEY }}
|
|
108
148
|
```
|
package/action.yml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
name: webmcp-gauge
|
|
2
2
|
description: >-
|
|
3
|
-
Lint a page's WebMCP manifest, or measure whether an agent actually invokes
|
|
4
|
-
tools, and fail
|
|
3
|
+
Lint a page's WebMCP manifest, or measure whether an agent actually invokes
|
|
4
|
+
its tools, and fail on a regression.
|
|
5
5
|
author: Sahan Vishwa
|
|
6
6
|
|
|
7
7
|
# Two modes, because they cost different things and mean different things.
|
|
@@ -54,7 +54,11 @@ inputs:
|
|
|
54
54
|
required: false
|
|
55
55
|
default: ''
|
|
56
56
|
base-url:
|
|
57
|
-
description: 'run mode: OpenAI-compatible
|
|
57
|
+
description: 'run mode: judge endpoint. OpenAI-compatible by default; set judge-shape for an anthropic-messages endpoint'
|
|
58
|
+
required: false
|
|
59
|
+
default: ''
|
|
60
|
+
judge-shape:
|
|
61
|
+
description: 'run mode: judge wire protocol, openai (default) or anthropic — the shape is the endpoint''s'
|
|
58
62
|
required: false
|
|
59
63
|
default: ''
|
|
60
64
|
badge-label:
|
|
@@ -131,6 +135,7 @@ runs:
|
|
|
131
135
|
[ -n "${{ inputs.fail-under }}" ] && args+=(--fail-under "${{ inputs.fail-under }}")
|
|
132
136
|
[ -n "${{ inputs.judge }}" ] && args+=(--judge "${{ inputs.judge }}")
|
|
133
137
|
[ -n "${{ inputs.base-url }}" ] && args+=(--base-url "${{ inputs.base-url }}")
|
|
138
|
+
[ -n "${{ inputs.judge-shape }}" ] && args+=(--judge-shape "${{ inputs.judge-shape }}")
|
|
134
139
|
fi
|
|
135
140
|
|
|
136
141
|
echo "webmcp-gauge ${args[*]}"
|
package/bin/webmcp-gauge.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { lintManifest, lintToText } from '../core/lint.mjs';
|
|
|
12
12
|
import { runSessions } from '../core/orchestrate.mjs';
|
|
13
13
|
import { buildPlan, readCheckpoint, readFailures, runSessionSweep, trialKey } from '../core/sweep.mjs';
|
|
14
14
|
import { runTrial } from '../core/trial.mjs';
|
|
15
|
+
import { createJudge as createAnthropicJudge } from '../judges/anthropic-messages.mjs';
|
|
15
16
|
import { createJudge } from '../judges/openai-compatible.mjs';
|
|
16
17
|
import { buildReport, toMarkdown } from '../report/emit.mjs';
|
|
17
18
|
import { buildBadge, renderBadgeSvg } from '../report/badge.mjs';
|
|
@@ -39,6 +40,9 @@ error, never a silently ignored default):
|
|
|
39
40
|
measurement can run against a fixture page in this repo
|
|
40
41
|
--judge <model> judge model id (env WEBMCP_GAUGE_JUDGE_MODEL)
|
|
41
42
|
--base-url <url> judge endpoint (env WEBMCP_GAUGE_JUDGE_BASE_URL)
|
|
43
|
+
--judge-shape <api> judge wire protocol: openai (default) or anthropic — the
|
|
44
|
+
shape is the endpoint's, and guessing it from the hostname
|
|
45
|
+
would be a silent default (env WEBMCP_GAUGE_JUDGE_SHAPE)
|
|
42
46
|
--port <n> attach to an existing Chrome instead of launching one
|
|
43
47
|
|
|
44
48
|
lint options (no judge required):
|
|
@@ -102,7 +106,7 @@ which one that was. See docs/getting-started.md and .env.example.`;
|
|
|
102
106
|
*/
|
|
103
107
|
const CLI_OPTIONS = {
|
|
104
108
|
values: [
|
|
105
|
-
'fixture', 'url', 'serve', 'judge', 'base-url', 'port',
|
|
109
|
+
'fixture', 'url', 'serve', 'judge', 'base-url', 'judge-shape', 'port',
|
|
106
110
|
'manifest', 'variant', 'fail-on', 'min-description', 'max-properties', 'budget-warn',
|
|
107
111
|
'tool', 'utterance',
|
|
108
112
|
'sessions', 'repeats', 'concurrency', 'gap', 'tools', 'out', 'subject', 'fail-under', 'badge-label',
|
|
@@ -178,6 +182,17 @@ const judgeModel =
|
|
|
178
182
|
const judgeBaseUrl =
|
|
179
183
|
(typeof flags['base-url'] === 'string' && flags['base-url']) ||
|
|
180
184
|
process.env.WEBMCP_GAUGE_JUDGE_BASE_URL;
|
|
185
|
+
const judgeShape =
|
|
186
|
+
(typeof flags['judge-shape'] === 'string' && flags['judge-shape']) ||
|
|
187
|
+
process.env.WEBMCP_GAUGE_JUDGE_SHAPE ||
|
|
188
|
+
'openai';
|
|
189
|
+
if (judgeShape !== 'openai' && judgeShape !== 'anthropic') {
|
|
190
|
+
fail(`--judge-shape must be "openai" or "anthropic", got "${judgeShape}"`);
|
|
191
|
+
}
|
|
192
|
+
const buildJudge = () =>
|
|
193
|
+
judgeShape === 'anthropic'
|
|
194
|
+
? createAnthropicJudge({ baseUrl: judgeBaseUrl, model: judgeModel })
|
|
195
|
+
: createJudge({ baseUrl: judgeBaseUrl, model: judgeModel });
|
|
181
196
|
// L0 is the free on-ramp: it reads a manifest and calls no model, so demanding a
|
|
182
197
|
// judge for it would put an API key in front of the cheapest useful answer.
|
|
183
198
|
if (needsJudge && (!judgeModel || !judgeBaseUrl)) {
|
|
@@ -299,7 +314,7 @@ if (command === 'lint') {
|
|
|
299
314
|
process.exitCode = blocking > 0 ? EXIT.breach : EXIT.pass;
|
|
300
315
|
}
|
|
301
316
|
} else if (command === 'trial') {
|
|
302
|
-
const judge =
|
|
317
|
+
const judge = buildJudge();
|
|
303
318
|
const utteranceId = typeof flags.utterance === 'string' ? flags.utterance : null;
|
|
304
319
|
const toolName =
|
|
305
320
|
typeof flags.tool === 'string' ? flags.tool : utteranceId?.replace(/-\d+$/, '') ?? null;
|
|
@@ -348,7 +363,7 @@ if (command === 'lint') {
|
|
|
348
363
|
}
|
|
349
364
|
} else if (command === 'session') {
|
|
350
365
|
const session = Number(flags.session ?? 1);
|
|
351
|
-
const judge =
|
|
366
|
+
const judge = buildJudge();
|
|
352
367
|
const target = await openTarget();
|
|
353
368
|
|
|
354
369
|
// Each session owns its browser: a cold profile, its own port, its own process
|
|
@@ -438,6 +453,8 @@ if (command === 'lint') {
|
|
|
438
453
|
judgeModel,
|
|
439
454
|
'--base-url',
|
|
440
455
|
judgeBaseUrl,
|
|
456
|
+
'--judge-shape',
|
|
457
|
+
judgeShape,
|
|
441
458
|
'--out',
|
|
442
459
|
outDir,
|
|
443
460
|
'--repeats',
|
package/docs/concept.md
CHANGED
|
@@ -347,7 +347,7 @@ Verified 2026-08-29. Building any of the left column again would be waste.
|
|
|
347
347
|
| 3 | Sep 1–3 | 🟡 **Part done, and one part closed unanswered** — Mode B adapters + Mode A↔B correlation study + GitHub Action | **#268 refuted** and the GitHub Action shipped (2026-09-01). Mode B's browser layer is driven; its **agent layer is closed on a recorded negative** (2026-09-02) — the agent reads pages through a Chrome extension bridge into the user's own Chrome, and instrumenting that was declined — so the **correlation study has no result and Gate 3 is unanswered rather than passed** |
|
|
348
348
|
| 4 | by Sep 3, runs Sep 4 | ✅ **Done — staged 2026-09-01, three days early; run complete 2026-09-25/26** — Cohort snapshot script, staged and ready, then run to completion | `core\cohort.mjs` + `core\gallery.mjs` (32 tests), `probes\cohort-snapshot.mjs` + `probes\gallery-harvest.mjs`, rehearsed against three published galleries, three real project pages and a local two-page fixture. The gallery opened Sep 24; all 2,474 submissions were walked (2,185 usable) and **2,183 captured** across three recoverable parts — a browser death and a machine sleep each cost one in-flight target. The census: [`../reports/census-2026-09-26.md`](../reports/census-2026-09-26.md) |
|
|
349
349
|
| 5 | Sep 4–21 | ✅ **Done 2026-09-26** — Private per-builder scorecards; aggregate-only public stats; spec-issue data contributions | **The spec contribution is filed** (2026-09-02, `webmcp#227`, twice — the frame-scope baseline and the cross-origin follow-up). The scorecard **renderer** is built, exercised on 344 real captures, and delivery was decided as **offer on request** — the standing offer is in the published report. The aggregate stats are published: [`../reports/census-2026-09-26.md`](../reports/census-2026-09-26.md) |
|
|
350
|
-
| 6 | Sep 23+ | ✅ **Done 2026-09-26** — Public report + open dataset + harness release | The report is published ([`../reports/public-report-draft.md`](../reports/public-report-draft.md)), the repo went public with `EXPECTED_VISIBILITY=public` landed in the flip commit itself,
|
|
350
|
+
| 6 | Sep 23+ | ✅ **Done 2026-09-26** — Public report + open dataset + harness release | The report is published ([`../reports/public-report-draft.md`](../reports/public-report-draft.md)), the repo went public with `EXPECTED_VISIBILITY=public` landed in the flip commit itself, the dataset is **aggregate-only by item 8's recorded decision** (the derived tables are the open dataset; the raw JSONL stays local unless revisited), and the npm package is **published** — `webmcp-gauge@0.1.0`, verified from the registry |
|
|
351
351
|
|
|
352
352
|
**Milestone 4 is time-critical and cannot be recovered later.** The Devpost gallery was still unpublished as of 2026-08-29 and opens with judging. Those ~165 live demo URLs sit on free hosting tiers; within a few months a large share will 404. A snapshot of that many real `registerTool` implementations, taken on the one day they are all simultaneously live, is not reconstructible after the fact. It is the largest corpus of real-world WebMCP code that will ever exist for this period, and capturing it costs one day of scripting.
|
|
353
353
|
|
|
@@ -402,10 +402,10 @@ Unresolved, and each is answerable with a small spike:
|
|
|
402
402
|
Ordered, and each independently checkable:
|
|
403
403
|
|
|
404
404
|
1. A stable, variance-quantified invocation rate for any WebMCP page, in at least three clients. 🟡 **Two measured clients now — both Chromium.** The rate is stable (σ ≤ 0.094 between sessions, ≤ 0.085 at 9–17 h spacing, Chrome 960 trials) and Edge `153.0.4234.13` reproduces it at 160 trials — 99% (139/140), every tool inside or above its Chrome interval ([`reports/airlock-1.3.0-glm-5.3-edge-s1r1.md`](../reports/airlock-1.3.0-glm-5.3-edge-s1r1.md), 2026-09-03) — but that is a point measurement, not a variance one, and both runs share one judge model (`glm-5.3`), so the client coordinate moved and the judge coordinate did not. A third client that is not Chromium is the remaining gap.
|
|
405
|
-
2. A published compatibility matrix that a developer would actually link to when explaining "why does my tool work in Brave but not Chrome?" 🟡 **Assembled 2026-09-03 — [`reports/compatibility-matrix.md`](../reports/compatibility-matrix.md): 20 behaviours across four build columns (Chrome `152.0.7977.65`; the ChatGPT desktop app's Chromium at `151.0.7922.174` and again at `152.0.7977.64` after it updated mid-project; Edge `153.0.4234.13`, where the Chrome activation recipe works unchanged), each cell dated and traced to a re-runnable probe, with a standing Corrections section for the one row that turned out to be our own bug. Publishable as it stands; "published" still waits on the repo going public (criterion 5).** And the honest limit of it: **Brave
|
|
405
|
+
2. A published compatibility matrix that a developer would actually link to when explaining "why does my tool work in Brave but not Chrome?" 🟡 **Assembled 2026-09-03 — [`reports/compatibility-matrix.md`](../reports/compatibility-matrix.md): 20 behaviours across four build columns (Chrome `152.0.7977.65`; the ChatGPT desktop app's Chromium at `151.0.7922.174` and again at `152.0.7977.64` after it updated mid-project; Edge `153.0.4234.13`, where the Chrome activation recipe works unchanged), each cell dated and traced to a re-runnable probe, with a standing Corrections section for the one row that turned out to be our own bug. Publishable as it stands; "published" still waits on the repo going public (criterion 5).** And the honest limit of it, updated 2026-09-26: **Brave `154.1.96.59` was installed and surface-measured — the matrix now carries a fifth column, and the #227 frame answers reproduce on a third engine — but its invocation-rate sweep is blocked on the judge provider** (agentrouter serves the key no channel for `glm-5.3`), and Edge was measured at 153 rather than the 151 that report names, so the matrix answers "why does my tool behave differently in ChatGPT's browser, Edge or Brave?" at the surface level while the rate comparison still rests on two clients.
|
|
406
406
|
3. At least one reproducible data contribution accepted into a live spec issue. ✅ **Filed 2026-09-02** — two comments on `webmcp#227`, each carrying a build string and a reproduction that was run before it was posted. *Filed* is done; *accepted* is not this project's to decide.
|
|
407
|
-
4. A cohort dataset captured on gallery-publish day and preserved.
|
|
408
|
-
5. The linter installed by developers who have never heard of the author.
|
|
407
|
+
4. A cohort dataset captured on gallery-publish day and preserved. ✅ **Done 2026-09-25/26** — the gallery opened Sep 24; all 2,474 submissions walked (2,185 usable) and **2,183 captured** across three recoverable parts, reconciled and published as [`../reports/census-2026-09-26.md`](../reports/census-2026-09-26.md). The one-day preservation claim holds: every target was visited exactly once inside the ~13-hour window, with the dead-record composition checked rather than assumed.
|
|
408
|
+
5. The linter installed by developers who have never heard of the author. ✅ **Met 2026-09-26, three ways** — the repo is public with `action.yml` live, **`webmcp-gauge@0.1.0` is on npm** (verified by installing it fresh from the registry and running `npx webmcp-gauge lint --url <page>` against a real page), and **the Action is on the GitHub Marketplace** as `Svishwa2004/webmcp-gauge` (`v1.0.0`, published from a release whose `action.yml` carries the required branding; the first description tripped the Marketplace's 125-character limit and was shortened inside the tag before publishing, since the listing reads `action.yml` at the tag). The moving `v1` tag is the interface promise: fixes move within the major, breaking changes get a `v2`. What this document cannot close by itself is the adoption half: that needs strangers, not commits.
|
|
409
409
|
6. A report that becomes the default citation for "do WebMCP tools actually get called?" 🟡 **Published 2026-09-26 — [`reports/public-report-draft.md`](../reports/public-report-draft.md).** The capture landed (criterion 4) and the repository went public with the report in the same window (criterion 5's on-ramp). Every section is sourced to the write-up that produced it, including Gate 3 stated as unanswered and the complete cohort census. Whether it becomes anyone's default citation is not this project's to decide — see criterion 3's split between *filed* and *accepted*.
|
|
410
410
|
|
|
411
411
|
Criterion 3 is the cheapest and the most valuable. The WebMCP community is small enough — a few named Chrome engineers, spec editors and library authors — that two or three good field-data contributions to open issues make an unknown solo developer a known one. That is the actual early advantage on offer here, and it does not require the product to succeed commercially. **It is also the one criterion already met, on day five, for the cost of running a test before writing a comment.**
|
package/docs/getting-started.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
Verification legend used throughout: ✅ verified on this machine · ⚠️ unverified · ❌ known wrong.
|
|
8
8
|
|
|
9
|
+
> **Just want to run the tool?** The README's [Usage](../README.md#usage) section is the stranger's path: `npx webmcp-gauge lint --url <page>` needs no install and no API key, and the requirements table there says exactly which modes need a browser and which need a judge key. This document is the author's own machine record — the paths, browsers and credentials below are how *this* project was stood up, kept because the log's verification claims trace to it.
|
|
10
|
+
|
|
9
11
|
---
|
|
10
12
|
|
|
11
13
|
## 0. What is already in place
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Judge adapter for Anthropic-messages endpoints (POST {base}/v1/messages).
|
|
3
|
+
*
|
|
4
|
+
* Same judge-level contract as judges/openai-compatible.mjs — { id, baseUrl,
|
|
5
|
+
* keySource, complete({system, user, maxTokens}) } — differing only in the wire
|
|
6
|
+
* shape, so the sweep and trial paths cannot tell the two apart and every
|
|
7
|
+
* report keeps stamping the same judge fields. Chosen explicitly with
|
|
8
|
+
* --judge-shape anthropic, because the shape is a property of the endpoint and
|
|
9
|
+
* guessing it from the hostname would be one more silent default. Just Work
|
|
10
|
+
* (api.justwoker.icu) serves claude models over this protocol and refuses the
|
|
11
|
+
* OpenAI shape outright. No retrying and no repair, same rule as the OpenAI
|
|
12
|
+
* adapter: a judge that cannot follow the response contract is a measurement
|
|
13
|
+
* result, not an error to paper over.
|
|
14
|
+
*/
|
|
15
|
+
import { resolveApiKey } from './openai-compatible.mjs';
|
|
16
|
+
|
|
17
|
+
const DEFAULT_TIMEOUT_MS = 60000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reasoning budgets sit higher than a tool-choice answer needs, and Anthropic
|
|
21
|
+
* charges for thinking tokens the same way — the ceiling exists so truncation
|
|
22
|
+
* is unlikely, and `truncated` says so when it happens anyway.
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_MAX_TOKENS = 4096;
|
|
25
|
+
|
|
26
|
+
export const createJudge = ({
|
|
27
|
+
baseUrl,
|
|
28
|
+
model,
|
|
29
|
+
apiKeyVar,
|
|
30
|
+
env = process.env,
|
|
31
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
32
|
+
temperature = 0,
|
|
33
|
+
}) => {
|
|
34
|
+
if (!baseUrl || !model) throw new Error('createJudge requires baseUrl and model');
|
|
35
|
+
const { key, source } = resolveApiKey(env, apiKeyVar, baseUrl);
|
|
36
|
+
const endpoint = `${baseUrl.replace(/\/$/, '')}/v1/messages`;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
id: model,
|
|
40
|
+
baseUrl,
|
|
41
|
+
keySource: source,
|
|
42
|
+
|
|
43
|
+
async complete({ system, user, maxTokens = DEFAULT_MAX_TOKENS }) {
|
|
44
|
+
const body = {
|
|
45
|
+
model,
|
|
46
|
+
temperature,
|
|
47
|
+
max_tokens: maxTokens,
|
|
48
|
+
...(system ? { system } : {}),
|
|
49
|
+
messages: [{ role: 'user', content: user }],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const startedAt = Date.now();
|
|
53
|
+
const response = await fetch(endpoint, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
'x-api-key': key,
|
|
58
|
+
Authorization: `Bearer ${key}`,
|
|
59
|
+
'anthropic-version': '2023-06-01',
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(body),
|
|
62
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const text = await response.text();
|
|
66
|
+
const elapsedMs = Date.now() - startedAt;
|
|
67
|
+
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
// The body carries the useful part - a quota rejection reads very
|
|
70
|
+
// differently from a malformed request - so it is surfaced, not swallowed.
|
|
71
|
+
throw new Error(`judge HTTP ${response.status}: ${text.slice(0, 500)}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const payload = JSON.parse(text);
|
|
75
|
+
const content = (payload.content ?? [])
|
|
76
|
+
.filter((block) => block.type === 'text')
|
|
77
|
+
.map((block) => block.text)
|
|
78
|
+
.join('')
|
|
79
|
+
.trim();
|
|
80
|
+
const finishReason = payload.stop_reason ?? null;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
content,
|
|
84
|
+
raw: text,
|
|
85
|
+
model: payload.model ?? model,
|
|
86
|
+
finishReason,
|
|
87
|
+
truncated: finishReason === 'max_tokens',
|
|
88
|
+
// Normalised to the OpenAI field names so a report never carries two
|
|
89
|
+
// usage shapes depending on which adapter served the trials.
|
|
90
|
+
usage: payload.usage
|
|
91
|
+
? { prompt_tokens: payload.usage.input_tokens, completion_tokens: payload.usage.output_tokens }
|
|
92
|
+
: null,
|
|
93
|
+
elapsedMs,
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
};
|