# @lensmcp/vite-plugin

> The single Vite plugin that makes a frontend app observable by [LensMCP](https://github.com/kiwiapps-ltd/lensmcp).

LensMCP is an observability lens for coding agents. `@lensmcp/vite-plugin` is how a Vite-based frontend opts into that lens: add it to your `vite.config.ts` and your app starts streaming React renders, Valtio state changes, console output, network calls, route changes, runtime errors, and build events back to LensMCP — with no other source changes.

Under the hood the plugin does three things: it aliases `valtio` to the instrumented drop-in [`@lensmcp/valtio-instrumentation`](https://www.npmjs.com/package/@lensmcp/valtio-instrumentation), it applies the [`@lensmcp/react-instrumentation`](https://www.npmjs.com/package/@lensmcp/react-instrumentation) Babel source transform to your JSX/TSX, and it injects a browser event bridge (a client runtime) that ships captured events to the lens over a local WebSocket. The plugin is **passive** — it never mutates your app's state or behaviour — and is meant to run in dev only.

## Install

```bash
yarn add -D @lensmcp/vite-plugin
```

`vite` is a peer dependency (`^5 || ^6 || ^7 || ^8`).

## Usage

Add `lensmcpVitePlugin()` to your `plugins` array. Zero config is the intended path — every capability is on by default:

```ts
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { lensmcpVitePlugin } from '@lensmcp/vite-plugin';

export default defineConfig({
  plugins: [
    react(),
    lensmcpVitePlugin(),
  ],
});
```

The plugin runs at `enforce: 'pre'`, so its source transform sees raw JSX/TSX before `@vitejs/plugin-react` (Babel), `@vitejs/plugin-react@6` (oxc), or `@vitejs/plugin-react-swc` (SWC) turn JSX into calls — it coexists with whichever React compiler you use.

It is dev-only on its own: the plugin attaches to `vite serve` and detaches from `vite build`, so a
production bundle carries none of it — no WebSocket bridge, no injected client runtime, no
`data-agent-source` attributes, no source paths. You do not need to gate it yourself.

If you deliberately want an instrumented build — a staging deploy you intend to point the lens at —
say so:

```ts
lensmcpVitePlugin({ applyOnBuild: true }); // or LENSMCP_VITE_APPLY_ON_BUILD=1
```

## What it does

- **`valtio` → `@lensmcp/valtio-instrumentation` alias.** A `config()` hook rewrites bare `import … from 'valtio'` to the instrumented drop-in, so existing Valtio stores become observable without edits. The alias uses an exact `/^valtio$/` match so the drop-in's own `valtio/vanilla` and `valtio/react` imports are left untouched. (Toggle with `aliasValtio`.)
- **Its own dependency cache in a worktree.** When the checkout's `node_modules` is a symlink to another checkout's (how agent worktrees are often made), the `config()` hook moves Vite's `cacheDir` to the checkout's own `.lensmcp/cache/vite/…`, so two checkouts' Vites never optimize into one folder. A checkout that owns `node_modules` is untouched.
- **`@lensmcp/react-instrumentation` Babel transform.** A `transform()` hook runs the LensMCP Babel plugin (`lensmcpBabelPlugin`) over every `.jsx`/`.tsx` file outside `node_modules`, wrapping the app root, instrumenting hooks, and stamping JSX identity props. The output is plain JSX/TSX that downstream React compilers preserve. (Toggle with `transformSource`.)
- **Browser event bridge / client runtime injection.** `transformIndexHtml()` injects `<script type="module" src="/@lensmcp/client-runtime">` into the page head; the plugin resolves that virtual module to a small client runtime. The runtime opens a WebSocket back to the plugin and forwards browser events — runtime errors, unhandled rejections, `console.*`, `fetch`, History/route changes, plus the pre-formed `publish` envelopes from the in-page React and Valtio instrumentation (via `window.__LENSMCP_PUBLISH__`). Each tab gets a stable per-tab id so multiple tabs stay distinct.
- **Build + HMR events.** `handleHotUpdate()` emits an HMR event with the affected files, and `generateBundle()` emits a bundle-size report (per-chunk and per-module byte sizes). Vite's native `vite:error` stream is forwarded too.

Incoming browser envelopes and Vite/Rollup events are normalised into LensMCP's `BaseEvent` shape and published. When an in-process `EventBus` is bound the plugin publishes directly; otherwise it forwards events cross-process over the same env-configured sink as the NestJS instrumentation — `LENSMCP_EVENT_FILE`, `LENSMCP_UDS`, or `LENSMCP_IPC_SOCKET` (`host:port`) — falling back to `console.debug` only for error-severity events when no sink is set.

## Options

`lensmcpVitePlugin(options?)` accepts `LensmcpViteOptions` — all fields are optional:

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `enabled` | `boolean` | `true` | Whether the plugin attaches anything at all. |
| `applyOnBuild` | `boolean` | `false` | Also attach during `vite build`. Off by default — everything here is a development tool. `LENSMCP_VITE_APPLY_ON_BUILD=1` sets it too. |
| `wsPort` | `number` | ephemeral | WebSocket port the client runtime posts events to. An unused port is chosen automatically when omitted. |
| `wsHost` | `string` | `'127.0.0.1'` | Bind address for the WebSocket. |
| `wsAllowedOrigins` | `string[]` | — | Browser origins the WebSocket admits beyond loopback and the origins this dev server serves the runtime to (gateway hosts and LAN addresses are learned automatically). Entries: `https://app.test:8443`, `app.test`, `.app.test`, `*`. `LENSMCP_BRIDGE_ALLOWED_ORIGINS` adds to it. |
| `injectClient` | `boolean` | `true` | Inject the client runtime via a `<script>` tag. |
| `aliasValtio` | `boolean` | `true` | Alias `valtio` to the instrumented drop-in in dev. |
| `transformSource` | `boolean` | `true` | Run the LensMCP source transform (root-wrap + hooks + JSX ids). |
| `hmrRecovery` | `boolean` | `true` | When Vite gives up on its HMR socket at page load, the runtime always reports it (`hmr` `ws-failed`); with this on it also reloads the page once — only in a tab whose socket worked earlier, at most once per two minutes. `false` = report only. |

The returned plugin also exposes a `wsPort(): number \| undefined` accessor (`LensmcpViteRuntime`), available after Vite's `configureServer` hook fires.

The WebSocket is not open to whoever finds the port: each dev-server start mints a token that is baked into the served runtime and required on every upgrade, the runtime itself is only served same-origin or to an origin the WebSocket admits anyway, and a page from any other origin is refused.

## How it fits

`@lensmcp/vite-plugin` is the front door for the browser-side of LensMCP. It pulls in [`@lensmcp/react-instrumentation`](https://www.npmjs.com/package/@lensmcp/react-instrumentation) (the Babel transform) and [`@lensmcp/valtio-instrumentation`](https://www.npmjs.com/package/@lensmcp/valtio-instrumentation) (the Valtio drop-in), then injects the bridge that those instrumentation layers publish through. The bridge ships the resulting events to the LensMCP runtime, where the gateway / MCP server collect and reduce them into resources (`react://`, `valtio://`, `runtime://browser/*`, `browser://tabs`) that a coding agent can query through the lens.

You typically add this one plugin to a frontend app, pair it with the NestJS instrumentation on the backend, and the agent sees both sides of the running system.

---

Part of [LensMCP](https://github.com/kiwiapps-ltd/lensmcp). Apache-2.0.
