Skip to content

MCP server for coding agents - #1

Merged
dev-hydrogen merged 2 commits into
dev-hydrogen:mainfrom
avtara:mcp-server
Oct 18, 2026
Merged

dev-hydrogen merged 2 commits into
dev-hydrogen:mainfrom
avtara:mcp-server

Conversation

@avtara

@avtara avtara commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an MCP server (apps/mcp) so coding agents (Claude Code, Cursor, Codex…) can author Bedrock particle effects with Oxygen's own engine, analysis and viewport. It also adds what came out of using it for real: a whole-file write, other effects in the scene, opening effects in the desktop app, and a fix for an engine bug the MCP surfaced (paused redraws).

Important

Depends on dev-hydrogen/oxygen-wintersky#1. The second commit bumps vendor/wintersky to d94434e, which for now only exists in avtara/oxygen-wintersky. Please merge that PR first, then CI can check out the submodule (re-run the checks, or I'll rebase onto the merged commit).

Review commit by commit. They're split so each can be read on its own:

Commit What Size
1. Add an MCP server for coding agents The server as first written, plus the small packages/ui exports it needs 23 files, mostly new apps/mcp
2. MCP: write_effect, scene effects, open_in_editor; fix paused redraws New tools and options, desktop single instance, submodule bump, docs 16 files

Commit 1: the MCP server

A stdio server (apps/mcp/dist/server.cjs, built with esbuild). The preview page is built with Vite and driven headless through Playwright/Chromium.

Tool Does
reference Components (fields, defaults, evaluation phase), materials, Molang queries and maths, plus docs/accuracy.md and docs/molang.md as topics
list_examples, create_effect Start from the starting effect or a bundled example under a new identifier
read_effect, check_effect Summary (components, curves, events, variables, scenarios) and the Problems panel's problems
edit_effect Surgical edits by JSON path (JsonDocument): formatting, key order and comments kept; all-or-nothing; replies with a semantic diff
simulate_effect, preview_effect EffectSession + ViewportScene stepped on a fixed 60 fps clock and seed: counts, bounds, emitters, events, runtime problems, pictures or a contact sheet

Where to look:

  • apps/mcp/src/server.ts: tool schemas and handlers.
  • src/effects.ts: analysis and edits.
  • src/pack.ts: pack root, child effects, textures.
  • preview/main.ts: headless stepping.
  • src/browser.ts: Playwright and the local file server.

Outside apps/mcp:

  • packages/ui: exports ViewportScene, DEFAULT_GIZMOS and the texture/MERS providers. EffectSession.refreshStats() becomes public. ViewportScene.frame(dt, draw = true) can step without drawing. No behaviour change for the apps.
  • CI builds the server, vitest.config.ts includes apps/mcp/test, and README/docs/building.md/docs/agents.md cover setup.

Commit 2: improvements found by using it

Engine fix (submodule bump). Redrawing a paused frame re-ran per_render_expression. Render-pass counters (7 bundled examples) then drifted to wrong colours and sizes whenever the preview was paused. preview_effect hit this on every picture, because it draws paused frames until textures settle. The editor hit it whenever you paused. Details and the regression test are in oxygen-wintersky#1.

Unmodified spiral_galaxy example through preview_effect, same camera, t = 3 s
Before before
After after

MCP

Change Why Where
write_effect: replace or create a whole file; refuses invalid JSON; semantic diff or new-file summary; dry_run Rewrites (e.g. a new components block) took a long list of set edits server.ts, effects.ts rewriteDiff
others: [{effect, position}] on simulate_effect / preview_effect Composing effects (galaxy behind a black hole). Uses the session's existing scene.extras, no new engine path server.ts runRequest, preview/main.ts, protocol.ts extras
Scene extras saved in the file now reach the preview childEffects only followed events, so file-kept extras never got their JSON pack.ts childEffects(…, also)
preview_effect return_images: false (requires save_to) A 1200×870 PNG per call is a lot of context when the agent only needs the file server.ts
open_in_editor Show the user the result; the app already follows disk changes, so later agent edits appear live src/desktop.ts

open_in_editor looks for Oxygen in this order: OXYGEN_DESKTOP, then the desktop build of the repository the server was built in, then /Applications/Oxygen.app (macOS) or %LOCALAPPDATA%\Programs\Oxygen\Oxygen.exe (Windows). It clears ELECTRON_RUN_AS_NODE for the child, because agents built on Electron (VS Code, Cursor) set it for MCP servers.

Desktop: one instance

  • requestSingleInstanceLock(). A second launch hands its files to the first through second-instance, and macOS open-file takes the same path. The renderer opens them, or focuses the existing tab (Workspace.open already dedupes), then the second instance quits.
  • Files that arrive before the renderer has asked for its start files are queued and returned by openedAtStart. The window is only raised once the renderer runs, so there's no blank window flash.
  • The lock is per userData, so OXYGEN_USER_DATA test profiles stay independent.
  • Code: apps/desktop/src/main.ts, preload.ts, api.ts (onOpenRequested) and renderer/main.ts.

Docs

  • agents.md: new tools and options, plus "Opening effects in Oxygen".
  • accuracy.md: per-particle values in the render pass, because variables are per emitter.
  • building.md: the Electron cache workaround and single instance.
  • CHANGELOG.

Testing

