summer-engine 0.0.1 → 1.0.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 +72 -8
- package/dist/bin/postinstall.d.ts +2 -0
- package/dist/bin/postinstall.js +3 -0
- package/dist/bin/summer.d.ts +2 -0
- package/dist/bin/summer.js +34 -0
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.js +99 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +135 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.js +48 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +75 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +26 -0
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +7 -0
- package/dist/commands/open.d.ts +2 -0
- package/dist/commands/open.js +32 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +69 -0
- package/dist/commands/skills.d.ts +2 -0
- package/dist/commands/skills.js +187 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +43 -0
- package/dist/lib/api-client.d.ts +17 -0
- package/dist/lib/api-client.js +69 -0
- package/dist/lib/auth.d.ts +13 -0
- package/dist/lib/auth.js +39 -0
- package/dist/lib/banner.d.ts +4 -0
- package/dist/lib/banner.js +122 -0
- package/dist/lib/engine.d.ts +12 -0
- package/dist/lib/engine.js +36 -0
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.js +40 -0
- package/dist/mcp/tools/asset-tools.d.ts +2 -0
- package/dist/mcp/tools/asset-tools.js +247 -0
- package/dist/mcp/tools/debug-tools.d.ts +2 -0
- package/dist/mcp/tools/debug-tools.js +49 -0
- package/dist/mcp/tools/project-tools.d.ts +2 -0
- package/dist/mcp/tools/project-tools.js +55 -0
- package/dist/mcp/tools/scene-tools.d.ts +2 -0
- package/dist/mcp/tools/scene-tools.js +139 -0
- package/dist/mcp/tools/with-engine.d.ts +10 -0
- package/dist/mcp/tools/with-engine.js +65 -0
- package/package.json +22 -5
- package/skills/3d-lighting/SKILL.md +103 -0
- package/skills/fps-controller/SKILL.md +131 -0
- package/skills/gdscript-patterns/SKILL.md +96 -0
- package/skills/gdscript-patterns/reference.md +55 -0
- package/skills/scene-composition/SKILL.md +108 -0
- package/skills/ui-basics/SKILL.md +121 -0
- package/bin/summer.js +0 -3
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getAuthToken } from "../../lib/auth.js";
|
|
3
|
+
import { getClient } from "../server.js";
|
|
4
|
+
const GATEWAY_URL = process.env.SUMMER_GATEWAY_URL || "https://www.summerengine.com";
|
|
5
|
+
/** Kenney Cloudinary URL pattern: .../summer_art/kenney/3d/{pack-slug}/{filename}.glb */
|
|
6
|
+
const KENNEY_URL_PATTERN = /\/kenney\/3d\/([^/]+)\//;
|
|
7
|
+
function getPackSlugFromUrl(fileUrl) {
|
|
8
|
+
return fileUrl.match(KENNEY_URL_PATTERN)?.[1] ?? null;
|
|
9
|
+
}
|
|
10
|
+
function buildKenneyTextureUrl(fileUrl) {
|
|
11
|
+
const lastSlash = fileUrl.lastIndexOf("/");
|
|
12
|
+
const base = lastSlash >= 0 ? fileUrl.slice(0, lastSlash) : fileUrl;
|
|
13
|
+
return `${base}/Textures/colormap.png`;
|
|
14
|
+
}
|
|
15
|
+
async function textureExists(url) {
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(5000) });
|
|
18
|
+
return res.ok;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build import entries for Kenney 3D assets: texture first, then GLB.
|
|
26
|
+
* Pack-scoped paths prevent texture collision (each pack has its own Textures/colormap.png).
|
|
27
|
+
* See: publicsummerengine/Docs/ASSET_IMPORT_END_TO_END.md
|
|
28
|
+
*/
|
|
29
|
+
async function buildKenneyImportEntries(fileUrl, packSlug, fileName) {
|
|
30
|
+
const textureUrl = buildKenneyTextureUrl(fileUrl);
|
|
31
|
+
const hasTexture = await textureExists(textureUrl);
|
|
32
|
+
if (!hasTexture) {
|
|
33
|
+
return [{ url: fileUrl, path: `res://assets/models/kenney/${packSlug}/${fileName}` }];
|
|
34
|
+
}
|
|
35
|
+
const glbPath = `res://assets/models/kenney/${packSlug}/${fileName}`;
|
|
36
|
+
const glbDir = glbPath.replace(/\/[^/]+$/, "");
|
|
37
|
+
const texturePath = `${glbDir}/Textures/colormap.png`;
|
|
38
|
+
return [
|
|
39
|
+
{ url: textureUrl, path: texturePath },
|
|
40
|
+
{ url: fileUrl, path: glbPath },
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
async function searchAssetsApi(params) {
|
|
44
|
+
const token = await getAuthToken();
|
|
45
|
+
if (!token) {
|
|
46
|
+
return {
|
|
47
|
+
error: "Not logged in",
|
|
48
|
+
message: "Not signed in. The user needs to run this in their terminal:\n npx summer-engine login\nOr open: https://www.summerengine.com/login\nAsset search requires a Summer Engine account (free to create).",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const searchParams = new URLSearchParams();
|
|
52
|
+
searchParams.set("query", params.query);
|
|
53
|
+
if (params.assetType && params.assetType !== "all") {
|
|
54
|
+
searchParams.set("assetType", params.assetType);
|
|
55
|
+
}
|
|
56
|
+
if (params.limit) {
|
|
57
|
+
searchParams.set("limit", String(params.limit));
|
|
58
|
+
}
|
|
59
|
+
const res = await fetch(`${GATEWAY_URL}/api/mcp/assets?${searchParams}`, {
|
|
60
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
61
|
+
signal: AbortSignal.timeout(15000),
|
|
62
|
+
});
|
|
63
|
+
const data = (await res.json());
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
if (res.status === 402) {
|
|
66
|
+
return {
|
|
67
|
+
error: "upgrade_required",
|
|
68
|
+
message: (data.message || "Asset search requires a Pro subscription.") +
|
|
69
|
+
"\nUpgrade at: https://www.summerengine.com/pricing\nFree tier includes all other MCP tools (scene editing, debugging, etc).",
|
|
70
|
+
plan: data.plan,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (res.status === 401) {
|
|
74
|
+
return {
|
|
75
|
+
error: "unauthorized",
|
|
76
|
+
message: (data.message || "Auth token expired.") +
|
|
77
|
+
" The user needs to re-authenticate:\n npx summer-engine login --force",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
error: data.error || "Search failed",
|
|
82
|
+
message: data.message || "Asset search failed",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
export function registerAssetTools(server) {
|
|
88
|
+
server.tool("summer_search_assets", `Search the Summer Engine asset library (25k+ game assets) by description.
|
|
89
|
+
|
|
90
|
+
Uses hybrid search: keywords + semantic similarity. Finds assets by name AND by meaning.
|
|
91
|
+
Returns asset names, types, preview URLs, and import-ready file URLs.
|
|
92
|
+
|
|
93
|
+
Requires authentication. If the user gets an auth error, they need to run 'npx summer-engine login' in their terminal first.`, {
|
|
94
|
+
query: z.string().describe("Natural language search, e.g. 'low-poly tree', 'sci-fi weapon', 'wooden crate'"),
|
|
95
|
+
assetType: z.enum(["2d_image", "animation", "3d_model", "audio", "music", "all"]).default("all").describe("Filter by asset type"),
|
|
96
|
+
limit: z.number().default(10).describe("Max results (1-20)"),
|
|
97
|
+
}, async ({ query, assetType, limit }) => {
|
|
98
|
+
const result = await searchAssetsApi({ query, assetType, limit: Math.min(limit, 20) });
|
|
99
|
+
if (result.error) {
|
|
100
|
+
return {
|
|
101
|
+
content: [
|
|
102
|
+
{
|
|
103
|
+
type: "text",
|
|
104
|
+
text: JSON.stringify({ error: result.error, message: result.message, plan: result.plan }, null, 2),
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
isError: true,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
content: [
|
|
112
|
+
{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: JSON.stringify({
|
|
115
|
+
assets: result.assets,
|
|
116
|
+
count: result.count,
|
|
117
|
+
summary: result.summary,
|
|
118
|
+
message: result.message,
|
|
119
|
+
}, null, 2),
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
server.tool("summer_import_asset", `Search the asset library and import the best match into the project in one step.
|
|
125
|
+
|
|
126
|
+
Use when the user wants a specific type of asset added: "Add a tree to the scene", "Import a wooden barrel".
|
|
127
|
+
Searches, picks the top result, downloads and imports it, then optionally adds it to the scene.
|
|
128
|
+
|
|
129
|
+
Requires authentication. If the user gets an auth error, they need to run 'npx summer-engine login' in their terminal first. Summer Engine must be running.`, {
|
|
130
|
+
query: z.string().describe("What to find, e.g. 'low-poly tree', 'wooden crate'"),
|
|
131
|
+
parent: z.string().optional().describe("Parent node path to add the asset under, e.g. './World'. If omitted, only imports (no scene placement)"),
|
|
132
|
+
assetType: z.enum(["2d_image", "animation", "3d_model", "audio", "music", "all"]).default("3d_model").describe("Preferred asset type"),
|
|
133
|
+
}, async ({ query, parent, assetType }) => {
|
|
134
|
+
const searchResult = await searchAssetsApi({ query, assetType, limit: 5 });
|
|
135
|
+
if (searchResult.error) {
|
|
136
|
+
return {
|
|
137
|
+
content: [
|
|
138
|
+
{
|
|
139
|
+
type: "text",
|
|
140
|
+
text: JSON.stringify({ error: searchResult.error, message: searchResult.message }, null, 2),
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
isError: true,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const assets = searchResult.assets || [];
|
|
147
|
+
if (assets.length === 0) {
|
|
148
|
+
return {
|
|
149
|
+
content: [
|
|
150
|
+
{
|
|
151
|
+
type: "text",
|
|
152
|
+
text: JSON.stringify({ error: "No results", message: `No assets found for "${query}". Try different keywords.` }, null, 2),
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
isError: true,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const best = assets[0];
|
|
159
|
+
const fileUrl = best.fileUrl;
|
|
160
|
+
if (!fileUrl) {
|
|
161
|
+
return {
|
|
162
|
+
content: [
|
|
163
|
+
{
|
|
164
|
+
type: "text",
|
|
165
|
+
text: JSON.stringify({ error: "Invalid asset", message: "Asset has no download URL." }, null, 2),
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
isError: true,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const fileName = fileUrl.split("/").pop()?.split("?")[0] || "asset";
|
|
172
|
+
const packSlug = best.packSlug ?? getPackSlugFromUrl(fileUrl) ?? "misc";
|
|
173
|
+
let imports;
|
|
174
|
+
if (best.type === "3d_model" && fileUrl.includes("kenney/3d/")) {
|
|
175
|
+
imports = await buildKenneyImportEntries(fileUrl, packSlug, fileName);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const path = best.type === "3d_model"
|
|
179
|
+
? `res://assets/models/${fileName}`
|
|
180
|
+
: `res://assets/${fileName}`;
|
|
181
|
+
imports = [{ url: fileUrl, path }];
|
|
182
|
+
}
|
|
183
|
+
const importPath = imports[imports.length - 1].path;
|
|
184
|
+
try {
|
|
185
|
+
const client = await getClient();
|
|
186
|
+
const importResult = imports.length === 1
|
|
187
|
+
? await client.executeOps([{ op: "ImportFromUrl", url: imports[0].url, path: imports[0].path }])
|
|
188
|
+
: await client.executeOps([{ op: "ImportFromUrlBatch", imports }]);
|
|
189
|
+
const ok = importResult?.results?.[0]?.ok;
|
|
190
|
+
if (!ok) {
|
|
191
|
+
return {
|
|
192
|
+
content: [
|
|
193
|
+
{
|
|
194
|
+
type: "text",
|
|
195
|
+
text: JSON.stringify({
|
|
196
|
+
error: "Import failed",
|
|
197
|
+
message: "Could not import asset. Check engine logs.",
|
|
198
|
+
asset: best.title,
|
|
199
|
+
}, null, 2),
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
isError: true,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (parent && best.type === "3d_model") {
|
|
206
|
+
await client.executeOps([
|
|
207
|
+
{ op: "InstantiateScene", parent, scene: importPath, name: best.title.replace(/\s+/g, "_") },
|
|
208
|
+
]);
|
|
209
|
+
}
|
|
210
|
+
// 2D sprites and audio: import only; user adds to scene manually
|
|
211
|
+
return {
|
|
212
|
+
content: [
|
|
213
|
+
{
|
|
214
|
+
type: "text",
|
|
215
|
+
text: JSON.stringify({
|
|
216
|
+
success: true,
|
|
217
|
+
asset: best.title,
|
|
218
|
+
type: best.type,
|
|
219
|
+
importedTo: importPath,
|
|
220
|
+
addedToScene: parent ? true : false,
|
|
221
|
+
parent: parent || null,
|
|
222
|
+
message: parent
|
|
223
|
+
? `Imported "${best.title}" and added to ${parent}`
|
|
224
|
+
: `Imported "${best.title}" to ${importPath}`,
|
|
225
|
+
}, null, 2),
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
232
|
+
return {
|
|
233
|
+
content: [
|
|
234
|
+
{
|
|
235
|
+
type: "text",
|
|
236
|
+
text: JSON.stringify({
|
|
237
|
+
error: "Engine error",
|
|
238
|
+
message: msg,
|
|
239
|
+
hint: "Make sure Summer Engine is running.",
|
|
240
|
+
}, null, 2),
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
isError: true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { withEngine } from "./with-engine.js";
|
|
3
|
+
export function registerDebugTools(server) {
|
|
4
|
+
server.tool("summer_get_diagnostics", `Quick overview of all errors and warnings from both the editor console and the runtime debugger. Returns error counts and a guidance message.
|
|
5
|
+
|
|
6
|
+
ALWAYS call this FIRST before diving into summer_get_console or summer_get_debugger_errors. It tells you where to look.
|
|
7
|
+
|
|
8
|
+
Typical workflow after making changes:
|
|
9
|
+
1. summer_get_diagnostics — are there issues?
|
|
10
|
+
2. If errors: summer_get_console or summer_get_debugger_errors for details
|
|
11
|
+
3. Fix the issues
|
|
12
|
+
4. summer_get_diagnostics again to verify`, {}, async () => withEngine(async (client) => client.getDiagnostics()));
|
|
13
|
+
server.tool("summer_get_console", "Read recent messages from the editor's Output panel. Shows print statements, warnings, and errors from both the editor and scripts. Use after summer_get_diagnostics indicates console issues.", {
|
|
14
|
+
max_lines: z.number().optional().default(100).describe("Max lines to return (default 100)"),
|
|
15
|
+
filter: z.string().optional().describe("Only return lines containing this string"),
|
|
16
|
+
type: z.enum(["error", "warning", "std", "editor"]).optional().describe("Filter by message type"),
|
|
17
|
+
}, async ({ max_lines, filter, type }) => withEngine(async (client) => {
|
|
18
|
+
const op = { op: "GetConsoleOutput", max_lines };
|
|
19
|
+
if (filter)
|
|
20
|
+
op.filter = filter;
|
|
21
|
+
if (type)
|
|
22
|
+
op.type = type;
|
|
23
|
+
return client.executeOps([op]);
|
|
24
|
+
}));
|
|
25
|
+
server.tool("summer_clear_console", "Clear the editor's Output panel. Useful before running the game to get a clean slate for error checking.", {}, async () => withEngine(async (client) => client.executeOps([{ op: "ClearConsoleOutput" }])));
|
|
26
|
+
server.tool("summer_get_debugger_errors", "Read runtime errors and warnings from the debugger. These are errors that occur while the game is running (null references, missing nodes, physics errors). Different from console output — these come from the debugger, not print statements.", {
|
|
27
|
+
max_errors: z.number().optional().default(50).describe("Max errors to return"),
|
|
28
|
+
include_stack: z.boolean().optional().describe("Include stack traces for each error"),
|
|
29
|
+
}, async ({ max_errors, include_stack }) => withEngine(async (client) => {
|
|
30
|
+
const op = { op: "GetDebuggerErrors", max_errors };
|
|
31
|
+
if (include_stack !== undefined)
|
|
32
|
+
op.include_stack = include_stack;
|
|
33
|
+
return client.executeOps([op]);
|
|
34
|
+
}));
|
|
35
|
+
server.tool("summer_play", `Start running the game in the engine. The game runs inside Summer Engine's viewport.
|
|
36
|
+
|
|
37
|
+
After starting, use summer_get_diagnostics to check for runtime errors.
|
|
38
|
+
|
|
39
|
+
You can run a specific scene instead of the main scene — useful for testing individual levels or UI screens.`, {
|
|
40
|
+
scene: z.string().optional().describe("Scene to run instead of main scene, e.g. 'res://levels/test_level.tscn'"),
|
|
41
|
+
}, async ({ scene }) => withEngine(async (client) => client.play(scene)));
|
|
42
|
+
server.tool("summer_stop", "Stop the running game. Call this before making scene changes — some operations require the game to not be running.", {}, async () => withEngine(async (client) => client.stop()));
|
|
43
|
+
server.tool("summer_is_running", "Check if the game is currently running. Returns the active scene path if running.", {}, async () => withEngine(async (client) => client.executeOps([{ op: "IsGameRunning" }])));
|
|
44
|
+
server.tool("summer_get_script_errors", `Check a GDScript file for parse/compile errors without running the game.
|
|
45
|
+
|
|
46
|
+
Use after writing or editing a .gd file to verify it compiles. Returns line numbers, error messages, and severity. Much faster than running the game to discover script errors.`, {
|
|
47
|
+
path: z.string().describe("Script path, e.g. 'res://scripts/player.gd' or 'res://player_controller.gd'"),
|
|
48
|
+
}, async ({ path }) => withEngine(async (client) => client.getScriptErrors(path)));
|
|
49
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { withEngine } from "./with-engine.js";
|
|
3
|
+
export function registerProjectTools(server) {
|
|
4
|
+
server.tool("summer_project_setting", `Set a project setting in project.godot. Common settings:
|
|
5
|
+
- "application/config/name" — project name
|
|
6
|
+
- "application/run/main_scene" — main scene path
|
|
7
|
+
- "rendering/renderer/rendering_method" — "forward_plus", "mobile", or "gl_compatibility"
|
|
8
|
+
- "display/window/size/viewport_width" — window width
|
|
9
|
+
- "display/window/size/viewport_height" — window height
|
|
10
|
+
- "physics/3d/default_gravity" — gravity value (float)`, {
|
|
11
|
+
key: z.string().describe("Setting key path, e.g. 'application/config/name'"),
|
|
12
|
+
value: z.union([z.string(), z.number(), z.boolean()]).describe("Setting value"),
|
|
13
|
+
}, async ({ key, value }) => withEngine(async (client) => client.executeOps([{ op: "ProjectSetting", key, value }])));
|
|
14
|
+
server.tool("summer_input_map_bind", `Set up input controls. Creates the action if it doesn't exist, then binds events to it.
|
|
15
|
+
|
|
16
|
+
Event format:
|
|
17
|
+
- Keyboard: { type: "key", key: "W" } or { type: "key", key: "Space" }
|
|
18
|
+
- Mouse button: { type: "mouse_button", button: 1 } (1=left, 2=right, 3=middle)
|
|
19
|
+
- Common keys: "W", "A", "S", "D", "Space", "Shift", "E", "Escape", "Up", "Down", "Left", "Right"
|
|
20
|
+
|
|
21
|
+
Example: Bind jump to Space and W:
|
|
22
|
+
name: "jump", events: [{ type: "key", key: "Space" }, { type: "key", key: "W" }]`, {
|
|
23
|
+
name: z.string().describe("Action name, e.g. 'jump', 'move_forward', 'interact'"),
|
|
24
|
+
events: z.array(z.record(z.unknown())).describe("Array of input event objects"),
|
|
25
|
+
}, async ({ name, events }) => withEngine(async (client) => {
|
|
26
|
+
const ops = [
|
|
27
|
+
{ op: "InputMapAddAction", name },
|
|
28
|
+
{ op: "InputMapBind", name, events },
|
|
29
|
+
];
|
|
30
|
+
return client.executeOps(ops);
|
|
31
|
+
}));
|
|
32
|
+
server.tool("summer_get_scene_tree", "Get the full scene tree structure of the currently open scene. Returns all nodes with their types, paths, and children. Use this to understand the scene before making changes.", {}, async () => withEngine(async (client) => client.getSceneState()));
|
|
33
|
+
server.tool("summer_import_from_url", `Download a file from a URL and import it into the project. Triggers Godot's full import pipeline — generates .import files, extracts textures from .glb models, creates materials.
|
|
34
|
+
|
|
35
|
+
Use this for:
|
|
36
|
+
- 3D models (.glb, .gltf, .obj)
|
|
37
|
+
- Textures (.png, .jpg, .webp)
|
|
38
|
+
- Audio (.ogg, .wav, .mp3)
|
|
39
|
+
|
|
40
|
+
The path is auto-inferred from the URL filename if not specified. After import, the asset is immediately usable in scenes.`, {
|
|
41
|
+
url: z.string().describe("HTTP(S) URL to download from"),
|
|
42
|
+
path: z.string().optional().describe("Target path in project, e.g. 'res://assets/player.glb'. Auto-inferred from URL if omitted."),
|
|
43
|
+
}, async ({ url, path }) => withEngine(async (client) => {
|
|
44
|
+
const op = { op: "ImportFromUrl", url };
|
|
45
|
+
if (path)
|
|
46
|
+
op.path = path;
|
|
47
|
+
return client.executeOps([op]);
|
|
48
|
+
}));
|
|
49
|
+
server.tool("summer_import_from_url_batch", "Download multiple files from URLs in one operation. Performs a single filesystem scan after all downloads, which is faster than importing one at a time.", {
|
|
50
|
+
imports: z.array(z.object({
|
|
51
|
+
url: z.string().describe("URL to download"),
|
|
52
|
+
path: z.string().describe("Target path in project"),
|
|
53
|
+
})).describe("Array of {url, path} objects"),
|
|
54
|
+
}, async ({ imports }) => withEngine(async (client) => client.executeOps([{ op: "ImportFromUrlBatch", imports }])));
|
|
55
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { withEngine } from "./with-engine.js";
|
|
3
|
+
export function registerSceneTools(server) {
|
|
4
|
+
server.tool("summer_add_node", `Add a new node to the scene tree.
|
|
5
|
+
|
|
6
|
+
Common node types:
|
|
7
|
+
- 3D: Node3D, MeshInstance3D, CharacterBody3D, RigidBody3D, StaticBody3D, Camera3D, DirectionalLight3D, OmniLight3D, SpotLight3D, WorldEnvironment, CollisionShape3D, Area3D
|
|
8
|
+
- 2D: Node2D, Sprite2D, CharacterBody2D, RigidBody2D, StaticBody2D, Camera2D, CollisionShape2D, Area2D, TileMapLayer
|
|
9
|
+
- UI: Control, Label, Button, TextEdit, Panel, VBoxContainer, HBoxContainer, MarginContainer
|
|
10
|
+
- Audio: AudioStreamPlayer, AudioStreamPlayer3D
|
|
11
|
+
|
|
12
|
+
The parent path uses "./" prefix for relative paths from scene root. E.g., "./World" means the "World" child of the root node.`, {
|
|
13
|
+
parent: z.string().describe("Parent node path, e.g. './World' or './World/Enemies'"),
|
|
14
|
+
type: z.string().describe("Godot node type, e.g. 'MeshInstance3D', 'CharacterBody3D'"),
|
|
15
|
+
name: z.string().describe("Name for the new node, e.g. 'Player', 'MainCamera'"),
|
|
16
|
+
}, async ({ parent, type, name }) => withEngine(async (client) => client.executeOps([{ op: "AddNode", parent, type, name }])));
|
|
17
|
+
server.tool("summer_set_prop", `Set a property on a node. This is the primary way to configure nodes after adding them.
|
|
18
|
+
|
|
19
|
+
VALUE FORMAT — Godot string syntax for complex types:
|
|
20
|
+
- Vector3: "Vector3(0, 10, 0)" — position, scale, rotation_degrees
|
|
21
|
+
- Vector2: "Vector2(100, 200)" — 2D position, size
|
|
22
|
+
- Color: "Color(1, 0.5, 0, 1)" — RGBA, always 4 components, values 0.0-1.0
|
|
23
|
+
- Transform3D: "Transform3D(1,0,0, 0,1,0, 0,0,1, 0,5,0)" — basis + origin
|
|
24
|
+
- Resource class name: "BoxMesh", "SphereMesh", "StandardMaterial3D" — auto-instantiated
|
|
25
|
+
- Numbers: 1.5, 42 — native JSON
|
|
26
|
+
- Booleans: true, false — native JSON
|
|
27
|
+
- Strings: "hello" — native JSON
|
|
28
|
+
|
|
29
|
+
COMMON PROPERTIES:
|
|
30
|
+
- position: "Vector3(x, y, z)" — world position
|
|
31
|
+
- rotation_degrees: "Vector3(rx, ry, rz)" — rotation in degrees
|
|
32
|
+
- scale: "Vector3(sx, sy, sz)" — scale factor
|
|
33
|
+
- visible: true/false — visibility
|
|
34
|
+
- mesh: "BoxMesh", "SphereMesh", "CapsuleMesh", "CylinderMesh", "PlaneMesh"
|
|
35
|
+
- shadow_enabled: true — for lights
|
|
36
|
+
- light_energy: 1.5 — light intensity
|
|
37
|
+
- fov: 75.0 — camera field of view`, {
|
|
38
|
+
path: z.string().describe("Node path, e.g. './World/Player'"),
|
|
39
|
+
key: z.string().describe("Property name, e.g. 'position', 'mesh', 'visible'"),
|
|
40
|
+
value: z.union([z.string(), z.number(), z.boolean()]).describe("Value in Godot string format for complex types, native JSON for primitives"),
|
|
41
|
+
}, async ({ path, key, value }) => withEngine(async (client) => client.executeOps([{ op: "SetProp", path, key, value }])));
|
|
42
|
+
server.tool("summer_set_resource_property", `Set a nested property on a resource attached to a node.
|
|
43
|
+
|
|
44
|
+
Use when you need to modify a sub-property of a resource, like:
|
|
45
|
+
- CollisionShape3D shape size: nodePath="./Player/CollisionShape3D", resourceProperty="shape", subProperty="size", value="Vector3(1, 2, 1)"
|
|
46
|
+
- Material albedo color: nodePath="./Floor", resourceProperty="material_override", subProperty="albedo_color", value="Color(0.2, 0.5, 0.2, 1)"
|
|
47
|
+
- Mesh size: nodePath="./Box", resourceProperty="mesh", subProperty="size", value="Vector3(2, 2, 2)"`, {
|
|
48
|
+
nodePath: z.string().describe("Node path, e.g. './Player/CollisionShape3D'"),
|
|
49
|
+
resourceProperty: z.string().describe("Resource property on the node, e.g. 'shape', 'mesh', 'material_override'"),
|
|
50
|
+
subProperty: z.string().describe("Property on the resource, e.g. 'size', 'radius', 'albedo_color'"),
|
|
51
|
+
value: z.union([z.string(), z.number(), z.boolean()]).describe("Value in Godot string format"),
|
|
52
|
+
}, async ({ nodePath, resourceProperty, subProperty, value }) => withEngine(async (client) => client.executeOps([
|
|
53
|
+
{ op: "SetResourceProperty", nodePath, resourceProperty, subProperty, value },
|
|
54
|
+
])));
|
|
55
|
+
server.tool("summer_remove_node", "Remove a node from the scene tree. All children are removed too. Cannot remove the root node. Supports undo.", { path: z.string().describe("Node path to remove, e.g. './World/OldEnemy'") }, async ({ path }) => withEngine(async (client) => client.executeOps([{ op: "RemoveNode", path }])));
|
|
56
|
+
server.tool("summer_save_scene", "Save the current scene to disk. Call this after making scene changes to persist them. Without saving, changes only exist in the editor's memory.", { path: z.string().optional().describe("Save-as path for creating a new scene file, e.g. 'res://levels/level2.tscn'") }, async ({ path }) => withEngine(async (client) => {
|
|
57
|
+
const op = { op: "SaveScene" };
|
|
58
|
+
if (path)
|
|
59
|
+
op.path = path;
|
|
60
|
+
return client.executeOps([op]);
|
|
61
|
+
}));
|
|
62
|
+
server.tool("summer_open_scene", "Open a scene file in the editor. Use this to switch between scenes (e.g., open a level to edit it).", { path: z.string().describe("Scene path, e.g. 'res://main.tscn' or 'res://levels/level1.tscn'") }, async ({ path }) => withEngine(async (client) => client.executeOps([{ op: "OpenScene", path }])));
|
|
63
|
+
server.tool("summer_instantiate_scene", `Add an existing scene or 3D model as a child node. Use this to:
|
|
64
|
+
- Add a .tscn prefab (reusable scene) as a child
|
|
65
|
+
- Add a .glb/.gltf 3D model into the scene
|
|
66
|
+
- Compose scenes from smaller scenes (e.g., add a "Player" scene into a "Level" scene)
|
|
67
|
+
|
|
68
|
+
The scene must already exist in the project. Use summer_import_from_url first if importing from external sources.`, {
|
|
69
|
+
parent: z.string().describe("Parent node path, e.g. './World'"),
|
|
70
|
+
scene: z.string().describe("Scene/model path, e.g. 'res://player.tscn' or 'res://models/tree.glb'"),
|
|
71
|
+
name: z.string().optional().describe("Override the instance name"),
|
|
72
|
+
}, async ({ parent, scene, name }) => withEngine(async (client) => {
|
|
73
|
+
const op = { op: "InstantiateScene", parent, scene };
|
|
74
|
+
if (name)
|
|
75
|
+
op.name = name;
|
|
76
|
+
return client.executeOps([op]);
|
|
77
|
+
}));
|
|
78
|
+
server.tool("summer_connect_signal", `Connect a signal between two nodes. Signals are Godot's event system — they notify when something happens.
|
|
79
|
+
|
|
80
|
+
Common signals:
|
|
81
|
+
- "body_entered" / "body_exited" — Area3D/Area2D detects physics bodies
|
|
82
|
+
- "pressed" — Button clicked
|
|
83
|
+
- "timeout" — Timer finished
|
|
84
|
+
- "area_entered" — Area detects another area
|
|
85
|
+
- "input_event" — CollisionObject received input
|
|
86
|
+
|
|
87
|
+
The receiver node must have a script with the specified method.`, {
|
|
88
|
+
emitter: z.string().describe("Node that fires the signal, e.g. './Player/HitArea'"),
|
|
89
|
+
signal: z.string().describe("Signal name, e.g. 'body_entered'"),
|
|
90
|
+
receiver: z.string().describe("Node with the handler script, e.g. './Player'"),
|
|
91
|
+
method: z.string().describe("Method name in the receiver's script, e.g. '_on_hit_area_body_entered'"),
|
|
92
|
+
}, async ({ emitter, signal, receiver, method }) => withEngine(async (client) => client.executeOps([
|
|
93
|
+
{ op: "ConnectSignal", emitter, signal, receiver, method },
|
|
94
|
+
])));
|
|
95
|
+
server.tool("summer_select_node", "Select a node in the editor's scene tree and show it in the inspector panel. Useful for focusing the editor on a specific node.", {
|
|
96
|
+
nodePath: z.string().describe("Node path to select"),
|
|
97
|
+
scenePath: z.string().optional().describe("Open this scene first, then select the node"),
|
|
98
|
+
}, async ({ nodePath, scenePath }) => withEngine(async (client) => {
|
|
99
|
+
const op = { op: "SelectNode", nodePath };
|
|
100
|
+
if (scenePath)
|
|
101
|
+
op.scenePath = scenePath;
|
|
102
|
+
return client.executeOps([op]);
|
|
103
|
+
}));
|
|
104
|
+
server.tool("summer_replace_node", "Replace a node with a different type or scene, preserving its position in the tree and its children. Useful for changing a StaticBody3D to a RigidBody3D, or swapping a placeholder with a proper prefab.", {
|
|
105
|
+
path: z.string().describe("Node path to replace"),
|
|
106
|
+
type: z.string().optional().describe("New node type, e.g. 'RigidBody3D'"),
|
|
107
|
+
scene: z.string().optional().describe("Scene to replace with, e.g. 'res://enemies/boss.tscn'"),
|
|
108
|
+
}, async ({ path, type, scene }) => withEngine(async (client) => {
|
|
109
|
+
const op = { op: "ReplaceNode", path };
|
|
110
|
+
if (type)
|
|
111
|
+
op.type = type;
|
|
112
|
+
if (scene)
|
|
113
|
+
op.scene = scene;
|
|
114
|
+
return client.executeOps([op]);
|
|
115
|
+
}));
|
|
116
|
+
server.tool("summer_inspect_node", `Get all editable properties of a node with their current values, types, and resource info.
|
|
117
|
+
|
|
118
|
+
Call this before modifying a node to understand its current state. Returns every property the Godot inspector would show.
|
|
119
|
+
|
|
120
|
+
Example: inspect a light to see its energy, color, shadow settings before changing them.`, {
|
|
121
|
+
path: z.string().describe("Node path from scene tree, e.g. 'Player', 'World/Enemies/Boss', 'DirectionalLight3D'"),
|
|
122
|
+
}, async ({ path }) => withEngine(async (client) => client.inspectNode(path)));
|
|
123
|
+
server.tool("summer_inspect_resource", `Get all properties of a resource (material, mesh, shape, environment, etc).
|
|
124
|
+
|
|
125
|
+
Use when you need the sub-properties of a resource attached to a node. For example, summer_inspect_node tells you a MeshInstance3D has a "StandardMaterial3D" material — this tool tells you that material's albedo_color, metallic, roughness, etc.`, {
|
|
126
|
+
path: z.string().describe("Resource path, e.g. 'res://materials/ground.tres' or 'res://models/player.glb'"),
|
|
127
|
+
}, async ({ path }) => withEngine(async (client) => client.inspectResource(path)));
|
|
128
|
+
server.tool("summer_batch", `Execute multiple operations in a single call, grouped into one undo step.
|
|
129
|
+
|
|
130
|
+
The user can undo everything with a single Ctrl+Z. Use this when building something that involves multiple nodes and properties — e.g., creating a player character with collision, camera, and properties.
|
|
131
|
+
|
|
132
|
+
Each op in the array uses the same format as the individual tools:
|
|
133
|
+
- {"op": "AddNode", "parent": "/", "type": "MeshInstance3D", "name": "Floor"}
|
|
134
|
+
- {"op": "SetProp", "path": "Floor", "key": "position", "value": "Vector3(0, -1, 0)"}
|
|
135
|
+
- {"op": "SetProp", "path": "Floor", "key": "mesh", "value": "PlaneMesh"}
|
|
136
|
+
- {"op": "SetResourceProperty", "nodePath": "Floor", "resourceProperty": "mesh", "subProperty": "size", "value": "Vector2(20, 20)"}`, {
|
|
137
|
+
ops: z.array(z.record(z.unknown())).describe("Array of operation objects, each with 'op' plus its parameters"),
|
|
138
|
+
}, async ({ ops }) => withEngine(async (client) => client.executeOps(ops, { groupUndo: true })));
|
|
139
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { getClient } from "../server.js";
|
|
2
|
+
type ToolResult = {
|
|
3
|
+
content: {
|
|
4
|
+
type: "text";
|
|
5
|
+
text: string;
|
|
6
|
+
}[];
|
|
7
|
+
isError?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function withEngine<T>(fn: (client: Awaited<ReturnType<typeof getClient>>) => Promise<T>): Promise<ToolResult>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { getClient, resetClient } from "../server.js";
|
|
2
|
+
import { getAuthToken } from "../../lib/auth.js";
|
|
3
|
+
const GATEWAY_URL = process.env.SUMMER_GATEWAY_URL || "https://www.summerengine.com";
|
|
4
|
+
async function checkMcpQuota() {
|
|
5
|
+
const token = await getAuthToken();
|
|
6
|
+
if (!token) {
|
|
7
|
+
return {
|
|
8
|
+
allowed: false,
|
|
9
|
+
message: "Not signed in. The user needs to run this in their terminal:\n npx summer-engine login\nOr open: https://www.summerengine.com/login\nMCP tools require a Summer Engine account (free to create).",
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const res = await fetch(`${GATEWAY_URL}/api/mcp/log-local-call`, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: {
|
|
16
|
+
Authorization: `Bearer ${token}`,
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
},
|
|
19
|
+
body: JSON.stringify({}),
|
|
20
|
+
signal: AbortSignal.timeout(10000),
|
|
21
|
+
});
|
|
22
|
+
const data = (await res.json().catch(() => ({})));
|
|
23
|
+
// Hard enforcement errors should block tool execution.
|
|
24
|
+
if ([401, 402, 403, 429].includes(res.status)) {
|
|
25
|
+
return {
|
|
26
|
+
allowed: false,
|
|
27
|
+
message: data.message || "Quota check failed. Try again.",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Soft-fail on service/gateway issues so local tools keep working.
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
return { allowed: true };
|
|
33
|
+
}
|
|
34
|
+
if (data.allowed === false) {
|
|
35
|
+
return {
|
|
36
|
+
allowed: false,
|
|
37
|
+
message: data.message || "MCP call limit reached. Upgrade at https://summerengine.com/pricing",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return { allowed: true };
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
// Network/transient errors should not brick local MCP usage.
|
|
44
|
+
return { allowed: true };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function withEngine(fn) {
|
|
48
|
+
try {
|
|
49
|
+
const quota = await checkMcpQuota();
|
|
50
|
+
if (!quota.allowed) {
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: "text", text: quota.message || "Quota exceeded" }],
|
|
53
|
+
isError: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const client = await getClient();
|
|
57
|
+
const result = await fn(client);
|
|
58
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
resetClient();
|
|
62
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
63
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
64
|
+
}
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "summer-engine",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "CLI and MCP tools for Summer Engine
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI and MCP tools for Summer Engine — the AI-native game engine",
|
|
5
5
|
"keywords": ["game-engine", "godot", "ai", "mcp", "gamedev", "3d", "summer-engine"],
|
|
6
6
|
"homepage": "https://summerengine.com",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/
|
|
9
|
+
"url": "https://github.com/SummerEngine/summer-engine-cli"
|
|
10
10
|
},
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"author": "Summer Engine <founders@summerengine.com>",
|
|
13
|
+
"type": "module",
|
|
13
14
|
"bin": {
|
|
14
|
-
"summer": "./bin/summer.js"
|
|
15
|
+
"summer": "./dist/bin/summer.js"
|
|
16
|
+
},
|
|
17
|
+
"files": ["dist/", "skills/", "README.md"],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsc --watch",
|
|
21
|
+
"summer": "node dist/bin/summer.js",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
15
23
|
},
|
|
16
|
-
"files": ["bin/", "README.md"],
|
|
17
24
|
"engines": {
|
|
18
25
|
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
29
|
+
"commander": "^12.0.0",
|
|
30
|
+
"open": "^10.0.0",
|
|
31
|
+
"zod": "^3.23.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^20.0.0",
|
|
35
|
+
"typescript": "^5.5.0"
|
|
19
36
|
}
|
|
20
37
|
}
|