construct-shader-graph-mcp 0.2.0 → 0.3.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 +568 -396
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,587 @@ 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.string().describe("Session id returned by list_projects."),
|
|
231
|
+
},
|
|
232
|
+
outputSchema: {
|
|
233
|
+
sessionId: z.string(),
|
|
234
|
+
project: z.any(),
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
handler: async ({ sessionId }) => {
|
|
238
|
+
const session = ensureSession(sessionId);
|
|
239
|
+
selectedSessionId = sessionId;
|
|
240
|
+
const result = {
|
|
241
|
+
sessionId,
|
|
242
|
+
project: session.project,
|
|
243
|
+
};
|
|
244
|
+
return {
|
|
245
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
246
|
+
structuredContent: result,
|
|
247
|
+
};
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: "get_project_manifest",
|
|
252
|
+
config: {
|
|
253
|
+
description:
|
|
254
|
+
"Get the machine-readable API manifest for the selected project.",
|
|
255
|
+
inputSchema: {
|
|
256
|
+
sessionId: z
|
|
257
|
+
.string()
|
|
258
|
+
.optional()
|
|
259
|
+
.describe("Optional session id; defaults to the selected project."),
|
|
260
|
+
},
|
|
261
|
+
outputSchema: {
|
|
262
|
+
sessionId: z.string(),
|
|
263
|
+
project: z.any(),
|
|
264
|
+
manifest: z.any(),
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
handler: async ({ sessionId }) => {
|
|
268
|
+
const session = sessionId
|
|
269
|
+
? ensureSession(sessionId)
|
|
270
|
+
: ensureSelectedSession();
|
|
271
|
+
const result = {
|
|
272
|
+
sessionId: session.sessionId,
|
|
273
|
+
project: session.project,
|
|
274
|
+
manifest: session.manifest,
|
|
275
|
+
};
|
|
276
|
+
return {
|
|
277
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
278
|
+
structuredContent: result,
|
|
279
|
+
};
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: "call_project_method",
|
|
284
|
+
config: {
|
|
285
|
+
description:
|
|
286
|
+
"Call one method from the selected project's shaderGraphAPI and return its exact result.",
|
|
287
|
+
inputSchema: {
|
|
288
|
+
sessionId: z
|
|
289
|
+
.string()
|
|
290
|
+
.optional()
|
|
291
|
+
.describe("Optional session id; defaults to the selected project."),
|
|
292
|
+
method: z
|
|
293
|
+
.string()
|
|
294
|
+
.describe("Manifest method path, for example nodes.create or shader.getInfo."),
|
|
295
|
+
args: z
|
|
296
|
+
.array(z.any())
|
|
297
|
+
.optional()
|
|
298
|
+
.describe("Positional arguments to pass to the API method."),
|
|
299
|
+
},
|
|
300
|
+
outputSchema: {
|
|
301
|
+
sessionId: z.string(),
|
|
302
|
+
project: z.any(),
|
|
303
|
+
method: z.string(),
|
|
304
|
+
args: z.array(z.any()),
|
|
305
|
+
durationMs: z.number(),
|
|
306
|
+
result: z.any(),
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
handler: async ({ sessionId, method, args = [] }) => {
|
|
310
|
+
const session = sessionId
|
|
311
|
+
? ensureSession(sessionId)
|
|
312
|
+
: ensureSelectedSession();
|
|
313
|
+
const response = await invokeSession(session, method, args);
|
|
314
|
+
const result = {
|
|
315
|
+
sessionId: session.sessionId,
|
|
316
|
+
project: session.project,
|
|
317
|
+
method,
|
|
318
|
+
args,
|
|
319
|
+
durationMs: response.durationMs ?? 0,
|
|
320
|
+
result: response.result,
|
|
321
|
+
};
|
|
322
|
+
return {
|
|
323
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
324
|
+
structuredContent: result,
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
];
|
|
329
|
+
}
|
|
145
330
|
|
|
146
|
-
|
|
147
|
-
|
|
331
|
+
function registerResources(server) {
|
|
332
|
+
server.registerResource(
|
|
333
|
+
"skill-guidance",
|
|
334
|
+
"construct-shader-graph://guidance/skill",
|
|
335
|
+
{
|
|
336
|
+
title: "Construct Shader Graph MCP Guidance",
|
|
337
|
+
description:
|
|
338
|
+
"Full best-practices guidance for using Construct Shader Graph through MCP.",
|
|
339
|
+
mimeType: "text/markdown",
|
|
340
|
+
},
|
|
341
|
+
async (uri) => ({
|
|
342
|
+
contents: [
|
|
343
|
+
{
|
|
344
|
+
uri: uri.href,
|
|
345
|
+
mimeType: "text/markdown",
|
|
346
|
+
text: loadSkillText(),
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
server.registerResource(
|
|
353
|
+
"quickstart-guidance",
|
|
354
|
+
"construct-shader-graph://guidance/quickstart",
|
|
355
|
+
{
|
|
356
|
+
title: "Construct Shader Graph MCP Quickstart",
|
|
357
|
+
description: "Short workflow guidance for reliable MCP use.",
|
|
358
|
+
mimeType: "text/markdown",
|
|
359
|
+
},
|
|
360
|
+
async (uri) => ({
|
|
361
|
+
contents: [
|
|
362
|
+
{
|
|
363
|
+
uri: uri.href,
|
|
364
|
+
mimeType: "text/markdown",
|
|
365
|
+
text: loadQuickstartText(),
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
}),
|
|
369
|
+
);
|
|
370
|
+
}
|
|
148
371
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
372
|
+
function registerPrompts(server) {
|
|
373
|
+
server.registerPrompt(
|
|
374
|
+
"work-with-shader-graph",
|
|
375
|
+
{
|
|
376
|
+
title: "Work With Shader Graph",
|
|
377
|
+
description:
|
|
378
|
+
"General prompt for safely inspecting and editing a Construct Shader Graph project.",
|
|
379
|
+
argsSchema: z.object({
|
|
380
|
+
task: z.string().optional().describe("The user task to accomplish."),
|
|
381
|
+
}),
|
|
382
|
+
},
|
|
383
|
+
({ task }) => ({
|
|
384
|
+
messages: [
|
|
385
|
+
{
|
|
386
|
+
role: "user",
|
|
387
|
+
content: {
|
|
388
|
+
type: "text",
|
|
389
|
+
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."}`,
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
server.registerPrompt(
|
|
397
|
+
"inspect-graph",
|
|
398
|
+
{
|
|
399
|
+
title: "Inspect Graph",
|
|
400
|
+
description: "Prompt for safely inspecting the current graph before any edits.",
|
|
401
|
+
argsSchema: z.object({
|
|
402
|
+
focus: z
|
|
403
|
+
.string()
|
|
404
|
+
.optional()
|
|
405
|
+
.describe("Optional area to inspect, like uniforms, preview, or node types."),
|
|
406
|
+
}),
|
|
407
|
+
},
|
|
408
|
+
({ focus }) => ({
|
|
409
|
+
messages: [
|
|
410
|
+
{
|
|
411
|
+
role: "user",
|
|
412
|
+
content: {
|
|
413
|
+
type: "text",
|
|
414
|
+
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}.` : ""}`,
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
}),
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
server.registerPrompt(
|
|
422
|
+
"edit-graph-safely",
|
|
423
|
+
{
|
|
424
|
+
title: "Edit Graph Safely",
|
|
425
|
+
description: "Prompt for making a small validated graph edit with MCP.",
|
|
426
|
+
argsSchema: z.object({
|
|
427
|
+
task: z.string().describe("The graph edit to perform."),
|
|
428
|
+
}),
|
|
429
|
+
},
|
|
430
|
+
({ task }) => ({
|
|
431
|
+
messages: [
|
|
432
|
+
{
|
|
433
|
+
role: "user",
|
|
434
|
+
content: {
|
|
435
|
+
type: "text",
|
|
436
|
+
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.`,
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
}),
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
server.registerPrompt(
|
|
444
|
+
"debug-preview-errors",
|
|
445
|
+
{
|
|
446
|
+
title: "Debug Preview Errors",
|
|
447
|
+
description:
|
|
448
|
+
"Prompt for debugging generated code or preview issues in a shader graph project.",
|
|
449
|
+
argsSchema: z.object({
|
|
450
|
+
issue: z.string().optional().describe("Optional description of the observed preview issue."),
|
|
451
|
+
}),
|
|
452
|
+
},
|
|
453
|
+
({ issue }) => ({
|
|
454
|
+
messages: [
|
|
455
|
+
{
|
|
456
|
+
role: "user",
|
|
457
|
+
content: {
|
|
458
|
+
type: "text",
|
|
459
|
+
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}` : ""}`,
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
}),
|
|
464
|
+
);
|
|
465
|
+
}
|
|
156
466
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
467
|
+
function createLocalServer() {
|
|
468
|
+
const server = new McpServer({
|
|
469
|
+
name: "construct-shader-graph",
|
|
470
|
+
version: "0.1.0",
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
registerResources(server);
|
|
474
|
+
registerPrompts(server);
|
|
475
|
+
createToolDefinitions().forEach((tool) => {
|
|
476
|
+
server.registerTool(tool.name, tool.config, tool.handler);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
return server;
|
|
480
|
+
}
|
|
160
481
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
482
|
+
async function startPrimaryBackend() {
|
|
483
|
+
isPrimaryInstance = true;
|
|
484
|
+
localServer = createLocalServer();
|
|
485
|
+
|
|
486
|
+
bridge = new WebSocketServer({ noServer: true });
|
|
487
|
+
const httpServer = http.createServer();
|
|
488
|
+
|
|
489
|
+
httpServer.on("upgrade", (request, socket, head) => {
|
|
490
|
+
bridge.handleUpgrade(request, socket, head, (ws) => {
|
|
491
|
+
bridge.emit("connection", ws, request);
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
await new Promise((resolve, reject) => {
|
|
496
|
+
httpServer.once("error", reject);
|
|
497
|
+
httpServer.listen(BRIDGE_PORT, "127.0.0.1", () => {
|
|
498
|
+
httpServer.off("error", reject);
|
|
499
|
+
resolve();
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
bridge.on("connection", (socket) => {
|
|
504
|
+
let activeSessionId = null;
|
|
505
|
+
|
|
506
|
+
socket.on("message", (raw) => {
|
|
507
|
+
let message;
|
|
508
|
+
try {
|
|
509
|
+
message = JSON.parse(String(raw));
|
|
510
|
+
} catch {
|
|
165
511
|
return;
|
|
166
512
|
}
|
|
167
513
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
socket,
|
|
171
|
-
project: message.project || {
|
|
172
|
-
name: "Untitled Shader",
|
|
173
|
-
version: "0.0.0.0",
|
|
174
|
-
},
|
|
175
|
-
manifest: message.manifest || null,
|
|
176
|
-
connectedAt: nowIso(),
|
|
177
|
-
updatedAt: nowIso(),
|
|
178
|
-
pending: new Map(),
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
sessions.set(sessionId, session);
|
|
182
|
-
activeSessionId = sessionId;
|
|
183
|
-
if (!selectedSessionId) {
|
|
184
|
-
selectedSessionId = sessionId;
|
|
514
|
+
if (!message || typeof message !== "object") {
|
|
515
|
+
return;
|
|
185
516
|
}
|
|
186
517
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
518
|
+
if (message.type === "register") {
|
|
519
|
+
const sessionId = String(message.sessionId || "").trim();
|
|
520
|
+
if (!sessionId) {
|
|
521
|
+
sendWsJson(socket, { type: "error", message: "Missing sessionId" });
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const session = {
|
|
526
|
+
sessionId,
|
|
527
|
+
socket,
|
|
528
|
+
project: message.project || {
|
|
529
|
+
name: "Untitled Shader",
|
|
530
|
+
version: "0.0.0.0",
|
|
531
|
+
},
|
|
532
|
+
manifest: message.manifest || null,
|
|
533
|
+
connectedAt: nowIso(),
|
|
534
|
+
updatedAt: nowIso(),
|
|
535
|
+
pending: new Map(),
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
sessions.set(sessionId, session);
|
|
539
|
+
activeSessionId = sessionId;
|
|
540
|
+
if (!selectedSessionId) {
|
|
541
|
+
selectedSessionId = sessionId;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
log(`registered ${sessionId} (${session.project?.name || "Untitled Shader"})`);
|
|
545
|
+
sendWsJson(socket, {
|
|
546
|
+
type: "registered",
|
|
547
|
+
sessionId,
|
|
548
|
+
selected: selectedSessionId === sessionId,
|
|
549
|
+
});
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
195
552
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
553
|
+
if (!activeSessionId) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
199
556
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
557
|
+
const session = sessions.get(activeSessionId);
|
|
558
|
+
if (!session) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
204
561
|
|
|
205
|
-
|
|
562
|
+
session.updatedAt = nowIso();
|
|
206
563
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
564
|
+
if (message.type === "project-updated") {
|
|
565
|
+
session.project = message.project || session.project;
|
|
566
|
+
session.manifest = message.manifest || session.manifest;
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (message.type === "result") {
|
|
571
|
+
const pending = session.pending.get(message.requestId);
|
|
572
|
+
if (!pending) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
clearTimeout(pending.timeoutId);
|
|
577
|
+
session.pending.delete(message.requestId);
|
|
578
|
+
|
|
579
|
+
if (message.ok) {
|
|
580
|
+
pending.resolve(message);
|
|
581
|
+
} else {
|
|
582
|
+
const error = new Error(
|
|
583
|
+
message.error?.message || `Call '${pending.method}' failed`,
|
|
584
|
+
);
|
|
585
|
+
error.stack = message.error?.stack || error.stack;
|
|
586
|
+
pending.reject(error);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
});
|
|
212
590
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (!pending) {
|
|
591
|
+
socket.on("close", () => {
|
|
592
|
+
if (!activeSessionId) {
|
|
216
593
|
return;
|
|
217
594
|
}
|
|
218
595
|
|
|
219
|
-
|
|
220
|
-
session
|
|
596
|
+
const session = sessions.get(activeSessionId);
|
|
597
|
+
if (!session) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
221
600
|
|
|
222
|
-
|
|
223
|
-
pending.
|
|
224
|
-
|
|
225
|
-
const error = new Error(
|
|
226
|
-
message.error?.message || `Call '${pending.method}' failed`,
|
|
227
|
-
);
|
|
228
|
-
error.stack = message.error?.stack || error.stack;
|
|
229
|
-
pending.reject(error);
|
|
601
|
+
for (const pending of session.pending.values()) {
|
|
602
|
+
clearTimeout(pending.timeoutId);
|
|
603
|
+
pending.reject(new Error(`Session '${activeSessionId}' disconnected`));
|
|
230
604
|
}
|
|
231
|
-
|
|
605
|
+
|
|
606
|
+
sessions.delete(activeSessionId);
|
|
607
|
+
if (selectedSessionId === activeSessionId) {
|
|
608
|
+
selectedSessionId = sessions.keys().next().value || null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
log(`disconnected ${activeSessionId}`);
|
|
612
|
+
});
|
|
232
613
|
});
|
|
233
614
|
|
|
234
|
-
|
|
235
|
-
if (!activeSessionId) {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
615
|
+
log(`bridge listening on ws://127.0.0.1:${BRIDGE_PORT}`);
|
|
238
616
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
617
|
+
controlServer = net.createServer((socket) => {
|
|
618
|
+
const rl = readline.createInterface({ input: socket });
|
|
243
619
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
620
|
+
rl.on("line", async (line) => {
|
|
621
|
+
let request;
|
|
622
|
+
try {
|
|
623
|
+
request = JSON.parse(line);
|
|
624
|
+
} catch {
|
|
625
|
+
sendJson(socket, { ok: false, error: "Invalid JSON request" });
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
248
628
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
629
|
+
if (!request || request.type !== "rpc") {
|
|
630
|
+
sendJson(socket, { ok: false, error: "Invalid control request" });
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const tool = createToolDefinitions().find((entry) => entry.name === request.tool);
|
|
635
|
+
if (!tool) {
|
|
636
|
+
sendJson(socket, { id: request.id, ok: false, error: `Unknown tool '${request.tool}'` });
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
253
639
|
|
|
254
|
-
|
|
640
|
+
try {
|
|
641
|
+
const response = await tool.handler(request.input || {});
|
|
642
|
+
sendJson(socket, { id: request.id, ok: true, response });
|
|
643
|
+
} catch (error) {
|
|
644
|
+
sendJson(socket, {
|
|
645
|
+
id: request.id,
|
|
646
|
+
ok: false,
|
|
647
|
+
error: error instanceof Error ? error.message : String(error),
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
});
|
|
255
651
|
});
|
|
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
|
-
);
|
|
565
652
|
|
|
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();
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
log(`control listening on tcp://127.0.0.1:${CONTROL_PORT}`);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function createProxyServer() {
|
|
665
|
+
const server = new McpServer({
|
|
666
|
+
name: "construct-shader-graph",
|
|
667
|
+
version: "0.1.0",
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
registerResources(server);
|
|
671
|
+
registerPrompts(server);
|
|
672
|
+
createToolDefinitions().forEach((tool) => {
|
|
673
|
+
server.registerTool(tool.name, tool.config, async (input = {}) => {
|
|
674
|
+
return callPrimaryTool(tool.name, input);
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
return server;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function callPrimaryTool(tool, input) {
|
|
682
|
+
return new Promise((resolve, reject) => {
|
|
683
|
+
const socket = net.createConnection({ host: "127.0.0.1", port: CONTROL_PORT });
|
|
684
|
+
const requestId = `rpc_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
685
|
+
const rl = readline.createInterface({ input: socket });
|
|
686
|
+
|
|
687
|
+
socket.on("error", (error) => {
|
|
688
|
+
reject(error);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
rl.on("line", (line) => {
|
|
692
|
+
let response;
|
|
693
|
+
try {
|
|
694
|
+
response = JSON.parse(line);
|
|
695
|
+
} catch {
|
|
696
|
+
reject(new Error("Invalid control response"));
|
|
697
|
+
socket.destroy();
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (response.id !== requestId) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
rl.close();
|
|
706
|
+
socket.end();
|
|
707
|
+
if (response.ok) {
|
|
708
|
+
resolve(response.response);
|
|
709
|
+
} else {
|
|
710
|
+
reject(new Error(response.error || "Primary MCP request failed"));
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
sendJson(socket, {
|
|
715
|
+
type: "rpc",
|
|
716
|
+
id: requestId,
|
|
717
|
+
tool,
|
|
718
|
+
input,
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
async function ensureBackend() {
|
|
724
|
+
try {
|
|
725
|
+
await startPrimaryBackend();
|
|
726
|
+
} catch (error) {
|
|
727
|
+
if (error?.code !== "EADDRINUSE") {
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
log(`bridge already running on ${BRIDGE_PORT}; starting follower proxy`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
await ensureBackend();
|
|
736
|
+
|
|
737
|
+
const server = isPrimaryInstance ? localServer : createProxyServer();
|
|
566
738
|
const transport = new StdioServerTransport();
|
|
567
739
|
await server.connect(transport);
|