packages/tools/nme-mcp-server/NME_MCP_SESSION_BRIDGE.md
You are working in the Babylon.js monorepo at packages/tools/. There are two targets:
packages/tools/nme-mcp-server/ — the Node Material Editor MCP server (runs locally via stdio)packages/tools/nodeEditor/ — the Node Material Editor UI (React/TypeScript, eventually hosted at nme.babylonjs.com)The goal is to implement a bidirectional live session between the two: the MCP server broadcasts changes to the editor, and the editor can push user changes back to the MCP server.
Read the following files before writing any code to understand existing patterns:
packages/tools/nme-mcp-server/src/index.tspackages/tools/nme-mcp-server/src/materialGraph.tspackages/tools/scene-mcp-server/src/previewServer.ts — the SSE/HTTP server pattern to replicatepackages/tools/nodeEditor/src/globalState.tspackages/tools/nodeEditor/src/graphEditor.tsxpackages/tools/nodeEditor/src/components/propertyTab/propertyTabComponent.tsxnme-mcp-server)Create a new file packages/tools/nme-mcp-server/src/sessionServer.ts. Model it closely on packages/tools/scene-mcp-server/src/previewServer.ts (zero new npm dependencies — use only Node built-ins: http, crypto).
The session server is a local HTTP server with the following routes. It maintains a Map<sessionId, materialName> so one server instance can serve multiple simultaneous sessions (one per material).
Routes:
GET /session/:id/events — SSE stream. The editor subscribes here. Whenever the MCP updates the material for this session, it sends a data: event with the full material JSON. Send a keepalive comment (: ping\n\n) every 15 seconds.GET /session/:id/material — Returns the current material JSON (for initial load on connect). Responds with Content-Type: application/json.POST /session/:id/material — The editor posts updated JSON here after the user makes changes. The server parses the JSON, calls manager.importJSON(materialName, json) to replace the in-memory graph, then broadcasts the update to all other SSE subscribers on the same session. Responds 200 OK or an error.GET / — A plain-text status page listing active sessions.Add CORS headers on every response (Access-Control-Allow-Origin: *). Handle OPTIONS preflight.
Singleton lifecycle (same pattern as previewServer.ts):
startSessionServer(manager: MaterialGraphManager, port?: number): Promise<number> — starts the server if not running, returns the portstopSessionServer(): Promise<void>isSessionServerRunning(): booleancreateSession(materialName: string): string — generates a random 8-char alphanumeric session ID, stores the mapping, returns the IDnotifyMaterialUpdate(sessionId: string) — pushes the latest JSON to all SSE subscribers for that session (called internally whenever an MCP tool modifies a material that has an active session)getSessionUrl(sessionId: string, port: number): string — returns http://localhost:{port}/session/{id}The MaterialGraphManager reference must be passed in and stored (as a module-level variable, same as previewServer.ts stores _manager).
index.ts)In packages/tools/nme-mcp-server/src/index.ts:
Import startSessionServer, createSession, notifyMaterialUpdate, getSessionUrl, isSessionServerRunning from ./sessionServer.js.
Modify create_material tool: after successfully creating a material, automatically:
startSessionServer(manager) if not already running (use port 3001 by default)createSession(materialName) to get a session ID\n\nMCP Session URL: http://localhost:3001/session/{sessionId}\nPaste this URL in the Node Material Editor's "Connect to MCP Session" panel to see live updates.Add a get_session_url tool that, given a materialName, finds its active session and returns the session URL. If no session exists for that material, creates one (starting the server if needed).
After every tool that modifies a material (i.e. add_block, remove_block, connect_blocks, disconnect_input, set_block_properties, add_blocks_batch, connect_blocks_batch), call notifyMaterialUpdate(sessionId) for any active session associated with that material. To do this efficiently, keep a reverse Map<materialName, sessionId> in sessionServer.ts and expose a getSessionForMaterial(materialName): string | undefined helper.
nodeEditor)In packages/tools/nodeEditor/src/globalState.ts, add:
mcpSessionUrl: string | null = null;
mcpSessionConnected: boolean = false;
onMcpSessionStateChangedObservable: Observable<boolean>; // fires on connect/disconnect
Create a new file packages/tools/nodeEditor/src/components/mcpSession/mcpSessionComponent.tsx:
This is a small React component (functional, with hooks) that renders:
http://localhost:3001/session/ABC123){sessionUrl}/material using fetchConnect behavior (triggered by clicking Connect):
globalState.mcpSessionUrlGET {sessionUrl}/material request; if successful, call NodeMaterial.Parse(json, scene) to load the material and globalState.onNewNodeCreatedObservable etc. to refresh the editor (look at how import_from_snippet works in the existing property tab for the right sequence of calls to re-initialize the editor with a new NodeMaterial instance)EventSource SSE connection to {sessionUrl}/eventsmessage event: parse the JSON, reload the material the same way as step 2error: update status to disconnectedglobalState.mcpSessionConnected = true and notify onMcpSessionStateChangedObservableDisconnect behavior: close the EventSource, set mcpSessionConnected = false.
Push to MCP behavior: serialize current material via SerializationTools.Serialize(globalState.nodeMaterial, globalState), POST JSON string to {sessionUrl}/material.
Store the EventSource instance in a useRef so it can be closed on disconnect/unmount.
In packages/tools/nodeEditor/src/graphEditor.tsx, add the <McpSessionComponent> to the left or bottom of the property panel (look at how other sidebar components are mounted). It should always be visible regardless of what is selected in the graph.
In packages/tools/nme-mcp-server/src/sessionServer.ts, add a JSDoc comment at the top of the file:
* **No lock mechanism**: The MCP server and the user can both modify the material,
* but NOT simultaneously. If the user edits the material in the editor while the AI
* agent is calling MCP tools, their changes will be overwritten by the next agent
* push. Users should finish their own edits before asking the agent to continue,
* and vice versa.
previewServer.ts if it does that, otherwise just fail with a clear error message).EventSource in the browser connects to the MCP server running locally — this only works when the user is running the MCP server on their own machine, which is always the case for MCP.EventSource on client, standard HTTP chunked response on server).import_from_snippet feature uses to swap in a new NodeMaterial — do not invent a new code path.npm run lint after implementation and fix any lint errors.MaterialGraphManager type is the default export / main class in materialGraph.ts — check the exact type name before referencing it.nme-mcp-server/package.json.materialGraph.ts for this feature — all integration is in index.ts and the new sessionServer.ts.