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.
- package/package.json +1 -1
- package/src/server.mjs +115 -17
package/package.json
CHANGED
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
|
|
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(
|
|
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:
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
646
|
+
const tool = createToolDefinitions().find(
|
|
647
|
+
(entry) => entry.name === request.tool,
|
|
648
|
+
);
|
|
635
649
|
if (!tool) {
|
|
636
|
-
sendJson(socket, {
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
controlServer.
|
|
657
|
-
|
|
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
|
|
710
|
+
function callPrimaryToolDirect(tool, input) {
|
|
682
711
|
return new Promise((resolve, reject) => {
|
|
683
|
-
const socket = net.createConnection({
|
|
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
|
}
|