construct-shader-graph-mcp 0.1.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 +183 -0
- package/bin/construct-shader-graph-mcp.js +3 -0
- package/package.json +42 -0
- package/src/guidance/skill.md +97 -0
- package/src/server.mjs +394 -0
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./caw-icon-512.png" alt="Construct Shader Graph MCP icon" width="128" height="128">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# Construct Shader Graph MCP
|
|
6
|
+
|
|
7
|
+
Standalone MCP server for controlling Construct Shader Graph through its browser bridge.
|
|
8
|
+
|
|
9
|
+
It exposes project discovery, manifest inspection, and exact method execution for the live app, while also bundling the guidance the model needs to work well with the tool.
|
|
10
|
+
|
|
11
|
+
Construct Shader Graph is a visual editor for building Construct effect shaders as node graphs. You can find the app here:
|
|
12
|
+
|
|
13
|
+
- `https://skymen.github.io/construct-shader-graph/`
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- MCP tools for project discovery and method execution
|
|
18
|
+
- local WebSocket bridge on `ws://127.0.0.1:6359` by default
|
|
19
|
+
- built-in skill guidance available directly from the MCP
|
|
20
|
+
- works with hosts like Claude Desktop and OpenCode
|
|
21
|
+
|
|
22
|
+
## MCP tools
|
|
23
|
+
|
|
24
|
+
- `get_skill_guidance`
|
|
25
|
+
- `list_projects`
|
|
26
|
+
- `select_project`
|
|
27
|
+
- `get_project_manifest`
|
|
28
|
+
- `call_project_method`
|
|
29
|
+
|
|
30
|
+
## Install as a package
|
|
31
|
+
|
|
32
|
+
Global install:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install -g construct-shader-graph-mcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Run after installing globally:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
construct-shader-graph-mcp
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or run without installing globally:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx -y construct-shader-graph-mcp
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Local development
|
|
51
|
+
|
|
52
|
+
Clone the repo and install dependencies:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git clone https://github.com/skymen/construct-shader-graph-mcp.git
|
|
56
|
+
cd construct-shader-graph-mcp
|
|
57
|
+
npm install
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Run locally:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm start
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Configuration
|
|
67
|
+
|
|
68
|
+
Optional environment variable:
|
|
69
|
+
|
|
70
|
+
- `MCP_BRIDGE_PORT` to change the browser bridge port from `6359`
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
MCP_BRIDGE_PORT=6360 construct-shader-graph-mcp
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## How it works
|
|
79
|
+
|
|
80
|
+
There are two sides to the integration:
|
|
81
|
+
|
|
82
|
+
1. The MCP host launches this package over stdio.
|
|
83
|
+
2. The Construct Shader Graph page connects to the local WebSocket bridge.
|
|
84
|
+
|
|
85
|
+
The page should:
|
|
86
|
+
|
|
87
|
+
- connect to `ws://127.0.0.1:6359` by default
|
|
88
|
+
- register itself with project metadata from `shader.getInfo()`
|
|
89
|
+
- answer `invoke` messages with exact API return values
|
|
90
|
+
|
|
91
|
+
## Claude Desktop setup
|
|
92
|
+
|
|
93
|
+
If installed globally:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"mcpServers": {
|
|
98
|
+
"construct-shader-graph": {
|
|
99
|
+
"command": "construct-shader-graph-mcp"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
If using `npx`:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"mcpServers": {
|
|
110
|
+
"construct-shader-graph": {
|
|
111
|
+
"command": "npx",
|
|
112
|
+
"args": ["-y", "construct-shader-graph-mcp"]
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## OpenCode setup
|
|
119
|
+
|
|
120
|
+
Use the same command shape in your MCP configuration.
|
|
121
|
+
|
|
122
|
+
Global install example:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"mcpServers": {
|
|
127
|
+
"construct-shader-graph": {
|
|
128
|
+
"command": "construct-shader-graph-mcp"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`npx` example:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"mcpServers": {
|
|
139
|
+
"construct-shader-graph": {
|
|
140
|
+
"command": "npx",
|
|
141
|
+
"args": ["-y", "construct-shader-graph-mcp"]
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Typical usage flow
|
|
148
|
+
|
|
149
|
+
1. Start the MCP server from your host.
|
|
150
|
+
2. Open Construct Shader Graph.
|
|
151
|
+
3. In the app, connect to the MCP bridge from the Help menu.
|
|
152
|
+
4. The host can now:
|
|
153
|
+
- call `list_projects`
|
|
154
|
+
- select the right project with `select_project`
|
|
155
|
+
- inspect available methods with `get_project_manifest`
|
|
156
|
+
- execute API calls with `call_project_method`
|
|
157
|
+
|
|
158
|
+
## Publish notes
|
|
159
|
+
|
|
160
|
+
This package is configured for npm publishing with:
|
|
161
|
+
|
|
162
|
+
- package name: `construct-shader-graph-mcp`
|
|
163
|
+
- CLI binary: `construct-shader-graph-mcp`
|
|
164
|
+
- limited published files through the `files` field
|
|
165
|
+
|
|
166
|
+
Check package contents before publishing:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npm run pack:check
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Publish publicly:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
npm publish
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Repo layout
|
|
179
|
+
|
|
180
|
+
- `src/server.mjs` - MCP server and bridge
|
|
181
|
+
- `src/guidance/skill.md` - bundled AI guidance and best practices
|
|
182
|
+
- `bin/construct-shader-graph-mcp.js` - CLI entrypoint
|
|
183
|
+
- `caw-icon.png` - package/readme icon
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "construct-shader-graph-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Standalone MCP server for Construct Shader Graph",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"bin",
|
|
8
|
+
"src",
|
|
9
|
+
"README.md",
|
|
10
|
+
"caw-icon.png"
|
|
11
|
+
],
|
|
12
|
+
"bin": {
|
|
13
|
+
"construct-shader-graph-mcp": "./bin/construct-shader-graph-mcp.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node ./bin/construct-shader-graph-mcp.js",
|
|
17
|
+
"dev": "node ./src/server.mjs",
|
|
18
|
+
"pack:check": "npm pack --dry-run"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"construct",
|
|
23
|
+
"shader-graph",
|
|
24
|
+
"claude",
|
|
25
|
+
"opencode"
|
|
26
|
+
],
|
|
27
|
+
"author": "skymen",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/skymen/construct-shader-graph-mcp.git"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/skymen/construct-shader-graph-mcp#readme",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/skymen/construct-shader-graph-mcp/issues"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
39
|
+
"ws": "^8.19.0",
|
|
40
|
+
"zod": "^4.3.6"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Construct Shader Graph MCP Skill
|
|
2
|
+
|
|
3
|
+
Use this guidance when working with Construct Shader Graph through the MCP bridge.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
- Use the MCP tools as the only execution surface.
|
|
8
|
+
- Inspect the current graph, make targeted graph edits, validate the result, and report progress clearly.
|
|
9
|
+
- Treat the graph as the source of truth for shader logic. Use preview controls only to inspect or demonstrate the result.
|
|
10
|
+
|
|
11
|
+
## MCP tool contract
|
|
12
|
+
|
|
13
|
+
Use these tools for all work:
|
|
14
|
+
|
|
15
|
+
- `list_projects`
|
|
16
|
+
- `select_project`
|
|
17
|
+
- `get_project_manifest`
|
|
18
|
+
- `call_project_method`
|
|
19
|
+
|
|
20
|
+
Execution rules:
|
|
21
|
+
|
|
22
|
+
- Always use MCP tools instead of browser console access.
|
|
23
|
+
- Always identify the active project from shader metadata returned by `shader.getInfo()`.
|
|
24
|
+
- Always use exact return values from MCP calls; do not guess state.
|
|
25
|
+
- Always read the manifest when capabilities or argument shapes are unclear.
|
|
26
|
+
|
|
27
|
+
## Operating contract
|
|
28
|
+
|
|
29
|
+
- Preserve existing user work unless the task clearly requires replacing it.
|
|
30
|
+
- Identify the correct connected project before mutating anything.
|
|
31
|
+
- Inspect first, mutate second.
|
|
32
|
+
- Make the smallest valid change that satisfies the request.
|
|
33
|
+
- Verify after every structural edit such as creating nodes, deleting nodes, or wiring ports.
|
|
34
|
+
- Use stable ids from API results; do not rely on labels, visual position, or selection alone.
|
|
35
|
+
- Do not open arbitrary local files or save project files autonomously.
|
|
36
|
+
- Built-in examples are safe to open.
|
|
37
|
+
- Export is allowed because it triggers a download rather than silently overwriting a project.
|
|
38
|
+
|
|
39
|
+
## Preferred workflow
|
|
40
|
+
|
|
41
|
+
1. Call `list_projects`.
|
|
42
|
+
2. Select the correct project with `select_project`.
|
|
43
|
+
3. Read `get_project_manifest` once per task or when capabilities are unclear.
|
|
44
|
+
4. Start the session.
|
|
45
|
+
5. Inspect current graph state.
|
|
46
|
+
6. Identify exact node ids, port refs, uniform ids, or settings keys.
|
|
47
|
+
7. Apply one atomic edit or one tightly related batch.
|
|
48
|
+
8. Re-read the affected nodes, ports, wires, or settings.
|
|
49
|
+
9. Check preview or generated code if relevant.
|
|
50
|
+
10. Repeat only if needed.
|
|
51
|
+
11. End the session with a recap.
|
|
52
|
+
|
|
53
|
+
## Core rules
|
|
54
|
+
|
|
55
|
+
- Always call `session.initAIWork()` when starting a task.
|
|
56
|
+
- Always call `session.endAIWork()` when finishing a task.
|
|
57
|
+
- Use `session.updateAIWork()` only for short phase updates.
|
|
58
|
+
- Always inspect preview errors after meaningful shader edits.
|
|
59
|
+
- Always use preview and screenshots for non-trivial visual validation.
|
|
60
|
+
- Prefer setting editable input port values directly before adding constant/vector nodes.
|
|
61
|
+
- Never assume a node id, port index, or wire id without reading it first.
|
|
62
|
+
- Never connect ports without checking the actual node ports.
|
|
63
|
+
- Never replace an output connection blindly; inspect the affected ports first.
|
|
64
|
+
- Never use startup scripts as a substitute for graph logic.
|
|
65
|
+
- Never create or edit custom node definitions unless explicitly asked.
|
|
66
|
+
|
|
67
|
+
## Graph editing guidance
|
|
68
|
+
|
|
69
|
+
- Always inspect ports before creating wires.
|
|
70
|
+
- Use explicit port refs: `{ nodeId, kind, index }`.
|
|
71
|
+
- Prefer `index` over `name` for automation stability.
|
|
72
|
+
- Use `declaredType` and `resolvedType` to understand generic or dynamic nodes.
|
|
73
|
+
- If an input port is editable and unconnected, prefer setting its value directly instead of creating a separate constant node.
|
|
74
|
+
- If one output would feed many distant nodes, prefer variables instead of many long wires.
|
|
75
|
+
|
|
76
|
+
## Preview and verification guidance
|
|
77
|
+
|
|
78
|
+
- Default preview compiles from `Output`.
|
|
79
|
+
- Use node preview for masks, UVs, gradients, lighting terms, and intermediate values.
|
|
80
|
+
- Use the preview console as part of the normal debug loop.
|
|
81
|
+
- Use screenshots to confirm that the visual result matches the intent.
|
|
82
|
+
- Prefer `ai.runDebugCheck()` for combined validation.
|
|
83
|
+
|
|
84
|
+
## Construct-specific guidance
|
|
85
|
+
|
|
86
|
+
- Important shader settings include `blendsBackground`, `usesDepth`, `crossSampling`, `animated`, `mustPredraw`, `supports3DDirectRendering`, `extendBoxH`, and `extendBoxV`.
|
|
87
|
+
- Background sampling only makes sense when `blendsBackground` is enabled.
|
|
88
|
+
- Depth sampling only makes sense when `usesDepth` is enabled.
|
|
89
|
+
- Construct uses premultiplied alpha, so many color workflows should use `unpremultiply` before edits and `premultiply` before output.
|
|
90
|
+
- Prefer existing Construct helper nodes instead of rebuilding common math manually.
|
|
91
|
+
|
|
92
|
+
## Troubleshooting
|
|
93
|
+
|
|
94
|
+
- If no projects are listed, make sure the page is connected to the MCP bridge.
|
|
95
|
+
- If the wrong project is selected, compare `shader.getInfo()` metadata and reselect.
|
|
96
|
+
- If wire creation fails, inspect both nodes with `nodes.getPorts` and check `resolvedType`.
|
|
97
|
+
- If preview looks wrong, inspect preview settings, preview errors, node preview, and screenshots.
|
package/src/server.mjs
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { WebSocketServer } from "ws";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
const BRIDGE_PORT = Number(process.env.MCP_BRIDGE_PORT || 6359);
|
|
13
|
+
const SKILL_PATH = path.resolve(__dirname, "guidance/skill.md");
|
|
14
|
+
|
|
15
|
+
const sessions = new Map();
|
|
16
|
+
let selectedSessionId = null;
|
|
17
|
+
|
|
18
|
+
function log(message, ...args) {
|
|
19
|
+
console.error(`[construct-shader-graph-mcp] ${message}`, ...args);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function nowIso() {
|
|
23
|
+
return new Date().toISOString();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function loadSkillText() {
|
|
27
|
+
return fs.readFileSync(SKILL_PATH, "utf8");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getSessionSummary(session) {
|
|
31
|
+
return {
|
|
32
|
+
sessionId: session.sessionId,
|
|
33
|
+
project: session.project,
|
|
34
|
+
connectedAt: session.connectedAt,
|
|
35
|
+
updatedAt: session.updatedAt,
|
|
36
|
+
manifestVersion: session.manifest?.version || null,
|
|
37
|
+
methodCount: Array.isArray(session.manifest?.methods)
|
|
38
|
+
? session.manifest.methods.length
|
|
39
|
+
: 0,
|
|
40
|
+
selected: selectedSessionId === session.sessionId,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ensureSession(sessionId) {
|
|
45
|
+
const session = sessions.get(sessionId);
|
|
46
|
+
if (!session) {
|
|
47
|
+
throw new Error(`Unknown session '${sessionId}'`);
|
|
48
|
+
}
|
|
49
|
+
return session;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ensureSelectedSession() {
|
|
53
|
+
if (!selectedSessionId) {
|
|
54
|
+
throw new Error("No project selected. Call select_project first.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return ensureSession(selectedSessionId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sendJson(socket, payload) {
|
|
61
|
+
socket.send(JSON.stringify(payload));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function invokeSession(session, method, args = []) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
if (session.socket.readyState !== session.socket.OPEN) {
|
|
67
|
+
reject(new Error(`Session '${session.sessionId}' is not connected`));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const requestId = `req_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
72
|
+
const timeoutId = setTimeout(() => {
|
|
73
|
+
session.pending.delete(requestId);
|
|
74
|
+
reject(
|
|
75
|
+
new Error(
|
|
76
|
+
`Timed out waiting for '${method}' result from session '${session.sessionId}'`,
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
}, 15000);
|
|
80
|
+
|
|
81
|
+
session.pending.set(requestId, {
|
|
82
|
+
resolve,
|
|
83
|
+
reject,
|
|
84
|
+
timeoutId,
|
|
85
|
+
method,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
sendJson(session.socket, {
|
|
89
|
+
type: "invoke",
|
|
90
|
+
requestId,
|
|
91
|
+
method,
|
|
92
|
+
args,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const bridge = new WebSocketServer({ host: "127.0.0.1", port: BRIDGE_PORT });
|
|
98
|
+
|
|
99
|
+
bridge.on("connection", (socket) => {
|
|
100
|
+
let activeSessionId = null;
|
|
101
|
+
|
|
102
|
+
socket.on("message", (raw) => {
|
|
103
|
+
let message;
|
|
104
|
+
try {
|
|
105
|
+
message = JSON.parse(String(raw));
|
|
106
|
+
} catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!message || typeof message !== "object") {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (message.type === "register") {
|
|
115
|
+
const sessionId = String(message.sessionId || "").trim();
|
|
116
|
+
if (!sessionId) {
|
|
117
|
+
sendJson(socket, { type: "error", message: "Missing sessionId" });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const session = {
|
|
122
|
+
sessionId,
|
|
123
|
+
socket,
|
|
124
|
+
project: message.project || {
|
|
125
|
+
name: "Untitled Shader",
|
|
126
|
+
version: "0.0.0.0",
|
|
127
|
+
},
|
|
128
|
+
manifest: message.manifest || null,
|
|
129
|
+
connectedAt: nowIso(),
|
|
130
|
+
updatedAt: nowIso(),
|
|
131
|
+
pending: new Map(),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
sessions.set(sessionId, session);
|
|
135
|
+
activeSessionId = sessionId;
|
|
136
|
+
if (!selectedSessionId) {
|
|
137
|
+
selectedSessionId = sessionId;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
log(`registered ${sessionId} (${session.project?.name || "Untitled Shader"})`);
|
|
141
|
+
sendJson(socket, {
|
|
142
|
+
type: "registered",
|
|
143
|
+
sessionId,
|
|
144
|
+
selected: selectedSessionId === sessionId,
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!activeSessionId) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const session = sessions.get(activeSessionId);
|
|
154
|
+
if (!session) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
session.updatedAt = nowIso();
|
|
159
|
+
|
|
160
|
+
if (message.type === "project-updated") {
|
|
161
|
+
session.project = message.project || session.project;
|
|
162
|
+
session.manifest = message.manifest || session.manifest;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (message.type === "result") {
|
|
167
|
+
const pending = session.pending.get(message.requestId);
|
|
168
|
+
if (!pending) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
clearTimeout(pending.timeoutId);
|
|
173
|
+
session.pending.delete(message.requestId);
|
|
174
|
+
|
|
175
|
+
if (message.ok) {
|
|
176
|
+
pending.resolve(message);
|
|
177
|
+
} else {
|
|
178
|
+
const error = new Error(
|
|
179
|
+
message.error?.message || `Call '${pending.method}' failed`,
|
|
180
|
+
);
|
|
181
|
+
error.stack = message.error?.stack || error.stack;
|
|
182
|
+
pending.reject(error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
socket.on("close", () => {
|
|
188
|
+
if (!activeSessionId) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const session = sessions.get(activeSessionId);
|
|
193
|
+
if (!session) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const pending of session.pending.values()) {
|
|
198
|
+
clearTimeout(pending.timeoutId);
|
|
199
|
+
pending.reject(new Error(`Session '${activeSessionId}' disconnected`));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
sessions.delete(activeSessionId);
|
|
203
|
+
if (selectedSessionId === activeSessionId) {
|
|
204
|
+
selectedSessionId = sessions.keys().next().value || null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
log(`disconnected ${activeSessionId}`);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
log(`bridge listening on ws://127.0.0.1:${BRIDGE_PORT}`);
|
|
212
|
+
|
|
213
|
+
const server = new McpServer({
|
|
214
|
+
name: "construct-shader-graph",
|
|
215
|
+
version: "0.1.0",
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
server.registerTool(
|
|
219
|
+
"get_skill_guidance",
|
|
220
|
+
{
|
|
221
|
+
description:
|
|
222
|
+
"Return the full Construct Shader Graph MCP guidance and best practices.",
|
|
223
|
+
inputSchema: {},
|
|
224
|
+
outputSchema: {
|
|
225
|
+
title: z.string(),
|
|
226
|
+
content: z.string(),
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
async () => {
|
|
230
|
+
const result = {
|
|
231
|
+
title: "Construct Shader Graph MCP Skill",
|
|
232
|
+
content: loadSkillText(),
|
|
233
|
+
};
|
|
234
|
+
return {
|
|
235
|
+
content: [{ type: "text", text: result.content }],
|
|
236
|
+
structuredContent: result,
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
server.registerTool(
|
|
242
|
+
"list_projects",
|
|
243
|
+
{
|
|
244
|
+
description:
|
|
245
|
+
"List connected Construct Shader Graph tabs registered with the local bridge.",
|
|
246
|
+
inputSchema: {},
|
|
247
|
+
outputSchema: {
|
|
248
|
+
projects: z.array(
|
|
249
|
+
z.object({
|
|
250
|
+
sessionId: z.string(),
|
|
251
|
+
project: z.object({
|
|
252
|
+
name: z.string(),
|
|
253
|
+
version: z.string().optional(),
|
|
254
|
+
author: z.string().optional(),
|
|
255
|
+
category: z.string().optional(),
|
|
256
|
+
description: z.string().optional(),
|
|
257
|
+
shaderInfo: z.any().optional(),
|
|
258
|
+
}),
|
|
259
|
+
connectedAt: z.string(),
|
|
260
|
+
updatedAt: z.string(),
|
|
261
|
+
manifestVersion: z.string().nullable(),
|
|
262
|
+
methodCount: z.number(),
|
|
263
|
+
selected: z.boolean(),
|
|
264
|
+
}),
|
|
265
|
+
),
|
|
266
|
+
selectedSessionId: z.string().nullable(),
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
async () => {
|
|
270
|
+
const projects = [...sessions.values()].map(getSessionSummary);
|
|
271
|
+
return {
|
|
272
|
+
content: [
|
|
273
|
+
{
|
|
274
|
+
type: "text",
|
|
275
|
+
text: JSON.stringify({ projects, selectedSessionId }, null, 2),
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
structuredContent: {
|
|
279
|
+
projects,
|
|
280
|
+
selectedSessionId,
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
server.registerTool(
|
|
287
|
+
"select_project",
|
|
288
|
+
{
|
|
289
|
+
description:
|
|
290
|
+
"Choose which connected shader graph tab future MCP calls should target.",
|
|
291
|
+
inputSchema: {
|
|
292
|
+
sessionId: z.string().describe("Session id returned by list_projects."),
|
|
293
|
+
},
|
|
294
|
+
outputSchema: {
|
|
295
|
+
sessionId: z.string(),
|
|
296
|
+
project: z.any(),
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
async ({ sessionId }) => {
|
|
300
|
+
const session = ensureSession(sessionId);
|
|
301
|
+
selectedSessionId = sessionId;
|
|
302
|
+
const result = {
|
|
303
|
+
sessionId,
|
|
304
|
+
project: session.project,
|
|
305
|
+
};
|
|
306
|
+
return {
|
|
307
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
308
|
+
structuredContent: result,
|
|
309
|
+
};
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
server.registerTool(
|
|
314
|
+
"get_project_manifest",
|
|
315
|
+
{
|
|
316
|
+
description:
|
|
317
|
+
"Get the machine-readable API manifest for the selected project.",
|
|
318
|
+
inputSchema: {
|
|
319
|
+
sessionId: z
|
|
320
|
+
.string()
|
|
321
|
+
.optional()
|
|
322
|
+
.describe("Optional session id; defaults to the selected project."),
|
|
323
|
+
},
|
|
324
|
+
outputSchema: {
|
|
325
|
+
sessionId: z.string(),
|
|
326
|
+
project: z.any(),
|
|
327
|
+
manifest: z.any(),
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
async ({ sessionId }) => {
|
|
331
|
+
const session = sessionId
|
|
332
|
+
? ensureSession(sessionId)
|
|
333
|
+
: ensureSelectedSession();
|
|
334
|
+
const result = {
|
|
335
|
+
sessionId: session.sessionId,
|
|
336
|
+
project: session.project,
|
|
337
|
+
manifest: session.manifest,
|
|
338
|
+
};
|
|
339
|
+
return {
|
|
340
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
341
|
+
structuredContent: result,
|
|
342
|
+
};
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
server.registerTool(
|
|
347
|
+
"call_project_method",
|
|
348
|
+
{
|
|
349
|
+
description:
|
|
350
|
+
"Call one method from the selected project's shaderGraphAPI and return its exact result.",
|
|
351
|
+
inputSchema: {
|
|
352
|
+
sessionId: z
|
|
353
|
+
.string()
|
|
354
|
+
.optional()
|
|
355
|
+
.describe("Optional session id; defaults to the selected project."),
|
|
356
|
+
method: z
|
|
357
|
+
.string()
|
|
358
|
+
.describe("Manifest method path, for example nodes.create or shader.getInfo."),
|
|
359
|
+
args: z
|
|
360
|
+
.array(z.any())
|
|
361
|
+
.optional()
|
|
362
|
+
.describe("Positional arguments to pass to the API method."),
|
|
363
|
+
},
|
|
364
|
+
outputSchema: {
|
|
365
|
+
sessionId: z.string(),
|
|
366
|
+
project: z.any(),
|
|
367
|
+
method: z.string(),
|
|
368
|
+
args: z.array(z.any()),
|
|
369
|
+
durationMs: z.number(),
|
|
370
|
+
result: z.any(),
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
async ({ sessionId, method, args = [] }) => {
|
|
374
|
+
const session = sessionId
|
|
375
|
+
? ensureSession(sessionId)
|
|
376
|
+
: ensureSelectedSession();
|
|
377
|
+
const response = await invokeSession(session, method, args);
|
|
378
|
+
const result = {
|
|
379
|
+
sessionId: session.sessionId,
|
|
380
|
+
project: session.project,
|
|
381
|
+
method,
|
|
382
|
+
args,
|
|
383
|
+
durationMs: response.durationMs ?? 0,
|
|
384
|
+
result: response.result,
|
|
385
|
+
};
|
|
386
|
+
return {
|
|
387
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
388
|
+
structuredContent: result,
|
|
389
|
+
};
|
|
390
|
+
},
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
const transport = new StdioServerTransport();
|
|
394
|
+
await server.connect(transport);
|