Skip to content

feat: add WebMCP challenge web app - #40

Merged
3ssiri merged 16 commits into
mainfrom
feat/webmcp-challenge
Sep 1, 2026
Merged

3ssiri merged 16 commits into
mainfrom
feat/webmcp-challenge

Conversation

@3ssiri

@3ssiri 3ssiri commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Summary

Optional web layer for the OpenAI WebMCP challenge: a one-page dashboard over the existing scan/compare engine, plus four read-only tools so a human and an agent share the same page state.

  • FastAPI adapter in webapp/ (extra web); not packaged in repopulse-cli.
  • Same core path as the CLI: parse_github_urlGitHubClientbuild_health_report / build_comparison.
  • Tools: scan_repository, get_attention_items, get_check_details, compare_refs via document.modelContext.registerTool().
  • Public github.com only; GITHUB_TOKEN is server-side; private repos rejected before tree/file reads.
  • XSS: GitHub-derived data rendered with textContent / createElement only.
  • Shared-state ordering is guarded so a fresh scan can switch repositories while stale scan/compare results cannot overwrite newer state.
  • WebMCP registration cleans up both synchronous registerTool() throws and asynchronous registration rejections.
  • A private repository the server-side token can read answers with the same 404 repository_not_found as an inaccessible one, so the API cannot be used to discover private repository names.
  • Deployment config only (vercel.json, requirements.txt, [tool.vercel]): no change to scoring, checks, HealthReport / ComparisonReport schemas, CLI contracts, the GitHub Action, or the published package. python -m build sdist and wheel file listings are identical before and after.

Docs: docs/webmcp-challenge.md (includes a Deployment section).

Verification on head 4895d7e

Quality gates (local)

Gate Result
ruff check . pass
mypy repopulse pass — 25 source files
mypy webapp pass — 2 source files
pytest 168 passed
python -m build pass; sdist + wheel contents unchanged vs. pre-Vercel-config build
git diff --check clean
repopulse scan . 3ssiri/RepoPulse: 100 / 100 - Excellent
repopulse scan https://github.com/3ssiri/RepoPulse 3ssiri/RepoPulse: 100 / 100 - Excellent

CI on this head: test (3.11), test (3.12), Action smoke test, CodeQL, Analyze Python — all green.

Local API smoke (uvicorn + real GitHub API)

  • GET /api/health → 200 {"status":"ok","service":"repopulse-web","version":"0.3.6"}
  • POST /api/scan3ssiri/RepoPulse, 100/100, Excellent, 11 checks, scan_truncated=false, schema_version 1.1
  • POST /api/compare v0.3.5v0.3.6 → delta 0, 11 unchanged, 0 improved, 0 regressed
  • Negative: non-github host → 400 invalid_repository_url; 300-char URL-derived ref → 400 invalid_ref; 300-char body ref → 400 invalid_ref; unknown ref → 404 ref_not_found
  • No traceback, GitHub payload, Authorization header or environment secret appears in any response body or server log.

Browser — host shim, not a native WebMCP browser (Chromium 141.0.7390.37, Playwright)

These results were produced against a host shim implementing exactly the surface webapp/static/app.js targets — document.modelContext.registerTool(tool, { signal }) and execute(input, { signal }). The Chromium available to that session shipped no WebMCP implementation. For the native-browser run, see Native WebMCP browser run below.

Against the real local server and the real GitHub API — 16/17 checks pass; the one non-pass is a GET /favicon.ico 404, a browser-default request the app has no route for (no JS runtime errors, no page errors).

  • page loads 200; four tools register; badge reads WebMCP Available; all four carry readOnlyHint + untrustedContentHint
  • scan_repository → returns the report and the visible dashboard updates (11 checks rendered, attention section shown)
  • get_attention_items → reads page state, issues zero network requests
  • get_check_details → full detail for a real key; unknown key returns available_keys recovery
  • compare_refs → returns the comparison and the visible dashboard updates
  • Human scan and compare through the on-page forms; compare-before-scan shows the visible error

Deterministic second pass (12/12) for orderings that need controlled latency:

  • Human + agent shared state: agent scans A → human scans B → dashboard becomes B → agent scans A → dashboard returns to A; the scan form stays in sync
  • Switching repository clears the stale comparison
  • Race: a slow compare for A landing after a fast scan of B does not overwrite B's state and does not restore currentComparison
  • Cancellation: an AbortSignal passed to scan_repository aborts the fetch, leaves status: idle, re-enables both buttons
  • Registration failure, synchronous registerTool() throw and asynchronous rejection: registration.abort() runs, badge falls back to WebMCP Unavailable, zero tools left registered

