construct-shader-graph-mcp 0.4.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 +62 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "construct-shader-graph-mcp",
3
- "version": "0.4.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);
@@ -706,7 +707,7 @@ function createProxyServer() {
706
707
  return server;
707
708
  }
708
709
 
709
- function callPrimaryTool(tool, input) {
710
+ function callPrimaryToolDirect(tool, input) {
710
711
  return new Promise((resolve, reject) => {
711
712
  const socket = net.createConnection({
712
713
  host: "127.0.0.1",
@@ -751,6 +752,66 @@ function callPrimaryTool(tool, input) {
751
752
  });
752
753
  }
753
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
+
754
815
  async function ensureBackend() {
755
816
  try {
756
817
  await startPrimaryBackend();