Inspiration

Watch an AI agent use a website today and it is doing an impression of a person. It screenshots the page, guesses which pixels are a button, clicks, and hopes nothing moved. We built a GUI-agent demo ourselves and the lesson was blunt: the agent was not slow because the model was weak. It was slow because the app refused to tell it anything. Every capability the app already had — typed, validated, permission-checked on the server — was hidden behind a rendering of itself.

WebMCP inverts that. The page stops being a picture to be interpreted and starts being a host that hands the agent real tools. Once we saw that, the question stopped being "how do we automate the browser better" and became "what would a fullstack framework look like if agents were a first-class audience from line one?" That framework is Janux.

What it does

Janux is a fullstack framework for the Agentic Web. You write your app once; humans get a UI and agents get tools, from the same definitions.

An action you already wrote for a form is published three ways with no extra code: as the UI handler, as a WebMCP tool inside the browser session, and as an MCP tool over the wire for out-of-page clients. Same types, same auth, same permission checks. The framework also ships the rest of the stack the Agentic Web needs — llms.txt so an agent can discover what your site does, and Web Bot Auth so it can prove who it is — plus the safety layer that opening this surface demands.

And you do not have to bet your stack on it: Janux runs the React ecosystem through a compatibility layer, so it can be adopted page by page.

How we built it

The framework is Bun-native. The agent surface is not a plugin bolted on top; it is derived from one place. A single server-side manifest describes every tool, its schema, its guard level and whether it can touch untrusted content — the browser bridge, the MCP endpoint and the security pipeline all read from that one description rather than each maintaining its own idea of what exists.

Every tool declares a guard. auto runs on the agent's word. confirm produces a signed proposal a human has to approve. Tools can also declare effect: 'irreversible', which changes how the pipeline treats them under pressure.

Our hardest rule was a process one: zero workarounds in the example app. Every time building a demo required a hack, the hack was banned and the friction became a framework change with a test written first. That is where most of the real API came from — event-based tool targeting, keeping runtime nodes alive across SPA navigation, resolving highlight targets for DOM the tool itself creates, disambiguating controls that share an intent.

Challenges we ran into

Exposing your app's real capabilities to an untrusted caller is a security project wearing a DX project's clothes. Four things we did not see coming:

The worst hole was a GET. Our API handler never looked at the HTTP method — it parsed the body and ran the tool. So GET /_janux/api/payments.transfer executed with the defaults from the input schema. That is an <img src> tag on any website in the world, no JavaScript, no CORS preflight, nothing. Invocation endpoints now return 405 for read-only verbs: they have no reads to offer.

Sec-Fetch-Site: same-site is not a defense. Measured in real Chrome: two different ports on localhost are same-site. Trusting that header would have left the actual attack open, and in production it is the shape of a subdomain takeover. It falls through to an origin comparison like cross-site does.

Computing a preview can be the side effect. Our proposal flow shadow-runs an intent to show the human a diff. When we made tainted irreversible tools downgrade from auto to confirm, we routed exactly the most dangerous operations through the path that runs their body before anyone approves. The shadow only shadows state — an outbound call still really happens. Caught reading our own diff, not by a test.

Machine output was wearing the human's role. For compatibility with OpenAI-shaped APIs, tool results came back into the conversation as role: 'user'. Our injection guard was faithfully classifying scraped third-party content as "the last thing the human said." Untrusted content is now fenced with a per-call random nonce — without the nonce, a payload can simply close the fence itself and continue as trusted prose.

What we learned

Green tests were not enough for any of this. What convinced us was mutation: delete a rule, and watch the exact rows of the attack corpus that rule exists for turn red. If nothing goes red, the rule was decoration.

The deeper lesson is that WebMCP is not a transport problem, it is a trust-boundary problem. Handing an agent a tool is the easy half. The hard half is that your app now has a caller that can be persuaded by a web page it read a second ago — and the framework, not each app author, should be the one holding that line.

What's next

More of the standard surface, adoption paths from existing React apps, and pushing the security defaults further so that being agent-ready is the boring default rather than a feature you have to remember to turn on.

Try it: bunx create-janux my-app (requires Bun >= 1.3) — https://janux.build

Built With

Share this project:

Updates

Submission history