Check Result
pnpm test (vitest) 361/361
tsc / svelte-check: wintersky, @oxygen/mcp, @oxygen/desktop, @oxygen/ui 0 errors
Commit 1 on its own: @oxygen/mcp typecheck + apps/mcp tests clean, 6/6
pnpm test:desktop all pass, including the new "starting Oxygen again with a file opens it in the running window"
Engine regression test fails without the fix (0.21 vs 0.01), passes with it
stdio smoke of every changed tool ✅ (see below)

Smoke results through the built server (JSON-RPC over stdio):

  • write_effect: invalid JSON → error and no file written; new file → summary; dry_run → diff and the file unchanged; identical text → "No change".
  • others: poc:galaxy at [0, 0, -8] plays alongside (2 emitters, bounds reach z = −11.4); an unknown identifier is reported.
  • return_images: false: 0 images in the reply; without save_to the call is refused.
  • open_in_editor: with Oxygen already running, the file opened as a new tab in that window, and only one Electron main process remained.

Proof of concept: effects made by an agent through this MCP

Everything below was built by Claude Code driving only these tools (create_effect, edit_effect, write_effect, check_effect, preview_effect, open_in_editor), working from a reference. The files are on the pr-assets branch, kept out of this diff.

Black hole and spiral galaxy

Reference Result in Oxygen desktop (mp4)
black hole reference black hole and galaxy
  • Black hole (poc:black_hole, one particles_add effect):
    • an accretion disk with Keplerian-ish orbit speed, white-hot inside fading to red outside
    • a vertical lensed halo and a photon ring
    • the shadow, made by hiding disk particles that sit behind the hole from the viewing direction
  • The MCP shaped this design. It warned that q.rotation_to_camera is constant for /particle (camera-query-constant), so the halo faces a fixed view, adjustable through v.view_yaw from a script.
  • Galaxy (poc:galaxy): the spiral galaxy example, with per-particle values moved to particle_random. The update-render-carry warning caught a wrong first attempt.

SAO death (holographic Steve shattering)

Reference Result in Oxygen desktop (mp4)
SAO death reference SAO death

poc:sao_death is one /particle effect of 493 particles, with no child events (a child spawned under /particle lands at the world origin):

  • Holographic Steve: 72 double-sided direction_z quads, i.e. 6 parts × 6 faces at real Steve proportions, with a custom wireframe texture.
  • Flash: a chest glow that ramps up from 0.12 s to 0.3 s.
  • Shards: 420 triangles burst out of the body volume, tumble, settle at y ≥ 0 and fade out by about 2.3 s.

Faces use update/render counters; that's what showed the paused-redraw bug. Contact sheet: sao-death-sheet.png.

Not covered

  • The PoC effects weren't run in Minecraft; they were checked with Oxygen's preview only.
  • open_in_editor was exercised on macOS with the repository build. The Windows path and OXYGEN_DESKTOP were not run.

apps/mcp is a stdio MCP server that lets coding agents (Claude Code, Cursor, Codex, ...) author Bedrock
particle effects with Oxygen's own engine, analysis and viewport:

- reference: components with fields, defaults and evaluation phase; materials; Molang queries and maths;
  Oxygen's measured game rules and Molang guide
- list_examples / create_effect: start from the starting effect or a bundled example
- read_effect / check_effect: summary and the problems the Problems panel shows
- edit_effect: surgical edits by JSON path that keep formatting, key order and comments, reported as a
  semantic diff
- simulate_effect / preview_effect: run the effect headless in Chromium (Playwright) on a fixed 60 fps
  clock and seed, report counts, bounds, emitters and events, and render pictures or a contact sheet with
  the editor's viewport

To drive the viewport headless, packages/ui exports ViewportScene, the gizmo defaults and the texture
providers, makes EffectSession.refreshStats public and lets ViewportScene.frame step without drawing.
CI builds the server and the tests include apps/mcp. Setup is in docs/agents.md.

Signed-off-by: avtara k <avtarakhrisna1@gmail.com>
Engine (vendor/wintersky, bumped): redrawing a paused frame no longer re-runs its render writes. Effects
that lay particles out with a render-pass counter (spiral galaxy, aurora, lightning, magic circle, ...)
drifted to wrong colours and sizes whenever the preview was paused, in the editor and in preview_effect,
which draws paused frames until textures settle.

MCP server:
- write_effect replaces a file's whole text (or creates it); refuses invalid JSON, reports the semantic
  diff or the new file's summary, supports dry_run
- simulate_effect / preview_effect take `others` (identifiers and positions) to play other effects in the
  same scene, through the session's existing scene extras; without it, the extras the file's scene keeps
  now reach the preview too (they weren't sent before)
- preview_effect `return_images: false` (needs save_to) keeps big pictures out of the reply
- open_in_editor opens the file in the desktop app: OXYGEN_DESKTOP, else the repository's desktop build,
  else an installed app; ELECTRON_RUN_AS_NODE is cleared for the child

Desktop: one instance (requestSingleInstanceLock). Starting Oxygen again with files, or macOS open-file,
hands them to the running window, which opens them or focuses their tabs; files arriving before the
renderer asks for its start files are queued. tools/desktop-test.mjs checks the hand-over.

Docs: agents.md (new tools and options), accuracy.md (per-particle values in the render pass),
building.md (Electron cache, single instance), CHANGELOG.

Signed-off-by: avtara k <avtarakhrisna1@gmail.com>
@dev-hydrogen

Copy link
Copy Markdown
Owner

Thank you!

@dev-hydrogen
dev-hydrogen merged commit 0c476ee into dev-hydrogen:main Oct 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants