The argument. Every way an agent can help you on a website today requires taking your authority and handing it to the agent. A scraper gets your whole session and guesses meaning from markup. An MCP connector gets a token you pasted and acts on your behalf forever, out of sight of the page. In both cases the agent's power is a copy of yours, and the site has no say in what it can do.

WebMCP inverts that: the publisher defines the capabilities, so the publisher defines the limits. That is the whole reason this project exists. A careers site can expose "fill in my application" while making "submit it" structurally impossible for an agent — not guarded, not confirmed, impossible — because the site owns both the tool surface and the code behind it.

Live demo: https://careers-webmcp.vercel.app/careers/open-positions Repository: https://github.com/alliecatowo/careers-webmcp

The problem

Job seekers do an enormous amount of unpaid data entry, and candidate funnels leak in three predictable places: search that can't answer the question you actually have ("staff or above, SF or remote, at least $220k" is not a keyword); the account wall standing between a person and a role they have already decided to apply for; and retyping the same nine fields at every employer. All three are data entry. None of them is a decision. The audience is candidates browsing employer career sites — and the employers who lose them.

Why WebMCP specifically

A careers site is a destination you visit occasionally and would never configure as a permanent integration. Nobody installs an MCP connector for one employer's job board, uses it for ninety minutes, and maintains it. WebMCP covers exactly that long tail: the capability arrives with the page, runs on the session the human already has, and leaves when the visit ends.

What it is

A working careers portal for a fictional employer, fully usable with no agent present. When WebMCP is available it registers eighteen candidate-facing tools on document.modelContext: page context, semantic job search, job detail, navigation to any page on the site by name, control of the site's own search view, the employer's own careers content as structured data, saved jobs, application drafts with revision-safe patching, field focus, account sign-up, submission hand-off, and handle-based CSV export.

Try it

Open https://careers-webmcp.vercel.app/careers/open-positions and ask your agent to find staff-level-or-above engineering roles in San Francisco or remote, paying at least $220k base. Watch the site's own search box type that query and the job list narrow live — that's the real page reacting, not a summary in a side panel. Ask it to start an application on one of those roles and fill in what's missing; the draft's revision counter moves on every write, whether the write came from the agent or from you. Then try to get it to submit that application. It can't. grep -cE "submitApplication\(|completeSignUp\(" src/webmcp/tools.ts returns 0 — submitApplication and completeSignUp were never registered as tools, so there is nothing for an agent to call. The last click is always yours.

How this makes the UX better

Today, "better UX with an agent" almost always means a chat panel bolted onto the side of a page, describing back to you what it thinks you see. Here the agent's output is the page: a compound filter query becomes the site's own search box typing itself and the visible list narrowing; "find that job" becomes the real job page opening in your real tab; "fill in my application" becomes the real form filling in front of you, field by field, while you can still click into any of it and correct it by hand. A visitor with no agent sees zero extra UI and loses nothing; a visitor with an agent gets the same site, faster, with the boring parts done for them.

What people and agents can do together that was hard before

You ask one compound question the board has no filter UI for, and the agent answers it and applies it to the page you are looking at — typing into the site's own search box, character by character, while the visible list narrows. You click Save with your own hand and the agent sees it, because it reads the same store the button writes to, not a copy. You and the agent co-edit one application draft: it fills the empty fields, you write your own cover note, and every agent write carries the revision it last read, so a stale write is rejected with STALE_APPLICATION and your text always wins. And the agent can reason over the entire catalog without pulling twenty job descriptions into its context, because the site hands it a CSV handle it pages through.

Where the agent stops, and why you can verify it

Two tools deliberately never complete. careers_create_account fills the real sign-up form and returns; careers_submit_application runs the same validation the human Submit button runs, opens the draft and rings the button. Both return status: "awaiting_human_confirmation". There is no confirm: true escape hatch because there is no second code path: src/webmcp/tools.ts never imports the function that submits an application or the function that creates a session, and each of those has exactly one caller in the entire codebase, both inside a human-clicked button. One grep proves it. The agent does the typing; the person stays the person, and sends the thing.

How WebMCP was implemented

A client provider feature-detects document.modelContext and registers eighteen tools once per page load, unregistering via an AbortSignal, guarded by a WeakSet against React StrictMode double-mounts; registration failure is swallowed so it can never break the site. Nothing reads or drives the DOM. The human UI and the tools import the same module-level functions from src/domain — search runs the identical deterministic scorer behind the visible job list, so the page and the agent cannot disagree about what matched; mutations run the same validators as the form; navigation uses the app router, so the user's real tab moves. Context isn't scraped or parsed: the pages publish it. Tools carry JSON Schema inputs, WebMCP annotations, AbortSignal support, a structured recoverable error model, and central output bounds with a truncated flag. A presence layer wraps each tool's execute at registration and pushes to a store the site's normal components subscribe to, so the agent's work appears in the real UI, and it builds its captions from counts and enums only, never from site text, so a prompt injection in a job description has nowhere to land.

No LLM, no AI SDK, no chat panel, no MCP server, no recommendation model, no DOM automation. The site is the integration.

The bigger picture

Imagine every Greenhouse board, Workday portal, university portal, marketplace and support site exposing its own semantics this way. Nobody installs hundreds of connectors; the open web becomes its own integration registry. This is one employer's site, built the way the rest could be.

Built on the MIT-licensed Baalvion Jobs Portal, pinned at a specific upstream commit. The careers UI and admin dashboards are upstream; the WebMCP layer, presence layer, context bridge, semantic search, candidate session, saved jobs, revision-protected application drafts, sign-up hand-off, exports, tests and docs are the challenge-period contribution — 192 files changed, over 14,000 lines added, on top of that pinned commit.

How we built it

Next.js, React, TypeScript, Tailwind, and Zustand for client state, on top of the MIT-licensed Baalvion Jobs Portal template. The WebMCP layer is additive: a client provider registers tools once per page load and every tool calls straight into the same domain functions the visible UI already uses, so there is exactly one implementation of "what jobs match this search" or "is this application valid," not two that can quietly drift apart.

Challenges we ran into

The hardest part wasn't registering tools — it was making sure two specific ones structurally could not finish without a human. That meant auditing the whole call graph so that careers_create_account and careers_submit_application have no path to the functions that actually create a session or submit an application, rather than relying on an if-check that a future edit could accidentally remove. Getting revision-safe co-editing right for the application draft was its own puzzle: a human typing a cover note and an agent filling other fields at the same time needed a merge rule where the human's text always wins, not just whoever saved last.

Accomplishments that we're proud of

Every mutation an agent can trigger runs through the exact same validators and store as a human click — there is no shadow code path for the agent, which is also what makes the "the agent can literally never press Submit" claim independently verifiable with a single grep rather than something you have to trust. And the presence layer renders zero DOM until a tool actually runs, so a visitor with no agent gets a completely normal careers site with zero overhead.

What we learned

A revision-protection model built for "don't let an agent silently overwrite a human" turns out to be exactly the same problem as classic optimistic-concurrency-control for two humans editing together — treating the agent as just another writer that has to prove it read the current version before it's allowed to write, rather than a privileged client, made a lot of the hard access-control questions dissolve.

What's next for Careers WebMCP

Extending the same tool surface to employer-side dashboards (screening, pipeline management) with the same "agent proposes, human presses the irreversible button" pattern, and packaging the presence layer and revision-safe draft pattern as a reusable library for other Baalvion-based sites.

Built With

Share this project:

Updates

Submission history