Skip to content

Repository files navigation

SSWorld

npm license

中文说明

Agents can't see what they render. SSWorld closes the loop.

GuanlanRoadTemplate0924

An MCP server that lets any MCP-capable agent app (Claude Code, Codex, Hermes, Cursor …) author, compile, render and inspect 3D geographic scenes — written in SSDL, a QML-like scene description language — on the SSEngine WebGPU runtime. The picture above is a scene an agent built: 3.28 km² of real Guanlan (Shenzhen) streets rebuilt from OpenStreetMap; the pipeline that generates it is in examples/GuanlanRoadTemplate0924.

Why this is not another scene generator

  • A real engine, not a preview. Deferred rendering, atmospheric sky, shadows, post-processing, glTF assets, GPU instancing. Scenes stand on a real globe: terrain, imagery layers, 3D Tiles and GeoJSON, anchored by longitude and latitude. 60 fps with hundreds of thousands of instanced objects.
  • The agent gets the frame back. ssworld_capture_frame renders through the engine and returns luma percentiles, exposure tails, colour coverage per 3×3 region, top colours, the camera pose it actually used and the deviation from the one requested — plus a receipt binding the frame to the source digest and the answering page. An agent can tell that its sky came out orange-brown, and fix it, without a human looking.
  • Traps that cost a real session become compile errors. Every failure below was first hit on real hardware, where it showed up as a blank page with nothing in the log. Each one now names the node and the member at compile time: a sky tinted through rayleighScattering, a Lathe radius of 0, a normalMap on geometry without tangents, a heights list counted as cells instead of corners.

Examples

GuanlanRoadTemplate0924 SkylineGarden0919 DaxiongHall0924
Real streets rebuilt from OpenStreetMap by a reusable pipeline: point it at another bbox and it builds that site. An original procedural city with traffic, signals, a train, park life, fountains and a day–night cycle. A Chinese temple hall — double eaves, bracket sets, marble terrace, incense smoke — with a cinematic camera tour.

All three carry only their hand-written sources and generators — meshes, textures and the .ssdl components are regenerated by the scripts — so each is under 400 KB. Every mesh and texture is generated; the one external input is the Guanlan street network, © OpenStreetMap contributors (ODbL), fetched at build time. See examples/.

Install (one command)

npx -y ssworld-mcp install

This installs the package globally, registers the ssworld MCP server with every agent app it finds (--client=claude,codex,hermes,cursor,dsh to choose), and downloads the pinned engine (≈54 MB) into ~/.ssworld/engine/.

Requirements: Node.js ≥ 22, npm, a WebGPU browser (Chrome/Edge) to look at previews.

Or install as a native plugin

Claude Code and Codex both read plugin manifests straight out of this repository, so one command registers the MCP server and the SSDL authoring skill with no global npm install. The pinned engine still downloads on the first preview.

# Claude Code
/plugin marketplace add DataArche/SSWorld
/plugin install ssworld@ssworld
# Codex
codex plugin marketplace add DataArche/SSWorld
codex plugin add ssworld --marketplace ssworld

Hermes, Cursor and DSH have no plugin package that can carry an MCP server — each registers servers in its own config — so ssworld-mcp install is the native path there and writes exactly what that host reads: mcp_servers: in ~/.hermes/config.yaml, mcpServers in ~/.cursor/mcp.json, and an @deepseek-ai/dsh-mcp-client row in DSH's user patch layer ~/.dsh/cordis.patch.yml. Hermes and DSH also get the skill copied into their own skills/ directory, which is the whole registration.

npx -y ssworld-mcp install --client=hermes,dsh

Manual registration for any other client:

{ "mcpServers": { "ssworld": { "command": "node", "args": ["<npm root -g>/ssworld-mcp/bin/ssworld-mcp.mjs"] } } }

Tools

