@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.
@@ -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-OSCNM66O.js";
12
+ } from "./chunk-NIBSLCCZ.js";
9
13
  import {
10
14
  version
11
- } from "./chunk-TNN4S5K4.js";
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 z2 } from "zod";
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(clientId2, at = /* @__PURE__ */ new Date()) {
455
+ function defaultTracePath(clientId3, at = /* @__PURE__ */ new Date()) {
452
456
  const stamp = at.toISOString().replace(/[:.]/g, "-");
453
- return join(tmpdir(), "mavonengine-traces", `trace-${clientId2}-${stamp}.json`);
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: clientId2 }) => run(() => client.client("status", {}, clientId2)));
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: clientId2, near, ...query }) => run(() => client.client("entities", { ...query, near: near && `${near.x},${near.y},${near.z}` }, clientId2)));
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: clientId2 }) => run(async () => {
489
- const { entity } = await client.client("entity", { id }, clientId2);
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: clientId2, includeObjects }) => run(async () => {
500
- const { scene } = await client.client("scene", {}, clientId2);
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: clientId2 }) => run(() => client.client("network", {}, clientId2)));
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: clientId2, level, contains, afterId, limit }) => run(async () => {
526
- const { entries = [], latestId = 0 } = await client.client("logs", { after: afterId }, clientId2);
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: clientId2, maxWidth, format }) => {
549
+ }, async ({ clientId: clientId3, maxWidth, format }) => {
546
550
  try {
547
- const shot = await client.client("screenshot", { maxWidth, format }, clientId2);
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: clientId2, durationMs, jsProfile, sampleIntervalMs, gpu, maxHitches, savePath, save }) => run(async () => {
573
- const capture = await client.client("trace", { durationMs, jsProfile: String(jsProfile), sampleIntervalMs, gpu: String(gpu) }, clientId2);
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(clientId2 ?? "auto");
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: clientId2 }) => run(async () => {
593
- const [onServer, onClient] = await Promise.all([client.entity(id), client.client("entity", { id }, clientId2)]);
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: z2.boolean().default(false).describe("Include the per-sample history arrays") },
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: z2.boolean().default(true).describe("Include the scene objects, not just the summary") },
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: z2.string().optional().describe("Entity class / networked type name, or scene object type, case-insensitive"),
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: z2.number().nonnegative().optional().describe("Max distance from `near`"),
669
- limit: z2.number().int().min(1).max(500).default(50),
670
- offset: z2.number().int().min(0).default(0)
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: z2.string().min(1) },
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: z2.enum(LOG_LEVELS).optional().describe("Only this level and more severe ones, e.g. warn includes error"),
690
- contains: z2.string().optional().describe("Only lines containing this text, case-insensitive"),
691
- afterId: z2.number().int().min(0).default(0),
692
- limit: z2.number().int().min(1).max(500).default(100).describe("Newest lines kept when more match")
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: z2.string().optional(),
714
- minStatus: z2.number().int().optional().describe("e.g. 400 for errors only"),
715
- afterId: z2.number().int().min(0).default(0),
716
- limit: z2.number().int().min(1).max(500).default(50).describe("Newest requests kept when more match")
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;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mavonengine/cli",
3
3
  "type": "module",
4
- "version": "0.0.2",
4
+ "version": "0.0.3",
5
5
  "description": "MavonEngine command line tools",
6
6
  "license": "MIT",
7
7
  "keywords": [