[GH-ISSUE #4205] [Bug] Receiver name suffix creates a false static WebMCP declaration #1650

Closed
opened 2026-10-16 21:08:27 +00:00 by bsakaguchi · 2 comments
Owner

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

Before submitting

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

Area

Evidence, artifacts, or reconstruction

Observed behavior

analyze-web-bundle reports a static WebMCP declaration for an unrelated local object's empty registerTool method when its receiver identifier ends in modelContext. Both appmodelContext.registerTool and app.analyticsmodelContext.registerTool produce a catalog declaration with page-declared-untrusted status. Renaming the otherwise equivalent local receiver to appContext removes the declaration.

Reproduction

Use the source revision and pinned toolchain below with locked dependencies installed. Build with npm run build:cached, then run this from the repository root. This executed recipe supplies inert JavaScript through the repository's fake CDP HTTP/WebSocket fixture and invokes the compiled public CLI. It requires no real browser or external service. Disposable HOME/XDG directories isolate client settings, and cleanup removes the fixture copies.

node --input-type=module <<'JS'
import { execFile } from 'node:child_process';
import { mkdtemp, mkdir, readFile, writeFile, symlink, rm } from 'node:fs/promises';
import { stripTypeScriptTypes } from 'node:module';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { promisify } from 'node:util';
const run = promisify(execFile);
const repo = process.cwd();
const work = await mkdtemp(join(tmpdir(), 'webmcp-boundary-'));
try {
  await mkdir(join(work, 'home'));
  for (const name of ['fakeCdpBrowser', 'fakeCdpBrowserEvents', 'fakeCdpBrowserResults', 'fakeCdpBrowserTargets']) {
    await writeFile(join(work, `${name}.js`), stripTypeScriptTypes(await readFile(`tests/fixtures/${name}.ts`, 'utf8')));
  }
  await writeFile(join(work, 'package.json'), '{"type":"module"}');
  await symlink(join(repo, 'node_modules'), join(work, 'node_modules'), 'dir');
  const { startFakeCdpBrowser } = await import(pathToFileURL(join(work, 'fakeCdpBrowser.js')).href);
  const cases = {
    document: 'document.modelContext.registerTool({name:"catalog"});',
    navigator: 'navigator.modelContext.registerTool({name:"catalog"});',
    bare: 'modelContext.registerTool({name:"catalog"});',
    nested: 'window.app.modelContext.registerTool({name:"catalog"});',
    suffix: 'const appmodelContext={registerTool(){}}; appmodelContext.registerTool({name:"catalog"});',
    nestedSuffix: 'const app={analyticsmodelContext:{registerTool(){}}}; app.analyticsmodelContext.registerTool({name:"catalog"});',
    renamed: 'const appContext={registerTool(){}}; appContext.registerTool({name:"catalog"});',
    dynamicMethod: 'const method="render"; document.modelContext[method]({name:"catalog"});',
  };
  for (const [label, scriptSource] of Object.entries(cases)) {
    const fake = await startFakeCdpBrowser({commandResult: command => command.method === 'Debugger.getScriptSource' ? {scriptSource} : undefined});
    try {
      const options = {cwd: repo, env: {...process.env, HOME: join(work, 'home'), XDG_CONFIG_HOME: join(work, 'home', 'config'), XDG_CACHE_HOME: join(work, 'home', 'cache'), XDG_DATA_HOME: join(work, 'home', 'data')}, maxBuffer: 20_000_000};
      const {stdout, stderr} = await run(process.execPath, ['scripts/rea.mjs', 'analyze-web-bundle', fake.endpoint, 'allowed-page', '--observation-ms', '0', '--json'], options);
      const result = JSON.parse(stdout).normalized_result;
      console.log(JSON.stringify({label, exit:0, declarations:result.observations.webmcp_declarations.map(({name, trust})=>({name,trust})), coverage:result.coverage, completeness:result.completeness.status, stderr}));
      if (label === 'suffix') {
        const capture = await run(process.execPath, ['scripts/rea.mjs', 'inspect-web-page', fake.endpoint, 'allowed-page', '--observation-ms', '0', '--include-script-sources', '--json'], options);
        const scripts = JSON.parse(capture.stdout).normalized_result.scripts.items;
        console.log(JSON.stringify({rawSourceRetained:scripts.some(item=>item.source?.artifact?.text===scriptSource)}));
      }
    } finally {await fake.close();}
  }
} finally {await rm(work, {recursive:true, force:true});}
JS

The suffix and nestedSuffix cases define ordinary local objects with empty methods. The renamed case preserves that method shape under a different receiver name. The other cases check existing exact static callee matches and an unresolved computed method. One executed round made eight analysis calls and one inspection call; all completed successfully. The results are shown below.

Expected behavior

Static declaration extraction should distinguish the member name modelContext from a suffix within a different identifier. Both unrelated local empty-method calls should return empty declaration arrays. Keep the document, navigator, bare and nested exact-syntax controls and their untrusted status; keep the renamed and unresolved computed-method controls empty. This expectation concerns exact source syntax. It does not require general alias analysis or prove that a recognized call ran or registered a tool.

The browser observation guide documents static WebMCP declaration extraction, and the tool contract describes page-declared metadata derived without JavaScript execution. The existing analyzer controls preserve exact and optional syntax while leaving unresolved computed methods empty.

Impact and scope

P2, supported. The analysis workflow returns incorrect static tool metadata for the two tested receiver names, which can mislead an analyst reconstructing a page's declared tools. This is a bounded evidence defect. Every analysis call in the executed round returned completeness.status: "complete" and empty stderr. Real-application frequency, other platforms and downstream incidents were not measured.

A manual workaround is available for this fixture: inspect-web-page --include-script-sources retained the exact source, so an analyst can inspect the local receiver and discard the false positive. This does not establish a general automatic workaround.

REA version

Source-built runtime at 62bf09809ef7bb224028916424f8f07652a20f4f. The package version field is 6.5.0; an installed published-release artifact was not tested. Current main at 3406dc4d85bf29414319b33b98bd1421d573f663 was inspected separately, and the relevant suffix matcher remains present. Current-main applicability is source-inspected, not runtime-reproduced.

Runtime and platform

Node v24.18.0, npm 11.16.0, macOS arm64, locked dependencies and a freshly compiled baseline runtime.

Target and artifact context

Inert inline JavaScript source delivered by the repository's simulated CDP provider. No real Chrome, actual WebMCP registration or invocation, MCP transport, or target-source execution was used.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The executed public CLI recipe produced:

document: [{"name":"catalog","trust":"page-declared-untrusted"}]
navigator: [{"name":"catalog","trust":"page-declared-untrusted"}]
bare: [{"name":"catalog","trust":"page-declared-untrusted"}]
nested: [{"name":"catalog","trust":"page-declared-untrusted"}]
suffix: [{"name":"catalog","trust":"page-declared-untrusted"}]
nestedSuffix: [{"name":"catalog","trust":"page-declared-untrusted"}]
renamed: []
dynamicMethod: []
rawSourceRetained: true

The inspection result directly establishes source retention for the supplied fixture. The enclosing fixture process emits Node's experimental stripTypeScriptTypes warning; each analysis child returned empty stderr. These observations establish simulated-provider CLI behavior. They do not establish real Chrome or MCP behavior.

Typed outcome or recovery expected

Rerun the recipe after a correction. Both suffix cases must return empty declaration arrays, the four exact-syntax controls must retain their current declaration and untrusted status, and the renamed and unresolved computed-method controls must remain empty. Exact captured source must remain available through inspection.

Analysis or suspected cause

Source-derived explanation: the static declaration matcher accepts any callee string ending in modelContext.registerTool without checking a member-name boundary. The callee helper preserves the complete identifier and member segments, including the unrelated receiver names. Current main adds type-erased expression unwrapping to that helper; the supplied ordinary identifier/member cases contain no such wrappers. The provider path captures sources and invokes the production analyzer. This is a lexical false positive in static declaration metadata, with no runtime-registration claim or prescribed implementation.

Additional context

Related closed report #2678 concerns Electron renderer and service-worker nodes in application analysis. Its triggers, graph outputs and closure checks differ from this static WebMCP declaration inventory; fixing those classifications does not remove the matcher demonstrated here.

Evidence provenance: observed. Local verification: reproduced. Reproduction completeness: complete. The public CLI round traversed target discovery and selection, origin admission, source capture, production analysis, output-schema validation and Evidence serialization. NOT_ENOUGH_INFO: real-provider behavior, population frequency, published-release applicability and other-platform behavior were not tested. The closure checks above are proposed verification for a future correction, not a claimed fix.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4205 ### 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 Evidence, artifacts, or reconstruction ### Observed behavior `analyze-web-bundle` reports a static WebMCP declaration for an unrelated local object's empty `registerTool` method when its receiver identifier ends in `modelContext`. Both `appmodelContext.registerTool` and `app.analyticsmodelContext.registerTool` produce a `catalog` declaration with `page-declared-untrusted` status. Renaming the otherwise equivalent local receiver to `appContext` removes the declaration. ### Reproduction Use the source revision and pinned toolchain below with locked dependencies installed. Build with `npm run build:cached`, then run this from the repository root. This executed recipe supplies inert JavaScript through the repository's fake CDP HTTP/WebSocket fixture and invokes the compiled public CLI. It requires no real browser or external service. Disposable HOME/XDG directories isolate client settings, and cleanup removes the fixture copies. ```sh node --input-type=module <<'JS' import { execFile } from 'node:child_process'; import { mkdtemp, mkdir, readFile, writeFile, symlink, rm } from 'node:fs/promises'; import { stripTypeScriptTypes } from 'node:module'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { promisify } from 'node:util'; const run = promisify(execFile); const repo = process.cwd(); const work = await mkdtemp(join(tmpdir(), 'webmcp-boundary-')); try { await mkdir(join(work, 'home')); for (const name of ['fakeCdpBrowser', 'fakeCdpBrowserEvents', 'fakeCdpBrowserResults', 'fakeCdpBrowserTargets']) { await writeFile(join(work, `${name}.js`), stripTypeScriptTypes(await readFile(`tests/fixtures/${name}.ts`, 'utf8'))); } await writeFile(join(work, 'package.json'), '{"type":"module"}'); await symlink(join(repo, 'node_modules'), join(work, 'node_modules'), 'dir'); const { startFakeCdpBrowser } = await import(pathToFileURL(join(work, 'fakeCdpBrowser.js')).href); const cases = { document: 'document.modelContext.registerTool({name:"catalog"});', navigator: 'navigator.modelContext.registerTool({name:"catalog"});', bare: 'modelContext.registerTool({name:"catalog"});', nested: 'window.app.modelContext.registerTool({name:"catalog"});', suffix: 'const appmodelContext={registerTool(){}}; appmodelContext.registerTool({name:"catalog"});', nestedSuffix: 'const app={analyticsmodelContext:{registerTool(){}}}; app.analyticsmodelContext.registerTool({name:"catalog"});', renamed: 'const appContext={registerTool(){}}; appContext.registerTool({name:"catalog"});', dynamicMethod: 'const method="render"; document.modelContext[method]({name:"catalog"});', }; for (const [label, scriptSource] of Object.entries(cases)) { const fake = await startFakeCdpBrowser({commandResult: command => command.method === 'Debugger.getScriptSource' ? {scriptSource} : undefined}); try { const options = {cwd: repo, env: {...process.env, HOME: join(work, 'home'), XDG_CONFIG_HOME: join(work, 'home', 'config'), XDG_CACHE_HOME: join(work, 'home', 'cache'), XDG_DATA_HOME: join(work, 'home', 'data')}, maxBuffer: 20_000_000}; const {stdout, stderr} = await run(process.execPath, ['scripts/rea.mjs', 'analyze-web-bundle', fake.endpoint, 'allowed-page', '--observation-ms', '0', '--json'], options); const result = JSON.parse(stdout).normalized_result; console.log(JSON.stringify({label, exit:0, declarations:result.observations.webmcp_declarations.map(({name, trust})=>({name,trust})), coverage:result.coverage, completeness:result.completeness.status, stderr})); if (label === 'suffix') { const capture = await run(process.execPath, ['scripts/rea.mjs', 'inspect-web-page', fake.endpoint, 'allowed-page', '--observation-ms', '0', '--include-script-sources', '--json'], options); const scripts = JSON.parse(capture.stdout).normalized_result.scripts.items; console.log(JSON.stringify({rawSourceRetained:scripts.some(item=>item.source?.artifact?.text===scriptSource)})); } } finally {await fake.close();} } } finally {await rm(work, {recursive:true, force:true});} JS ``` The `suffix` and `nestedSuffix` cases define ordinary local objects with empty methods. The `renamed` case preserves that method shape under a different receiver name. The other cases check existing exact static callee matches and an unresolved computed method. One executed round made eight analysis calls and one inspection call; all completed successfully. The results are shown below. ### Expected behavior Static declaration extraction should distinguish the member name `modelContext` from a suffix within a different identifier. Both unrelated local empty-method calls should return empty declaration arrays. Keep the document, navigator, bare and nested exact-syntax controls and their untrusted status; keep the renamed and unresolved computed-method controls empty. This expectation concerns exact source syntax. It does not require general alias analysis or prove that a recognized call ran or registered a tool. The [browser observation guide](https://github.com/morluto/rea/blob/3406dc4d85bf29414319b33b98bd1421d573f663/docs/browser-observation.md#L16-L21) documents static WebMCP declaration extraction, and the [tool contract](https://github.com/morluto/rea/blob/3406dc4d85bf29414319b33b98bd1421d573f663/src/contracts/browserToolContracts.ts#L174-L179) describes page-declared metadata derived without JavaScript execution. The [existing analyzer controls](https://github.com/morluto/rea/blob/3406dc4d85bf29414319b33b98bd1421d573f663/src/domain/webBundleAnalyzer.test.ts#L1040-L1123) preserve exact and optional syntax while leaving unresolved computed methods empty. ### Impact and scope P2, supported. The analysis workflow returns incorrect static tool metadata for the two tested receiver names, which can mislead an analyst reconstructing a page's declared tools. This is a bounded evidence defect. Every analysis call in the executed round returned `completeness.status: "complete"` and empty stderr. Real-application frequency, other platforms and downstream incidents were not measured. A manual workaround is available for this fixture: `inspect-web-page --include-script-sources` retained the exact source, so an analyst can inspect the local receiver and discard the false positive. This does not establish a general automatic workaround. ### REA version Source-built runtime at [62bf09809ef7bb224028916424f8f07652a20f4f](https://github.com/morluto/rea/commit/62bf09809ef7bb224028916424f8f07652a20f4f). The package version field is 6.5.0; an installed published-release artifact was not tested. Current main at `3406dc4d85bf29414319b33b98bd1421d573f663` was inspected separately, and the relevant suffix matcher remains present. Current-main applicability is source-inspected, not runtime-reproduced. ### Runtime and platform Node v24.18.0, npm 11.16.0, macOS arm64, locked dependencies and a freshly compiled baseline runtime. ### Target and artifact context Inert inline JavaScript source delivered by the repository's simulated CDP provider. No real Chrome, actual WebMCP registration or invocation, MCP transport, or target-source execution was used. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The executed public CLI recipe produced: ```text document: [{"name":"catalog","trust":"page-declared-untrusted"}] navigator: [{"name":"catalog","trust":"page-declared-untrusted"}] bare: [{"name":"catalog","trust":"page-declared-untrusted"}] nested: [{"name":"catalog","trust":"page-declared-untrusted"}] suffix: [{"name":"catalog","trust":"page-declared-untrusted"}] nestedSuffix: [{"name":"catalog","trust":"page-declared-untrusted"}] renamed: [] dynamicMethod: [] rawSourceRetained: true ``` The inspection result directly establishes source retention for the supplied fixture. The enclosing fixture process emits Node's experimental `stripTypeScriptTypes` warning; each analysis child returned empty stderr. These observations establish simulated-provider CLI behavior. They do not establish real Chrome or MCP behavior. ### Typed outcome or recovery expected Rerun the recipe after a correction. Both suffix cases must return empty declaration arrays, the four exact-syntax controls must retain their current declaration and untrusted status, and the renamed and unresolved computed-method controls must remain empty. Exact captured source must remain available through inspection. ### Analysis or suspected cause Source-derived explanation: the [static declaration matcher](https://github.com/morluto/rea/blob/3406dc4d85bf29414319b33b98bd1421d573f663/src/domain/webBundleAnalyzerInspection.ts#L208-L212) accepts any callee string ending in `modelContext.registerTool` without checking a member-name boundary. The [callee helper](https://github.com/morluto/rea/blob/3406dc4d85bf29414319b33b98bd1421d573f663/src/domain/javascript/javascriptStaticAnalysisHelpers.ts#L682-L715) preserves the complete identifier and member segments, including the unrelated receiver names. Current main adds type-erased expression unwrapping to that helper; the supplied ordinary identifier/member cases contain no such wrappers. The [provider path](https://github.com/morluto/rea/blob/3406dc4d85bf29414319b33b98bd1421d573f663/src/browser/CdpBrowserProvider.ts#L151-L175) captures sources and invokes the production analyzer. This is a lexical false positive in static declaration metadata, with no runtime-registration claim or prescribed implementation. ### Additional context Related closed report [#2678](https://github.com/morluto/rea/issues/2678) concerns Electron renderer and service-worker nodes in application analysis. Its triggers, graph outputs and closure checks differ from this static WebMCP declaration inventory; fixing those classifications does not remove the matcher demonstrated here. Evidence provenance: observed. Local verification: reproduced. Reproduction completeness: complete. The public CLI round traversed target discovery and selection, origin admission, source capture, production analysis, output-schema validation and Evidence serialization. `NOT_ENOUGH_INFO`: real-provider behavior, population frequency, published-release applicability and other-platform behavior were not tested. The closure checks above are proposed verification for a future correction, not a claimed fix.
Author
Owner

@hemv-857 commented on GitHub (Oct 15, 2026):

Taking this one. Root cause: the declaration matcher in webBundleAnalyzerInspection.ts uses name.endsWith("modelContext.registerTool"), so any receiver identifier whose spelling ends in modelContext matches (appmodelContext, analyticsmodelContext). Fix: segment-exact match on the callee path so the receiver must be exactly modelContext (bare or nested), keeping the documented exact-syntax controls and leaving dynamic computed and renamed receivers empty. Tests in webBundleAnalyzer.test.ts.

<!-- gh-comment-id:6200430780 --> @hemv-857 commented on GitHub (Oct 15, 2026): Taking this one. Root cause: the declaration matcher in `webBundleAnalyzerInspection.ts` uses `name.endsWith("modelContext.registerTool")`, so any receiver identifier whose spelling ends in `modelContext` matches (`appmodelContext`, `analyticsmodelContext`). Fix: segment-exact match on the callee path so the receiver must be exactly `modelContext` (bare or nested), keeping the documented exact-syntax controls and leaving dynamic computed and renamed receivers empty. Tests in `webBundleAnalyzer.test.ts`.
Author
Owner

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

Fixed on main in 38f92d108. Static WebMCP detection compares exact AST property segments instead of flattened receiver suffixes, preventing suffix and dotted-key collisions.

Analyzer and controlled CLI regressions cover accepted/rejected receivers and retained source provenance; no real browser execution is claimed.

Hosted main CI is still pending; this closure is based on the implemented fix and the scoped regression evidence.

<!-- gh-comment-id:6206022666 --> @morluto commented on GitHub (Oct 16, 2026): Fixed on `main` in [38f92d108](https://github.com/morluto/rea/commit/38f92d108e3148cdf47e4ffbfe3de8a76e795d51). Static WebMCP detection compares exact AST property segments instead of flattened receiver suffixes, preventing suffix and dotted-key collisions. Analyzer and controlled CLI regressions cover accepted/rejected receivers and retained source provenance; no real browser execution is claimed. Hosted main CI is still pending; this closure is based on the implemented fix and the scoped regression evidence.
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#1650
No description provided.