One component. Two faces.
The agent-native fullstack UI framework: a live view for humans, typed MCP tools & resources for AI agents — generated from the same definition, so they can never drift.
From the creator of Brisa
bun create janux my-app
bunx create-janux my-app is the same command. Requires Bun ≥ 1.3 — the dev server, the build and the production server all run on it.
The agent calls the same intents a human clicks, and createCopilot({ visualize }) is the whole of the feedback — chips, the gradient ring, the backdrop veil. Source: examples/with-web-agent.
The same definition, seen by both audiences
😀 What you write
import { component, intent, schema, int } from 'janux';
export const Counter = component({
name: 'counter',
state: schema({ count: int() }),
intents: {
inc: intent({
description: 'Increment the counter',
input: schema({ by: int().default(1) }),
run: ({ state, input }) => (state.count += input.by),
}),
reset: intent({ guard: 'confirm', run: ({ state }) => (state.count = 0) }),
},
view: ({ state, intents }) => (
<section class="flex flex-col items-center gap-5 pt-16 font-sans">
<h1 class="text-6xl font-extrabold tracking-tight">{state.count}</h1>
<div class="flex gap-3">
<button onClick={intents.inc}
class="rounded-xl bg-blue-600 px-6 py-2.5 font-bold text-white
shadow-lg shadow-blue-600/30 hover:bg-blue-700">
+1
</button>
<button onClick={intents.reset}
class="rounded-xl border border-neutral-300 px-6 py-2.5
font-semibold text-neutral-500 hover:border-neutral-400">
Reset
</button>
</div>
</section>
),
});🤖 What agents see — generated, always in sync
{
"resources": [
"ui://counter"
],
"tools": [
{
"name": "counter.inc",
"description": "Increment the counter",
"guard": "auto",
"input": {
"type": "object",
"properties": {
"by": {
"type": "integer",
"default": 1
}
},
"required": [],
"additionalProperties": false
},
"ready": true
},
{
"name": "counter.reset",
"guard": "confirm",
"ready": true
}
]
}import { foreign } from 'janux/interop';
import { ReactFlow } from '@xyflow/react';
const Flow = foreign(ReactFlow, {
props: (own) => ({ nodes: own.state.nodes }),
on: { onNodeDrag: 'moveNode' },
hydrate: 'visible',
});A better model. Without giving up the React ecosystem.
Nobody rewrites their stack for a new framework, and you don't have to. foreign() mounts any React component unchanged — in a real embedded React root, inside a Janux view. React Flow, data grids, animation libraries, PDF viewers: they keep working.
Wrap it once in a bifacial shell and the opaque widget becomes agent-drivable too. And react stays an optional peer: an app with no foreign island ships zero React.
Ship an MCP server. Get WebMCP for free.
Two agent surfaces, no integration work: the one every MCP client already speaks comes out of your server functions, the browser one out of your components. You define neither.
Pointing Claude, Cursor or any MCP client at your app takes a URL, not an integration project — and it works today:
claude mcp add --transport http my-app https://your.app/_janux/mcp
🔌 MCP server — tools over HTTP
export const refund = api({
description: 'Refund an order. Irreversible.',
input: schema({ orderId: str(), amount: money() }),
guard: 'confirm',
run: ({ input }) => payments.refund(input),
});api comes from @janux/server. One definition is a validated endpoint, a ~100-byte client stub and a tool on your app's MCP server at /_janux/mcp — where guard: 'confirm' reaches clients as annotations.requiresApproval.
🌐 WebMCP — the same tools, in the browser
export const Cart = component({
name: 'cart',
state: schema({ items: list({ sku: str() }) }),
intents: {
add: intent({
description: 'Add a product to the cart',
input: schema({ sku: str() }),
run: ({ state, input }) => state.items.push(input),
}),
},
view: ({ state }) => <b>{state.items.length}</b>,
});component, intent and schema come from janux. Every intent is registered with document.modelContext the moment the island mounts, so Chrome's agent and the DevTools WebMCP panel see it — and the button a human clicks runs that same code.
api() reference →External MCP clients →Agent & copilot →Debugging WebMCP →
Built for how software gets written now
AI agents write the code. Humans review it. Agents operate the result. Janux is the first framework designed for that whole loop — not just the rendering.
😶🌫️ Easy for agents to write
One schema-typed definition per component — no hooks, no lifecycle traps, no hidden state. The most predictable target a model can generate, right on the first try.
🔍 Easy for humans to review
State, actions, guards and view live in a single definition, so the PR is the whole truth of the component. Diffs read in minutes — no archaeology across files.
🤖 Easy for agents to operate
Every app ships a hosted MCP endpoint on the server, and every component its tools as WebMCP in the browser. Connecting Claude, ChatGPT or your own copilot is a URL, not an integration project.
Contracts can't drift — they're generated from the code that renders, with human approval built in where it matters. No other framework ships this loop out of the box.
One definition, three projections
A component is a view, a resource (ui://counter) and typed tools at once. The mounted tree IS the MCP tree.
0 KB JS static pages
Components without state compile to plain HTML. No islands on the page? No <script> at all.
Structural resumability
Resume from JSON snapshots — no hydration replay, no closure serialization. Zero component code until first interaction.
Guards built in
auto / confirm / forbidden on every action. Agent proposals get approved by humans, with an audit trail.
api() — three things at once
A server function is a validated endpoint, a ~100-byte client stub and an agent tool. Defined once.
Zero-config copilot
Set JANUX_MODEL or one provider key. Manifest, agent endpoint and window.janux bridge come with every app.