construct-shader-graph-mcp 0.2.0 → 0.4.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/README.md +14 -0
- package/package.json +1 -1
- package/src/server.mjs +604 -395
package/README.md
CHANGED
|
@@ -80,6 +80,7 @@ npm start
|
|
|
80
80
|
Optional environment variable:
|
|
81
81
|
|
|
82
82
|
- `MCP_BRIDGE_PORT` to change the browser bridge port from `6359`
|
|
83
|
+
- `MCP_CONTROL_PORT` to change the internal local control port used for multi-host sharing
|
|
83
84
|
|
|
84
85
|
Example:
|
|
85
86
|
|
|
@@ -87,6 +88,19 @@ Example:
|
|
|
87
88
|
MCP_BRIDGE_PORT=6360 construct-shader-graph-mcp
|
|
88
89
|
```
|
|
89
90
|
|
|
91
|
+
## Multiple MCP hosts
|
|
92
|
+
|
|
93
|
+
This package supports multiple MCP clients on the same machine.
|
|
94
|
+
|
|
95
|
+
- The first process becomes the primary backend and owns the browser bridge port.
|
|
96
|
+
- Later processes detect the running backend and act as lightweight followers.
|
|
97
|
+
- This allows tools like Claude Desktop and LM Studio to share the same live Construct Shader Graph connection.
|
|
98
|
+
|
|
99
|
+
By default:
|
|
100
|
+
|
|
101
|
+
- browser bridge: `6359`
|
|
102
|
+
- local control port: `6360`
|
|
103
|
+
|
|
90
104
|
## How it works
|
|
91
105
|
|
|
92
106
|
There are two sides to the integration:
|
package/package.json
CHANGED
package/src/server.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import net from "node:net";
|
|
2
4
|
import path from "node:path";
|
|
5
|
+
import readline from "node:readline";
|
|
3
6
|
import { fileURLToPath } from "node:url";
|
|
4
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -10,10 +13,15 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
10
13
|
const __dirname = path.dirname(__filename);
|
|
11
14
|
|
|
12
15
|
const BRIDGE_PORT = Number(process.env.MCP_BRIDGE_PORT || 6359);
|
|
16
|
+
const CONTROL_PORT = Number(process.env.MCP_CONTROL_PORT || BRIDGE_PORT + 1);
|
|
13
17
|
const SKILL_PATH = path.resolve(__dirname, "guidance/skill.md");
|
|
14
18
|
|
|
15
19
|
const sessions = new Map();
|
|
16
20
|
let selectedSessionId = null;
|
|
21
|
+
let localServer = null;
|
|
22
|
+
let bridge = null;
|
|
23
|
+
let controlServer = null;
|
|
24
|
+
let isPrimaryInstance = false;
|
|
17
25
|
|
|
18
26
|
function log(message, ...args) {
|
|
19
27
|
console.error(`[construct-shader-graph-mcp] ${message}`, ...args);
|
|
@@ -105,6 +113,10 @@ function ensureSelectedSession() {
|
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
function sendJson(socket, payload) {
|
|
116
|
+
socket.write(`${JSON.stringify(payload)}\n`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function sendWsJson(socket, payload) {
|
|
108
120
|
socket.send(JSON.stringify(payload));
|
|
109
121
|
}
|
|
110
122
|
|
|
@@ -132,7 +144,7 @@ function invokeSession(session, method, args = []) {
|
|
|
132
144
|
method,
|
|
133
145
|
});
|
|
134
146
|
|
|
135
|
-
|
|
147
|
+
sendWsJson(session.socket, {
|
|
136
148
|
type: "invoke",
|
|
137
149
|
requestId,
|
|
138
150
|
method,
|
|
@@ -141,427 +153,624 @@ function invokeSession(session, method, args = []) {
|
|
|
141
153
|
});
|
|
142
154
|
}
|
|
143
155
|
|
|
144
|
-
|
|
156
|
+
function createToolDefinitions() {
|
|
157
|
+
return [
|
|
158
|
+
{
|
|
159
|
+
name: "get_skill_guidance",
|
|
160
|
+
config: {
|
|
161
|
+
description:
|
|
162
|
+
"Return the full Construct Shader Graph MCP guidance and best practices.",
|
|
163
|
+
inputSchema: {},
|
|
164
|
+
outputSchema: {
|
|
165
|
+
title: z.string(),
|
|
166
|
+
content: z.string(),
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
handler: async () => {
|
|
170
|
+
const result = {
|
|
171
|
+
title: "Construct Shader Graph MCP Guidance",
|
|
172
|
+
content: loadSkillText(),
|
|
173
|
+
};
|
|
174
|
+
return {
|
|
175
|
+
content: [{ type: "text", text: result.content }],
|
|
176
|
+
structuredContent: result,
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: "list_projects",
|
|
182
|
+
config: {
|
|
183
|
+
description:
|
|
184
|
+
"List connected Construct Shader Graph tabs registered with the local bridge.",
|
|
185
|
+
inputSchema: {},
|
|
186
|
+
outputSchema: {
|
|
187
|
+
projects: z.array(
|
|
188
|
+
z.object({
|
|
189
|
+
sessionId: z.string(),
|
|
190
|
+
project: z.object({
|
|
191
|
+
name: z.string(),
|
|
192
|
+
version: z.string().optional(),
|
|
193
|
+
author: z.string().optional(),
|
|
194
|
+
category: z.string().optional(),
|
|
195
|
+
description: z.string().optional(),
|
|
196
|
+
shaderInfo: z.any().optional(),
|
|
197
|
+
}),
|
|
198
|
+
connectedAt: z.string(),
|
|
199
|
+
updatedAt: z.string(),
|
|
200
|
+
manifestVersion: z.string().nullable(),
|
|
201
|
+
methodCount: z.number(),
|
|
202
|
+
selected: z.boolean(),
|
|
203
|
+
}),
|
|
204
|
+
),
|
|
205
|
+
selectedSessionId: z.string().nullable(),
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
handler: async () => {
|
|
209
|
+
const projects = [...sessions.values()].map(getSessionSummary);
|
|
210
|
+
return {
|
|
211
|
+
content: [
|
|
212
|
+
{
|
|
213
|
+
type: "text",
|
|
214
|
+
text: JSON.stringify({ projects, selectedSessionId }, null, 2),
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
structuredContent: {
|
|
218
|
+
projects,
|
|
219
|
+
selectedSessionId,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: "select_project",
|
|
226
|
+
config: {
|
|
227
|
+
description:
|
|
228
|
+
"Choose which connected shader graph tab future MCP calls should target.",
|
|
229
|
+
inputSchema: {
|
|
230
|
+
sessionId: z
|
|
231
|
+
.string()
|
|
232
|
+
.describe("Session id returned by list_projects."),
|
|
233
|
+
},
|
|
234
|
+
outputSchema: {
|
|
235
|
+
sessionId: z.string(),
|
|
236
|
+
project: z.any(),
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
handler: async ({ sessionId }) => {
|
|
240
|
+
const session = ensureSession(sessionId);
|
|
241
|
+
selectedSessionId = sessionId;
|
|
242
|
+
const result = {
|
|
243
|
+
sessionId,
|
|
244
|
+
project: session.project,
|
|
245
|
+
};
|
|
246
|
+
return {
|
|
247
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
248
|
+
structuredContent: result,
|
|
249
|
+
};
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: "get_project_manifest",
|
|
254
|
+
config: {
|
|
255
|
+
description:
|
|
256
|
+
"Get the machine-readable API manifest for the selected project.",
|
|
257
|
+
inputSchema: {
|
|
258
|
+
sessionId: z
|
|
259
|
+
.string()
|
|
260
|
+
.optional()
|
|
261
|
+
.describe("Optional session id; defaults to the selected project."),
|
|
262
|
+
},
|
|
263
|
+
outputSchema: {
|
|
264
|
+
sessionId: z.string(),
|
|
265
|
+
project: z.any(),
|
|
266
|
+
manifest: z.any(),
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
handler: async ({ sessionId }) => {
|
|
270
|
+
const session = sessionId
|
|
271
|
+
? ensureSession(sessionId)
|
|
272
|
+
: ensureSelectedSession();
|
|
273
|
+
const result = {
|
|
274
|
+
sessionId: session.sessionId,
|
|
275
|
+
project: session.project,
|
|
276
|
+
manifest: session.manifest,
|
|
277
|
+
};
|
|
278
|
+
return {
|
|
279
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
280
|
+
structuredContent: result,
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: "call_project_method",
|
|
286
|
+
config: {
|
|
287
|
+
description:
|
|
288
|
+
"Call one method from the selected project's shaderGraphAPI and return its exact result.",
|
|
289
|
+
inputSchema: {
|
|
290
|
+
sessionId: z
|
|
291
|
+
.string()
|
|
292
|
+
.optional()
|
|
293
|
+
.describe("Optional session id; defaults to the selected project."),
|
|
294
|
+
method: z
|
|
295
|
+
.string()
|
|
296
|
+
.describe(
|
|
297
|
+
"Manifest method path, for example nodes.create or shader.getInfo.",
|
|
298
|
+
),
|
|
299
|
+
args: z
|
|
300
|
+
.array(z.any())
|
|
301
|
+
.optional()
|
|
302
|
+
.describe("Positional arguments to pass to the API method."),
|
|
303
|
+
},
|
|
304
|
+
outputSchema: {
|
|
305
|
+
sessionId: z.string(),
|
|
306
|
+
project: z.any(),
|
|
307
|
+
method: z.string(),
|
|
308
|
+
args: z.array(z.any()),
|
|
309
|
+
durationMs: z.number(),
|
|
310
|
+
result: z.any(),
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
handler: async ({ sessionId, method, args = [] }) => {
|
|
314
|
+
const session = sessionId
|
|
315
|
+
? ensureSession(sessionId)
|
|
316
|
+
: ensureSelectedSession();
|
|
317
|
+
const response = await invokeSession(session, method, args);
|
|
318
|
+
const result = {
|
|
319
|
+
sessionId: session.sessionId,
|
|
320
|
+
project: session.project,
|
|
321
|
+
method,
|
|
322
|
+
args,
|
|
323
|
+
durationMs: response.durationMs ?? 0,
|
|
324
|
+
result: response.result,
|
|
325
|
+
};
|
|
326
|
+
return {
|
|
327
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
328
|
+
structuredContent: result,
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
];
|
|
333
|
+
}
|
|
145
334
|
|
|
146
|
-
|
|
147
|
-
|
|
335
|
+
function registerResources(server) {
|
|
336
|
+
server.registerResource(
|
|
337
|
+
"skill-guidance",
|
|
338
|
+
"construct-shader-graph://guidance/skill",
|
|
339
|
+
{
|
|
340
|
+
title: "Construct Shader Graph MCP Guidance",
|
|
341
|
+
description:
|
|
342
|
+
"Full best-practices guidance for using Construct Shader Graph through MCP.",
|
|
343
|
+
mimeType: "text/markdown",
|
|
344
|
+
},
|
|
345
|
+
async (uri) => ({
|
|
346
|
+
contents: [
|
|
347
|
+
{
|
|
348
|
+
uri: uri.href,
|
|
349
|
+
mimeType: "text/markdown",
|
|
350
|
+
text: loadSkillText(),
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
server.registerResource(
|
|
357
|
+
"quickstart-guidance",
|
|
358
|
+
"construct-shader-graph://guidance/quickstart",
|
|
359
|
+
{
|
|
360
|
+
title: "Construct Shader Graph MCP Quickstart",
|
|
361
|
+
description: "Short workflow guidance for reliable MCP use.",
|
|
362
|
+
mimeType: "text/markdown",
|
|
363
|
+
},
|
|
364
|
+
async (uri) => ({
|
|
365
|
+
contents: [
|
|
366
|
+
{
|
|
367
|
+
uri: uri.href,
|
|
368
|
+
mimeType: "text/markdown",
|
|
369
|
+
text: loadQuickstartText(),
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
}),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
148
375
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
376
|
+
function registerPrompts(server) {
|
|
377
|
+
server.registerPrompt(
|
|
378
|
+
"work-with-shader-graph",
|
|
379
|
+
{
|
|
380
|
+
title: "Work With Shader Graph",
|
|
381
|
+
description:
|
|
382
|
+
"General prompt for safely inspecting and editing a Construct Shader Graph project.",
|
|
383
|
+
argsSchema: z.object({
|
|
384
|
+
task: z.string().optional().describe("The user task to accomplish."),
|
|
385
|
+
}),
|
|
386
|
+
},
|
|
387
|
+
({ task }) => ({
|
|
388
|
+
messages: [
|
|
389
|
+
{
|
|
390
|
+
role: "user",
|
|
391
|
+
content: {
|
|
392
|
+
type: "text",
|
|
393
|
+
text: `${getPromptPreamble()}\n\nFollow the full guidance resource if more detail is needed.\n\nTask: ${task || "Inspect the current project, understand its graph state, and proceed safely."}`,
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
}),
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
server.registerPrompt(
|
|
401
|
+
"inspect-graph",
|
|
402
|
+
{
|
|
403
|
+
title: "Inspect Graph",
|
|
404
|
+
description:
|
|
405
|
+
"Prompt for safely inspecting the current graph before any edits.",
|
|
406
|
+
argsSchema: z.object({
|
|
407
|
+
focus: z
|
|
408
|
+
.string()
|
|
409
|
+
.optional()
|
|
410
|
+
.describe(
|
|
411
|
+
"Optional area to inspect, like uniforms, preview, or node types.",
|
|
412
|
+
),
|
|
413
|
+
}),
|
|
414
|
+
},
|
|
415
|
+
({ focus }) => ({
|
|
416
|
+
messages: [
|
|
417
|
+
{
|
|
418
|
+
role: "user",
|
|
419
|
+
content: {
|
|
420
|
+
type: "text",
|
|
421
|
+
text: `${getPromptPreamble()}\n\nInspect the current graph without mutating it. Read nodes, wires, uniforms, shader info, and any relevant settings first. ${focus ? `Focus on: ${focus}.` : ""}`,
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
}),
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
server.registerPrompt(
|
|
429
|
+
"edit-graph-safely",
|
|
430
|
+
{
|
|
431
|
+
title: "Edit Graph Safely",
|
|
432
|
+
description: "Prompt for making a small validated graph edit with MCP.",
|
|
433
|
+
argsSchema: z.object({
|
|
434
|
+
task: z.string().describe("The graph edit to perform."),
|
|
435
|
+
}),
|
|
436
|
+
},
|
|
437
|
+
({ task }) => ({
|
|
438
|
+
messages: [
|
|
439
|
+
{
|
|
440
|
+
role: "user",
|
|
441
|
+
content: {
|
|
442
|
+
type: "text",
|
|
443
|
+
text: `${getPromptPreamble()}\n\nMake the smallest valid change that satisfies this task: ${task}\n\nBefore wiring, inspect ports. Before choosing a node type, use nodeTypes.search or nodeTypes.list. After each structural edit, re-read affected nodes or ports and validate preview/code if relevant.`,
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
}),
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
server.registerPrompt(
|
|
451
|
+
"debug-preview-errors",
|
|
452
|
+
{
|
|
453
|
+
title: "Debug Preview Errors",
|
|
454
|
+
description:
|
|
455
|
+
"Prompt for debugging generated code or preview issues in a shader graph project.",
|
|
456
|
+
argsSchema: z.object({
|
|
457
|
+
issue: z
|
|
458
|
+
.string()
|
|
459
|
+
.optional()
|
|
460
|
+
.describe("Optional description of the observed preview issue."),
|
|
461
|
+
}),
|
|
462
|
+
},
|
|
463
|
+
({ issue }) => ({
|
|
464
|
+
messages: [
|
|
465
|
+
{
|
|
466
|
+
role: "user",
|
|
467
|
+
content: {
|
|
468
|
+
type: "text",
|
|
469
|
+
text: `${getPromptPreamble()}\n\nDebug the current shader graph by inspecting shader.getGeneratedCode, preview.getErrors, preview settings, node preview, and ai.runDebugCheck. ${issue ? `Observed issue: ${issue}` : ""}`,
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
}),
|
|
474
|
+
);
|
|
475
|
+
}
|
|
156
476
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
477
|
+
function createLocalServer() {
|
|
478
|
+
const server = new McpServer({
|
|
479
|
+
name: "construct-shader-graph",
|
|
480
|
+
version: "0.1.0",
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
registerResources(server);
|
|
484
|
+
registerPrompts(server);
|
|
485
|
+
createToolDefinitions().forEach((tool) => {
|
|
486
|
+
server.registerTool(tool.name, tool.config, tool.handler);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
return server;
|
|
490
|
+
}
|
|
160
491
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
492
|
+
async function startPrimaryBackend() {
|
|
493
|
+
localServer = createLocalServer();
|
|
494
|
+
|
|
495
|
+
bridge = new WebSocketServer({ noServer: true });
|
|
496
|
+
const httpServer = http.createServer();
|
|
497
|
+
|
|
498
|
+
httpServer.on("upgrade", (request, socket, head) => {
|
|
499
|
+
bridge.handleUpgrade(request, socket, head, (ws) => {
|
|
500
|
+
bridge.emit("connection", ws, request);
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
await new Promise((resolve, reject) => {
|
|
505
|
+
httpServer.once("error", reject);
|
|
506
|
+
httpServer.listen(BRIDGE_PORT, "127.0.0.1", () => {
|
|
507
|
+
httpServer.off("error", reject);
|
|
508
|
+
resolve();
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
bridge.on("connection", (socket) => {
|
|
513
|
+
let activeSessionId = null;
|
|
514
|
+
|
|
515
|
+
socket.on("message", (raw) => {
|
|
516
|
+
let message;
|
|
517
|
+
try {
|
|
518
|
+
message = JSON.parse(String(raw));
|
|
519
|
+
} catch {
|
|
165
520
|
return;
|
|
166
521
|
}
|
|
167
522
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
523
|
+
if (!message || typeof message !== "object") {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (message.type === "register") {
|
|
528
|
+
const sessionId = String(message.sessionId || "").trim();
|
|
529
|
+
if (!sessionId) {
|
|
530
|
+
sendWsJson(socket, { type: "error", message: "Missing sessionId" });
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const session = {
|
|
535
|
+
sessionId,
|
|
536
|
+
socket,
|
|
537
|
+
project: message.project || {
|
|
538
|
+
name: "Untitled Shader",
|
|
539
|
+
version: "0.0.0.0",
|
|
540
|
+
},
|
|
541
|
+
manifest: message.manifest || null,
|
|
542
|
+
connectedAt: nowIso(),
|
|
543
|
+
updatedAt: nowIso(),
|
|
544
|
+
pending: new Map(),
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
sessions.set(sessionId, session);
|
|
548
|
+
activeSessionId = sessionId;
|
|
549
|
+
if (!selectedSessionId) {
|
|
550
|
+
selectedSessionId = sessionId;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
log(
|
|
554
|
+
`registered ${sessionId} (${session.project?.name || "Untitled Shader"})`,
|
|
555
|
+
);
|
|
556
|
+
sendWsJson(socket, {
|
|
557
|
+
type: "registered",
|
|
558
|
+
sessionId,
|
|
559
|
+
selected: selectedSessionId === sessionId,
|
|
560
|
+
});
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (!activeSessionId) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const session = sessions.get(activeSessionId);
|
|
569
|
+
if (!session) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
session.updatedAt = nowIso();
|
|
574
|
+
|
|
575
|
+
if (message.type === "project-updated") {
|
|
576
|
+
session.project = message.project || session.project;
|
|
577
|
+
session.manifest = message.manifest || session.manifest;
|
|
578
|
+
return;
|
|
185
579
|
}
|
|
186
580
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
581
|
+
if (message.type === "result") {
|
|
582
|
+
const pending = session.pending.get(message.requestId);
|
|
583
|
+
if (!pending) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
clearTimeout(pending.timeoutId);
|
|
588
|
+
session.pending.delete(message.requestId);
|
|
589
|
+
|
|
590
|
+
if (message.ok) {
|
|
591
|
+
pending.resolve(message);
|
|
592
|
+
} else {
|
|
593
|
+
const error = new Error(
|
|
594
|
+
message.error?.message || `Call '${pending.method}' failed`,
|
|
595
|
+
);
|
|
596
|
+
error.stack = message.error?.stack || error.stack;
|
|
597
|
+
pending.reject(error);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
socket.on("close", () => {
|
|
603
|
+
if (!activeSessionId) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const session = sessions.get(activeSessionId);
|
|
608
|
+
if (!session) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
for (const pending of session.pending.values()) {
|
|
613
|
+
clearTimeout(pending.timeoutId);
|
|
614
|
+
pending.reject(new Error(`Session '${activeSessionId}' disconnected`));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
sessions.delete(activeSessionId);
|
|
618
|
+
if (selectedSessionId === activeSessionId) {
|
|
619
|
+
selectedSessionId = sessions.keys().next().value || null;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
log(`disconnected ${activeSessionId}`);
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
log(`bridge listening on ws://127.0.0.1:${BRIDGE_PORT}`);
|
|
627
|
+
|
|
628
|
+
controlServer = net.createServer((socket) => {
|
|
629
|
+
const rl = readline.createInterface({ input: socket });
|
|
630
|
+
|
|
631
|
+
rl.on("line", async (line) => {
|
|
632
|
+
let request;
|
|
633
|
+
try {
|
|
634
|
+
request = JSON.parse(line);
|
|
635
|
+
} catch {
|
|
636
|
+
sendJson(socket, { ok: false, error: "Invalid JSON request" });
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (!request || request.type !== "rpc") {
|
|
641
|
+
sendJson(socket, { ok: false, error: "Invalid control request" });
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const tool = createToolDefinitions().find(
|
|
646
|
+
(entry) => entry.name === request.tool,
|
|
647
|
+
);
|
|
648
|
+
if (!tool) {
|
|
649
|
+
sendJson(socket, {
|
|
650
|
+
id: request.id,
|
|
651
|
+
ok: false,
|
|
652
|
+
error: `Unknown tool '${request.tool}'`,
|
|
653
|
+
});
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
const response = await tool.handler(request.input || {});
|
|
659
|
+
sendJson(socket, { id: request.id, ok: true, response });
|
|
660
|
+
} catch (error) {
|
|
661
|
+
sendJson(socket, {
|
|
662
|
+
id: request.id,
|
|
663
|
+
ok: false,
|
|
664
|
+
error: error instanceof Error ? error.message : String(error),
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
try {
|
|
671
|
+
await new Promise((resolve, reject) => {
|
|
672
|
+
controlServer.once("error", reject);
|
|
673
|
+
controlServer.listen(CONTROL_PORT, "127.0.0.1", () => {
|
|
674
|
+
controlServer.off("error", reject);
|
|
675
|
+
resolve();
|
|
192
676
|
});
|
|
193
|
-
|
|
194
|
-
|
|
677
|
+
});
|
|
678
|
+
} catch (err) {
|
|
679
|
+
httpServer.close();
|
|
680
|
+
bridge.close();
|
|
681
|
+
throw err;
|
|
682
|
+
}
|
|
195
683
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
684
|
+
// Permanent error handlers to prevent unhandled error crashes
|
|
685
|
+
httpServer.on("error", (err) => log("bridge http error:", err.message));
|
|
686
|
+
controlServer.on("error", (err) => log("control server error:", err.message));
|
|
199
687
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
688
|
+
isPrimaryInstance = true;
|
|
689
|
+
log(`control listening on tcp://127.0.0.1:${CONTROL_PORT}`);
|
|
690
|
+
}
|
|
204
691
|
|
|
205
|
-
|
|
692
|
+
function createProxyServer() {
|
|
693
|
+
const server = new McpServer({
|
|
694
|
+
name: "construct-shader-graph",
|
|
695
|
+
version: "0.1.0",
|
|
696
|
+
});
|
|
206
697
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
698
|
+
registerResources(server);
|
|
699
|
+
registerPrompts(server);
|
|
700
|
+
createToolDefinitions().forEach((tool) => {
|
|
701
|
+
server.registerTool(tool.name, tool.config, async (input = {}) => {
|
|
702
|
+
return callPrimaryTool(tool.name, input);
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
return server;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function callPrimaryTool(tool, input) {
|
|
710
|
+
return new Promise((resolve, reject) => {
|
|
711
|
+
const socket = net.createConnection({
|
|
712
|
+
host: "127.0.0.1",
|
|
713
|
+
port: CONTROL_PORT,
|
|
714
|
+
});
|
|
715
|
+
const requestId = `rpc_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
716
|
+
const rl = readline.createInterface({ input: socket });
|
|
717
|
+
|
|
718
|
+
socket.on("error", (error) => {
|
|
719
|
+
reject(error);
|
|
720
|
+
});
|
|
212
721
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
722
|
+
rl.on("line", (line) => {
|
|
723
|
+
let response;
|
|
724
|
+
try {
|
|
725
|
+
response = JSON.parse(line);
|
|
726
|
+
} catch {
|
|
727
|
+
reject(new Error("Invalid control response"));
|
|
728
|
+
socket.destroy();
|
|
216
729
|
return;
|
|
217
730
|
}
|
|
218
731
|
|
|
219
|
-
|
|
220
|
-
|
|
732
|
+
if (response.id !== requestId) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
221
735
|
|
|
222
|
-
|
|
223
|
-
|
|
736
|
+
rl.close();
|
|
737
|
+
socket.end();
|
|
738
|
+
if (response.ok) {
|
|
739
|
+
resolve(response.response);
|
|
224
740
|
} else {
|
|
225
|
-
|
|
226
|
-
message.error?.message || `Call '${pending.method}' failed`,
|
|
227
|
-
);
|
|
228
|
-
error.stack = message.error?.stack || error.stack;
|
|
229
|
-
pending.reject(error);
|
|
741
|
+
reject(new Error(response.error || "Primary MCP request failed"));
|
|
230
742
|
}
|
|
231
|
-
}
|
|
232
|
-
});
|
|
743
|
+
});
|
|
233
744
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
745
|
+
sendJson(socket, {
|
|
746
|
+
type: "rpc",
|
|
747
|
+
id: requestId,
|
|
748
|
+
tool,
|
|
749
|
+
input,
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
}
|
|
238
753
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
754
|
+
async function ensureBackend() {
|
|
755
|
+
try {
|
|
756
|
+
await startPrimaryBackend();
|
|
757
|
+
} catch (error) {
|
|
758
|
+
if (error?.code !== "EADDRINUSE") {
|
|
759
|
+
throw error;
|
|
242
760
|
}
|
|
243
761
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
762
|
+
// Clean up any partially created resources
|
|
763
|
+
isPrimaryInstance = false;
|
|
764
|
+
localServer = null;
|
|
765
|
+
bridge = null;
|
|
766
|
+
controlServer = null;
|
|
248
767
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
768
|
+
log(`bridge already running on ${BRIDGE_PORT}; starting follower proxy`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
253
771
|
|
|
254
|
-
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
log(`bridge listening on ws://127.0.0.1:${BRIDGE_PORT}`);
|
|
259
|
-
|
|
260
|
-
const server = new McpServer({
|
|
261
|
-
name: "construct-shader-graph",
|
|
262
|
-
version: "0.1.0",
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
server.registerResource(
|
|
266
|
-
"skill-guidance",
|
|
267
|
-
"construct-shader-graph://guidance/skill",
|
|
268
|
-
{
|
|
269
|
-
title: "Construct Shader Graph MCP Guidance",
|
|
270
|
-
description: "Full best-practices guidance for using Construct Shader Graph through MCP.",
|
|
271
|
-
mimeType: "text/markdown",
|
|
272
|
-
},
|
|
273
|
-
async (uri) => ({
|
|
274
|
-
contents: [
|
|
275
|
-
{
|
|
276
|
-
uri: uri.href,
|
|
277
|
-
mimeType: "text/markdown",
|
|
278
|
-
text: loadSkillText(),
|
|
279
|
-
},
|
|
280
|
-
],
|
|
281
|
-
}),
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
server.registerResource(
|
|
285
|
-
"quickstart-guidance",
|
|
286
|
-
"construct-shader-graph://guidance/quickstart",
|
|
287
|
-
{
|
|
288
|
-
title: "Construct Shader Graph MCP Quickstart",
|
|
289
|
-
description: "Short workflow guidance for reliable MCP use.",
|
|
290
|
-
mimeType: "text/markdown",
|
|
291
|
-
},
|
|
292
|
-
async (uri) => ({
|
|
293
|
-
contents: [
|
|
294
|
-
{
|
|
295
|
-
uri: uri.href,
|
|
296
|
-
mimeType: "text/markdown",
|
|
297
|
-
text: loadQuickstartText(),
|
|
298
|
-
},
|
|
299
|
-
],
|
|
300
|
-
}),
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
server.registerPrompt(
|
|
304
|
-
"work-with-shader-graph",
|
|
305
|
-
{
|
|
306
|
-
title: "Work With Shader Graph",
|
|
307
|
-
description: "General prompt for safely inspecting and editing a Construct Shader Graph project.",
|
|
308
|
-
argsSchema: z.object({
|
|
309
|
-
task: z.string().optional().describe("The user task to accomplish."),
|
|
310
|
-
}),
|
|
311
|
-
},
|
|
312
|
-
({ task }) => ({
|
|
313
|
-
messages: [
|
|
314
|
-
{
|
|
315
|
-
role: "user",
|
|
316
|
-
content: {
|
|
317
|
-
type: "text",
|
|
318
|
-
text: `${getPromptPreamble()}\n\nFollow the full guidance resource if more detail is needed.\n\nTask: ${task || "Inspect the current project, understand its graph state, and proceed safely."}`,
|
|
319
|
-
},
|
|
320
|
-
},
|
|
321
|
-
],
|
|
322
|
-
}),
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
server.registerPrompt(
|
|
326
|
-
"inspect-graph",
|
|
327
|
-
{
|
|
328
|
-
title: "Inspect Graph",
|
|
329
|
-
description: "Prompt for safely inspecting the current graph before any edits.",
|
|
330
|
-
argsSchema: z.object({
|
|
331
|
-
focus: z.string().optional().describe("Optional area to inspect, like uniforms, preview, or node types."),
|
|
332
|
-
}),
|
|
333
|
-
},
|
|
334
|
-
({ focus }) => ({
|
|
335
|
-
messages: [
|
|
336
|
-
{
|
|
337
|
-
role: "user",
|
|
338
|
-
content: {
|
|
339
|
-
type: "text",
|
|
340
|
-
text: `${getPromptPreamble()}\n\nInspect the current graph without mutating it. Read nodes, wires, uniforms, shader info, and any relevant settings first. ${focus ? `Focus on: ${focus}.` : ""}`,
|
|
341
|
-
},
|
|
342
|
-
},
|
|
343
|
-
],
|
|
344
|
-
}),
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
server.registerPrompt(
|
|
348
|
-
"edit-graph-safely",
|
|
349
|
-
{
|
|
350
|
-
title: "Edit Graph Safely",
|
|
351
|
-
description: "Prompt for making a small validated graph edit with MCP.",
|
|
352
|
-
argsSchema: z.object({
|
|
353
|
-
task: z.string().describe("The graph edit to perform."),
|
|
354
|
-
}),
|
|
355
|
-
},
|
|
356
|
-
({ task }) => ({
|
|
357
|
-
messages: [
|
|
358
|
-
{
|
|
359
|
-
role: "user",
|
|
360
|
-
content: {
|
|
361
|
-
type: "text",
|
|
362
|
-
text: `${getPromptPreamble()}\n\nMake the smallest valid change that satisfies this task: ${task}\n\nBefore wiring, inspect ports. Before choosing a node type, use nodeTypes.search or nodeTypes.list. After each structural edit, re-read affected nodes or ports and validate preview/code if relevant.`,
|
|
363
|
-
},
|
|
364
|
-
},
|
|
365
|
-
],
|
|
366
|
-
}),
|
|
367
|
-
);
|
|
368
|
-
|
|
369
|
-
server.registerPrompt(
|
|
370
|
-
"debug-preview-errors",
|
|
371
|
-
{
|
|
372
|
-
title: "Debug Preview Errors",
|
|
373
|
-
description: "Prompt for debugging generated code or preview issues in a shader graph project.",
|
|
374
|
-
argsSchema: z.object({
|
|
375
|
-
issue: z.string().optional().describe("Optional description of the observed preview issue."),
|
|
376
|
-
}),
|
|
377
|
-
},
|
|
378
|
-
({ issue }) => ({
|
|
379
|
-
messages: [
|
|
380
|
-
{
|
|
381
|
-
role: "user",
|
|
382
|
-
content: {
|
|
383
|
-
type: "text",
|
|
384
|
-
text: `${getPromptPreamble()}\n\nDebug the current shader graph by inspecting shader.getGeneratedCode, preview.getErrors, preview settings, node preview, and ai.runDebugCheck. ${issue ? `Observed issue: ${issue}` : ""}`,
|
|
385
|
-
},
|
|
386
|
-
},
|
|
387
|
-
],
|
|
388
|
-
}),
|
|
389
|
-
);
|
|
390
|
-
|
|
391
|
-
server.registerTool(
|
|
392
|
-
"get_skill_guidance",
|
|
393
|
-
{
|
|
394
|
-
description:
|
|
395
|
-
"Return the full Construct Shader Graph MCP guidance and best practices.",
|
|
396
|
-
inputSchema: {},
|
|
397
|
-
outputSchema: {
|
|
398
|
-
title: z.string(),
|
|
399
|
-
content: z.string(),
|
|
400
|
-
},
|
|
401
|
-
},
|
|
402
|
-
async () => {
|
|
403
|
-
const result = {
|
|
404
|
-
title: "Construct Shader Graph MCP Skill",
|
|
405
|
-
content: loadSkillText(),
|
|
406
|
-
};
|
|
407
|
-
return {
|
|
408
|
-
content: [{ type: "text", text: result.content }],
|
|
409
|
-
structuredContent: result,
|
|
410
|
-
};
|
|
411
|
-
},
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
server.registerTool(
|
|
415
|
-
"list_projects",
|
|
416
|
-
{
|
|
417
|
-
description:
|
|
418
|
-
"List connected Construct Shader Graph tabs registered with the local bridge.",
|
|
419
|
-
inputSchema: {},
|
|
420
|
-
outputSchema: {
|
|
421
|
-
projects: z.array(
|
|
422
|
-
z.object({
|
|
423
|
-
sessionId: z.string(),
|
|
424
|
-
project: z.object({
|
|
425
|
-
name: z.string(),
|
|
426
|
-
version: z.string().optional(),
|
|
427
|
-
author: z.string().optional(),
|
|
428
|
-
category: z.string().optional(),
|
|
429
|
-
description: z.string().optional(),
|
|
430
|
-
shaderInfo: z.any().optional(),
|
|
431
|
-
}),
|
|
432
|
-
connectedAt: z.string(),
|
|
433
|
-
updatedAt: z.string(),
|
|
434
|
-
manifestVersion: z.string().nullable(),
|
|
435
|
-
methodCount: z.number(),
|
|
436
|
-
selected: z.boolean(),
|
|
437
|
-
}),
|
|
438
|
-
),
|
|
439
|
-
selectedSessionId: z.string().nullable(),
|
|
440
|
-
},
|
|
441
|
-
},
|
|
442
|
-
async () => {
|
|
443
|
-
const projects = [...sessions.values()].map(getSessionSummary);
|
|
444
|
-
return {
|
|
445
|
-
content: [
|
|
446
|
-
{
|
|
447
|
-
type: "text",
|
|
448
|
-
text: JSON.stringify({ projects, selectedSessionId }, null, 2),
|
|
449
|
-
},
|
|
450
|
-
],
|
|
451
|
-
structuredContent: {
|
|
452
|
-
projects,
|
|
453
|
-
selectedSessionId,
|
|
454
|
-
},
|
|
455
|
-
};
|
|
456
|
-
},
|
|
457
|
-
);
|
|
458
|
-
|
|
459
|
-
server.registerTool(
|
|
460
|
-
"select_project",
|
|
461
|
-
{
|
|
462
|
-
description:
|
|
463
|
-
"Choose which connected shader graph tab future MCP calls should target.",
|
|
464
|
-
inputSchema: {
|
|
465
|
-
sessionId: z.string().describe("Session id returned by list_projects."),
|
|
466
|
-
},
|
|
467
|
-
outputSchema: {
|
|
468
|
-
sessionId: z.string(),
|
|
469
|
-
project: z.any(),
|
|
470
|
-
},
|
|
471
|
-
},
|
|
472
|
-
async ({ sessionId }) => {
|
|
473
|
-
const session = ensureSession(sessionId);
|
|
474
|
-
selectedSessionId = sessionId;
|
|
475
|
-
const result = {
|
|
476
|
-
sessionId,
|
|
477
|
-
project: session.project,
|
|
478
|
-
};
|
|
479
|
-
return {
|
|
480
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
481
|
-
structuredContent: result,
|
|
482
|
-
};
|
|
483
|
-
},
|
|
484
|
-
);
|
|
485
|
-
|
|
486
|
-
server.registerTool(
|
|
487
|
-
"get_project_manifest",
|
|
488
|
-
{
|
|
489
|
-
description:
|
|
490
|
-
"Get the machine-readable API manifest for the selected project.",
|
|
491
|
-
inputSchema: {
|
|
492
|
-
sessionId: z
|
|
493
|
-
.string()
|
|
494
|
-
.optional()
|
|
495
|
-
.describe("Optional session id; defaults to the selected project."),
|
|
496
|
-
},
|
|
497
|
-
outputSchema: {
|
|
498
|
-
sessionId: z.string(),
|
|
499
|
-
project: z.any(),
|
|
500
|
-
manifest: z.any(),
|
|
501
|
-
},
|
|
502
|
-
},
|
|
503
|
-
async ({ sessionId }) => {
|
|
504
|
-
const session = sessionId
|
|
505
|
-
? ensureSession(sessionId)
|
|
506
|
-
: ensureSelectedSession();
|
|
507
|
-
const result = {
|
|
508
|
-
sessionId: session.sessionId,
|
|
509
|
-
project: session.project,
|
|
510
|
-
manifest: session.manifest,
|
|
511
|
-
};
|
|
512
|
-
return {
|
|
513
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
514
|
-
structuredContent: result,
|
|
515
|
-
};
|
|
516
|
-
},
|
|
517
|
-
);
|
|
518
|
-
|
|
519
|
-
server.registerTool(
|
|
520
|
-
"call_project_method",
|
|
521
|
-
{
|
|
522
|
-
description:
|
|
523
|
-
"Call one method from the selected project's shaderGraphAPI and return its exact result.",
|
|
524
|
-
inputSchema: {
|
|
525
|
-
sessionId: z
|
|
526
|
-
.string()
|
|
527
|
-
.optional()
|
|
528
|
-
.describe("Optional session id; defaults to the selected project."),
|
|
529
|
-
method: z
|
|
530
|
-
.string()
|
|
531
|
-
.describe("Manifest method path, for example nodes.create or shader.getInfo."),
|
|
532
|
-
args: z
|
|
533
|
-
.array(z.any())
|
|
534
|
-
.optional()
|
|
535
|
-
.describe("Positional arguments to pass to the API method."),
|
|
536
|
-
},
|
|
537
|
-
outputSchema: {
|
|
538
|
-
sessionId: z.string(),
|
|
539
|
-
project: z.any(),
|
|
540
|
-
method: z.string(),
|
|
541
|
-
args: z.array(z.any()),
|
|
542
|
-
durationMs: z.number(),
|
|
543
|
-
result: z.any(),
|
|
544
|
-
},
|
|
545
|
-
},
|
|
546
|
-
async ({ sessionId, method, args = [] }) => {
|
|
547
|
-
const session = sessionId
|
|
548
|
-
? ensureSession(sessionId)
|
|
549
|
-
: ensureSelectedSession();
|
|
550
|
-
const response = await invokeSession(session, method, args);
|
|
551
|
-
const result = {
|
|
552
|
-
sessionId: session.sessionId,
|
|
553
|
-
project: session.project,
|
|
554
|
-
method,
|
|
555
|
-
args,
|
|
556
|
-
durationMs: response.durationMs ?? 0,
|
|
557
|
-
result: response.result,
|
|
558
|
-
};
|
|
559
|
-
return {
|
|
560
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
561
|
-
structuredContent: result,
|
|
562
|
-
};
|
|
563
|
-
},
|
|
564
|
-
);
|
|
772
|
+
await ensureBackend();
|
|
565
773
|
|
|
774
|
+
const server = isPrimaryInstance ? localServer : createProxyServer();
|
|
566
775
|
const transport = new StdioServerTransport();
|
|
567
776
|
await server.connect(transport);
|