sbox-mcp-server 1.2.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/LICENSE +21 -0
- package/README.md +90 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +155 -0
- package/dist/tools/assets.d.ts +8 -0
- package/dist/tools/assets.js +80 -0
- package/dist/tools/audio.d.ts +8 -0
- package/dist/tools/audio.js +101 -0
- package/dist/tools/components.d.ts +11 -0
- package/dist/tools/components.js +78 -0
- package/dist/tools/console.d.ts +8 -0
- package/dist/tools/console.js +59 -0
- package/dist/tools/discovery.d.ts +13 -0
- package/dist/tools/discovery.js +58 -0
- package/dist/tools/gameobjects.d.ts +4 -0
- package/dist/tools/gameobjects.js +197 -0
- package/dist/tools/materials.d.ts +8 -0
- package/dist/tools/materials.js +82 -0
- package/dist/tools/networking.d.ts +11 -0
- package/dist/tools/networking.js +227 -0
- package/dist/tools/physics.d.ts +8 -0
- package/dist/tools/physics.js +130 -0
- package/dist/tools/playmode.d.ts +11 -0
- package/dist/tools/playmode.js +140 -0
- package/dist/tools/prefabs.d.ts +8 -0
- package/dist/tools/prefabs.js +94 -0
- package/dist/tools/project.d.ts +12 -0
- package/dist/tools/project.js +90 -0
- package/dist/tools/publishing.d.ts +11 -0
- package/dist/tools/publishing.js +168 -0
- package/dist/tools/scenes.d.ts +8 -0
- package/dist/tools/scenes.js +75 -0
- package/dist/tools/scripts.d.ts +9 -0
- package/dist/tools/scripts.js +132 -0
- package/dist/tools/status.d.ts +8 -0
- package/dist/tools/status.js +49 -0
- package/dist/tools/templates.d.ts +11 -0
- package/dist/tools/templates.js +135 -0
- package/dist/tools/ui.d.ts +8 -0
- package/dist/tools/ui.js +116 -0
- package/dist/tools/world.d.ts +20 -0
- package/dist/tools/world.js +272 -0
- package/dist/transport/bridge-client.d.ts +60 -0
- package/dist/transport/bridge-client.js +239 -0
- package/package.json +54 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
/**
|
|
5
|
+
* File-based IPC client that communicates with the s&box Bridge Addon.
|
|
6
|
+
*/
|
|
7
|
+
export class BridgeClient {
|
|
8
|
+
requestCounter = 0;
|
|
9
|
+
ipcDir;
|
|
10
|
+
connected = false;
|
|
11
|
+
lastPongTime = 0;
|
|
12
|
+
host;
|
|
13
|
+
port;
|
|
14
|
+
static POLL_INTERVAL_MS = 50; // 50ms polling for responses
|
|
15
|
+
static STATUS_CHECK_INTERVAL_MS = 5000;
|
|
16
|
+
constructor(host = "127.0.0.1", port = 29015) {
|
|
17
|
+
this.host = host;
|
|
18
|
+
this.port = port;
|
|
19
|
+
this.ipcDir = path.join(os.tmpdir(), "sbox-bridge-ipc");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check if the s&box Bridge is running by looking for the status file.
|
|
23
|
+
*/
|
|
24
|
+
async connect() {
|
|
25
|
+
// Ensure IPC directory exists
|
|
26
|
+
if (!fs.existsSync(this.ipcDir)) {
|
|
27
|
+
fs.mkdirSync(this.ipcDir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
const statusPath = path.join(this.ipcDir, "status.json");
|
|
30
|
+
if (fs.existsSync(statusPath)) {
|
|
31
|
+
try {
|
|
32
|
+
const status = JSON.parse(fs.readFileSync(statusPath, "utf8"));
|
|
33
|
+
if (status.running) {
|
|
34
|
+
this.connected = true;
|
|
35
|
+
this.lastPongTime = Date.now();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Status file exists but is malformed
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Cannot connect to s&box Bridge. No status file found at ${statusPath}. Is s&box running with the Bridge Addon?`);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Send a command to the s&box Bridge and wait for its response.
|
|
47
|
+
*/
|
|
48
|
+
async send(command, params = {}, timeoutMs = 30000) {
|
|
49
|
+
// Try to connect if not connected
|
|
50
|
+
if (!this.connected) {
|
|
51
|
+
try {
|
|
52
|
+
await this.connect();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return {
|
|
56
|
+
id: "",
|
|
57
|
+
success: false,
|
|
58
|
+
error: "Not connected to s&box Bridge. Make sure s&box is running with the Bridge Addon installed.",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const id = `${++this.requestCounter}_${Date.now()}`;
|
|
63
|
+
const request = { id, command, params };
|
|
64
|
+
// Ensure IPC directory exists
|
|
65
|
+
if (!fs.existsSync(this.ipcDir)) {
|
|
66
|
+
fs.mkdirSync(this.ipcDir, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
// Write request file
|
|
69
|
+
const reqPath = path.join(this.ipcDir, `req_${id}.json`);
|
|
70
|
+
const resPath = path.join(this.ipcDir, `res_${id}.json`);
|
|
71
|
+
try {
|
|
72
|
+
fs.writeFileSync(reqPath, JSON.stringify(request), "utf8");
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
return {
|
|
76
|
+
id,
|
|
77
|
+
success: false,
|
|
78
|
+
error: `Failed to write request file: ${err}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// Poll for response file
|
|
82
|
+
const startTime = Date.now();
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
const poll = setInterval(() => {
|
|
85
|
+
// Check timeout
|
|
86
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
87
|
+
clearInterval(poll);
|
|
88
|
+
// Clean up request file if still there
|
|
89
|
+
try {
|
|
90
|
+
if (fs.existsSync(reqPath))
|
|
91
|
+
fs.unlinkSync(reqPath);
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
resolve({
|
|
95
|
+
id,
|
|
96
|
+
success: false,
|
|
97
|
+
error: `Request timed out after ${timeoutMs}ms`,
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Check for response file
|
|
102
|
+
if (fs.existsSync(resPath)) {
|
|
103
|
+
try {
|
|
104
|
+
// Strip UTF-8 BOM that C#'s File.WriteAllText prepends
|
|
105
|
+
const responseJson = fs.readFileSync(resPath, "utf8").replace(/^\uFEFF/, "");
|
|
106
|
+
const response = JSON.parse(responseJson);
|
|
107
|
+
// Clean up response file
|
|
108
|
+
try {
|
|
109
|
+
fs.unlinkSync(resPath);
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
112
|
+
clearInterval(poll);
|
|
113
|
+
this.lastPongTime = Date.now();
|
|
114
|
+
resolve(response);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Response file might be partially written, try again next poll
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}, BridgeClient.POLL_INTERVAL_MS);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Send multiple commands as a batch.
|
|
125
|
+
*/
|
|
126
|
+
async sendBatch(commands, timeoutMs = 30000) {
|
|
127
|
+
if (!this.connected) {
|
|
128
|
+
try {
|
|
129
|
+
await this.connect();
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return {
|
|
133
|
+
id: "",
|
|
134
|
+
success: false,
|
|
135
|
+
error: "Not connected to s&box Bridge.",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const id = `batch_${++this.requestCounter}_${Date.now()}`;
|
|
140
|
+
const request = { id, commands };
|
|
141
|
+
if (!fs.existsSync(this.ipcDir)) {
|
|
142
|
+
fs.mkdirSync(this.ipcDir, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
const reqPath = path.join(this.ipcDir, `req_${id}.json`);
|
|
145
|
+
try {
|
|
146
|
+
fs.writeFileSync(reqPath, JSON.stringify(request), "utf8");
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
return {
|
|
150
|
+
id,
|
|
151
|
+
success: false,
|
|
152
|
+
error: `Failed to write request file: ${err}`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const resPath = path.join(this.ipcDir, `res_${id}.json`);
|
|
156
|
+
const startTime = Date.now();
|
|
157
|
+
return new Promise((resolve) => {
|
|
158
|
+
const poll = setInterval(() => {
|
|
159
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
160
|
+
clearInterval(poll);
|
|
161
|
+
try {
|
|
162
|
+
if (fs.existsSync(reqPath))
|
|
163
|
+
fs.unlinkSync(reqPath);
|
|
164
|
+
}
|
|
165
|
+
catch { }
|
|
166
|
+
resolve({
|
|
167
|
+
id,
|
|
168
|
+
success: false,
|
|
169
|
+
error: `Batch request timed out after ${timeoutMs}ms`,
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (fs.existsSync(resPath)) {
|
|
174
|
+
try {
|
|
175
|
+
// Strip UTF-8 BOM that C#'s File.WriteAllText prepends
|
|
176
|
+
const responseJson = fs.readFileSync(resPath, "utf8").replace(/^\uFEFF/, "");
|
|
177
|
+
const response = JSON.parse(responseJson);
|
|
178
|
+
try {
|
|
179
|
+
fs.unlinkSync(resPath);
|
|
180
|
+
}
|
|
181
|
+
catch { }
|
|
182
|
+
clearInterval(poll);
|
|
183
|
+
this.lastPongTime = Date.now();
|
|
184
|
+
resolve(response);
|
|
185
|
+
}
|
|
186
|
+
catch { }
|
|
187
|
+
}
|
|
188
|
+
}, BridgeClient.POLL_INTERVAL_MS);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Check if bridge is alive by looking for status file.
|
|
193
|
+
*/
|
|
194
|
+
async ping() {
|
|
195
|
+
const statusPath = path.join(this.ipcDir, "status.json");
|
|
196
|
+
const start = Date.now();
|
|
197
|
+
try {
|
|
198
|
+
if (fs.existsSync(statusPath)) {
|
|
199
|
+
const status = JSON.parse(fs.readFileSync(statusPath, "utf8"));
|
|
200
|
+
if (status.running) {
|
|
201
|
+
this.lastPongTime = Date.now();
|
|
202
|
+
return Date.now() - start;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch { }
|
|
207
|
+
return -1;
|
|
208
|
+
}
|
|
209
|
+
isConnected() {
|
|
210
|
+
// Re-check status file
|
|
211
|
+
const statusPath = path.join(this.ipcDir, "status.json");
|
|
212
|
+
try {
|
|
213
|
+
if (fs.existsSync(statusPath)) {
|
|
214
|
+
const status = JSON.parse(fs.readFileSync(statusPath, "utf8"));
|
|
215
|
+
this.connected = !!status.running;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
this.connected = false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
this.connected = false;
|
|
223
|
+
}
|
|
224
|
+
return this.connected;
|
|
225
|
+
}
|
|
226
|
+
getHost() {
|
|
227
|
+
return this.host;
|
|
228
|
+
}
|
|
229
|
+
getPort() {
|
|
230
|
+
return this.port;
|
|
231
|
+
}
|
|
232
|
+
getLastPongTime() {
|
|
233
|
+
return this.lastPongTime;
|
|
234
|
+
}
|
|
235
|
+
disconnect() {
|
|
236
|
+
this.connected = false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=bridge-client.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sbox-mcp-server",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "MCP Server for s&box game engine — enables Claude to build games through conversation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sbox-mcp-server": "dist/index.js",
|
|
9
|
+
"sbox-mcp": "dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/**/*.js",
|
|
13
|
+
"dist/**/*.d.ts",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"start": "node dist/index.js",
|
|
20
|
+
"dev": "tsc --watch",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"mcp",
|
|
25
|
+
"sbox",
|
|
26
|
+
"s&box",
|
|
27
|
+
"game-engine",
|
|
28
|
+
"claude",
|
|
29
|
+
"ai",
|
|
30
|
+
"source2",
|
|
31
|
+
"model-context-protocol"
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/LouSputthole/Sbox-Claude.git"
|
|
36
|
+
},
|
|
37
|
+
"author": "sboxskins.gg (https://sboxskins.gg)",
|
|
38
|
+
"homepage": "https://sboxskins.gg",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/LouSputthole/Sbox-Claude/issues"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
44
|
+
"zod": "^3.24.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^22.0.0",
|
|
48
|
+
"typescript": "^5.7.0"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18.0.0"
|
|
52
|
+
},
|
|
53
|
+
"license": "GPL-3.0-or-later"
|
|
54
|
+
}
|