Deployment

Vercel project repopulse-webmcp (team assiri-f0925b28), linked to this repository, fluid: true,
maxDuration: 60 on webapp/app.py, no GITHUB_TOKEN set.

The first deployment returned 500 (ModuleNotFoundError: No module named 'fastapi'). A probe deployment
established that Vercel's Python runtime installs only [project].dependencies from pyproject.toml
[project.optional-dependencies], PEP 735 [dependency-groups] and requirements.txt are all ignored.
Since FastAPI must not become a dependency of the published repopulse-cli, the web-only requirements are
installed by a buildCommand that runs after the framework install.

Production has been promoted off that broken build. The branch deployment carrying 4895d7e was
promoted with vercel promote; the served webapp/static/app.js was byte-compared against the local
4895d7e copy before promoting. Production now answers:

GET https://repopulse-webmcp.vercel.app/api/health
200 {"status":"ok","service":"repopulse-web","version":"0.3.6"}

Verification against the live deployment

All of the following ran against https://repopulse-webmcp.vercel.app (production alias, HTTPS).

Live HTTPS API smoke

Request Result
GET /api/health 200 {"status":"ok","service":"repopulse-web","version":"0.3.6"}
POST /api/scan 3ssiri/RepoPulse 200 — 100/100 Excellent, scan_truncated=false, schema_version 1.1, private=false
POST /api/scan psf/requests 200 — 97/100 Excellent, scan_truncated=false, schema_version 1.1
POST /api/compare v0.3.5v0.3.6 200 — score_delta 0, 11 checks unchanged
POST /api/scan non-github host 400 invalid_repository_url
POST /api/scan 300-char ref in the URL 400 invalid_ref
POST /api/compare 300-char baseline_ref in the body 400 invalid_ref
POST /api/scan 3ssiri/nope-does-not-exist 404 repository_not_found

No traceback, raw GitHub payload, Authorization header or environment secret appears in any response body.

Cold / warm latency on the deployed function

1 2 3
GET /api/health 0.47 s 0.41 s 0.43 s
POST /api/scan (3ssiri/RepoPulse) 1.06 s 0.53 s

Heaviest repositories, measured against the same production URL:

Repository Wall time Result
psf/requests 2.07 s 97/100
microsoft/vscode 6.92 s 82/100, scan_truncated=false
torvalds/linux (local backend) 8.35 s 69/100, scan_truncated=true

torvalds/linux is the first live confirmation that the truncation flag fires on a repository large
enough to hit the GitHub tree API's own truncated response — the renderers warn rather than reporting a
silently partial scan. maxDuration stays at 60 s: the slowest production scan measured is 6.92 s, and a
15 s ceiling would have cut off a microsoft/vscode scan measured at 15.5 s over a slower link.

Native WebMCP browser run

Chrome 152.0.7977.65 (stable) ships a real WebMCP implementation: flag enable-webmcp-testing,
feature WebMCPTesting, and a WebMCP CDP domain. The browser was launched with
--enable-features=WebMCPTesting and driven from the agent side through
WebMCP.invokeTool / WebMCP.cancelInvocationno shim, no injected script, and no direct call into
page functions
.

Against the live HTTPS deployment — 11/11:

  • window.isSecureContext === true; typeof document.modelContext.registerTool === "function" with nothing injected
  • the browser's own tool registry discovered exactly scan_repository, get_attention_items,
    get_check_details, compare_refs — no write tools, no issue creation
  • badge reads WebMCP Available
  • scan_repositorystatus: Completed, and the visible dashboard grew from 228 to 1307 characters of
    rendered report
  • get_attention_itemsCompleted with zero new network requests
  • get_check_details("readme")Completed with zero new network requests
  • get_check_details with an unknown key → returns available_keys, does not throw

Against a local uvicorn webapp.app:app with a GITHUB_TOKEN in the process environment — 21/21:
everything above, plus compare_refs rendering into the same dashboard, the human + agent shared-state
sequence (agent scans A → human scans B through the page form → dashboard becomes B and the stale
comparison is cleared → agent scans A → dashboard returns to A), and WebMCP.cancelInvocation producing
status: Canceled. Zero uncaught console errors in both runs.

