Janus, the two-faced Roman god

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.

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.