Fourteen tools — click to expand the full table
tool purpose
ssworld_catalog component index / one contract / components: [...] in one call (detail: compact hoists shared members); catalog_digest + if_digest for cache hits; units and rotation conventions annotated per member
ssworld_project_list projects in ~/.ssworld/projects
ssworld_project_create new runnable project (anchored at lon/lat/height), compiled
ssworld_source_read / ssworld_source_write bounded reads (max_chars, offset/limit with has_more/next_offset; mode: metadata for digests, sizes and compile staleness without text; node: "<id>" for one block; file: "*" for all files) and whole-file writes of .ssdl sources; editing the files on disk with any tool also works (ssworld_compile rebuilds from disk) but bypasses the digest lock
ssworld_source_patch exact-span edit (old_string → new_string, uniqueness checked, replace_all opt-in) with the same digest guard
ssworld_source_batch atomic multi-edit: text patches and node-level {node_id, set, unset} across files; all validated in memory before anything is written; validate: compile + rollback restores the previous sources on a failed compile
ssworld_scene_inspect compiled-scene facts without rendering: budget ratio, subtrees by size, leaf children by type, nodes per source file, primitive extent, the requested camera (lookAt-derived heading/pitch); render stats honestly unavailable
ssworld_compile SSDL 0.3 compiler with real diagnostics, node_count, budget usage and logic (declared properties, states, host calls); catalog/runtime mismatches are compile errors (Label without a font, material animations on a Group/Model, undeclared host calls)
ssworld_preview starts the local preview server, returns http://127.0.0.1:8880/projects/<name>/index.html, whether the page is open, page.clients (every browser syncing the page: id, visibility, canvas, user agent) and a structured next (open_webgpu_viewer / capture_frame / bring_page_to_front)
ssworld_capture_frame screenshot of the open preview through the engine (saveImage2Base64); stats with luma percentiles, exposure tails, colour-class coverage overall and per 3×3 region, top colours; runtime errors deduplicated and mapped to scene.ssdl:line:column; camera pose with `source: scene
ssworld_environment_read ask the engine what it actually received for the environment: the adopted sun's direction read back from the native sun and converted to azimuth/elevation at the anchor (with the deviation from the scene's request), which DirectionalLight drives the atmosphere, and every environment component's live native values. The probe lives in the project's index.html, which is project-owned, so a page created before 0.9.8 answers page_probe_unavailable and the error names the handler to paste in
ssworld_geo_read ask the engine what it holds for the geographic layers: whether the terrain provider loaded, anchor_above_terrain_m (how far the scene's local z: 0 sits above the ground under the anchor, because terrain moves the ground and not the scene), the imagery stack in real draw order with each layer's native_index, and every Tileset / GeoJsonLayer's readiness, extent and feature count. Same project-owned probe as the environment read, so an older index.html answers page_probe_unavailable
ssworld_geometry_read measure the scene from the engine instead of a screenshot: every geometry node's and Model's dimensions (object axes, metres), world position/rotation/scale composed through the live scene graph, axis-aligned world_bounds (tight or an envelope), ground contact (on / buried / above z = 0, and a Box is centred on its position so z: 0 buries half of it), mesh facts the engine measured (vertex/triangle counts, closed, manifold, area_m2 / volume_m3); Groups and GeoAnchors as the union of their descendants, Instances as the native union, the scene as its whole extent; overlaps: true lists intersecting boxes with the overlap volume, ids narrows, detail: brief trims. Same project-owned probe, so an older index.html answers page_probe_unavailable
ssworld_logic_read / ssworld_logic_write read the open page's scene logic (and whether the scene module actually mounted: runtime.state plus errors mapped to scene.ssdl:line) without a screenshot, or set declared properties in one transaction ({set: {p: 0.4, pace: 0.0025}}) to put a game into a situation before capturing; a refused value rolls the whole set back and names the failing binding
ssworld_engine_status engine pair installed? (install: true to download)

How big a scene, measured

Budgets, what actually runs out, and in which order

Budgets are per-project values in showcase.manifest.json; the defaults and the ceilings come from src/ssdl/tools/probe_engine_limits.py (which asks every facade for its own capabilities()) and src/ssdl/tools/probe_scale_limits.py (which raises the budgets and builds a scene past them on real hardware). Defaults: 4096 native objects / 4096 material shells / 32 distinct images / 4096 bindings / 2048 handlers / 256 timers / 256 timelines / 1024 Group locators / 1024 Prefabs / 524288 instance rows. Four of those are the engine's own and a manifest cannot raise them (resolveBudgets clamps distinct images, timers, timelines and Group locators), because a scene past them compiles and then fails to mount.

What actually runs out, in order:

  • Not rendering. 6401 native objects with 409 600 instance rows across 200 prefabs compiled in 1.2 s, mounted in 4.0 s and ran at 61 fps with no runtime errors.
  • ssworld_capture_frame first. The offscreen readback dies of a wasm "memory access out of bounds" somewhere between 4001 native objects (captures, render_verified, ~21k distinct colours) and 5001 (does not). Past roughly 4000 nodes a scene runs and cannot be screenshotted, which is why native_objects sits just under that and why instancing — 409 600 rows for 200 native objects — is the way to build something large. The capture timeout is now the caller's to set: the page used to give the readback a fixed 8 s whatever timeout_ms said, so a scene big enough to be worth a screenshot always timed out.
  • Bindings used to be the hard wall, and are not any more. AnimationFacade.captureBatch refuses a batch of more than 64 items and refuses it whole, so a scene whose first frame carried 65 bindings failed to mount with a receipt that said "evaluated 65, committed 0, failed 65" and named nothing — while the shipped budget said 256. The runtime now flushes bindings in chunks of 64 and restores the chunks that already landed if a later one fails, so the flush stays all-or-nothing to the author. Measured after the fix: 1500 bindings + 600 handlers + 914 locators mount in 2.2 s at 44 fps.

Three engine ceilings have no budget dimension and are compile errors of their own: scene_depth_exceeded (SceneGraphFacade.max_depth, 16 levels of nesting), model_budget (ModelFacade.max_model_instances, 64 Model mounts — use one as a Prefab source instead of a copy per placement) and texture_budget (MaterialFacade.max_texture_bytes_total, 64 MiB of distinct images however small each file is).

Authoring reference

Colour space, what the compiler refuses, procedural geometry, geographic layers, time and sky, model assets, scene logic, runtime evidence

Colours are authored as sRGB

#rrggbb is read as sRGB — the value a colour picker gives you. The runtime converts it to linear before handing it to the engine, so the frame shows the colour that was picked and reading the value back returns the same hex. Before 0.9.8 that conversion was missing and every flat colour rendered roughly three times too bright (#16260f came out as a light green). Native materials keep 8 bits of linear light per channel, so very dark colours quantise: #16260f reads back as #16260d, and anything below about #0a0a0a collapses towards black. Verified on real hardware with src/ssdl/tools/probe_color_space.py (headless Chromium + WebGPU, frames captured through the engine).

What the compiler refuses

Several failures that used to surface only as a dead page are now compile errors naming the node and the member:

  • sky_scattering_refused — SkyAtmosphere's rayleighScattering, mieScattering, mieAbsorption, otherAbsorption and skyLuminanceFactor are normalised direction vectors, not colours (their magnitude lives in the neighbouring *Scale). Writing a "neutral-looking" vector raises red and turns the whole sky orange-brown. Tint through the sun's lightColor instead; the error text carries each member's real engine default.
  • cloud_kilometres_expected — VolumetricCloud's layerBottomAltitude, layerHeight, tracingStartMaxDistance, tracingMaxDistance and shadowTracingDistance are in kilometres, the unit UE uses for this component, while the rest of SSDL is in metres. Metre values do not merely misplace the deck: the ray-march sample count is fixed (min(96 x viewSampleCountScale, 768)) and does not grow with the tracing distance, so a ray stretched a thousandfold aliases the cloud noise into vertical white streaks across the sky. A deck is layerBottomAltitude: 1.8; layerHeight: 0.5.
  • mesh_degenerate — a radius of 0 in a Lathe profile, exactly repeated adjacent points, a Tube or Sweep path that doubles back or repeats a point, two identical adjacent Loft rings, a Mesh face with zero area, a ring with no area. These produced GeometryFacade.createMesh: triangle is degenerate and took the entire scene module down without naming a node. (A vertical first Tube segment is fine: the parallel-transport frame switches reference axis at |tangent.z| >= 0.9.)
  • mesh_invalid — a parameter outside its range or a shape the generator cannot build: a Sweep profile or an ExtrudedPolygon outline that crosses itself, a taper/bevel that eats an edge (the message names the edge), a Mesh face index out of range, a Roof footprint that crosses itself or whose skeleton cannot be closed (the message names the edge to simplify), ridge on a polygon Roof or gables/lowEdge on a quadrilateral one, Capsule with height <= 2 * radius or a segment count not divisible by four, samples without smooth: "catmullrom", cap on a closed Loft.
  • placement_invalid — Instances members that belong to another placement mode, a ring without a positive radius, along_path with both or neither of step/count, more than 512 instances in one batch, or faceCenter / alignToPath next to an explicit rotation member.
  • spawnable_invalid — a pragma spawnable component that cannot be one: a root that is not a single Group, a property outside real / bool / string / length / degrees / duration or one without a default (the default is what the fragment's budget and mesh limits are checked against), a scene singleton (Camera, Environment, Globe, a light that owns the sun, a geographic layer), a KeyHandler or PointerHandler (both claim the whole window), or a Model (it loads asynchronously, and api.scene.spawn returns a live handle in the same turn). The entry scene.ssdl itself cannot be spawnable.
  • spawn_budget / spawn_parameter_invalid / spawn_parameter_immutable / spawn_handle_unknown / spawn_placement_invalid — runtime refusals from api.scene. spawn_budget names the dimension and shows static + already-spawned + this copy against the manifest limit, and nothing is built. spawn_parameter_immutable means the parameter feeds a create_only member, so changing it is dispose + spawn, not set.
  • spawn_replay_failed — a hot reload replayed the spawn log into the new generation and one entry no longer fits the component as it was edited. It does not stop the mount; the entries are in ssworld_logic_read under dynamic.spawn_failures, and the objects they named are simply gone.
  • runtime_unsupported — Label (no font), sunAzimuth/sunElevation without atmosphereSunLight: true, and the members a light may not write in the mode it is in.
  • multiple_writer / environment_duplicate — an Environment owns the sun direction, so a DirectionalLight that also pins sunAzimuth/sunElevation, or a SunSky beside it, is refused rather than left to a last-writer-of-the-frame race; and the scene-global Environment slot takes exactly one claimant.
  • sky_light_capture_owned — SkyLight.realTimeCapture: false beside an Environment. The Environment owns the sky light capture and keeps it running, so the false would be a no-op; it is refused with the reason instead.
  • material_requires_tangent — a normalMap on geometry without tangents (only HeightField/Lathe/Tube/Loft have them).
  • value_out_of_range — emissiveColor outside its [r, g, b] 0..16 range.

List values may span lines since 0.9.8: a newline after sections: [, one ring per line and comments inside the brackets all compile. A property still ends at a newline or ;, so this only applies inside [ ].

Procedural geometry

Four parametric generators compile to constant parameters and are tessellated by the runtime (MeshData/v1, ccw outward, ≤ 65535 vertices per node, mesh_budget beyond): HeightField { width; depth; columns; rows; heights } (columns/rows count cells, heights counts the (columns+1)*(rows+1) corners around them — row-major from -depth/2), Lathe { profile: [[radius, 0, height] …]; segments; closed }, Tube { path; radius; segments; closed }, Loft { sections: [[ring] …]; cap }. ssworld_compile reports their cost as usage.mesh. There are no per-vertex functions: arbitrary meshes are managed assets (Model).

Geographic layers

Every scene already stands on the globe at its manifest anchor; four components let it reach the globe itself, all of them direct children of Scene with no transform of their own (geographic longitude/latitude and the scene's local ENU metres are two different worlds, and SSDL does not pretend otherwise):

Globe { id: earth; terrain: "default"; lighting: false }
ImageryLayer { id: base; source: "https://<your tile service>/{z}/{x}/{y}.png"; webMercator: true }
Tileset { id: city; source: "https://<host>/tileset.json"; offset: [0, 0, -12] }
GeoJsonLayer { id: parks; source: "assets/parks.geojson"; geometry: "polygon"; fillColor: "#2e7d32" }
  • Globe — terrain, ground colour, opacity and lighting; at most one, and the last scene to write it hands the globe back as it found it on unload.
  • ImageryLayer — kind: "xyz" | "wms" | "arcgis" | "single". Draw order is declaration order, the first layer being the base map; visible and alpha can be bound and animated by scene logic. At most 8.
  • Tileset — 3D Tiles, or a Gaussian splat set with splat: true. offset/rotation/scale transform the tileset's own root (offset.z is the height correction most datasets need). This engine has no maximumScreenSpaceError; geometricErrorScale (0.2–12) is the LOD dial. At most 4.
  • GeoJsonLayer — one layer draws one kind of feature (geometry: "polygon" | "line" | "point"). The document is a managed assets/*.geojson whose geometry is checked against geometry at compile time, or a remote url the page fetches — a cross-origin server without Access-Control-Allow-Origin reports geojson_fetch_failed instead of leaving a silently empty layer. Polygons are filled: with extrudeHeightField they become solid blocks, without it a flat plane. At most 8.

Terrain moves the ground, not the scene: local z: 0 stays at the anchor's ellipsoid height, so a scene built flat can end up buried in a hillside the moment terrain loads. ssworld_geo_read answers with anchor_above_terrain_m, the imagery stack in real draw order and each layer's readiness. No basemap ships with this server — a tile service carries terms and often a key, and neither is ours to accept for you; "template": "geo" lays out the scene with the ImageryLayer line commented out, waiting for a URL.

Time, sky and weather

Environment is the scene clock and the astronomy solver. Give it an instant and a site and the sun, the moon and the star field go where they really were — Simon 1994 ephemerides, an IAU 2006 ICRF-to-fixed rotation, station azimuth and elevation, the same solve a planetarium does:

Environment {
  id: clock
  dateTime: "2026-09-12T18:30:00+08:00"   // ISO-8601 WITH an offset
  timeScale: 60                            // simulated seconds per real second; 0 freezes the sky
  latitude: 22.6433; longitude: 113.938    // omit both to follow the camera's ground point
  cloudCoverage: 1.8; windDirection: 240; windSpeed: 9
}
  • It owns the sun. While an Environment is declared, DirectionalLight.sunAzimuth/sunElevation and SunSky are refused as multiple_writer. To pin the sun without giving up the clock, use sunAzimuthOverride / sunElevationOverride on the Environment itself. One per scene, no parent.
  • dateTime needs an explicit offset. A naive instant would silently mean the viewer's timezone, so it is refused.
  • Stars fade on their own — fully out above 0° of sun elevation, fully in below −12° — so starsIntensity is a ceiling, not a switch. starsRealRotation puts the constellations where they belong.
  • sunIntensity is an absolute level (default 4.65, the Ultra Dynamic Sky Sun.SunLightIntensity). Nothing downstream renormalises it: at 1.0 the sky falls into the tonemapper's toe and reads as near black while the clouds still look lit. Leave it alone unless you are matching an Unreal project value by value.
  • Weather is opt-in. cloudCoverage follows the UDS scale (0..3, factory 1.14) and thickens the cloud deck and the height fog with it (fogDensityClear → fogDensityCloudy, the UDS curve); windDirection (0 = north, clockwise) and windSpeed drift the deck. Omit them and the clouds and fog stay exactly as authored on VolumetricCloud / ExponentialHeightFog — which is what you want when those values came out of Unreal.
  • It drives the sky light too. The ambient half of the sky is a cubemap the engine captures, convolves into an SH set for the diffuse ambient, and reuses as the sky reflected in water and metal. A declared Environment forces that capture on and keeps it running, so the ambient and those reflections follow the clock instead of holding one instant; SkyLight.realTimeCapture: false is refused beside it (sky_light_capture_owned) rather than silently ignored. The capture is sliced over five frames (sky faces, clouds, two passes of GGX pre-convolution for the reflection mips, then the diffuse SH). The frame as a whole settles far more slowly than one round: after a day -> night jump the mean luma was measured falling 26 -> 2 over ~30 s, which is why ssworld_capture_frame takes stable: {} (sample small frames until the brightness stops moving) for a capture right after the clock moves. The capture defaults to on in the engine, so a plain SkyLight already tracks a moving sun; realTimeCapture: false is how an author freezes the ambient on purpose, and that is the write the Environment takes away.
  • fogGetsColorFromAtmosphere (default true) makes the height fog take its inscattering colour from the atmosphere, so it reddens at sunset and goes dark after it instead of holding the authored daytime blue.
  • The cloud look is the rest of VolumetricCloud, and every member there is named after its UE / Ultra Dynamic Sky input — minimumErosion, hightFrequencyNoiseAmount (the UE spelling), extinctionScaleTop, noisePosition, and phaseG/phaseG2/phaseBlend/multiScattering* from the UE VolumetricAdvancedMaterialOutput node — so a cloudscape tuned in Unreal transfers value by value. The defaults are the UDS factory look.
  • SunSky is the UE 4.27 spelling of one fixed instant (month/day/solarTime/timeZone; UE's SunSky carries no year). It lowers onto the same solver and pins the clock, so reach for Environment whenever time should run.

Model assets

Copy glb files (and png/jpg textures) into <project>/assets/ and reference them by project-relative path: Model { id: tree; source: "assets/tree.glb"; position: [10, 0, 0] }. ssworld_compile discovers everything under assets/ (usage.assets), embeds each file's digest into SceneIR and the preview page fetches and verifies the bytes by that path. Limits: 32 MiB per glb, 8 MiB per texture, 256 assets per project (asset_budget); a path outside assets/ is asset_unresolved. A Model carries its own materials; only its position/rotation/scale/visible animate.

Scene logic and host interfaces

Scene state is declared on the Scene root (property real score: 0, types real/bool/string/length/degrees/duration/radians), changed by handler assignments (score = score + 1, one transaction per handler) and read by bindings (when: score >= 8 && misses < 3; operators + - * / === !== < <= > >= && || ! ?:, functions min/max/clamp/lerp). Host JavaScript is reached only through declared calls: host_interfaces.json holds the contract ({"Game":{"methods":{"hit":{"args":[{"name":"targetId","type":"string"}]}}}}), logic.mjs implements it (export function createHostInterfaces(api) { return { Game: { hit({ targetId }) { api.logical.write("score", …) } } }; }), and SSDL calls it with TapHandler { onTapped: { Game.hit(targetId: "balloonA"); } }. Mismatches are compile errors (host_interface_unknown, host_method_unknown, host_arg_missing, host_arg_unknown); a missing implementation refuses to mount (host_interface_missing); callbacks are synchronous, return nothing and change the scene only through api.logical.write. window.SSWorld.logical.read()/write() expose the same state on the page; ssworld_capture_frame returns it as logic. logic.mjs is re-imported on every hot reload and declared properties restart at their initial values (ssworld_compile says so as hot_reload.logic_reset while a page is open; ssworld_logic_write restores a situation).

Runtime evidence: rolled-back bindings and several open pages

Bindings and handler assignments commit as one transaction per event/frame. If any bound value is refused by its target (a negative width, a wrong type, a native refusal) the whole batch rolls back, the binding turns invalid and the affected values simply stop changing. The page reports this as a runtime error of kind binding_error (<node>.<property>: <code>: <message>, counted with repeats instead of flooding) and ssworld_capture_frame / ssworld_logic_read map it to the member line inside that node's block; logic.bindings.invalid lists the bindings concerned. Prefer clamp/lerp/min/max over a progress property to branchy ?: chains, and keep every branch inside the target's valid range. A Behavior eases every change of its target over duration, so on a per-frame property the presented value lags by about speed × duration; use it for discrete jumps.

A desktop preview pane and an automation browser can both have the page open. The preview server keeps one record per page (client id from the page's heartbeat): ssworld_preview lists page.clients, a capture is answered by the most recently synced visible page unless client names another, and every receipt carries receipt.client (id, visibility, canvas, user_agent) plus clients_connected, so a frame can be attributed to the page it came from.

The project template places the WebGPU canvas and the info panel side by side (nothing floats over the canvas, so taps reach the scene's TapHandlers). Pages are project-owned; older projects keep their layout.

Clients that support skills also receive skills/ssworld, so they pick the server on their own for 3D-scene requests: ssworld-mcp install copies it to $HERMES_HOME/skills/ssworld for Hermes and ~/.codex/skills/ssworld for Codex (Codex discovers skill directories on its own; [[skills.config]] only records the ones that were explicitly disabled).

CLI

ssworld-mcp                 # MCP stdio server (what agent apps launch)
ssworld-mcp install [--client=claude,codex] [--no-engine]
ssworld-mcp engine          # download / verify the pinned engine
ssworld-mcp preview [--port=8880]
ssworld-mcp city [--reset] [--pause=S] [--no-play]   # the real-site city demo: import the bundled site, serve it, play the chain
ssworld-mcp doctor

Environment: SSWORLD_HOME (default ~/.ssworld), SSWORLD_PREVIEW_PORT (8880), SSWORLD_ENGINE_DIR (use a local engine pair instead of the release download).

Layout

ssdl/compiler is the byte-identical SSDL 0.3 compiler closure, ssdl/runtime the browser runtime, ssdl/catalog the component catalog, engine.lock.json pins the SSmap.js/SSmap.wasm pair published as a GitHub Release asset. Built by src/ssdl/tools/pack_ssworld_mcp.py in the SSEngine3 repository; do not edit here.

License

Two tracks, on purpose.

The MCP server, the SSDL compiler, the browser runtime, the component catalog, the project template and the skill are Apache-2.0 (LICENSE) — read them, fork them, ship them.

The rendering engine (engine/SSmap.js, engine/SSmap.wasm, downloaded by the installer, never checked in) is proprietary and covered by ENGINE-LICENSE.txt, which grants free use and redistribution in binary form, including commercially. NOTICE lists the split file by file plus the third-party components (qtloader.js is Qt's, under its own terms).

About

MCP server for agent-authored 3D geo scenes: write SSDL, render on a real WebGPU engine, read the frame back.

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages