[GH-ISSUE #5735] [Bug] WebMCP discovery marks a valid tool without inputSchema as invalid protocol data #3481

Closed
opened 2026-10-31 00:40:58 +00:00 by bsakaguchi · 2 comments
Owner

Originally created by @coygeek on GitHub (Oct 21, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5735

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Process capture or runtime observation

Observed behavior

discover-webmcp-tools returns a successfully registered parameterless tool without inputSchema, but also records an invalid_protocol_value exclusion and marks webmcp_tools unavailable. The tool remains in the result with null schema and digest. Comparing the assembled capture against itself consequently returns dimensions.webmcp.status: "unknown".

An otherwise equivalent registration with inputSchema: {type: "object", properties: {}} has complete discovery coverage and compares as unchanged in the WebMCP dimension. The browser's own protocol declares inputSchema optional, and raw CDP replay confirms that successful registration omits the key.

Reproduction

From the built source checkout with locked dependencies installed, use Node.js with built-in WebSocket support, Python 3, curl, jq, and a Chrome executable exposed as CHROME. The tested browser supports --enable-features=WebMCPTesting. Choose unused loopback ports if necessary. This serves a local fixture and starts a separate headless browser with a disposable profile.

work_dir="$(mktemp -d)"
mkdir -p "$work_dir"/{home,cfg,data,cache,state,tmp,site}
cat > "$work_dir/site/page.html" <<'HTML'
<!doctype html><title>loading</title><script>
document.modelContext.registerTool({
  name: "solo", description: "parameterless",
  execute: async () => ({content: []})
});
document.title = "registered:solo";
</script>
HTML
python3 -I -m http.server 18759 --bind 127.0.0.1 \
  --directory "$work_dir/site" > "$work_dir/server.log" 2>&1 &
server_pid=$!
"$CHROME" --headless=new --remote-debugging-port=19759 \
  --user-data-dir="$work_dir/profile" --no-first-run \
  --disable-background-networking --disable-component-update \
  --enable-features=WebMCPTesting http://127.0.0.1:18759/page.html \
  > "$work_dir/chrome.log" 2>&1 &
chrome_pid=$!
trap 'kill "$chrome_pid" "$server_pid" 2>/dev/null; wait "$chrome_pid" "$server_pid" 2>/dev/null' EXIT
endpoint=http://127.0.0.1:19759
for attempt in $(seq 1 100); do
  target_id=$(curl -fsS "$endpoint/json/list" 2>/dev/null |
    jq -r '.[] | select(.title == "registered:solo") | .id' | head -1)
  [ -n "$target_id" ] && break
  sleep 0.1
done
[ -n "$target_id" ] || exit 1
rea_local() {
  env -i PATH="$PATH" HOME="$work_dir/home" \
    XDG_CONFIG_HOME="$work_dir/cfg" XDG_DATA_HOME="$work_dir/data" \
    XDG_CACHE_HOME="$work_dir/cache" XDG_STATE_HOME="$work_dir/state" \
    TMPDIR="$work_dir/tmp" REA_LOG_LEVEL=silent \
    node scripts/rea.mjs "$@"
}
rea_local list-browser-targets "$endpoint" --json > "$work_dir/targets.json"
rea_local discover-webmcp-tools "$endpoint" "$target_id" \
  --observation-ms 300 --json > "$work_dir/discovery.json"
jq '.normalized_result | {status,tools,completeness}' "$work_dir/discovery.json"

To observe the comparison consequence, continue in the same shell:

rea_local inspect-web-page "$endpoint" "$target_id" \
  --observation-ms 300 --json > "$work_dir/inspection.json"
jq -n --slurpfile i "$work_dir/inspection.json" \
  --slurpfile w "$work_dir/discovery.json" \
  '{inspection:$i[0].normalized_result,webmcp:$w[0].normalized_result}' \
  > "$work_dir/capture.json"
rea_local compare-web-captures "$work_dir/capture.json" \
  "$work_dir/capture.json" --json > "$work_dir/comparison.json"
jq '.normalized_result.dimensions.webmcp' "$work_dir/comparison.json"
kill "$chrome_pid" "$server_pid"
wait "$chrome_pid" "$server_pid" 2>/dev/null || true
trap - EXIT

For the control, add inputSchema: {type: "object", properties: {}} to the registration and repeat in a fresh browser profile. The independent verifier reproduced the failure and control twice at the baseline and executed the shell reproduction. Before submission, an equivalent isolated runner repeated raw protocol inspection, discovery, inspection, and both self-comparisons once against freshly built current main. All six REA CLI calls in that replay exited 0.

Expected behavior

Retain the valid registration with null schema and digest, without an invalid-value exclusion or unavailable WebMCP section caused solely by the omitted optional field. Its otherwise complete inventory should compare as unchanged against itself.

The tested browser's WebMCP.Tool.inputSchema protocol property explicitly has optional: true. REA's docs/browser-observation.md:21 promises discovery of in-scope registrations and distinguishes malformed protocol data; src/domain/webMcpDiscovery.ts:31-32 permits null schema and digest. Nullability alone is not the basis for treating the producer field as optional.

Impact and scope

P2: valid parameterless registrations falsely lose discovery completeness and prevent the WebMCP comparison dimension from establishing equality. Tool metadata remains present. This reproduced in both independent baseline runs and the current-main replay. Wider page prevalence, other browser versions, and MCP transport were not tested; no security boundary crossing or data loss was established.

Manual inspection of retained metadata is available. When callers own the page, the tested explicit empty-object schema avoids the exclusion. That workaround is unavailable for pages the caller cannot change, and manual inspection does not restore automated comparison completeness. Other incomplete passive-inspection dimensions independently keep overall comparison status unknown in both fixtures; overall equality is not the restoration signal here.

REA version

rea-agents 6.7.0, independently verified baseline 9d8e7803326a382cbbb3ffdb900f5e93e5442d68. Rechecked with npm run build:cached and runtime replay on origin/main at d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f, also 6.7.0: still reproduces. The lockfile is unchanged between these revisions.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1 arm64; Google Chrome 155.0.8059.85, headless with WebMCPTesting enabled. No Hopper/Ghidra or external API is involved.

Target and artifact context

Loopback HTML fixture with one imperative WebMCP registration. Separate headless Chrome, explicit debugging port, disposable browser profile, isolated REA HOME/XDG/temp directories. Captured normalized inspection and discovery JSON are combined for offline self-comparison. The owned browser and fixture server were stopped after replay.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

Current-main discovery returned this subset, with no dropped events:

{
  "status": "available",
  "tools": {"total": 1, "items": [{"name": "solo", "input_schema": null, "input_schema_sha256": null}]},
  "completeness": {
    "status": "attach_limited",
    "unavailable_sections": ["webmcp_tools"],
    "excluded": [{"section": "webmcp_tools", "reason": "invalid_protocol_value", "count": 1}]
  }
}

The same snapshot compared against itself returns:

{"total_changes":0,"changes":[],"status":"unknown","reason":"WebMCP discovery was unavailable or incomplete in at least one capture."}

The explicit-schema control returns completeness complete_within_window, empty exclusions and unavailable sections, the original schema, and digest efddc7bd8bbcef73a14eb1ace1ffdaec81e518ef1e13c1e9271d0b8acb694a49. Its WebMCP self-comparison is unchanged with zero changes.

Independent protocol observation from the browser's loopback /json/protocol:

{"name":"inputSchema","description":"Schema for the tool's input parameters.","optional":true,"type":"object"}

Raw WebMCP.toolsAdded replay contains the solo registration without an inputSchema key. Evidence provenance: observed; local verification: independently reproduced, then replayed on current main.

Typed outcome or recovery expected

Complete WebMCP coverage for the valid omitted-schema registration, null schema/digest, and an unchanged WebMCP self-comparison. Preserve explicit-schema discovery and its canonical digest. Genuinely malformed annotations must continue to produce the documented exclusion; that discriminating malformed-input check is proposed, not executed here.

Analysis or suspected cause

Source-derived reasoning at the baseline: normalizeTool, src/browser/CdpWebMcpDiscovery.ts:467-474 parses value.inputSchema unconditionally. The omitted key becomes undefined, fails JSON-value parsing, and records invalid_protocol_value. src/browser/CdpCaptureCompleteness.ts:62-79 marks that section unavailable; src/domain/webCaptureDiff.ts:678-733 then reports the WebMCP dimension unknown. Current main retains the unconditional schema parse and reproduced the same result. No bundled dependency was identified as the cause.

Additional context

Prepared with AI assistance. No known-good earlier version or successful restoration has been established.

Related #3989 and its open PR #4040 concern dropped WebMCP observations falsely proving equality. This report has zero dropped observations and instead concerns a valid optional-field omission falsely making discovery incomplete; its restoration requires admitting the omission without an invalid-value exclusion. PR #3837 covers genuinely malformed annotation payloads, a different input contract. Static WebMCP declaration reports such as #2624 and #2626 do not cover live CDP schema omission.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5735 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Process capture or runtime observation ### Observed behavior `discover-webmcp-tools` returns a successfully registered parameterless tool without `inputSchema`, but also records an `invalid_protocol_value` exclusion and marks `webmcp_tools` unavailable. The tool remains in the result with null schema and digest. Comparing the assembled capture against itself consequently returns `dimensions.webmcp.status: "unknown"`. An otherwise equivalent registration with `inputSchema: {type: "object", properties: {}}` has complete discovery coverage and compares as `unchanged` in the WebMCP dimension. The browser's own protocol declares `inputSchema` optional, and raw CDP replay confirms that successful registration omits the key. ### Reproduction From the built source checkout with locked dependencies installed, use Node.js with built-in WebSocket support, Python 3, curl, jq, and a Chrome executable exposed as `CHROME`. The tested browser supports `--enable-features=WebMCPTesting`. Choose unused loopback ports if necessary. This serves a local fixture and starts a separate headless browser with a disposable profile. ```sh work_dir="$(mktemp -d)" mkdir -p "$work_dir"/{home,cfg,data,cache,state,tmp,site} cat > "$work_dir/site/page.html" <<'HTML' <!doctype html><title>loading</title><script> document.modelContext.registerTool({ name: "solo", description: "parameterless", execute: async () => ({content: []}) }); document.title = "registered:solo"; </script> HTML python3 -I -m http.server 18759 --bind 127.0.0.1 \ --directory "$work_dir/site" > "$work_dir/server.log" 2>&1 & server_pid=$! "$CHROME" --headless=new --remote-debugging-port=19759 \ --user-data-dir="$work_dir/profile" --no-first-run \ --disable-background-networking --disable-component-update \ --enable-features=WebMCPTesting http://127.0.0.1:18759/page.html \ > "$work_dir/chrome.log" 2>&1 & chrome_pid=$! trap 'kill "$chrome_pid" "$server_pid" 2>/dev/null; wait "$chrome_pid" "$server_pid" 2>/dev/null' EXIT endpoint=http://127.0.0.1:19759 for attempt in $(seq 1 100); do target_id=$(curl -fsS "$endpoint/json/list" 2>/dev/null | jq -r '.[] | select(.title == "registered:solo") | .id' | head -1) [ -n "$target_id" ] && break sleep 0.1 done [ -n "$target_id" ] || exit 1 rea_local() { env -i PATH="$PATH" HOME="$work_dir/home" \ XDG_CONFIG_HOME="$work_dir/cfg" XDG_DATA_HOME="$work_dir/data" \ XDG_CACHE_HOME="$work_dir/cache" XDG_STATE_HOME="$work_dir/state" \ TMPDIR="$work_dir/tmp" REA_LOG_LEVEL=silent \ node scripts/rea.mjs "$@" } rea_local list-browser-targets "$endpoint" --json > "$work_dir/targets.json" rea_local discover-webmcp-tools "$endpoint" "$target_id" \ --observation-ms 300 --json > "$work_dir/discovery.json" jq '.normalized_result | {status,tools,completeness}' "$work_dir/discovery.json" ``` To observe the comparison consequence, continue in the same shell: ```sh rea_local inspect-web-page "$endpoint" "$target_id" \ --observation-ms 300 --json > "$work_dir/inspection.json" jq -n --slurpfile i "$work_dir/inspection.json" \ --slurpfile w "$work_dir/discovery.json" \ '{inspection:$i[0].normalized_result,webmcp:$w[0].normalized_result}' \ > "$work_dir/capture.json" rea_local compare-web-captures "$work_dir/capture.json" \ "$work_dir/capture.json" --json > "$work_dir/comparison.json" jq '.normalized_result.dimensions.webmcp' "$work_dir/comparison.json" kill "$chrome_pid" "$server_pid" wait "$chrome_pid" "$server_pid" 2>/dev/null || true trap - EXIT ``` For the control, add `inputSchema: {type: "object", properties: {}}` to the registration and repeat in a fresh browser profile. The independent verifier reproduced the failure and control twice at the baseline and executed the shell reproduction. Before submission, an equivalent isolated runner repeated raw protocol inspection, discovery, inspection, and both self-comparisons once against freshly built current main. All six REA CLI calls in that replay exited 0. ### Expected behavior Retain the valid registration with null schema and digest, without an invalid-value exclusion or unavailable WebMCP section caused solely by the omitted optional field. Its otherwise complete inventory should compare as unchanged against itself. The tested browser's `WebMCP.Tool.inputSchema` protocol property explicitly has `optional: true`. REA's `docs/browser-observation.md:21` promises discovery of in-scope registrations and distinguishes malformed protocol data; `src/domain/webMcpDiscovery.ts:31-32` permits null schema and digest. Nullability alone is not the basis for treating the producer field as optional. ### Impact and scope P2: valid parameterless registrations falsely lose discovery completeness and prevent the WebMCP comparison dimension from establishing equality. Tool metadata remains present. This reproduced in both independent baseline runs and the current-main replay. Wider page prevalence, other browser versions, and MCP transport were not tested; no security boundary crossing or data loss was established. Manual inspection of retained metadata is available. When callers own the page, the tested explicit empty-object schema avoids the exclusion. That workaround is unavailable for pages the caller cannot change, and manual inspection does not restore automated comparison completeness. Other incomplete passive-inspection dimensions independently keep overall comparison status unknown in both fixtures; overall equality is not the restoration signal here. ### REA version `rea-agents` 6.7.0, independently verified baseline `9d8e7803326a382cbbb3ffdb900f5e93e5442d68`. Rechecked with `npm run build:cached` and runtime replay on `origin/main` at `d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f`, also 6.7.0: still reproduces. The lockfile is unchanged between these revisions. ### Runtime and platform Node.js v26.12.0; macOS 27.0.1 arm64; Google Chrome 155.0.8059.85, headless with `WebMCPTesting` enabled. No Hopper/Ghidra or external API is involved. ### Target and artifact context Loopback HTML fixture with one imperative WebMCP registration. Separate headless Chrome, explicit debugging port, disposable browser profile, isolated REA HOME/XDG/temp directories. Captured normalized inspection and discovery JSON are combined for offline self-comparison. The owned browser and fixture server were stopped after replay. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence Current-main discovery returned this subset, with no dropped events: ```json { "status": "available", "tools": {"total": 1, "items": [{"name": "solo", "input_schema": null, "input_schema_sha256": null}]}, "completeness": { "status": "attach_limited", "unavailable_sections": ["webmcp_tools"], "excluded": [{"section": "webmcp_tools", "reason": "invalid_protocol_value", "count": 1}] } } ``` The same snapshot compared against itself returns: ```json {"total_changes":0,"changes":[],"status":"unknown","reason":"WebMCP discovery was unavailable or incomplete in at least one capture."} ``` The explicit-schema control returns completeness `complete_within_window`, empty exclusions and unavailable sections, the original schema, and digest `efddc7bd8bbcef73a14eb1ace1ffdaec81e518ef1e13c1e9271d0b8acb694a49`. Its WebMCP self-comparison is `unchanged` with zero changes. Independent protocol observation from the browser's loopback `/json/protocol`: ```json {"name":"inputSchema","description":"Schema for the tool's input parameters.","optional":true,"type":"object"} ``` Raw `WebMCP.toolsAdded` replay contains the `solo` registration without an `inputSchema` key. Evidence provenance: observed; local verification: independently reproduced, then replayed on current main. ### Typed outcome or recovery expected Complete WebMCP coverage for the valid omitted-schema registration, null schema/digest, and an unchanged WebMCP self-comparison. Preserve explicit-schema discovery and its canonical digest. Genuinely malformed annotations must continue to produce the documented exclusion; that discriminating malformed-input check is proposed, not executed here. ### Analysis or suspected cause Source-derived reasoning at the baseline: [`normalizeTool`, `src/browser/CdpWebMcpDiscovery.ts:467-474`](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/src/browser/CdpWebMcpDiscovery.ts#L467) parses `value.inputSchema` unconditionally. The omitted key becomes `undefined`, fails JSON-value parsing, and records `invalid_protocol_value`. `src/browser/CdpCaptureCompleteness.ts:62-79` marks that section unavailable; `src/domain/webCaptureDiff.ts:678-733` then reports the WebMCP dimension unknown. Current main retains the unconditional schema parse and reproduced the same result. No bundled dependency was identified as the cause. ### Additional context Prepared with AI assistance. No known-good earlier version or successful restoration has been established. Related #3989 and its open PR #4040 concern dropped WebMCP observations falsely proving equality. This report has zero dropped observations and instead concerns a valid optional-field omission falsely making discovery incomplete; its restoration requires admitting the omission without an invalid-value exclusion. PR #3837 covers genuinely malformed annotation payloads, a different input contract. Static WebMCP declaration reports such as #2624 and #2626 do not cover live CDP schema omission.
Author
Owner

@XinwangTao commented on GitHub (Oct 21, 2026):

Confirmed bug: the browser protocol explicitly declares inputSchema optional, raw CDP replay proves successful registrations omit the key, and the schema-null tool still gets marked invalid_protocol_value with webmcp_tools set unavailable. That makes the failure state wrong in two ways — it labels valid protocol data as invalid, and it flips the whole dimension to unavailable even though the tool is registered and usable. The fix should treat a missing inputSchema as an empty-parameter tool (or at least not an exclusion), and only mark the dimension unavailable when the registration itself failed, not when a descriptor field is absent. Worth adding the parameterless fixture to the discovery contract so the comparison dimension no longer ends up "unknown".

<!-- gh-comment-id:6305861171 --> @XinwangTao commented on GitHub (Oct 21, 2026): Confirmed bug: the browser protocol explicitly declares inputSchema optional, raw CDP replay proves successful registrations omit the key, and the schema-null tool still gets marked invalid_protocol_value with webmcp_tools set unavailable. That makes the failure state wrong in two ways — it labels valid protocol data as invalid, and it flips the whole dimension to unavailable even though the tool is registered and usable. The fix should treat a missing inputSchema as an empty-parameter tool (or at least not an exclusion), and only mark the dimension unavailable when the registration itself failed, not when a descriptor field is absent. Worth adding the parameterless fixture to the discovery contract so the comparison dimension no longer ends up "unknown".
Author
Owner

@morluto commented on GitHub (Oct 23, 2026):

Fixed in b29aa6043. WebMCP inputSchema omission is treated as optional rather than invalid protocol data; schema nullness and digest nullness survive without inventing unavailable coverage. The malformed-annotation control still rejects its separate invalid data. Verified by tests/boundary/browser/cdpWebMcpCoverage.test.ts. Verification used loopback fake CDP and production provider/error projection; no real-browser run is claimed.

<!-- gh-comment-id:6376080021 --> @morluto commented on GitHub (Oct 23, 2026): Fixed in [b29aa6043](https://github.com/morluto/rea/commit/b29aa6043f6469024b86ea28757dba464c04dbe2). WebMCP inputSchema omission is treated as optional rather than invalid protocol data; schema nullness and digest nullness survive without inventing unavailable coverage. The malformed-annotation control still rejects its separate invalid data. Verified by `tests/boundary/browser/cdpWebMcpCoverage.test.ts`. Verification used loopback fake CDP and production provider/error projection; no real-browser run is claimed.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3481
No description provided.