@graphty/babylonjs-inspector-mcp
# babylonjs-inspector-mcp
[](https://www.npmjs.com/package/babylonjs-inspector-mcp)
[](https://github.com/apowers313/babylonjs-inspector-mcp/actions/workflows/ci.yml)
[](https://opensource.org/licenses/MIT)
An MCP (Model Context Protocol) server that lets an LLM inspect a running BabylonJS 3D scene.
> **Status: early development (0.x).** The MCP server, the tool surface, the WebSocket
> protocol and the HTTP server are implemented. The browser-side client that actually talks
> to your BabylonJS scene is still a stub, so today the tools connect and validate but
> return "no scene connected" until that lands. Read
> [Implementation status](#implementation-status) for the exact breakdown before you
> install this.
## The problem
When an LLM writes BabylonJS code -- shaders, geometry, materials, lighting, animations,
post-processing -- it has no way to check the result. It cannot:
- See what the scene looks like, from any angle
- Tell whether two objects are actually where it intended them to be
- Read back shader compilation results or WebGL state
- Confirm that a material or light produces the effect it was asked for
- Debug a rendering artifact it cannot observe
The LLM writes code, the human looks at the screen, and the human becomes the LLM's eyes.
## The solution
An MCP server that gives the LLM three kinds of access to a live scene:
1. **Scene data** -- structured JSON from BabylonJS: scene graph, meshes, materials, cameras,
lights, animations, textures, physics, post-processing, performance counters.
2. **WebGL introspection** -- Spector.js frame captures: draw calls, shader source, WebGL
state, texture bindings.
3. **VLM scene understanding** -- Gemini vision analysis of screenshots, depth maps and
multi-view renders, combined with scene metadata (bring your own API key).
Plus a `scene_evaluate` escape hatch that runs arbitrary JavaScript against the scene, in the
spirit of Playwright's `browser_evaluate`.
## Architecture
The server is a Node.js process started by your MCP host. It speaks MCP over stdio to the host
and WebSocket to your browser. It does not embed a browser; your app connects to it.
```
+---------------------------+
| MCP client | Claude Code, Claude Desktop, or any MCP host
+---------------------------+
^
| stdio (JSON-RPC) -- MCP tool calls and results
v
+---------------------------+
| babylonjs-inspector-mcp | Node.js process spawned by the MCP host
| | - registers the MCP tools
| | - BrowserBridge: correlates requests by UUID
| | - HTTP + WebSocket server on port 9070
+---------------------------+
^
| WebSocket (ws://localhost:9070) -- commands and responses
v
+---------------------------+
| Browser | your Vite dev server, or any page
| inspector client | - discovers the BabylonJS engine
| + your BabylonJS scene | - runs commands against the live scene
+---------------------------+
```
A tool call travels the whole chain: the host calls `babylonjs_scene_inspect`, the server
dispatches it to the `BrowserBridge`, the bridge sends a WebSocket command tagged with a UUID,
the browser client executes it against the scene, and the response comes back up the chain.
Only one browser connection is tracked at a time. Every operation has a timeout tier:
| Operation | Timeout |
|-----------|---------|
| Scene query / inspect | 5s |
| Screenshot capture | 15s |
| Spector.js frame capture | 30s |
| Scene evaluate | 30s |
| Multi-view capture | 45s |
| Scene export | 60s |
## Requirements
- Node.js >= 20
- A BabylonJS application you can run locally
- Optionally, a Gemini API key for the VLM analysis tool
## Installation
```bash
npx -y babylonjs-inspector-mcp # run on demand
npm install -g babylonjs-inspector-mcp # binary on your PATH
npm install --save-dev babylonjs-inspector-mcp # Vite plugin / browser client
```
To work from a checkout instead:
```bash
git clone https://github.com/apowers313/babylonjs-inspector-mcp.git
cd babylonjs-inspector-mcp
npm install
npm run build
```
That produces `dist/`, which `bin/babylonjs-inspector-mcp.js` needs in order to run.
## MCP client configuration
Add the server to your MCP settings, pointing at your local build. For Claude Code:
```bash
claude mcp add babylonjs-inspector -- node /path/to/babylonjs-inspector-mcp/bin/babylonjs-inspector-mcp.js
```
Or configure it by hand -- this is the JSON block for
`claude_desktop_config.json`, `.mcp.json`, or your host's equivalent:
```json
{
"mcpServers": {
"babylonjs-inspector": {
"command": "node",
"args": ["/path/to/babylonjs-inspector-mcp/bin/babylonjs-inspector-mcp.js"],
"env": {
"BABYLONJS_INSPECTOR_PORT": "9070",
"GEMINI_API_KEY": "your-key-here"
}
}
}
}
```
After the package is published, `"command": "npx"` with
`"args": ["-y", "babylonjs-inspector-mcp"]` replaces those two lines, and a global
install lets you use `"command": "babylonjs-inspector-mcp"` with no `args`. Use `npx -y` so
that npx's install prompt can never end up on the stdio channel the MCP protocol is using.
`GEMINI_API_KEY` is optional. When it is absent, the `babylonjs_scene_analyze` tool is not
registered at all, so it never shows up in the LLM's tool list. When it is present, the tool
appears. Everything else works without it.
## Browser-side setup
The server needs your BabylonJS page to connect back to it over WebSocket. There are two
supported ways to make that happen.
### Vite plugin (zero instrumentation)
Add the plugin to your app's `vite.config.ts`. It only applies in dev mode (`apply: "serve"`),
so production builds are untouched, and it needs no changes to your application code.
```typescript
import { defineConfig } from "vite";
import { babylonjsInspector } from "babylonjs-inspector-mcp/vite";
export default defineConfig({
plugins: [
babylonjsInspector({
port: 9070, // must match BABYLONJS_INSPECTOR_PORT; default 9070
autoConnect: true, // connect on page load; default true
}),
],
});
```
The plugin appends a `<script type="module">` tag to your HTML that imports the inspector
client from `http://localhost:<port>/inspector-client.js`, which the MCP server serves, and
assigns the instance to `window.__babylonjsInspector`.
### Manual import
If you are not on Vite, or you want explicit control over when the inspector attaches:
```typescript
import { InspectorClient } from "babylonjs-inspector-mcp/client";
const inspector = new InspectorClient({
port: 9070, // default 9070
autoDiscover: true, // find the BabylonJS engine automatically; default true
scene: myScene, // optional: pass the scene explicitly instead
});
```
Both paths are wired up but not yet functional -- see below.
## MCP tools
Six tools are registered. Five are always present; `babylonjs_scene_analyze` is registered
only when `GEMINI_API_KEY` is set.
| Tool | Returns | Needs a live browser connection |
|------|---------|--------------------------------|
| `babylonjs_connection_status` | Connection state, engine type, canvas size, and which tool categories are currently usable | No |
| `babylonjs_scene_inspect` | Scene data for one of 15 categories, from a counts-only summary to a full scene graph | Yes |
| `babylonjs_scene_evaluate` | The JSON-serialized return value of a JavaScript function run against the scene | Yes |
| `babylonjs_screenshot` | A map of view name to saved image file path | Yes, and the engine must be able to render |
| `babylonjs_spector_capture` | Spector.js WebGL frame data: draw calls, shader source, GL state | Yes, plus WebGL and Spector.js in the page |
| `babylonjs_scene_analyze` | A Gemini vision answer to a question about the rendered scene | Yes, rendering capable, plus `GEMINI_API_KEY` |
Every browser-dependent tool degrades to a plain JSON `{ "error": "..." }` message rather than
throwing, so the LLM gets a readable reason instead of a protocol failure.
### babylonjs_connection_status
No parameters. Returns `connected`, `engineType`, `canRender`, `hasSpector`, `canvasSize`, and
an `availableTools` object reporting which of `sceneInspect`, `sceneEvaluate`, `screenshot`,
`spectorCapture` and `sceneAnalyze` are usable right now. Every entry is `false` while no
browser is attached, because every one of those tools answers "No BabylonJS scene connected"
in that state. Call this first when a tool reports that nothing is connected.
### babylonjs_scene_inspect
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `category` | enum | `summary` | What to inspect |
| `target` | string | -- | Name or ID of a specific mesh/material/light/texture, for the `*_detail` categories |
| `filter` | string | -- | Glob pattern matched against names, e.g. `wall*` or `*metal*` |
`category` accepts: `summary`, `full`, `meshes`, `mesh_detail`, `materials`,
`material_detail`, `cameras`, `lights`, `animations`, `textures`, `physics`,
`post_processing`, `performance`, `scene_tree`, `webgl_errors`.
`summary` and `full` return an overview of the whole scene; every other category drills into
one aspect. This is deliberately one parameterized tool rather than fifteen separate ones, to
keep the host's tool list short and the LLM's choice simple.
### babylonjs_scene_evaluate
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `function` | string | required | A JavaScript function body receiving `(scene, engine, BABYLON)` |
Example argument:
```javascript
(scene) => scene.meshes.map((m) => ({ name: m.name, pos: m.position.asArray() }))
```
The return value is serialized to JSON. This is a development tool with no sandbox -- the code
runs with full access to the page.
### babylonjs_screenshot
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `views` | array of enum | `["current"]` | Which views to capture |
| `customCamera` | object | -- | `{ position: [x,y,z], target: [x,y,z], fov?, projection? }` |
| `width` | number | `1024` | Image width in pixels |
| `height` | number | `1024` | Image height in pixels |
| `format` | `png` or `jpeg` | `png` | Image format |
`views` accepts: `current`, `front`, `top`, `side`, `isometric`, `depth`, `wireframe`,
`annotated`. `customCamera.projection` is `perspective` or `orthographic`.
Images are written to disk and the tool returns file paths, not base64, so a multi-view capture
does not flood the context window.
### babylonjs_spector_capture
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `detailLevel` | `summary`, `shaders`, `full` | `summary` | How much of the capture to return |
| `quickCapture` | boolean | `true` | Skip per-draw-call thumbnails for a faster capture |
`summary` returns counts and timings, `shaders` adds deduplicated shader source, `full` adds
per-draw-call WebGL state.
### babylonjs_scene_analyze
Registered only when `GEMINI_API_KEY` is set.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `question` | string | required | A specific question about the rendered scene |
| `views` | array of enum | `["current", "depth", "top"]` | Which renders to send to the model |
| `includeSceneMetadata` | boolean | `true` | Send structured scene data alongside the images |
| `resolution` | `low`, `medium`, `high` | `medium` | 512, 1024 or 2048 pixels |
`views` accepts: `current`, `front`, `top`, `side`, `isometric`, `depth`, `annotated`.
Good questions are concrete: "Are the shadows rendering correctly?", "Is the sphere positioned
above the plane?", "Does the PBR material look metallic?"
## Configuration
All configuration is by environment variable, read once at startup.
| Variable | Default | Description |
|----------|---------|-------------|
| `BABYLONJS_INSPECTOR_PORT` | `9070` | Port for the HTTP + WebSocket server. Must be 1-65535 or startup fails with an explicit error. |
| `BABYLONJS_INSPECTOR_HOST` | `127.0.0.1` | Interface the server binds to. Loopback by default so the bridge is not reachable from other machines. Must be non-empty. |
| `BABYLONJS_INSPECTOR_ALLOWED_ORIGINS` | (none) | Comma-separated list of extra browser origins allowed to open a WebSocket. Loopback origins are always allowed. `*` disables the check. |
| `GEMINI_API_KEY` | (none) | Gemini API key. When unset, `babylonjs_scene_analyze` is not registered. |
| `BABYLONJS_INSPECTOR_SCREENSHOT_DIR` | `./tmp/screenshots` | Directory for screenshot output. Read with a default applied, but not validated and not yet consumed by any code path. |
| `BABYLONJS_INSPECTOR_LOG_LEVEL` | `info` | One of `debug`, `info`, `warn`, `error`. Validated at startup, but no logger reads it yet. |
An invalid `BABYLONJS_INSPECTOR_PORT`, `BABYLONJS_INSPECTOR_HOST` or
`BABYLONJS_INSPECTOR_LOG_LEVEL` throws at startup rather than silently falling back to the
default.
## Security
The inspector exists to run code you asked for inside your own page, which means anything
that can reach the bridge can run `babylonjs_scene_evaluate` against your scene and feed the
answer to your LLM. Two defaults keep that surface small:
- **Loopback only.** The HTTP + WebSocket server binds `127.0.0.1`. Nothing else on your
network can reach it unless you set `BABYLONJS_INSPECTOR_HOST` yourself.
- **Origin-checked WebSocket upgrades.** WebSockets are exempt from CORS, so the server
checks the `Origin` header on the upgrade itself. Origins on `localhost`, `127.0.0.1` or
`[::1]` (any port, any scheme) are accepted; every other origin is rejected with a 403
unless you list it in `BABYLONJS_INSPECTOR_ALLOWED_ORIGINS`. Requests that send no `Origin`
at all -- curl, a native client -- are accepted, because they can only arrive over the
bound interface.
The HTTP routes echo `Access-Control-Allow-Origin` only for an origin that passes the same
check; there is no wildcard CORS. If a second browser connects, the previously attached
socket is closed rather than silently displaced.
## HTTP endpoints
Alongside the WebSocket upgrade handler, the server exposes:
| Endpoint | Purpose |
|----------|---------|
| `GET /health` | Returns `{"status":"ok","connected":<bool>}`. Useful for checking the server is up and whether a browser has attached. |
| `GET /inspector-client.js` | Serves the browser client bundle, with CORS headers echoed back to any origin that passes the WebSocket origin check, so a local dev server can load it. Currently a stub. |
## Implementation status
The package is built in phases. This is what actually runs today, verified against the source
rather than the design document.
**Working:**
- MCP server over stdio, with all six tools registered and their input schemas validated by zod
- Conditional registration of `babylonjs_scene_analyze` based on `GEMINI_API_KEY`
- HTTP + WebSocket server, `/health` endpoint, WebSocket upgrade handling, clean shutdown on
SIGINT and SIGTERM
- The WebSocket wire protocol: command/response/event/ping/pong message types, strict parsing
that rejects malformed messages, UUID request correlation, per-operation timeout tiers, and
rejection of all pending requests when the socket closes
- Configuration loading and validation from the environment
- Graceful, readable error responses from every tool when the precondition it needs is missing
**Not yet working:**
- **The browser client is a stub.** `InspectorClient` stores its options and does nothing else;
it does not open a WebSocket, discover a BabylonJS engine, or register command handlers.
- **`GET /inspector-client.js` serves a placeholder comment**, not a bundle. The Vite plugin
injects a script that imports `InspectorClient` from that URL, so the injection wiring is in
place but the import has nothing to resolve.
- Consequently every browser-dependent tool -- `scene_inspect`, `scene_evaluate`, `screenshot`,
`spector_capture`, `scene_analyze` -- returns "No BabylonJS scene connected" in practice.
`babylonjs_connection_status` is the only tool that returns real data today.
- **Spector.js is not integrated.** There is no browser-side capture code, so `hasSpector` is
never set true.
- **Gemini analysis is not implemented.** `GeminiClient.analyzeScene` rejects, and the
`babylonjs_scene_analyze` handler returns an explicit "not yet implemented" message even
with a valid API key and a connected browser.
- **Screenshots, depth maps, wireframe and annotated views** have no capture implementation.
- **NullEngine (in-process, headless) runtime** is designed for by the `BabylonJSRuntime`
interface but has no implementation; `BrowserBridge` is the only runtime.
- `BABYLONJS_INSPECTOR_SCREENSHOT_DIR` and `BABYLONJS_INSPECTOR_LOG_LEVEL` are read but
unused. `BABYLONJS_INSPECTOR_SCREENSHOT_DIR` is not validated either -- any string, empty
included, is accepted.
- **Nothing is published to npm yet**, so every `npx` and `npm install` route 404s. Build from
a checkout.
In short: the server half of the system is real, the browser half is scaffolding. Install this
if you want to follow along or contribute; it is not yet useful for debugging a scene.
## Development
```bash
npm install # install dependencies
npm run build # compile TypeScript to dist/
npm run build:watch # compile in watch mode
npm test # run tests in watch mode
npm run test:run # run tests once
npm run coverage # run tests with a coverage report
npm run lint # eslint plus a type check
npm run lint:fix # auto-fix what eslint can
npm run typecheck # tsc --noEmit only
npm run clean # remove dist/ and coverage/
```
Coverage thresholds are 80% lines, 80% functions, 80% statements and 75% branches, and they
are enforced: `npm run coverage` exits non-zero below any of them, and CI gates the release
on that job. The suite currently sits around 96%.
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
## License
MIT -- see [LICENSE](LICENSE).
TDQS
Scored across 5 tools
Each tool has a distinct primary purpose: status, inspection, evaluation, screenshots, and WebGL capture. scene_inspect and scene_evaluate have some potential overlap since both query the scene, but the descriptions clearly separate structured inspection from custom JavaScript execution.
All tools share the babylonjs_ prefix, and most follow a scene/domain + action pattern, such as scene_inspect and scene_evaluate. However, babylonjs_screenshot and babylonjs_connection_status break the verb-noun pattern slightly, making the naming mostly but not fully consistent.
Five tools is a well-scoped set for an inspector-focused MCP server. Each tool covers a major capability area without unnecessary redundancy, and the count feels appropriate for the server's purpose.
The tool surface covers connection status, scene inspection, custom evaluation, screenshots, and low-level WebGL capture, which covers the core inspection workflow. Direct mutation tools are absent, but babylonjs_scene_evaluate provides a flexible escape hatch, so major workflows are not blocked.