@mavonengine/cli 0.0.2 → 0.0.3
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 +86 -3
- package/dist/{chunk-OSCNM66O.js → chunk-NIBSLCCZ.js} +1 -1
- package/dist/{chunk-TNN4S5K4.js → chunk-XPKCYSMN.js} +1 -1
- package/dist/chunk-ZEMFSSJP.js +87 -0
- package/dist/{docs-4C7X6FUQ.js → docs-O2V2WSUG.js} +2 -2
- package/dist/index.js +1948 -538
- package/dist/{server-35RY6IGC.js → server-NNL4IMYV.js} +231 -37
- package/package.json +1 -1
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
fetchPluginReadme,
|
|
4
|
+
fetchPluginRegistry
|
|
5
|
+
} from "./chunk-ZEMFSSJP.js";
|
|
2
6
|
import {
|
|
3
7
|
LOG_LEVELS,
|
|
4
8
|
READ_ONLY,
|
|
5
9
|
registerDocsTools,
|
|
6
10
|
run,
|
|
7
11
|
vec3
|
|
8
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-NIBSLCCZ.js";
|
|
9
13
|
import {
|
|
10
14
|
version
|
|
11
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-XPKCYSMN.js";
|
|
12
16
|
|
|
13
17
|
// src/mcp/server.ts
|
|
14
18
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15
|
-
import { z as
|
|
19
|
+
import { z as z4 } from "zod";
|
|
16
20
|
|
|
17
21
|
// src/mcp/clientTools.ts
|
|
18
22
|
import { mkdir, writeFile } from "fs/promises";
|
|
@@ -448,9 +452,9 @@ function compareEntities(id, server, client) {
|
|
|
448
452
|
note: "Client state of remote entities is interpolated and the local player is predicted, so small deltas are expected while moving"
|
|
449
453
|
};
|
|
450
454
|
}
|
|
451
|
-
function defaultTracePath(
|
|
455
|
+
function defaultTracePath(clientId3, at = /* @__PURE__ */ new Date()) {
|
|
452
456
|
const stamp = at.toISOString().replace(/[:.]/g, "-");
|
|
453
|
-
return join(tmpdir(), "mavonengine-traces", `trace-${
|
|
457
|
+
return join(tmpdir(), "mavonengine-traces", `trace-${clientId3}-${stamp}.json`);
|
|
454
458
|
}
|
|
455
459
|
function registerClientTools(server, client) {
|
|
456
460
|
server.registerTool("list_clients", {
|
|
@@ -466,7 +470,7 @@ function registerClientTools(server, client) {
|
|
|
466
470
|
description: "One tab's rendering and state: FPS, draw calls / triangles / points / lines of one frame, GPU geometries / textures / shader programs, JS heap, canvas size, camera, entity count, active scene, resource loading.",
|
|
467
471
|
inputSchema: { clientId },
|
|
468
472
|
annotations: READ_ONLY
|
|
469
|
-
}, ({ clientId:
|
|
473
|
+
}, ({ clientId: clientId3 }) => run(() => client.client("status", {}, clientId3)));
|
|
470
474
|
server.registerTool("client_entities", {
|
|
471
475
|
title: "Entities on a client",
|
|
472
476
|
description: "Entities in a tab's world, in the same shape as list_entities on the server, plus `localPlayer` for the entity the tab controls. Filter by type and/or distance to a point (nearest first).",
|
|
@@ -479,14 +483,14 @@ function registerClientTools(server, client) {
|
|
|
479
483
|
offset: z.number().int().min(0).default(0)
|
|
480
484
|
},
|
|
481
485
|
annotations: READ_ONLY
|
|
482
|
-
}, ({ clientId:
|
|
486
|
+
}, ({ clientId: clientId3, near, ...query }) => run(() => client.client("entities", { ...query, near: near && `${near.x},${near.y},${near.z}` }, clientId3)));
|
|
483
487
|
server.registerTool("client_entity", {
|
|
484
488
|
title: "One entity on a client",
|
|
485
489
|
description: "One entity in a tab's world with its serialized state. The server id of the tab's own player finds its local player.",
|
|
486
490
|
inputSchema: { id: z.string().min(1), clientId },
|
|
487
491
|
annotations: READ_ONLY
|
|
488
|
-
}, ({ id, clientId:
|
|
489
|
-
const { entity } = await client.client("entity", { id },
|
|
492
|
+
}, ({ id, clientId: clientId3 }) => run(async () => {
|
|
493
|
+
const { entity } = await client.client("entity", { id }, clientId3);
|
|
490
494
|
if (!entity)
|
|
491
495
|
throw new Error(`The client has no entity with id "${id}". client_entities shows its ids`);
|
|
492
496
|
return entity;
|
|
@@ -496,8 +500,8 @@ function registerClientTools(server, client) {
|
|
|
496
500
|
description: "The scene loaded in a tab, in the same shape as get_scene: objects with `built` telling whether the client built them.",
|
|
497
501
|
inputSchema: { clientId, includeObjects: z.boolean().default(true) },
|
|
498
502
|
annotations: READ_ONLY
|
|
499
|
-
}, ({ clientId:
|
|
500
|
-
const { scene } = await client.client("scene", {},
|
|
503
|
+
}, ({ clientId: clientId3, includeObjects }) => run(async () => {
|
|
504
|
+
const { scene } = await client.client("scene", {}, clientId3);
|
|
501
505
|
if (!scene)
|
|
502
506
|
return { scene: null, note: "No scene is loaded in this tab. The client opens one with game.openScene(name)" };
|
|
503
507
|
if (includeObjects)
|
|
@@ -510,7 +514,7 @@ function registerClientTools(server, client) {
|
|
|
510
514
|
description: "A tab's connection to the game server: connected, player id, ping, editor mode, inputs sent but not yet acknowledged (pendingCommands, replayed for prediction) and the next input sequence id.",
|
|
511
515
|
inputSchema: { clientId },
|
|
512
516
|
annotations: READ_ONLY
|
|
513
|
-
}, ({ clientId:
|
|
517
|
+
}, ({ clientId: clientId3 }) => run(() => client.client("network", {}, clientId3)));
|
|
514
518
|
server.registerTool("client_logs", {
|
|
515
519
|
title: "Client console",
|
|
516
520
|
description: "A tab's console output, uncaught errors and unhandled promise rejections since it loaded (the last 500), oldest first. Pass the returned latestId as afterId later to get only newer lines.",
|
|
@@ -522,8 +526,8 @@ function registerClientTools(server, client) {
|
|
|
522
526
|
limit: z.number().int().min(1).max(500).default(100).describe("Newest lines kept when more match")
|
|
523
527
|
},
|
|
524
528
|
annotations: READ_ONLY
|
|
525
|
-
}, ({ clientId:
|
|
526
|
-
const { entries = [], latestId = 0 } = await client.client("logs", { after: afterId },
|
|
529
|
+
}, ({ clientId: clientId3, level, contains, afterId, limit }) => run(async () => {
|
|
530
|
+
const { entries = [], latestId = 0 } = await client.client("logs", { after: afterId }, clientId3);
|
|
527
531
|
const maxSeverity = level ? SEVERITY[level] : Infinity;
|
|
528
532
|
const needle = contains?.toLowerCase();
|
|
529
533
|
const matches = entries.filter((entry) => (SEVERITY[entry.level] ?? 2) <= maxSeverity && (!needle || entry.message.toLowerCase().includes(needle)));
|
|
@@ -542,9 +546,9 @@ function registerClientTools(server, client) {
|
|
|
542
546
|
format: z.enum(["jpeg", "png"]).default("jpeg")
|
|
543
547
|
},
|
|
544
548
|
annotations: READ_ONLY
|
|
545
|
-
}, async ({ clientId:
|
|
549
|
+
}, async ({ clientId: clientId3, maxWidth, format }) => {
|
|
546
550
|
try {
|
|
547
|
-
const shot = await client.client("screenshot", { maxWidth, format },
|
|
551
|
+
const shot = await client.client("screenshot", { maxWidth, format }, clientId3);
|
|
548
552
|
return {
|
|
549
553
|
content: [
|
|
550
554
|
{ type: "image", data: shot.data, mimeType: shot.mimeType },
|
|
@@ -569,12 +573,12 @@ function registerClientTools(server, client) {
|
|
|
569
573
|
save: z.boolean().default(true).describe("Write the trace file")
|
|
570
574
|
},
|
|
571
575
|
annotations: { ...READ_ONLY, idempotentHint: false }
|
|
572
|
-
}, ({ clientId:
|
|
573
|
-
const capture = await client.client("trace", { durationMs, jsProfile: String(jsProfile), sampleIntervalMs, gpu: String(gpu) },
|
|
576
|
+
}, ({ clientId: clientId3, durationMs, jsProfile, sampleIntervalMs, gpu, maxHitches, savePath, save }) => run(async () => {
|
|
577
|
+
const capture = await client.client("trace", { durationMs, jsProfile: String(jsProfile), sampleIntervalMs, gpu: String(gpu) }, clientId3);
|
|
574
578
|
const analysis = analyzeTrace(capture, { maxHitches });
|
|
575
579
|
let traceFile;
|
|
576
580
|
if (save) {
|
|
577
|
-
traceFile = savePath ? resolve(savePath) : defaultTracePath(
|
|
581
|
+
traceFile = savePath ? resolve(savePath) : defaultTracePath(clientId3 ?? "auto");
|
|
578
582
|
await mkdir(dirname(traceFile), { recursive: true });
|
|
579
583
|
await writeFile(traceFile, JSON.stringify(toTraceEvents(capture)));
|
|
580
584
|
}
|
|
@@ -589,12 +593,196 @@ function registerClientTools(server, client) {
|
|
|
589
593
|
description: "One entity as the server has it next to a tab's copy: position / rotation / scale deltas and the serialized fields that differ. For the tab's own player pass its server id (playerId from list_clients). Needs the server in development too.",
|
|
590
594
|
inputSchema: { id: z.string().min(1).describe("Entity id on the server"), clientId },
|
|
591
595
|
annotations: READ_ONLY
|
|
592
|
-
}, ({ id, clientId:
|
|
593
|
-
const [onServer, onClient] = await Promise.all([client.entity(id), client.client("entity", { id },
|
|
596
|
+
}, ({ id, clientId: clientId3 }) => run(async () => {
|
|
597
|
+
const [onServer, onClient] = await Promise.all([client.entity(id), client.client("entity", { id }, clientId3)]);
|
|
594
598
|
return compareEntities(id, onServer.entity, onClient.entity);
|
|
595
599
|
}));
|
|
596
600
|
}
|
|
597
601
|
|
|
602
|
+
// src/mcp/networkTools.ts
|
|
603
|
+
import { z as z2 } from "zod";
|
|
604
|
+
var clientId2 = z2.string().optional().describe("Tab id from list_clients. Default: the only open tab, or the only one not running the editor");
|
|
605
|
+
var packetFilters = {
|
|
606
|
+
kind: z2.enum(["packet", "event"]).optional().describe("packet: messages on the wire. event: connect, disconnect, sync_enter, sync_leave, dropped, state_gap\u2026"),
|
|
607
|
+
direction: z2.enum(["in", "out"]).optional().describe("Seen from the side that recorded it: out was sent, in was received"),
|
|
608
|
+
type: z2.string().optional().describe("Message or event name, e.g. sv_state or command. Several separated by commas"),
|
|
609
|
+
afterId: z2.number().int().min(0).default(0),
|
|
610
|
+
limit: z2.number().int().min(1).max(2e3).default(50).describe("Newest records kept when more match")
|
|
611
|
+
};
|
|
612
|
+
function withTime({ timestamp, ...record }) {
|
|
613
|
+
return { ...record, time: new Date(timestamp).toISOString() };
|
|
614
|
+
}
|
|
615
|
+
function listing(list) {
|
|
616
|
+
return {
|
|
617
|
+
latestId: list.latestId,
|
|
618
|
+
matched: list.matched,
|
|
619
|
+
records: list.records.map(withTime),
|
|
620
|
+
...list.enabled === false ? { note: "Nothing is being recorded on this side" } : {}
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function mergeTimeline(server, client) {
|
|
624
|
+
return [
|
|
625
|
+
...server.map((record) => ({ side: "server", ...record })),
|
|
626
|
+
...client.map((record) => ({ side: "client", ...record }))
|
|
627
|
+
].sort((a, b) => a.timestamp - b.timestamp || (a.side === b.side ? a.id - b.id : a.side === "server" ? -1 : 1));
|
|
628
|
+
}
|
|
629
|
+
function registerNetworkTools(server, client) {
|
|
630
|
+
server.registerTool("network_packets", {
|
|
631
|
+
title: "Packets on the server",
|
|
632
|
+
description: "Packets the game server sent and received and its network events, oldest first: time, direction, message name, client (peer), size in bytes, server tick, input sequence id and a few facts about the payload (entity count, ack, command). The server keeps the last 2000. Pass the returned latestId as afterId to get only newer ones; network_packet shows a payload. Development only.",
|
|
633
|
+
inputSchema: { ...packetFilters, peer: z2.string().optional().describe("Only one client: its player id (list_players, or playerId from list_clients)") },
|
|
634
|
+
annotations: READ_ONLY
|
|
635
|
+
}, ({ afterId, ...query }) => run(async () => listing(await client.packets({ ...query, after: afterId }))));
|
|
636
|
+
server.registerTool("network_packet", {
|
|
637
|
+
title: "One packet on the server",
|
|
638
|
+
description: "One record from network_packets with its payload, as it was serialized for the wire. Payloads of old records are dropped to bound memory (hasPayload false). Development only.",
|
|
639
|
+
inputSchema: { id: z2.number().int().min(1) },
|
|
640
|
+
annotations: READ_ONLY
|
|
641
|
+
}, ({ id }) => run(async () => {
|
|
642
|
+
const { record } = await client.packet(id);
|
|
643
|
+
if (!record)
|
|
644
|
+
throw new Error(`No record ${id}: it is older than the 2000 the server keeps, or was never recorded`);
|
|
645
|
+
return withTime(record);
|
|
646
|
+
}));
|
|
647
|
+
server.registerTool("network_sync", {
|
|
648
|
+
title: "State sync",
|
|
649
|
+
description: "Who is sent what: the current server tick, the sync distance, each player with the entities in its last state update (tracked), its last acknowledged input and the size of that update; and each synced entity with how many players track it (trackedBy, 0 means no player is in range), whether it changed since it was last synced and the tick it was last sent in. For entities that do not show up or stop updating on a client. Development only.",
|
|
650
|
+
inputSchema: {
|
|
651
|
+
player: z2.string().optional().describe("Only this player, and the entities with `tracked` telling whether it is sent them"),
|
|
652
|
+
includeEntities: z2.boolean().default(true)
|
|
653
|
+
},
|
|
654
|
+
annotations: READ_ONLY
|
|
655
|
+
}, ({ player, includeEntities }) => run(async () => {
|
|
656
|
+
const sync = await client.sync();
|
|
657
|
+
const { entities = [], players = [], ...rest } = sync;
|
|
658
|
+
if (!player)
|
|
659
|
+
return includeEntities ? sync : { ...rest, players, entityCount: entities.length };
|
|
660
|
+
const one = players.find((p) => p.id === player);
|
|
661
|
+
if (!one)
|
|
662
|
+
throw new Error(`No player "${player}". Players: ${players.map((p) => p.id).join(", ") || "none"}`);
|
|
663
|
+
const tracked = new Set(one.tracked);
|
|
664
|
+
return {
|
|
665
|
+
...rest,
|
|
666
|
+
player: one,
|
|
667
|
+
...includeEntities ? { entities: entities.map((entity) => ({ ...entity, tracked: tracked.has(entity.id) })) } : { entityCount: entities.length }
|
|
668
|
+
};
|
|
669
|
+
}));
|
|
670
|
+
server.registerTool("client_packets", {
|
|
671
|
+
title: "Packets on a client",
|
|
672
|
+
description: "Packets a tab sent and received and its network events (connect, disconnect, state_gap for lost state updates, state_out_of_order), in the same shape as network_packets. Recorded since the tab loaded, the last 2000.",
|
|
673
|
+
inputSchema: { clientId: clientId2, ...packetFilters },
|
|
674
|
+
annotations: READ_ONLY
|
|
675
|
+
}, ({ clientId: clientId3, afterId, ...query }) => run(async () => listing(await client.client("packets", { ...query, after: afterId }, clientId3))));
|
|
676
|
+
server.registerTool("client_packet", {
|
|
677
|
+
title: "One packet on a client",
|
|
678
|
+
description: "One record from client_packets with its payload.",
|
|
679
|
+
inputSchema: { id: z2.number().int().min(1), clientId: clientId2 },
|
|
680
|
+
annotations: READ_ONLY
|
|
681
|
+
}, ({ id, clientId: clientId3 }) => run(async () => {
|
|
682
|
+
const { record } = await client.client("packet", { id }, clientId3);
|
|
683
|
+
if (!record)
|
|
684
|
+
throw new Error(`No record ${id}: it is older than the 2000 the tab keeps, or was never recorded`);
|
|
685
|
+
return withTime(record);
|
|
686
|
+
}));
|
|
687
|
+
server.registerTool("network_timeline", {
|
|
688
|
+
title: "Network timeline of one client",
|
|
689
|
+
description: "What happened between the server and one tab, in order of time: the server's records for that client and the tab's own, merged, each marked with its side. Shows a packet leaving one side and arriving on the other (or not), inputs against the state updates that acknowledge them, and events like sync_enter / sync_leave, dropped and state_gap. Needs the server in development too.",
|
|
690
|
+
inputSchema: {
|
|
691
|
+
clientId: clientId2,
|
|
692
|
+
sinceMs: z2.number().int().min(100).max(6e5).default(5e3).describe("How far back to look"),
|
|
693
|
+
includePings: z2.boolean().default(false).describe("Keep ping / pong, one pair per second"),
|
|
694
|
+
eventsOnly: z2.boolean().default(false).describe("Leave the packets out"),
|
|
695
|
+
limit: z2.number().int().min(1).max(2e3).default(200).describe("Newest entries kept when more match")
|
|
696
|
+
},
|
|
697
|
+
annotations: READ_ONLY
|
|
698
|
+
}, ({ clientId: clientId3, sinceMs, includePings, eventsOnly, limit }) => run(async () => {
|
|
699
|
+
const { network } = await client.client("network", {}, clientId3);
|
|
700
|
+
if (!network?.playerId)
|
|
701
|
+
throw new Error("This tab is not connected to the game server, so the server has no records for it");
|
|
702
|
+
const kind = eventsOnly ? "event" : void 0;
|
|
703
|
+
const [onServer, onClient] = await Promise.all([
|
|
704
|
+
client.packets({ peer: network.playerId, kind, limit: 2e3 }),
|
|
705
|
+
client.client("packets", { kind, limit: 2e3 }, clientId3)
|
|
706
|
+
]);
|
|
707
|
+
const from = Date.now() - sinceMs;
|
|
708
|
+
const entries = mergeTimeline(onServer.records, onClient.records).filter((record) => record.timestamp >= from && (includePings || record.type !== "ping" && record.type !== "pong"));
|
|
709
|
+
return {
|
|
710
|
+
playerId: network.playerId,
|
|
711
|
+
pingMs: network.pingMs,
|
|
712
|
+
matched: entries.length,
|
|
713
|
+
entries: entries.slice(-limit).map(withTime),
|
|
714
|
+
note: "Times are each side's own clock. On one machine they line up; otherwise shift one side by the clock difference"
|
|
715
|
+
};
|
|
716
|
+
}));
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/mcp/pluginTools.ts
|
|
720
|
+
import { z as z3 } from "zod";
|
|
721
|
+
var installCommand = (pkg) => `npx @mavonengine/cli install ${pkg}`;
|
|
722
|
+
async function installedPlugins(client) {
|
|
723
|
+
const [{ plugins = [] }, routes] = await Promise.all([
|
|
724
|
+
client.plugins(),
|
|
725
|
+
// An older server lists its plugins but not its routes: the plugins are still worth showing
|
|
726
|
+
client.routes().then((result) => result.routes ?? [], () => null)
|
|
727
|
+
]);
|
|
728
|
+
return plugins.map((plugin) => ({
|
|
729
|
+
...plugin,
|
|
730
|
+
endpoints: routes ? routes.filter((route) => route.source === "plugin" && route.plugin === plugin.package).map(({ method, path, devOnly, summary }) => ({ method, path, devOnly, summary })) : null
|
|
731
|
+
}));
|
|
732
|
+
}
|
|
733
|
+
function registerPluginTools(server, client, list) {
|
|
734
|
+
server.registerTool("list_plugins", {
|
|
735
|
+
title: "Plugins of the project",
|
|
736
|
+
description: "The engine plugins in the project's mavonengine.json: version, whether each is enabled and runs on this server, where it runs (client, server or both), its prefabs and the HTTP endpoints it registered. A problem is reported for a plugin that is missing or invalid. See available_plugins for plugins the project could add.",
|
|
737
|
+
annotations: READ_ONLY
|
|
738
|
+
}, () => run(async () => ({ plugins: await installedPlugins(client) })));
|
|
739
|
+
if (!list)
|
|
740
|
+
return;
|
|
741
|
+
const registry = () => fetchPluginRegistry({ url: list.url, fetch: list.fetch });
|
|
742
|
+
server.registerTool("available_plugins", {
|
|
743
|
+
title: "Plugins that can be installed",
|
|
744
|
+
description: "The official MavonEngine plugins: what each does, where it runs, the engine versions it supports and the command that installs it. Says which ones the project already has. Check this before building something a plugin already does, such as chat or a monitoring dashboard, and read plugin_readme before suggesting one. Installing is for the user to do: this server is read-only.",
|
|
745
|
+
annotations: { ...READ_ONLY, openWorldHint: true }
|
|
746
|
+
}, () => run(async () => {
|
|
747
|
+
const [{ url, plugins }, installed] = await Promise.all([
|
|
748
|
+
registry(),
|
|
749
|
+
// Whether the project has them is extra: the list is worth showing without a running server
|
|
750
|
+
installedPlugins(client).then((found) => new Map(found.map((plugin) => [plugin.package, plugin])), () => null)
|
|
751
|
+
]);
|
|
752
|
+
return {
|
|
753
|
+
source: url,
|
|
754
|
+
projectPlugins: installed ? "known" : "unknown: the game server could not be asked",
|
|
755
|
+
plugins: plugins.map((plugin) => ({
|
|
756
|
+
package: plugin.package,
|
|
757
|
+
name: plugin.name,
|
|
758
|
+
description: plugin.description,
|
|
759
|
+
runsOn: plugin.target,
|
|
760
|
+
engine: plugin.engine,
|
|
761
|
+
homepage: plugin.homepage,
|
|
762
|
+
hasReadme: !!plugin.readme,
|
|
763
|
+
inProject: installed ? installed.has(plugin.package) : null,
|
|
764
|
+
install: installCommand(plugin.package)
|
|
765
|
+
}))
|
|
766
|
+
};
|
|
767
|
+
}));
|
|
768
|
+
server.registerTool("plugin_readme", {
|
|
769
|
+
title: "Read a plugin's readme",
|
|
770
|
+
description: "The README of an official MavonEngine plugin as Markdown, by its package name from available_plugins: what it does, its options for mavonengine.json, how a game hooks into it and its limits.",
|
|
771
|
+
inputSchema: { package: z3.string().describe("npm package of the plugin, e.g. @mavonengine/chat") },
|
|
772
|
+
annotations: { ...READ_ONLY, openWorldHint: true }
|
|
773
|
+
}, async ({ package: pkg }) => {
|
|
774
|
+
try {
|
|
775
|
+
const { plugins } = await registry();
|
|
776
|
+
const plugin = plugins.find((candidate) => candidate.package === pkg);
|
|
777
|
+
if (!plugin)
|
|
778
|
+
throw new Error(`${pkg} is not on the list of plugins. It has: ${plugins.map((candidate) => candidate.package).join(", ") || "nothing"}`);
|
|
779
|
+
return { content: [{ type: "text", text: await fetchPluginReadme(plugin, { fetch: list.fetch }) }] };
|
|
780
|
+
} catch (err) {
|
|
781
|
+
return { isError: true, content: [{ type: "text", text: err.message }] };
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
598
786
|
// src/mcp/server.ts
|
|
599
787
|
var INSTRUCTIONS = `Reads the live state of a running MavonEngine game: the game server (health, performance, players, scene,
|
|
600
788
|
entities, logs, HTTP requests) and, in development, the game tabs open in a browser (renderer stats, the client's world,
|
|
@@ -602,8 +790,12 @@ network state, console logs, screenshots). Every tool is read-only.
|
|
|
602
790
|
Server scene / entity tools need the server in development (NODE_ENV is not "production"); client tools need the client
|
|
603
791
|
served by Vite in dev. Start with server_status and list_clients. Use type / near filters rather than paging through a world.
|
|
604
792
|
compare_entity shows one entity on both sides, for prediction, interpolation and sync bugs.
|
|
793
|
+
network_packets / client_packets list the packets each side sent and received, network_timeline merges both sides for one
|
|
794
|
+
client, and network_sync tells which entities each player is sent: use them when something does not arrive or arrives late.
|
|
605
795
|
client_trace records a few seconds of a tab for frame drops, stutter and slow rendering: the bottleneck, hitches with
|
|
606
796
|
their causes, hot JS functions, WebGL load, and a trace file for flame charts.
|
|
797
|
+
list_plugins shows the plugins the project has, available_plugins the official ones it could add and plugin_readme how
|
|
798
|
+
one is set up and used: look there before building something a plugin already does, like chat.
|
|
607
799
|
docs_list_pages / docs_get_page read the MavonEngine documentation: check it before guessing how an engine API works.`;
|
|
608
800
|
function summarize(metrics) {
|
|
609
801
|
const { cpu, bandwidth, tick, players, ...rest } = metrics ?? {};
|
|
@@ -618,7 +810,7 @@ function summarize(metrics) {
|
|
|
618
810
|
function levelOf(level) {
|
|
619
811
|
return level.toLowerCase().replace(/[^a-z]/g, "");
|
|
620
812
|
}
|
|
621
|
-
function createMcpServer(client, docs) {
|
|
813
|
+
function createMcpServer(client, docs, pluginList) {
|
|
622
814
|
const server = new McpServer({ name: "mavonengine", version }, { instructions: INSTRUCTIONS });
|
|
623
815
|
server.registerTool("server_status", {
|
|
624
816
|
title: "Game server status",
|
|
@@ -628,7 +820,7 @@ function createMcpServer(client, docs) {
|
|
|
628
820
|
server.registerTool("server_metrics", {
|
|
629
821
|
title: "Full metrics snapshot",
|
|
630
822
|
description: "The full metrics snapshot. With includeHistory, also the last ~60 samples of CPU, bandwidth and tick timings, to spot spikes.",
|
|
631
|
-
inputSchema: { includeHistory:
|
|
823
|
+
inputSchema: { includeHistory: z4.boolean().default(false).describe("Include the per-sample history arrays") },
|
|
632
824
|
annotations: READ_ONLY
|
|
633
825
|
}, ({ includeHistory }) => run(async () => {
|
|
634
826
|
const metrics = await client.metrics();
|
|
@@ -648,7 +840,7 @@ function createMcpServer(client, docs) {
|
|
|
648
840
|
server.registerTool("get_scene", {
|
|
649
841
|
title: "Active scene",
|
|
650
842
|
description: "The scene loaded on the server: name, file, whether it hot-reloads, unsaved changes, settings, and its objects with type, transform, props, the plugin that registered the type and whether the server built it (false for client-only types). Development only.",
|
|
651
|
-
inputSchema: { includeObjects:
|
|
843
|
+
inputSchema: { includeObjects: z4.boolean().default(true).describe("Include the scene objects, not just the summary") },
|
|
652
844
|
annotations: READ_ONLY
|
|
653
845
|
}, ({ includeObjects }) => run(async () => {
|
|
654
846
|
const { scene } = await client.scene();
|
|
@@ -663,18 +855,18 @@ function createMcpServer(client, docs) {
|
|
|
663
855
|
title: "Entities in the world",
|
|
664
856
|
description: "Entities in the server world: id, type, position, rotation, scale, whether networked or a player, and the scene object it was built from. Filter by type and/or distance to a point (results are then nearest first). Returns the total match count. Development only.",
|
|
665
857
|
inputSchema: {
|
|
666
|
-
type:
|
|
858
|
+
type: z4.string().optional().describe("Entity class / networked type name, or scene object type, case-insensitive"),
|
|
667
859
|
near: vec3.optional().describe("Only entities around this point, nearest first"),
|
|
668
|
-
radius:
|
|
669
|
-
limit:
|
|
670
|
-
offset:
|
|
860
|
+
radius: z4.number().nonnegative().optional().describe("Max distance from `near`"),
|
|
861
|
+
limit: z4.number().int().min(1).max(500).default(50),
|
|
862
|
+
offset: z4.number().int().min(0).default(0)
|
|
671
863
|
},
|
|
672
864
|
annotations: READ_ONLY
|
|
673
865
|
}, (query) => run(() => client.entities(query)));
|
|
674
866
|
server.registerTool("get_entity", {
|
|
675
867
|
title: "One entity",
|
|
676
868
|
description: "One entity by id, with the state it sends to clients (its serialize() output). Development only.",
|
|
677
|
-
inputSchema: { id:
|
|
869
|
+
inputSchema: { id: z4.string().min(1) },
|
|
678
870
|
annotations: READ_ONLY
|
|
679
871
|
}, ({ id }) => run(async () => {
|
|
680
872
|
const { entity } = await client.entity(id);
|
|
@@ -686,10 +878,10 @@ function createMcpServer(client, docs) {
|
|
|
686
878
|
title: "Server logs",
|
|
687
879
|
description: "Recent game server log lines, oldest first. The server keeps the last 500 by default. Pass the returned latestId as afterId later to get only newer lines.",
|
|
688
880
|
inputSchema: {
|
|
689
|
-
level:
|
|
690
|
-
contains:
|
|
691
|
-
afterId:
|
|
692
|
-
limit:
|
|
881
|
+
level: z4.enum(LOG_LEVELS).optional().describe("Only this level and more severe ones, e.g. warn includes error"),
|
|
882
|
+
contains: z4.string().optional().describe("Only lines containing this text, case-insensitive"),
|
|
883
|
+
afterId: z4.number().int().min(0).default(0),
|
|
884
|
+
limit: z4.number().int().min(1).max(500).default(100).describe("Newest lines kept when more match")
|
|
693
885
|
},
|
|
694
886
|
annotations: READ_ONLY
|
|
695
887
|
}, ({ level, contains, afterId, limit }) => run(async () => {
|
|
@@ -710,10 +902,10 @@ function createMcpServer(client, docs) {
|
|
|
710
902
|
title: "HTTP requests",
|
|
711
903
|
description: "Recent HTTP requests the game server answered: method, path, status, duration, bytes, address. Pass the returned latestId as afterId later to get only newer ones.",
|
|
712
904
|
inputSchema: {
|
|
713
|
-
pathContains:
|
|
714
|
-
minStatus:
|
|
715
|
-
afterId:
|
|
716
|
-
limit:
|
|
905
|
+
pathContains: z4.string().optional(),
|
|
906
|
+
minStatus: z4.number().int().optional().describe("e.g. 400 for errors only"),
|
|
907
|
+
afterId: z4.number().int().min(0).default(0),
|
|
908
|
+
limit: z4.number().int().min(1).max(500).default(50).describe("Newest requests kept when more match")
|
|
717
909
|
},
|
|
718
910
|
annotations: READ_ONLY
|
|
719
911
|
}, ({ pathContains, minStatus, afterId, limit }) => run(async () => {
|
|
@@ -722,6 +914,8 @@ function createMcpServer(client, docs) {
|
|
|
722
914
|
return { latestId, matched: matches.length, entries: matches.slice(-limit) };
|
|
723
915
|
}));
|
|
724
916
|
registerClientTools(server, client);
|
|
917
|
+
registerNetworkTools(server, client);
|
|
918
|
+
registerPluginTools(server, client, pluginList);
|
|
725
919
|
if (docs)
|
|
726
920
|
registerDocsTools(server, docs);
|
|
727
921
|
return server;
|