construct-shader-graph-mcp 0.3.0 → 0.5.0

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.mjs +115 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "construct-shader-graph-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Standalone MCP server for Construct Shader Graph",
5
5
  "type": "module",
6
6
  "files": [
package/src/server.mjs CHANGED
@@ -22,6 +22,7 @@ let localServer = null;
22
22
  let bridge = null;
23
23
  let controlServer = null;
24
24
  let isPrimaryInstance = false;
25
+ let promotionInFlight = null;
25
26
 
26
27
  function log(message, ...args) {
27
28
  console.error(`[construct-shader-graph-mcp] ${message}`, ...args);
@@ -227,7 +228,9 @@ function createToolDefinitions() {
227
228
  description:
228
229
  "Choose which connected shader graph tab future MCP calls should target.",
229
230
  inputSchema: {
230
- sessionId: z.string().describe("Session id returned by list_projects."),
231
+ sessionId: z
232
+ .string()
233
+ .describe("Session id returned by list_projects."),
231
234
  },
232
235
  outputSchema: {
233
236
  sessionId: z.string(),
@@ -291,7 +294,9 @@ function createToolDefinitions() {
291
294
  .describe("Optional session id; defaults to the selected project."),
292
295
  method: z
293
296
  .string()
294
- .describe("Manifest method path, for example nodes.create or shader.getInfo."),
297
+ .describe(
298
+ "Manifest method path, for example nodes.create or shader.getInfo.",
299
+ ),
295
300
  args: z
296
301
  .array(z.any())
297
302
  .optional()
@@ -397,12 +402,15 @@ function registerPrompts(server) {
397
402
  "inspect-graph",
398
403
  {
399
404
  title: "Inspect Graph",
400
- description: "Prompt for safely inspecting the current graph before any edits.",
405
+ description:
406
+ "Prompt for safely inspecting the current graph before any edits.",
401
407
  argsSchema: z.object({
402
408
  focus: z
403
409
  .string()
404
410
  .optional()
405
- .describe("Optional area to inspect, like uniforms, preview, or node types."),
411
+ .describe(
412
+ "Optional area to inspect, like uniforms, preview, or node types.",
413
+ ),
406
414
  }),
407
415
  },
408
416
  ({ focus }) => ({
@@ -447,7 +455,10 @@ function registerPrompts(server) {
447
455
  description:
448
456
  "Prompt for debugging generated code or preview issues in a shader graph project.",
449
457
  argsSchema: z.object({
450
- issue: z.string().optional().describe("Optional description of the observed preview issue."),
458
+ issue: z
459
+ .string()
460
+ .optional()
461
+ .describe("Optional description of the observed preview issue."),
451
462
  }),
452
463
  },
453
464
  ({ issue }) => ({
@@ -480,7 +491,6 @@ function createLocalServer() {
480
491
  }
481
492
 
482
493
  async function startPrimaryBackend() {
483
- isPrimaryInstance = true;
484
494
  localServer = createLocalServer();
485
495
 
486
496
  bridge = new WebSocketServer({ noServer: true });
@@ -541,7 +551,9 @@ async function startPrimaryBackend() {
541
551
  selectedSessionId = sessionId;
542
552
  }
543
553
 
544
- log(`registered ${sessionId} (${session.project?.name || "Untitled Shader"})`);
554
+ log(
555
+ `registered ${sessionId} (${session.project?.name || "Untitled Shader"})`,
556
+ );
545
557
  sendWsJson(socket, {
546
558
  type: "registered",
547
559
  sessionId,
@@ -631,9 +643,15 @@ async function startPrimaryBackend() {
631
643
  return;
632
644
  }
633
645
 
634
- const tool = createToolDefinitions().find((entry) => entry.name === request.tool);
646
+ const tool = createToolDefinitions().find(
647
+ (entry) => entry.name === request.tool,
648
+ );
635
649
  if (!tool) {
636
- sendJson(socket, { id: request.id, ok: false, error: `Unknown tool '${request.tool}'` });
650
+ sendJson(socket, {
651
+ id: request.id,
652
+ ok: false,
653
+ error: `Unknown tool '${request.tool}'`,
654
+ });
637
655
  return;
638
656
  }
639
657
 
@@ -650,14 +668,25 @@ async function startPrimaryBackend() {
650
668
  });
651
669
  });
652
670
 
653
- await new Promise((resolve, reject) => {
654
- controlServer.once("error", reject);
655
- controlServer.listen(CONTROL_PORT, "127.0.0.1", () => {
656
- controlServer.off("error", reject);
657
- resolve();
671
+ try {
672
+ await new Promise((resolve, reject) => {
673
+ controlServer.once("error", reject);
674
+ controlServer.listen(CONTROL_PORT, "127.0.0.1", () => {
675
+ controlServer.off("error", reject);
676
+ resolve();
677
+ });
658
678
  });
659
- });
679
+ } catch (err) {
680
+ httpServer.close();
681
+ bridge.close();
682
+ throw err;
683
+ }
684
+
685
+ // Permanent error handlers to prevent unhandled error crashes
686
+ httpServer.on("error", (err) => log("bridge http error:", err.message));
687
+ controlServer.on("error", (err) => log("control server error:", err.message));
660
688
 
689
+ isPrimaryInstance = true;
661
690
  log(`control listening on tcp://127.0.0.1:${CONTROL_PORT}`);
662
691
  }
663
692
 
@@ -678,9 +707,12 @@ function createProxyServer() {
678
707
  return server;
679
708
  }
680
709
 
681
- function callPrimaryTool(tool, input) {
710
+ function callPrimaryToolDirect(tool, input) {
682
711
  return new Promise((resolve, reject) => {
683
- const socket = net.createConnection({ host: "127.0.0.1", port: CONTROL_PORT });
712
+ const socket = net.createConnection({
713
+ host: "127.0.0.1",
714
+ port: CONTROL_PORT,
715
+ });
684
716
  const requestId = `rpc_${Date.now()}_${Math.random().toString(16).slice(2)}`;
685
717
  const rl = readline.createInterface({ input: socket });
686
718
 
@@ -720,6 +752,66 @@ function callPrimaryTool(tool, input) {
720
752
  });
721
753
  }
722
754
 
755
+ async function tryPromoteToPrimary() {
756
+ if (isPrimaryInstance) return true;
757
+
758
+ // Deduplicate concurrent promotion attempts
759
+ if (promotionInFlight) return promotionInFlight;
760
+
761
+ promotionInFlight = (async () => {
762
+ try {
763
+ await startPrimaryBackend();
764
+ log("promoted to primary instance");
765
+ return true;
766
+ } catch (err) {
767
+ if (err?.code === "EADDRINUSE") {
768
+ log("promotion failed: another primary appeared");
769
+ } else {
770
+ log("promotion failed:", err.message);
771
+ }
772
+ // Reset partial state
773
+ isPrimaryInstance = false;
774
+ localServer = null;
775
+ bridge = null;
776
+ controlServer = null;
777
+ return false;
778
+ } finally {
779
+ promotionInFlight = null;
780
+ }
781
+ })();
782
+
783
+ return promotionInFlight;
784
+ }
785
+
786
+ function executeToolLocally(toolName, input) {
787
+ const tool = createToolDefinitions().find((t) => t.name === toolName);
788
+ if (!tool) throw new Error(`Unknown tool '${toolName}'`);
789
+ return tool.handler(input || {});
790
+ }
791
+
792
+ async function callPrimaryTool(toolName, input) {
793
+ // If we're already promoted, go local
794
+ if (isPrimaryInstance) {
795
+ return executeToolLocally(toolName, input);
796
+ }
797
+
798
+ try {
799
+ return await callPrimaryToolDirect(toolName, input);
800
+ } catch (error) {
801
+ // If connection refused, the primary is gone — try to take over
802
+ if (error?.code === "ECONNREFUSED") {
803
+ log("primary unreachable, attempting promotion...");
804
+ const promoted = await tryPromoteToPrimary();
805
+ if (promoted) {
806
+ return executeToolLocally(toolName, input);
807
+ }
808
+ // Another primary appeared while we promoted — retry via proxy
809
+ return callPrimaryToolDirect(toolName, input);
810
+ }
811
+ throw error;
812
+ }
813
+ }
814
+
723
815
  async function ensureBackend() {
724
816
  try {
725
817
  await startPrimaryBackend();
@@ -728,6 +820,12 @@ async function ensureBackend() {
728
820
  throw error;
729
821
  }
730
822
 
823
+ // Clean up any partially created resources
824
+ isPrimaryInstance = false;
825
+ localServer = null;
826
+ bridge = null;
827
+ controlServer = null;
828
+
731
829
  log(`bridge already running on ${BRIDGE_PORT}; starting follower proxy`);
732
830
  }
733
831
  }