playcanvas-mcp-server
v4.1.0
Published
One shared HTTP MCP server that lets any number of Claude Code sessions edit a live PlayCanvas Editor tab through a Chrome extension bridge, including reading/downloading and replacing a project's asset files via the PlayCanvas REST API. Every session con
Maintainers
Readme
playcanvas-mcp-server
One shared MCP server that lets Claude (or any MCP client) edit a live PlayCanvas Editor tab. Every Claude Code session connects to the same server over Streamable HTTP, and that one server holds the single WebSocket to the editor. There is exactly one server process per machine — no per-session processes, no election, no relaying.
It began as a from-scratch, plain-JavaScript rebuild of
@playcanvas/editor-mcp-server
(MIT) and keeps the same extension-facing RPC contract. Unofficial — not
affiliated with or endorsed by PlayCanvas. It pairs with the Chrome extension
in ../extension/ (or the original PlayCanvas extension, which
speaks the same protocol).
v4 restores the MCP protocol. v1 was a per-session stdio MCP server; v2 made it a single shared Streamable HTTP MCP server; v3 briefly dropped MCP for a plain HTTP API; v4 is the Streamable HTTP MCP server again, now meant to be registered for every session. The MCP client config is an HTTP
url(see below), and something has to start the server (the OnRush plugin's SessionStart hook starts and warms it; standalone users run it themselves).
How it works
Claude session 1 ──HTTP /mcp ──┐
Claude session 2 ──HTTP /mcp ──┼──► server.mjs ──(WebSocket :52000, path /)──◄── extension/main.js ──► window.editor API
Claude session N ──HTTP /mcp ──┘ (one process)- One server, many sessions. The server listens on a single port and serves
two kinds of client on it:
POST/GET/DELETE /mcp— every Claude Code session, over Streamable HTTP. Each session gets its own MCP session id (mcp-session-id); all of them register the same tools pointing at the same editor socket.ws://…/(path/) — the PlayCanvas Editor Chrome extension. One at a time; extra tabs are ignored.
- No per-session process. With HTTP transport the MCP client (Claude Code) connects to a URL — it does not spawn a child process per session. So the server must already be running; nothing collapses to zero the way a stdio server does when a window closes.
- Started by a SessionStart hook, stops when idle. The server has no
persistence of its own. The OnRush plugin registers the MCP for every session
and vendors a SessionStart hook (
hooks/ensure-server.sh) that starts it if it isn't already up and keeps it warm between sessions; it exits itself after--idle-timeoutminutes with no MCP sessions and no editor connected, and the next session's hook restarts it. (Because Claude Code enumerates a server's tools at session start, a cold start — the server wasn't up yet when the session began — needs a one-time session reconnect before its tools are usable; see the skill.) - Single instance. If a second server is launched on a port that's already
taken, it sees
EADDRINUSEand exits cleanly, leaving the incumbent alone.
Quick start (standalone / MCP client config)
// .mcp.json in your project root
{
"mcpServers": {
"playcanvas-editor": {
"type": "http",
"url": "http://127.0.0.1:52000/mcp"
}
}
}Then start the server (it isn't spawned for you with HTTP transport):
npx -y playcanvas-mcp-server@4 -p 52000
# or, from a checkout: node server.mjs -p 52000Load the Chrome extension, open your PlayCanvas Editor tab, and click CONNECT
in the extension popup (same port). The server declares its own MCP
instructions, so the client learns the connection rules automatically — no
per-project rules files needed.
Installed through the OnRush marketplace, all of this is automatic: onrush wires the
.mcp.jsonHTTP entry for every session and vendors a SessionStart hook that starts and warms the server — you don't run anything by hand.
Files
server.mjs— entry point. An Express app that serves the MCP endpoint (/mcp, one Streamable HTTP session per Claude window) plus a/healthprobe, and routes the/WebSocket upgrade to the extension bridge. Owns the idle-timeout and single-instance behavior. All of it on one port, one process.wss.mjs— the extension bridge (WSS). Holds the single editor socket, runs the RPC protocol ({ id, name, args }out,{ id, res }back), and exposesraw/call/callImagethat the tools use. It does not bind a port itself —server.mjshands it matching upgrades viahandleUpgrade.tools.mjs— the MCP tools Claude sees. Each handler just forwards to an extension method by name (create_entities→entities:create). The method names here and thewsc.method(...)names in../extension/main.jsare the contract between the two halves. Most tools drive the editor's live data model; a few (download_asset,replace_asset_file,set_script_text, the store tools) go through the PlayCanvas REST API from the editor tab instead, so they move raw asset file bytes and auto-target the tab's current project/branch.
Logging discipline: everything logs to stderr (console.error); stdout is
left clean.
Setup (from a checkout)
cd playcanvas/server
npm install
node server.mjs -p 52000Endpoints
GET /health→{ ok, pid, extension, sessions }. Used by the ensure hook to decide whether a server is already running before starting another.POST /mcp→ MCP requests. Aninitializewith no session id creates a new session; subsequent requests carry themcp-session-idheader.GET /mcp→ the server→client SSE stream for a session.DELETE /mcp→ tear a session down.
CLI
-p, --port <n> Port for BOTH /mcp and the extension WebSocket (default 52000).
--idle-timeout <min> Stop after <min> idle minutes — no MCP sessions and no
extension connected (default 15; 0 disables). Also via
PLAYCANVAS_IDLE_TIMEOUT.
-h, --helpBehavior notes
- Idle shutdown: the server is "occupied" while any MCP session is connected
or the editor extension is paired. When both are empty for the idle window it
exits and frees the port; the next session's hook starts a fresh one. Set
--idle-timeout 0(or run it manually) to keep it up indefinitely. - Server restart under a live session: if the server idle-stops (or is
killed) while a Claude window still thinks it has a session, the next tool call
gets
No valid sessionand the client re-initializes. Sessions are cheap to recreate; no editor state is lost (that lives in the editor, not the server). - Shared editor, shared state: all sessions relay through one extension socket, so calls from concurrent sessions interleave at the editor — last write wins on conflicting edits, same as two humans in one editor tab.
- The original wraps
viewport:captureasimage/jpeg; the extension actually produces WebP, so this rebuild reportsimage/webp.
