@mavonengine/cli 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Requires Node 24 or newer.
6
6
 
7
7
  ```sh
8
8
  npm install -D @mavonengine/cli
9
- npx mavonengine --help
9
+ npx @mavonengine/cli --help
10
10
  ```
11
11
 
12
12
  ## `mavonengine create`
@@ -30,15 +30,17 @@ Asks for a name if none is given. While the template downloads and `npm install`
30
30
 
31
31
  ## `mavonengine dev`
32
32
 
33
- Runs the `dev` script of the MavonEngine project you are in and shows the [dashboard](#mavonengine-dashboard) for it. For the multiplayer template that starts the game server and the client together.
33
+ Runs the dev script of the MavonEngine project you are in and shows the [dashboard](#mavonengine-dashboard) for it. For the multiplayer template that starts the game server and the client together.
34
+
35
+ It runs the project's `dev:server` script, or `dev` when there is none. So `dev` can be `mavonengine dev` itself, as in the multiplayer template, and `npm run dev` brings up the dashboard. A `dev` script that would start `mavonengine dev` again, without a `dev:server` next to it, is refused instead of looping.
34
36
 
35
37
  While the game server starts, a startup screen shows the latest output of the dev script; the dashboard takes over as soon as the server answers. Once the server is up, the game it serves opens in your default browser (not over SSH, and only if the server answers `/` with a page); `--no-open` turns that off, and `o` opens it again any time. Quitting the dashboard (`q` or Ctrl-C) stops the dev script and everything it started. If the dev script exits on its own, the dashboard closes and its output is printed.
36
38
 
37
39
  ```sh
38
- npx mavonengine dev
40
+ npx @mavonengine/cli dev
39
41
 
40
42
  # From somewhere else
41
- npx mavonengine dev --cwd path/to/my-game
43
+ npx @mavonengine/cli dev --cwd path/to/my-game
42
44
  ```
43
45
 
44
46
  The project root is the nearest `package.json`, from the current directory upwards, that depends on `@mavonengine/core` and has a `dev` script, so it also works from inside `client/` or `server/`. The package manager comes from the lockfile (`npm`, `pnpm`, `yarn` or `bun`, default `npm`). Without an interactive terminal, or with `--no-dashboard`, the dev script's output goes straight to the terminal instead. Ctrl-C stops everything, and the command exits with the dev script's exit code.
@@ -52,21 +54,66 @@ The project root is the nearest `package.json`, from the current directory upwar
52
54
  | `--transparent` | | Let a transparent terminal background show through |
53
55
  | `--no-open` | | Don't open the game in the browser once the server is up |
54
56
 
57
+ ## `mavonengine install`
58
+
59
+ Installs packages into the project you are in, with its package manager. For each one that is a MavonEngine [plugin](#writing-a-plugin), it shows where the plugin runs (client, server or both) and which prefabs it brings, and asks whether to add it to the `plugins` in `mavonengine.json`. Registered plugins load on the client and/or the game server and are built into the release.
60
+
61
+ ```sh
62
+ npx @mavonengine/cli install my-custom-plugin
63
+
64
+ # Anything your package manager takes
65
+ npx @mavonengine/cli install @acme/forest@^2 file:../my-plugin --yes
66
+ ```
67
+
68
+ `mavonengine.json` sits in the project root, next to `package.json`, and is created if it doesn't exist:
69
+
70
+ ```json
71
+ {
72
+ "plugins": {
73
+ "my-custom-plugin": { "enabled": true, "prefabs": ["Tree"], "options": { "density": 2 } }
74
+ }
75
+ }
76
+ ```
77
+
78
+ - `enabled`: set to `false` to turn a plugin off without uninstalling it.
79
+ - `prefabs`: the plugin prefabs the project uses. The editor adds a prefab here when you place it, and you can edit the list by hand. Only the assets of these prefabs (plus the plugin's shared assets) go into the release.
80
+ - `options`: passed to the plugin as `ctx.options`.
81
+
82
+ The client reloads when the plugins in `mavonengine.json` change. Restart the game server to load a new server plugin. Without an interactive terminal it only installs, unless `--yes` is given.
83
+
84
+ | Option | Default | |
85
+ | --- | --- | --- |
86
+ | `--cwd <dir>` | current directory | Where to start looking for the project |
87
+ | `-y`, `--yes` | | Add plugins to `mavonengine.json` without asking |
88
+ | `--no-register` | | Only install |
89
+
90
+ ## `mavonengine build`
91
+
92
+ Runs the project's `build` script, then lists what each plugin put into the release. Plugin assets are copied into the client's output under `plugins/<package>/`. Only the shared ones and those of the prefabs in `mavonengine.json` are copied. The client skips loading the others, so a release never requests a file it doesn't contain.
93
+
94
+ ```sh
95
+ npx @mavonengine/cli build
96
+ ```
97
+
98
+ | Option | Default | |
99
+ | --- | --- | --- |
100
+ | `--cwd <dir>` | current directory | Where to start looking for the project |
101
+
55
102
  ## `mavonengine dashboard`
56
103
 
57
- A live terminal dashboard for a running game server, built with [hqtui](https://github.com/profullstack/hqtui).
104
+ A live terminal dashboard for a running game server, built with [hqtui](https://github.com/profullstack/hqtui). For the same dashboard in a browser, behind a login, see the [`@mavonengine/http-monitoring`](https://github.com/MavonEngine/Core/tree/main/packages/plugins/http-monitoring) plugin.
58
105
 
59
106
  It shows CPU, network bandwidth, tick timings against the tick budget, world/physics/memory stats, the connected players, the server's recent log output and the HTTP requests it received. It reads them from the server's `GET /api/game/metrics`, `GET /api/game/logs` and `GET /api/game/requests` endpoints, which need a `@mavonengine/core` release that includes them.
60
107
 
61
108
  ```sh
62
109
  # Local server on the default HTTP port (8050)
63
- npx mavonengine dashboard
110
+ npx @mavonengine/cli dashboard
64
111
 
65
112
  # Remote server with a metrics token
66
- MAVONENGINE_METRICS_TOKEN=secret npx mavonengine dashboard --host game.example.com --port 8050
113
+ MAVONENGINE_METRICS_TOKEN=secret npx @mavonengine/cli dashboard --host game.example.com --port 8050
67
114
 
68
115
  # No server needed: simulated data
69
- npx mavonengine dashboard --sim
116
+ npx @mavonengine/cli dashboard --sim
70
117
  ```
71
118
 
72
119
  | Option | Default | |
@@ -105,3 +152,152 @@ const server = new MyServer(logger, physicsWorld, {
105
152
  - With `NODE_ENV=production` and no token, the endpoint is turned off.
106
153
  - Set `metrics.enabled: false` to turn it off everywhere.
107
154
  - The logs endpoint follows the same rules. It serves whatever passes your logger's level.
155
+
156
+ ## `mavonengine mcp`
157
+
158
+ A [Model Context Protocol](https://modelcontextprotocol.io) server over stdio, so an AI coding assistant can look at your running game: the game server, and the game tabs open in a browser. It works with any MCP client and any model: Claude Code, Cursor, VS Code, Codex, Gemini CLI, Zed, or a local model behind an MCP-capable client.
159
+
160
+ Projects made with `mavonengine create` get a `.mcp.json` that registers it. Elsewhere, add it to your client as a stdio server; most clients take this JSON:
161
+
162
+ ```json
163
+ {
164
+ "mcpServers": {
165
+ "mavonengine": { "command": "npx", "args": ["-y", "@mavonengine/cli", "mcp"] }
166
+ }
167
+ }
168
+ ```
169
+
170
+ Game server tools:
171
+
172
+ | Tool | |
173
+ | --- | --- |
174
+ | `server_status` | Uptime, tick time against its budget, CPU, bandwidth, memory, player / entity / physics counts |
175
+ | `server_metrics` | The full metrics snapshot, optionally with its per-second history |
176
+ | `list_players` | Connected players with position and connection details |
177
+ | `get_scene` | The active scene: file, hot reload, unsaved changes, settings and objects (dev only) |
178
+ | `list_entities` | Entities in the world, filtered by type and distance to a point (dev only) |
179
+ | `get_entity` | One entity with the state it sends to clients (dev only) |
180
+ | `recent_logs` | Server log lines, filtered by level and text |
181
+ | `recent_requests` | HTTP requests the server answered, filtered by path and status |
182
+
183
+ Client tools, for the game tabs open while Vite serves the client in dev:
184
+
185
+ | Tool | |
186
+ | --- | --- |
187
+ | `list_clients` | Open tabs: page, editor or player, the player's id on the server, visibility, FPS |
188
+ | `client_status` | Draw calls and triangles of one frame, GPU geometries / textures / programs, JS heap, canvas, camera |
189
+ | `client_entities` / `client_entity` | The tab's world, in the same shape as the server's, with its local player marked |
190
+ | `client_scene` | The scene loaded in the tab, and which objects the client built |
191
+ | `client_network` | Connection, ping, and inputs sent but not yet acknowledged by the server |
192
+ | `client_logs` | Console output, uncaught errors and unhandled rejections, filtered by level and text |
193
+ | `client_screenshot` | What the tab renders, as an image (the WebGL canvas, without HTML UI) |
194
+ | `client_trace` | Records a few seconds of the tab and analyses it: frame times, render CPU vs GPU, composer passes, ticks, WebGL work per frame, the bottleneck, and the worst hitches with their causes. Saves a flame chart trace file |
195
+ | `compare_entity` | One entity on the server next to a tab's copy: position / rotation / scale deltas and differing fields |
196
+
197
+ Documentation tools, passed through to the MCP server of [mavonengine.com](https://mavonengine.com) (`https://mavonengine.com/mcp`), so answers come from the current docs:
198
+
199
+ | Tool | |
200
+ | --- | --- |
201
+ | `docs_list_pages` | Every docs page with its path, title, description and category |
202
+ | `docs_get_page` | One docs page as Markdown, by path (e.g. `/networking/state-sync`) |
203
+
204
+ They connect to the site the first time one is used, so the other tools also work offline. `--docs-url` points them at another docs server, `--no-docs` leaves them out.
205
+
206
+ Client tools take a `clientId` from `list_clients`. Without one they ask the only open tab, or the only one not running the editor.
207
+
208
+ Every tool is read-only. It takes `--host`, `--port`, `--url` and `--token` like the dashboard, where `--url` is the server's base URL (`http://localhost:8050`). When Vite serves the client on its own port instead of through the game server, pass `--client-url`, e.g. `http://localhost:5173`.
209
+
210
+ The server scene and entity tools read `GET /api/game/dev/*` on the game server. Those routes are only registered when `NODE_ENV` is not `production`: a metrics token does not turn them on there. Otherwise they follow the [server access](#server-access) rules of the metrics endpoint.
211
+
212
+ The client tools go through the dev bridge of the Vite plugin in `@mavonengine/core/vite.config` (Vite 8). In dev it adds a small module to the page that answers over Vite's hot-reload WebSocket, and serves `GET /__mavonengine/clients` to localhost, or with `MAVONENGINE_METRICS_TOKEN` as bearer token when that is set for the dev server. Builds never include it. Turn it off with `mavonEnginePlugins({ devBridge: false })`.
213
+
214
+ `client_trace` patches the tab only while it records and puts everything back afterwards. It times every frame and composer pass, counts the WebGL calls on the context, keeps slow single calls (shader compiles, texture uploads, `readPixels` / `getError` stalls), long animation frames with the scripts that ran, and your own `performance.measure()` spans. GPU time per frame needs `EXT_disjoint_timer_query_webgl2` (Chrome and Edge on most GPUs; the three-perf panel pauses while tracing since it uses the same queries). JS call stacks come from the [JS Self-Profiling API](https://wicg.github.io/js-self-profiling/) in Chromium browsers: the Vite plugin serves dev pages with `Document-Policy: js-profiling` for it, so reload tabs opened before you updated. The trace file is [Chrome Trace Event](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU) JSON with tracks for frames, passes, GPU time, ticks, JS samples and slow WebGL calls: open it in [Perfetto](https://ui.perfetto.dev), `chrome://tracing` or [speedscope](https://www.speedscope.app). Keep the tab in the foreground while it records, browsers throttle hidden tabs.
215
+
216
+ To let `compare_entity` match your own player with the server's, set `game.localPlayer` to the entity the tab controls, or give it `isLocalPlayer = true`: the server keys players by socket id, which the client often doesn't.
217
+
218
+ ## Writing a plugin
219
+
220
+ A plugin is an npm package with a `mavonengine` field in its `package.json`:
221
+
222
+ ```json
223
+ {
224
+ "name": "my-custom-plugin",
225
+ "type": "module",
226
+ "mavonengine": {
227
+ "target": "both",
228
+ "client": "./dist/client.js",
229
+ "server": "./dist/server.js",
230
+ "engine": "^0.0.22",
231
+ "assets": {
232
+ "dir": "./assets",
233
+ "shared": ["textures/bark.png"],
234
+ "prefabs": {
235
+ "Tree": ["models/tree.glb"],
236
+ "Rock": ["models/rock.glb"]
237
+ }
238
+ }
239
+ }
240
+ }
241
+ ```
242
+
243
+ - `target`: `client`, `server` or `both`. The matching entries are required.
244
+ - `assets`: paths relative to `dir` (default `assets`). They are served at `/plugins/<package>/<path>`: all of them in dev, and only the shared ones and those of used prefabs in a release.
245
+
246
+ Each entry default-exports the plugin. Every hook gets the plugin's context last, which carries `package`, `options`, `game` and `logger`.
247
+
248
+ ```ts
249
+ // client.ts
250
+ import { defineClientPlugin } from '@mavonengine/core/Plugins/types'
251
+
252
+ export default defineClientPlugin({
253
+ assets: ctx => [{ name: 'tree', type: 'gltfModel', path: ctx.assetUrl('models/tree.glb') }],
254
+ setup(ctx) {
255
+ ctx.registerPrefab({
256
+ name: 'Tree',
257
+ spawn: position => ({ object: new Tree(position) }),
258
+ })
259
+ },
260
+ update(delta, ctx) {},
261
+ // Dev only, when the editor boots
262
+ editor(ctx) {
263
+ ctx.editor.on('objectSelected', () => {})
264
+ },
265
+ })
266
+ ```
267
+
268
+ ```ts
269
+ // server.ts
270
+ import { defineServerPlugin } from '@mavonengine/core/Plugins/types'
271
+
272
+ export default defineServerPlugin({
273
+ setup(ctx) {},
274
+ // Runs after the server's own onHttp routes, before the game client
275
+ http(app, ctx) {
276
+ app.get('/api/my-plugin/status', (_req, res) => res.json({ ok: true }))
277
+ },
278
+ onConnection(channel, client, ctx) {},
279
+ onDisconnect(channel, ctx) {},
280
+ tick(delta, ctx) {},
281
+ destroy(ctx) {},
282
+ })
283
+ ```
284
+
285
+ A hook that throws is logged and turned off for that plugin, and the game keeps running.
286
+
287
+ Projects made with `mavonengine create` are already set up for plugins. To set up an existing project:
288
+
289
+ ```ts
290
+ // client: Game.usePlugins() before new Game()
291
+ import Game from '@mavonengine/core/Game'
292
+ import plugins from 'virtual:mavonengine/plugins'
293
+
294
+ Game.usePlugins(plugins)
295
+ ```
296
+
297
+ ```ts
298
+ // server: the directory with mavonengine.json
299
+ const server = new MyServer(logger, physicsWorld, { plugins: { root: projectRoot } })
300
+ await server.start()
301
+ ```
302
+
303
+ The client's Vite config needs to extend `@mavonengine/core/vite.config`, which provides `virtual:mavonengine/plugins`, serves plugin assets in dev and copies them into the release.
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_DOCS_URL,
4
+ version
5
+ } from "./chunk-TNN4S5K4.js";
6
+
7
+ // src/mcp/docs.ts
8
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9
+ import { z as z2 } from "zod";
10
+
11
+ // src/mcp/result.ts
12
+ import { z } from "zod";
13
+ var READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
14
+ var LOG_LEVELS = ["error", "warn", "info", "http", "verbose", "debug", "silly"];
15
+ var vec3 = z.object({ x: z.number(), y: z.number(), z: z.number() });
16
+ function json(data) {
17
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
18
+ }
19
+ async function run(body) {
20
+ try {
21
+ return json(await body());
22
+ } catch (err) {
23
+ return { isError: true, content: [{ type: "text", text: err.message }] };
24
+ }
25
+ }
26
+
27
+ // src/mcp/docs.ts
28
+ var REQUEST_TIMEOUT_MS = 15e3;
29
+ var DocsClient = class {
30
+ constructor(url = DEFAULT_DOCS_URL, connect = connectHttp) {
31
+ this.url = url;
32
+ this.connect = connect;
33
+ }
34
+ url;
35
+ connect;
36
+ client;
37
+ async call(tool, args = {}) {
38
+ this.client ??= this.connect(this.url);
39
+ try {
40
+ const client = await this.client;
41
+ return await client.callTool({ name: tool, arguments: args }, void 0, { timeout: REQUEST_TIMEOUT_MS });
42
+ } catch (err) {
43
+ const failed = this.client;
44
+ this.client = void 0;
45
+ void failed.then((client) => client.close()).catch(() => {
46
+ });
47
+ throw new Error(`Could not read the MavonEngine docs at ${this.url}: ${err.message}`);
48
+ }
49
+ }
50
+ async close() {
51
+ const client = this.client;
52
+ this.client = void 0;
53
+ await client?.then((c) => c.close()).catch(() => {
54
+ });
55
+ }
56
+ };
57
+ async function connectHttp(url) {
58
+ const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
59
+ const client = new Client({ name: "mavonengine-cli", version });
60
+ await client.connect(new StreamableHTTPClientTransport(new URL(url)));
61
+ return client;
62
+ }
63
+ function registerDocsTools(server, docs) {
64
+ const forward = async (tool, args) => {
65
+ try {
66
+ return await docs.call(tool, args);
67
+ } catch (err) {
68
+ return { isError: true, content: [{ type: "text", text: err.message }] };
69
+ }
70
+ };
71
+ server.registerTool("docs_list_pages", {
72
+ title: "List MavonEngine docs pages",
73
+ description: "Every page of the MavonEngine documentation (mavonengine.com) with its path, title, description and category. Use it to find the page about an engine concept or API, then read it with docs_get_page.",
74
+ annotations: { ...READ_ONLY, openWorldHint: true }
75
+ }, () => forward("list-pages"));
76
+ server.registerTool("docs_get_page", {
77
+ title: "Read a MavonEngine docs page",
78
+ description: "One page of the MavonEngine documentation as Markdown, by its path from docs_list_pages (e.g. /networking/state-sync).",
79
+ inputSchema: { path: z2.string().min(1).describe("Page path, e.g. /getting-started/installation") },
80
+ annotations: { ...READ_ONLY, openWorldHint: true }
81
+ }, ({ path }) => forward("get-page", { path }));
82
+ }
83
+
84
+ export {
85
+ READ_ONLY,
86
+ LOG_LEVELS,
87
+ vec3,
88
+ run,
89
+ DocsClient,
90
+ registerDocsTools
91
+ };
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+
3
+ // package.json
4
+ var version = "0.0.2";
5
+
6
+ // src/mcp/config.ts
7
+ import { existsSync, readFileSync, writeFileSync } from "fs";
8
+ import { join } from "path";
9
+ var DEFAULT_DOCS_URL = "https://mavonengine.com/mcp";
10
+ var MCP_SERVER_NAME = "mavonengine";
11
+ var MCP_CONFIG_FILE = ".mcp.json";
12
+ function mcpServerEntry() {
13
+ return {
14
+ command: "npx",
15
+ args: ["-y", "@mavonengine/cli", "mcp"]
16
+ };
17
+ }
18
+ function writeMcpConfig(dir) {
19
+ const file = join(dir, MCP_CONFIG_FILE);
20
+ let config = {};
21
+ if (existsSync(file)) {
22
+ try {
23
+ config = JSON.parse(readFileSync(file, "utf8"));
24
+ } catch {
25
+ return false;
26
+ }
27
+ if (!config || typeof config !== "object" || Array.isArray(config))
28
+ return false;
29
+ }
30
+ const servers = config.mcpServers && typeof config.mcpServers === "object" ? config.mcpServers : {};
31
+ if (MCP_SERVER_NAME in servers)
32
+ return false;
33
+ config.mcpServers = { ...servers, [MCP_SERVER_NAME]: mcpServerEntry() };
34
+ writeFileSync(file, `${JSON.stringify(config, null, 2)}
35
+ `);
36
+ return true;
37
+ }
38
+
39
+ export {
40
+ version,
41
+ DEFAULT_DOCS_URL,
42
+ MCP_CONFIG_FILE,
43
+ writeMcpConfig
44
+ };
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DocsClient,
4
+ registerDocsTools
5
+ } from "./chunk-OSCNM66O.js";
6
+ import {
7
+ DEFAULT_DOCS_URL
8
+ } from "./chunk-TNN4S5K4.js";
9
+ export {
10
+ DEFAULT_DOCS_URL,
11
+ DocsClient,
12
+ registerDocsTools
13
+ };