Why the heavy scenarios ran locally rather than on production

The deployment has no GITHUB_TOKEN, so it uses GitHub's unauthenticated limit of 60 requests/hour per
egress IP. One scan costs ~11 requests and one compare ~21, so the full 21-check sequence needs ~55 and
cannot complete reliably — Vercel spreads calls across several egress IPs, so a probe can succeed while the
run that follows it hits an exhausted one. Observed directly: a full production run returned
429 github_rate_limited partway through.

The app degrades correctly under that limit — the 429 surfaces as
GitHub API rate limit exceeded. Try again later. with no traceback and no token, and propagates as
status: Error through the native WebMCP host rather than hanging.

Still not verified

  • Live HTTPS smoke against the deployment URL, including a second public repository (psf/requests)
  • Cold/warm latency measurement on the deployed function
  • All four tools executed in a native WebMCP-enabled Chrome against the deployed HTTPS URL
    (compare_refs and the shared-state sequence were exercised natively against a local backend only —
    see the rate-limit note above)
  • ChatGPT in-app browser run — NOT AVAILABLE, no such environment was reachable
  • Production alias promotion

Known and deliberately out of scope: GET /favicon.ico returns 404 — the app has no favicon route.

Changes since 4895d7e

f8f36bc — private repositories no longer answer differently from inaccessible ones (see the Summary).
Raised by an automated review on this PR; fixed test-first, with a test asserting the two responses are
byte-identical.

Re-run on f8f36bc: pytest 169 passed (one new test), ruff check clean, mypy repopulse webapp
clean over 27 source files, git diff --check clean. CI on this head: test (3.11), test (3.12),
Action smoke test, CodeQL, Analyze Python, Vercel — all green.

The live-deployment results above were produced against the deployed 4895d7e build. They remain
accurate for f8f36bc: the change only alters the response for a private repository, and the deployment
has no GITHUB_TOKEN, so it can see no private repository at all.

A second finding in the same review — that modelContext lives on navigator rather than document,
so no tools register — was checked directly in the Chrome build used above and is not reproducible:
document.modelContext is an object whose constructor is ModelContext, document.modelContext.registerTool
is a function, and navigator.modelContext is undefined. The browser's own WebMCP registry discovered
all four tools, which is what the run above exercises.

Remaining release gate

The two gates this PR was blocked on — the live HTTPS smoke and a native-browser WebMCP run — are now
closed. What remains open is the ChatGPT in-app browser run, and compare_refs plus the shared-state
sequence on the production URL specifically; both are blocked only by the missing GITHUB_TOKEN on the
deployment, not by application behaviour.

3ssiri added 2 commits August 31, 2026 23:50
Thin FastAPI adapter (webapp/) over the unchanged repopulse core:
GET /api/health, POST /api/scan, POST /api/compare with a stable
error contract, public-repos-only enforcement before tree reads, and
a vanilla-JS one-page dashboard. Four read-only WebMCP tools
(scan_repository, get_attention_items, get_check_details, compare_refs)
share the exact same state and code paths as the UI buttons.

Core engine, CLI, schemas, scoring, and action behavior untouched.
New optional 'web' extra (fastapi, uvicorn); base install unchanged.
Do not overwrite the selected repository until a scan succeeds, so compare and the visible report stay on the same repo. Use a human-facing compare error, strip/reject blank compare refs, and record the web layer in the changelog and translations.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2f42c1fad8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread webapp/static/app.js Outdated
Comment thread webapp/app.py Outdated
Comment thread webapp/static/app.js
Comment thread webapp/app.py Outdated

3ssiri commented Aug 31, 2026

Copy link
Copy Markdown
Owner Author

WebMCP follow-up — apply on the same branch

Please address these recommendations on the existing feat/webmcp-challenge branch and update PR #40. Do not open a new branch/PR and do not merge to main yet.

Required before merge

  1. Fix the shared-state race between scan and compare.

    • If a comparison for repository A is in flight and a scan for repository B succeeds first, the late comparison result for A must not overwrite currentComparison or render beside B's report.
    • Use a small request generation/id or capture the selected repository at request start and discard/abort stale results.
    • Add a regression test for this exact A → B overlap scenario.
  2. Surface Compare-before-Scan errors in the visible UI.

    • compareRefs() currently throws before the handled path, while the form submit handler swallows the rejected promise.
    • The human must see a clear error such as Scan a repository first.
    • Keep the WebMCP recoverable error for the agent as well.
  3. Validate URL-derived refs with the same 256-char rule as body refs.

    • A /tree/<ref> parsed from repository_url must pass the same normalization/length validation as payload.ref.
    • Add a test for an overlong URL-derived ref returning invalid_ref / 400.
  4. Clean up partial WebMCP registrations on failure.

    • Promise.all(registerTool(...)) can leave some tools registered if one registration rejects.
    • In the registration failure path, abort the registration controller before marking WebMCP unavailable.
    • Keep WebMCP as progressive enhancement; normal UI must continue to work.

Recommended if it stays surgical

  1. Avoid duplicate repository metadata calls.
    • _reject_private() calls get_repo(), then build_health_report() calls get_repo() again; compare currently does even more metadata calls.
    • Reuse metadata only if this can be done with a small backward-compatible change. Do not refactor the core broadly or duplicate analyzer logic just to save a request.

Verification required after the fixes

Run and report actual results for:

ruff check .
mypy repopulse
mypy webapp
pytest
python -m build
repopulse scan . --format summary --quiet
repopulse scan https://github.com/3ssiri/RepoPulse --format summary --quiet

Then run the web app locally and manually verify:

  • scan https://github.com/3ssiri/RepoPulse
  • compare v0.3.5v0.3.6
  • invalid URL / 404 / rate-limit error behavior
  • failed second scan keeps the last successful report selected
  • stale comparison cannot overwrite a newer repository selection
  • Compare-before-Scan shows a visible human-facing error

WebMCP verification — do not mark complete without actual tool execution

Execute all four tools in a supported WebMCP environment:

  • scan_repository
  • get_attention_items
  • get_check_details
  • compare_refs

Confirm that scan_repository and compare_refs update the same visible dashboard state seen by the human. Also verify the unsupported-browser fallback.

Finally deploy the same FastAPI app over HTTPS, add the live URL to the PR, and re-run the four WebMCP tools against the deployed app if the challenge environment requires it.

Keep the current architecture: same repository, same branch, top-level webapp/, FastAPI + vanilla JS, public GitHub repos only, read-only GitHub access, no auth/database/Next/React, no create_issue, no token in the browser, no changes to scoring/checks/schema contracts.

Show Compare-before-Scan errors in the UI, discard stale scan/compare results, enforce the 256-char ref limit on URL-derived refs, abort partial WebMCP registration, and reuse privacy-check repo metadata so scan/compare do not call get_repo twice.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

3ssiri commented Aug 31, 2026

Copy link
Copy Markdown
Owner Author

Pushed review fixes to this same branch (bbb2a43). No new PR, no merge.

Addressed

  1. Compare-before-Scan — the no-repository guard now throws inside try/catch, so setError() runs. The form still swallows the rejected promise, but the page shows: No repository is selected. Scan a repository first.
  2. Stale scan/compare — a request generation counter plus captured repository URL. If compare(A) is in flight and scan(B) succeeds first, A's result is discarded (no currentComparison write, no render). Same rule for overlapping scans. Superseded work is silent (no extra error).
  3. URL-derived refs — the 256-char limit applies when the /tree/ ref is used. Overlong URL ref without body ref400 / invalid_ref.
  4. Partial WebMCP registration — on Promise.all failure, registration.abort() runs before marking unavailable.
  5. Duplicate get_repo_reject_private() now returns the payload; build_health_report(..., repo_data=) reuses it (keyword-only, CLI callers unchanged). Scan: 1 metadata call. Compare: 1 instead of 3.

Verification (actual)

ruff check .          # All checks passed
mypy repopulse        # Success: 25 source files
mypy webapp           # Success: 2 source files
pytest                # 163 passed
python -m build       # repopulse_cli-0.3.6 sdist+wheel (webapp not packaged)
repopulse scan . --format summary --quiet
  # 3ssiri/RepoPulse: 100 / 100 - Excellent
repopulse scan https://github.com/3ssiri/RepoPulse --format summary --quiet
  # 3ssiri/RepoPulse: 100 / 100 - Excellent

Live local API (uvicorn on :8010):

  • GET /api/health → 200
  • invalid host → 400 invalid_repository_url
  • URL /tree/ + 300-char ref → 400 invalid_ref
  • missing repo → 404
  • scan https://github.com/3ssiri/RepoPulse → 200, score 100
  • compare v0.3.5v0.3.6 → 200, delta 0

Not done yet (out of this code round)

  • Four WebMCP tools were not executed in Chrome/ChatGPT (no WebMCP-enabled browser in this session).
  • No HTTPS live URL added.
  • Not merged to main.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0186d72c7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread webapp/static/app.js Outdated

3ssiri commented Sep 1, 2026

Copy link
Copy Markdown
Owner Author

Current readiness status

All code-review findings are now addressed on the existing feat/webmcp-challenge branch, including the latest synchronous registerTool() failure case. All review threads are resolved.

Current head: bd5cbc3

  • CI: green
  • CodeQL: green
  • Shared-state A→B scan / stale compare regression coverage: present
  • Sync + async WebMCP registration cleanup: covered
  • URL-derived ref limit / Compare-before-Scan / metadata reuse: covered

The remaining challenge gates are external integration checks rather than known code blockers:

  1. Deploy the same FastAPI app to a public HTTPS URL.
  2. Execute scan_repository, get_attention_items, get_check_details, and compare_refs in an actual WebMCP-enabled Chrome/ChatGPT browser and confirm scan_repository / compare_refs update the visible dashboard.

Do not merge until those two external checks are completed.

Deploys the existing FastAPI app (webapp/app.py) as-is:

- pyproject.toml [tool.vercel] entrypoint points at webapp.app:app, since
  webapp/app.py is outside Vercel's auto-detected entrypoint locations.
  setuptools ignores the table; sdist and wheel contents are unchanged.
- vercel.json enables fluid compute and raises maxDuration to 60s, because a
  scan makes several sequential GitHub API calls.
- requirements.txt lists only the runtime imports the web layer needs, so the
  function does not pull in the CLI-only dependencies.

No change to scoring, checks, CLI contracts, or the published package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158GxB1urMdo6XYFAkHfSFT
@vercel

vercel Bot commented Sep 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
repopulse-webmcp Ready Ready Preview Sep 1, 2026 9:01am UTC

The first deployment returned 500 with ModuleNotFoundError: No module named
'fastapi'. A probe deployment confirmed Vercel's Python runtime installs only
[project].dependencies from pyproject.toml: [project.optional-dependencies],
PEP 735 [dependency-groups], and requirements.txt are all ignored.

FastAPI must not become a dependency of the published `repopulse-cli` package,
so the web-only requirements are installed from requirements.txt by a
buildCommand that runs after the framework install and before the function
bundle is assembled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158GxB1urMdo6XYFAkHfSFT
Records what each deployment file is for and, in particular, why the web
dependencies cannot live in pyproject.toml: Vercel's Python runtime installs
only [project].dependencies, so FastAPI would otherwise have to become a
dependency of the published repopulse-cli package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158GxB1urMdo6XYFAkHfSFT

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4895d7e13c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread webapp/static/app.js
Comment thread webapp/app.py Outdated
A private repository the deployment's GITHUB_TOKEN could read returned
403 private_repository_not_supported, while a missing or inaccessible one
returned 404 repository_not_found. An anonymous caller could use that
difference to probe which private repository names the token can see.

Both cases now return the identical 404 response. The privacy check still
runs before any tree or file content is read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f8f36bcaea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread webapp/static/app.js
Comment thread webapp/app.py
After repository A scanned successfully and a scan of B failed, the form kept
showing B while state.repositoryUrl still held A. Compare then acted on A
without saying so, contradicting the repository the form displayed.

The failure path now resyncs the form to the retained selection, guarded on a
previous successful report so a first failed scan does not wipe the input.

Verified in Chrome against a real backend: after a failed scan of a
non-existent repository, the form shows the previously scanned repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@3ssiri
3ssiri merged commit 7e0cffe into main Sep 1, 2026
7 checks passed
@3ssiri
3ssiri deleted the feat/webmcp-challenge branch September 1, 2026 09:24

This branch was successfully deployed

2 active (1 outdated) deployments
Preview ace74736 Deployed Sep 1, 2026 by vercel[bot]
Production 4895d7e1 Deployed Sep 1, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant