pixi-inspector-mcp
v0.5.0
Published
Model-agnostic MCP connector for the PixiJS Devtool Pro inspector — exposes the live Pixi/Phaser scene graph and Spine/Sprite controls to any MCP client.
Maintainers
Readme
pixi-inspector-mcp
A model-agnostic MCP connector for the PixiJS Devtool Pro browser extension. It lets any MCP-capable client (Claude Code/Desktop, Cursor, Cline, Windsurf, …) inspect the live PixiJS/Phaser scene graph, drive Spine/Sprite controls, read diagnostics, and run a full Chrome DevTools Protocol layer against a running game — even when the DevTools panel is closed.
A browser extension cannot listen on a port, so this is a small local Node
process: the MCP server runs a WebSocket server on 127.0.0.1, and the
extension dials in as a client. Each MCP tool call becomes an RPC request the
extension forwards to the page (or to chrome.debugger for the cdp.* tools).
Install & connect
The connector runs as a local Node process. Pick one setup path, then add it to your client.
Path A — npx
macOS / Linux / Windows without nvm — nothing to install:
claude mcp add pixi-inspector -- npx -y pixi-inspector-mcpPath B — global install
Required on Windows + nvm (incl. nvm-windows), where bare npx fails with
'pixi-inspector-mcp' is not recognized as an internal or external command —
nvm's symlinked layout breaks npx's staged-bin PATH resolution (this affects any
npx package, not just this one). It's also a faster startup.
npm i -g pixi-inspector-mcp
claude mcp add pixi-inspector -- pixi-inspector-mcpThe global bin is pinned to the active node version — after
nvm use <other>, re-runnpm i -g pixi-inspector-mcp. (Or point the client at an absolutenode /abs/path/to/dist/index.js, which is version-agnostic.)
claude mcp add accepts --scope user (all projects), --scope project (shared
.mcp.json), or --scope local (default). Verify with claude mcp list.
Other clients (Cursor / Cline / Windsurf / Claude Desktop)
Add the mcpServers block to the client's config file — Cursor
~/.cursor/mcp.json (or .cursor/mcp.json), Cline its MCP settings JSON
(MCP Servers → Configure), Windsurf ~/.codeium/windsurf/mcp_config.json,
Claude Desktop claude_desktop_config.json (manual — Settings → Developer →
Edit Config; an agent cannot edit it). Use the npx command for Path A:
{
"mcpServers": {
"pixi-inspector": {
"command": "npx",
"args": ["-y", "pixi-inspector-mcp"]
}
}
}…or the bare bin for Path B (after npm i -g):
{
"mcpServers": {
"pixi-inspector": {
"command": "pixi-inspector-mcp"
}
}
}Streamable HTTP (optional)
Run the server yourself, then point the client at the HTTP endpoint. Every HTTP call requires the pairing token as a Bearer header (see Security):
pixi-inspector-mcp --http # serves POST http://127.0.0.1:8790/mcp{
"mcpServers": {
"pixi-inspector": {
"url": "http://127.0.0.1:8790/mcp",
"headers": {
"Authorization": "Bearer <token from ~/.pixi-inspector-mcp/token>"
}
}
}
}Claude Code HTTP form:
claude mcp add --transport http pixi-inspector http://127.0.0.1:8790/mcp \
--header "Authorization: Bearer <token from ~/.pixi-inspector-mcp/token>"Run directly
pixi-inspector-mcp # stdio (default)
pixi-inspector-mcp --http # Streamable HTTP on 127.0.0.1:8790
pixi-inspector-mcp --both # stdio + HTTPThe extension always connects to the WebSocket server on ws://127.0.0.1:8791.
Single instance & multiple clients
The extension dials into one WebSocket server, so only one
pixi-inspector-mcp process can own the bridge per machine (it always binds
8791). This shapes how you wire it up:
One client (Claude Code or Cursor or Claude Desktop) over stdio — just works; that client launches the server and the extension connects to it.
Several clients at once — do not add a stdio launch to each (the 2nd+ process can't bind
8791and exits, so that client shows the server failed to start / a generic-32000). Instead run one shared server and point every client at the HTTP URL:pixi-inspector-mcp --both # stdio for one client + HTTP for the restthen give the other clients the Streamable HTTP config above.
Multiple browsers → one endpoint each
That single server is a hub. Every Chrome profile attaches under its own label and is driven at its own endpoint, so two browsers never fight over the socket:
http://127.0.0.1:8790/mcp → the single attached browser
http://127.0.0.1:8790/mcp/dev17 → that browser only
http://127.0.0.1:8790/mcp/staging → that browser only- In each browser, open the PixiJS Pro panel (or the popup) and set "Name this
browser". The endpoint it was actually assigned is printed right under the field
— use that, not what you typed: labels are slugified (
Dev 17→dev-17) and a label already claimed by another browser gets-2appended rather than stolen. - Point each client at that endpoint.
The bare /mcp keeps working unchanged while one browser is attached; with two
it returns AMBIGUOUS_INSTANCE listing the labels instead of silently guessing. A
label nobody claims returns UNKNOWN_INSTANCE. A stdio client has no URL to put
a label in — pass --instance <label>.
Call status to see it all: instances[] (every attached browser + its
endpoint), instance (the one this endpoint drives), and endpointBound (whether
this endpoint resolves to exactly one browser).
Tab ids are unique only within a browser. Never carry a
tabIdfrom one endpoint to another — the other browser doesn't have that tab and rejects it. Requires extension 1.1.0+; an older one attaches as the singlelegacybrowser, and two of those still evict each other (statusthen reportsevictions.flappingwith the fix).
Multiple agents → multiple tabs
Within a browser, targeting is allowlist-based, not focus-based: a tab is reachable
only once the user ticks "Use this tab for MCP" on it, so status never drifts as you
switch windows.
- Call
browser.targets→ every tab with a live agent as{ tabId, url, title, engine, active, lastSeen, enabled }(engineis best-effort and may benulluntil the app initialises). - Pass the chosen
tabIdto anypixi.*/spine.*/sprite.*/browser.*/cdp.*tool. Omit it and the single enabled tab serves every agent; enable several and an untargeted call returnsAMBIGUOUS, so each agent passes its owntabId. A tab that is not enabled returnsNOT_ENABLED— the allowlist is a safety guard, so an agent can never touch a tab you didn't opt in (your email, say).
Run one shared server (--both) and let each agent address its own tab — no need to
keep a tab focused. This is cooperative addressing (not enforced isolation): two
agents driving cdp.* on the same tab still conflict (CDP_ATTACH_FAILED);
different tabs are independent.
For browser-level separation (parallel CI, one agent per environment), give each agent
its own Chrome profile and address it by label — …/mcp/<label> — against the same
shared server (see Multiple browsers above). Separate server processes on distinct
--ws-port / --http-port are only needed if you also want separate pairing tokens and
separate CDP debuggers.
Seeing which tab is driven. Every pixi.* / browser.* / cdp.* result carries a
↳ target: tab <id> · <title> · <url> line, so the CLI/chat shows which tab each call
hit. status reports activeTabId / activeTabUrl (the default target when no tabId
is passed). In the browser, the targeted tab flashes a small corner badge with the method
name (toggle it off via the panel/popup "In-page activity badge" checkbox).
Troubleshooting: server won't start / -32000 / "disconnected"
-32000 (or failed to start) in the client's MCP list almost always means the
server process exited at launch, not an extension fault. Most common cause: the
WS port 8791 (or HTTP 8790) is already in use by another
pixi-inspector-mcp instance — a second client's stdio copy, or a server you
started earlier and left running. The server logs the exact reason to stderr
(Cannot start: WebSocket port 8791 is already in use…). Fixes:
- Use the already-running instance (it owns the bridge), or stop it, or
- Run a single shared server (
--both) and point extra clients at the HTTP URL, or - Give this instance its own ports with
--ws-port/--http-port.
Remember: the MCP-list status only reflects whether the server started — for
the extension state, call the status tool.
Flags / env
| Flag | Env | Default |
| --------------------------------- | -------------------------- | ----------- |
| --stdio / --http / --both | PIXI_MCP_TRANSPORT | stdio |
| --ws-port <n> | PIXI_MCP_WS_PORT | 8791 |
| --http-port <n> | PIXI_MCP_HTTP_PORT | 8790 |
| --host <h> | PIXI_MCP_HOST | 127.0.0.1 |
| --timeout <ms> | PIXI_MCP_TIMEOUT_MS | 15000 |
| --allow-origin <o> (repeatable) | PIXI_MCP_ALLOWED_ORIGINS | (none) |
| (auto-pairing on/off) | PIXI_MCP_AUTO_PAIR | on |
Two caveats:
- Reload — a newly added server is usually picked up only after the client restarts/reloads, not live in the same session.
- Status meaning — the client's MCP-list status reflects MCP-server reachability (did the server start + handshake), NOT whether the browser extension is connected. For the extension state, call the
statustool.
Extension setup
The extension is not on the Chrome Web Store — load it unpacked. This is a one-time human GUI action; an agent cannot do it and must ask the user.
- Clone this repo, then build the extension:
pnpm install && pnpm run build - Open
chrome://extensions, enable Developer mode, click Load unpacked, and selectapps/chrome-extension/build. - Open the extension popup and enable the MCP connector (it is off by default — when off, the extension opens no socket and logs nothing).
- Start the server (
pixi-inspector-mcp …, afternpm i -g). The extension auto-connects. - Keep the inspected game tab active/focused so the connector targets it.
Diagnose at any point with the status tool — it needs no extension and
reports whether the connector is connected/paired (plus the remedy if not). When
the extension is connected it also returns engineDetected (true/false,
or null when unknown) — a quick read of whether a Pixi/Phaser engine is found
on the page.
Engine detection
The connector finds your app two ways:
Explicit global (preferred, deterministic). Expose one canonical global so the inspector has a context-independent handle.
app/game/stage/rendererare objects you already create — pick the one group that fits, one line is enough:// Most apps — expose the Application / Game (the inspector reads // app.stage / app.renderer from it). Pick the one for your engine: globalThis.__PIXI_APP_NEW__ = app; // PixiJS — your new PIXI.Application() globalThis.__PHASER_GAME__ = game; // Phaser — your new Phaser.Game() // No Application? Custom renderer + root container — expose both pieces: globalThis.__PIXI_STAGE__ = stage; // your root Container globalThis.__PIXI_RENDERER__ = renderer; // your PIXI.Renderer // Already using the official PixiJS devtools? Auto-recognised, no extra line: globalThis.__PIXI_DEVTOOLS__ = { app }; globalThis.__PIXI_APP__ = app;Auto-detection (fallback, always on). With no global, the extension still self-detects: it scans candidate canvases +
windowfor an app, and as a last resort patches the render engine (resolving on the next frame). This works with the DevTools panel closed and needs no project-code change — but it is best-effort, so the explicit global above is the more reliable option.
A setup agent should offer the global first and fall back to auto-detection
if you decline. Check status.engineDetected to see if detection already works.
Pairing
The owner-only ext.* tools (extension self-debug) require a paired
connection. Two ways, no shared secret is ever embedded:
- Auto-pairing (default, Trust-On-First-Use). The first time the real
extension connects, the server trusts its
chrome-extension://<id>origin and remembers it in~/.pixi-inspector-mcp/paired.json. This is safe because the gate is the WebSocket handshake Origin: a malicious web page reachingws://127.0.0.1carries a web origin and can never auto-pair — only extension origins can. No user action needed; the popup showspaired (auto). - Manual token. Set
PIXI_MCP_AUTO_PAIR=offto require a token instead. The server prints a token on first run (also at~/.pixi-inspector-mcp/token); provide it viachrome.storage.local.pixiMcpToken(or use the HTTP transport, which reads the token file). The popup then showspaired (token).
pixi.*, spine.*, sprite.*, browser.*, and cdp.* are not gated by
pairing — only ext.* is.
Security
- Binds to
127.0.0.1only. The HTTP transport additionally enforces a Host-header allow-list (DNS-rebinding protection), anOriginallow-list, and requiresAuthorization: Bearer <token>for every call. - Owner-only
ext.*(manifest/storage/logs/reload/targets) is default-deny: anyext.*call on an unpaired connection returnsUNAUTHORIZEDbefore any side effect. The token is compared in constant time and is never logged; it is redacted out ofext.storage.get. - Opt-in connector. The extension does nothing until you enable it in the popup.
Tools (61)
| Group | Count | Examples |
| --------------- | ----- | ------------------------------------------------------------------------------ |
| status | 1 | connector/pairing state — needs no extension, never blocks |
| pixi.tree.* | 5 | getTree, export, getNode, select, highlight |
| pixi.node.* | 2 | getProperties, setProperty |
| pixi.app.* | 3 | stats, textures, renderStats |
| spine.* | 10 | list, getInfo, setAnimation, setSkin, changeSkeleton |
| sprite.* | 1 | getInfo |
| browser.* | 5 | targets, reload, navigate, screenshot, eval |
| cdp.* | 27 | network.*, console.*, performance.trace, input.*, dom.*, emulate.* |
| ext.* (owner) | 7 | info, storage.get/set/remove, logs, targets, reload |
The cdp.* group drives the tab through chrome.debugger (Chrome DevTools
Protocol): bounded scene/DOM snapshots, passive network + console capture,
time-boxed performance traces (summary on the wire, full trace to a temp file),
synthetic input, DOM mutations, and reversible device/CPU/network emulation.
See PROGRESS.md for the frozen WebSocket wire contract and per-phase details.
License
MIT — © 2026 Andrii Karpus. See the bundled LICENSE file. This package is the MCP
server for PixiJS Devtool Pro, a modified version of
pixi-inspector (© 2015 Bob Fanger, MIT).
