summer-engine 2.5.0 → 2.6.1
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +5 -4
- package/.codex-plugin/plugin.json +5 -4
- package/.cursor-plugin/plugin.json +4 -3
- package/.factory-plugin/plugin.json +3 -3
- package/AGENTS.md +6 -4
- package/CHANGELOG.md +20 -2
- package/GEMINI.md +3 -3
- package/README.md +113 -100
- package/dist/bin/summer.js +6 -1
- package/dist/commands/cloud.d.ts +2 -0
- package/dist/commands/cloud.js +80 -0
- package/dist/commands/doctor.js +19 -0
- package/dist/commands/login.js +4 -1
- package/dist/commands/logout.js +1 -1
- package/dist/commands/open.js +1 -1
- package/dist/commands/plan.d.ts +2 -0
- package/dist/commands/plan.js +60 -0
- package/dist/commands/run.js +132 -35
- package/dist/lib/api-client.js +4 -1
- package/dist/lib/auth.d.ts +2 -0
- package/dist/lib/auth.js +13 -0
- package/dist/lib/cloud/api.d.ts +79 -0
- package/dist/lib/cloud/api.js +107 -0
- package/dist/lib/cloud/apply.d.ts +32 -0
- package/dist/lib/cloud/apply.js +294 -0
- package/dist/lib/cloud/checkpoint.d.ts +26 -0
- package/dist/lib/cloud/checkpoint.js +142 -0
- package/dist/lib/cloud/conflicts.d.ts +55 -0
- package/dist/lib/cloud/conflicts.js +131 -0
- package/dist/lib/cloud/containment.d.ts +8 -0
- package/dist/lib/cloud/containment.js +52 -0
- package/dist/lib/cloud/diff.d.ts +39 -0
- package/dist/lib/cloud/diff.js +138 -0
- package/dist/lib/cloud/engine-bridge.d.ts +21 -0
- package/dist/lib/cloud/engine-bridge.js +105 -0
- package/dist/lib/cloud/hash-cache.d.ts +23 -0
- package/dist/lib/cloud/hash-cache.js +44 -0
- package/dist/lib/cloud/hash.d.ts +10 -0
- package/dist/lib/cloud/hash.js +111 -0
- package/dist/lib/cloud/http.d.ts +17 -0
- package/dist/lib/cloud/http.js +47 -0
- package/dist/lib/cloud/lock-path.d.ts +1 -0
- package/dist/lib/cloud/lock-path.js +5 -0
- package/dist/lib/cloud/lock.d.ts +14 -0
- package/dist/lib/cloud/lock.js +143 -0
- package/dist/lib/cloud/manifest.d.ts +6 -0
- package/dist/lib/cloud/manifest.js +41 -0
- package/dist/lib/cloud/paths.d.ts +20 -0
- package/dist/lib/cloud/paths.js +90 -0
- package/dist/lib/cloud/project-godot-merge.d.ts +32 -0
- package/dist/lib/cloud/project-godot-merge.js +185 -0
- package/dist/lib/cloud/rules.d.ts +39 -0
- package/dist/lib/cloud/rules.js +159 -0
- package/dist/lib/cloud/sync.d.ts +21 -0
- package/dist/lib/cloud/sync.js +518 -0
- package/dist/lib/cloud/transfer.d.ts +23 -0
- package/dist/lib/cloud/transfer.js +184 -0
- package/dist/lib/cloud/types.d.ts +62 -0
- package/dist/lib/cloud/types.js +1 -0
- package/dist/lib/cloud/validate-path.d.ts +23 -0
- package/dist/lib/cloud/validate-path.js +103 -0
- package/dist/lib/engine.d.ts +6 -0
- package/dist/lib/engine.js +36 -1
- package/dist/lib/game-task-plan.d.ts +42 -0
- package/dist/lib/game-task-plan.js +376 -0
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/tools/asset-tools.js +265 -45
- package/dist/mcp/tools/cloud-tools.d.ts +2 -0
- package/dist/mcp/tools/cloud-tools.js +42 -0
- package/dist/mcp/tools/generate-tools.js +101 -13
- package/dist/mcp/tools/project-tools.js +59 -16
- package/gemini-extension.json +3 -3
- package/package.json +16 -5
- package/skills/3d-assets/character-model/SKILL.md +27 -5
- package/skills/3d-assets/environment-kit/SKILL.md +7 -2
- package/skills/3d-assets/organic-model/SKILL.md +4 -3
- package/skills/3d-assets/prop-model/SKILL.md +16 -8
- package/skills/3d-assets/vehicle-model/SKILL.md +4 -3
- package/skills/animation/animation-tree/SKILL.md +2 -0
- package/skills/animation/generate-motion/SKILL.md +25 -4
- package/skills/asset-pipeline/asset-strategy/SKILL.md +3 -2
- package/skills/workflow/mcpupdate/SKILL.md +134 -0
|
@@ -86,6 +86,143 @@ async function searchAssetsApi(params) {
|
|
|
86
86
|
}
|
|
87
87
|
return data;
|
|
88
88
|
}
|
|
89
|
+
async function authedGetJson(endpoint, timeoutMs = 15_000) {
|
|
90
|
+
const token = await getAuthToken();
|
|
91
|
+
if (!token) {
|
|
92
|
+
return {
|
|
93
|
+
status: 401,
|
|
94
|
+
error: "not_logged_in",
|
|
95
|
+
message: "Not signed in. Run in your terminal:\n npx summer-engine login",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const res = await fetch(`${GATEWAY_URL}${endpoint}`, {
|
|
99
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
100
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
101
|
+
});
|
|
102
|
+
const data = (await res.json());
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
return {
|
|
105
|
+
status: res.status,
|
|
106
|
+
data,
|
|
107
|
+
error: data?.error || "request_failed",
|
|
108
|
+
message: data?.message || "Request failed",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return { status: res.status, data };
|
|
112
|
+
}
|
|
113
|
+
async function getAssetApi(assetId) {
|
|
114
|
+
const result = await authedGetJson(`/api/mcp/assets/${encodeURIComponent(assetId)}`);
|
|
115
|
+
if (result.error) {
|
|
116
|
+
return { error: result.error, message: result.message };
|
|
117
|
+
}
|
|
118
|
+
return { asset: result.data?.asset };
|
|
119
|
+
}
|
|
120
|
+
async function getAssetDownloadUrlApi(assetId, role) {
|
|
121
|
+
const result = await authedGetJson(`/api/mcp/assets/${encodeURIComponent(assetId)}/download-url?role=${encodeURIComponent(role)}`);
|
|
122
|
+
if (result.error) {
|
|
123
|
+
return { error: result.error, message: result.message };
|
|
124
|
+
}
|
|
125
|
+
return { data: result.data };
|
|
126
|
+
}
|
|
127
|
+
function fileNameFromUrl(fileUrl) {
|
|
128
|
+
return fileUrl.split("/").pop()?.split("?")[0] || "asset";
|
|
129
|
+
}
|
|
130
|
+
function sanitizeNodeName(name) {
|
|
131
|
+
return ((name || "Asset")
|
|
132
|
+
.replace(/[^\p{L}\p{N}_]+/gu, "_")
|
|
133
|
+
.replace(/^_+|_+$/g, "") || "Asset");
|
|
134
|
+
}
|
|
135
|
+
async function buildImportEntriesForAsset(asset, targetPath) {
|
|
136
|
+
const fileUrl = asset.importUrl || asset.fileUrl;
|
|
137
|
+
if (!fileUrl)
|
|
138
|
+
throw new Error("Asset has no import URL.");
|
|
139
|
+
const fileName = targetPath?.split("/").pop() || asset.fileName || fileNameFromUrl(fileUrl);
|
|
140
|
+
const packSlug = asset.packSlug ?? getPackSlugFromUrl(fileUrl) ?? "misc";
|
|
141
|
+
if (targetPath) {
|
|
142
|
+
if (asset.type === "3d_model" && fileUrl.includes("kenney/3d/")) {
|
|
143
|
+
const textureUrl = buildKenneyTextureUrl(fileUrl);
|
|
144
|
+
const hasTexture = await textureExists(textureUrl);
|
|
145
|
+
if (hasTexture) {
|
|
146
|
+
const glbDir = targetPath.replace(/\/[^/]+$/, "");
|
|
147
|
+
return {
|
|
148
|
+
importPath: targetPath,
|
|
149
|
+
imports: [
|
|
150
|
+
{ url: textureUrl, path: `${glbDir}/Textures/colormap.png` },
|
|
151
|
+
{ url: fileUrl, path: targetPath },
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
importPath: targetPath,
|
|
158
|
+
imports: [{ url: fileUrl, path: targetPath }],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (asset.type === "3d_model" && fileUrl.includes("kenney/3d/")) {
|
|
162
|
+
const imports = await buildKenneyImportEntries(fileUrl, packSlug, fileName);
|
|
163
|
+
return { imports, importPath: imports[imports.length - 1].path };
|
|
164
|
+
}
|
|
165
|
+
const path = asset.type === "3d_model"
|
|
166
|
+
? `res://assets/models/${fileName}`
|
|
167
|
+
: `res://assets/${fileName}`;
|
|
168
|
+
return { imports: [{ url: fileUrl, path }], importPath: path };
|
|
169
|
+
}
|
|
170
|
+
async function importResolvedAsset(args) {
|
|
171
|
+
const { asset, parent, path, name } = args;
|
|
172
|
+
const { imports, importPath } = await buildImportEntriesForAsset(asset, path);
|
|
173
|
+
const client = await getClient();
|
|
174
|
+
const importResult = imports.length === 1
|
|
175
|
+
? await client.executeOps([
|
|
176
|
+
{ op: "ImportFromUrl", url: imports[0].url, path: imports[0].path },
|
|
177
|
+
])
|
|
178
|
+
: await client.executeOps([{ op: "ImportFromUrlBatch", imports }]);
|
|
179
|
+
const ok = importResult?.results?.[0]?.ok;
|
|
180
|
+
if (!ok) {
|
|
181
|
+
throw new Error("Could not import asset. Check engine logs.");
|
|
182
|
+
}
|
|
183
|
+
let addedToScene = false;
|
|
184
|
+
if (parent && asset.type === "3d_model") {
|
|
185
|
+
await client.executeOps([
|
|
186
|
+
{
|
|
187
|
+
op: "InstantiateScene",
|
|
188
|
+
parent,
|
|
189
|
+
scene: importPath,
|
|
190
|
+
name: sanitizeNodeName(name || asset.title),
|
|
191
|
+
},
|
|
192
|
+
]);
|
|
193
|
+
addedToScene = true;
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
success: true,
|
|
197
|
+
assetId: asset.id,
|
|
198
|
+
asset: asset.title,
|
|
199
|
+
type: asset.type,
|
|
200
|
+
importedTo: importPath,
|
|
201
|
+
addedToScene,
|
|
202
|
+
parent: parent || null,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function jsonSuccess(data) {
|
|
206
|
+
return {
|
|
207
|
+
content: [
|
|
208
|
+
{
|
|
209
|
+
type: "text",
|
|
210
|
+
text: JSON.stringify(data, null, 2),
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function jsonError(data) {
|
|
216
|
+
return {
|
|
217
|
+
content: [
|
|
218
|
+
{
|
|
219
|
+
type: "text",
|
|
220
|
+
text: JSON.stringify(data, null, 2),
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
isError: true,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
89
226
|
export function registerAssetTools(server) {
|
|
90
227
|
server.tool("summer_search_assets", `Search for game assets in the Summer Engine ecosystem. **Free for all users** with rate limits.
|
|
91
228
|
|
|
@@ -129,6 +266,124 @@ Requires authentication (so we can attribute usage and apply per-user rate limit
|
|
|
129
266
|
],
|
|
130
267
|
};
|
|
131
268
|
});
|
|
269
|
+
server.tool("summer_list_my_assets", `List or search the signed-in user's generated and uploaded assets.
|
|
270
|
+
|
|
271
|
+
Use this after generation jobs complete, when the user says "the model I made",
|
|
272
|
+
"my last character", or when you need to retrieve an asset before importing it
|
|
273
|
+
into the local project.
|
|
274
|
+
|
|
275
|
+
Returns exact asset IDs plus file/import URLs. Use summer_get_asset for full
|
|
276
|
+
metadata or summer_import_asset_by_id to import a specific result.`, {
|
|
277
|
+
query: z
|
|
278
|
+
.string()
|
|
279
|
+
.default("")
|
|
280
|
+
.describe("Optional search term. Empty string lists recent assets."),
|
|
281
|
+
assetType: z
|
|
282
|
+
.enum(["2d_image", "animation", "3d_model", "audio", "music", "all"])
|
|
283
|
+
.default("all")
|
|
284
|
+
.describe("Filter by asset type"),
|
|
285
|
+
limit: z.number().default(10).describe("Max results (1-20)"),
|
|
286
|
+
}, async ({ query, assetType, limit }) => {
|
|
287
|
+
const result = await searchAssetsApi({
|
|
288
|
+
query,
|
|
289
|
+
assetType,
|
|
290
|
+
limit: Math.min(limit, 20),
|
|
291
|
+
source: "my_assets",
|
|
292
|
+
});
|
|
293
|
+
if (result.error) {
|
|
294
|
+
return jsonError({ error: result.error, message: result.message });
|
|
295
|
+
}
|
|
296
|
+
return jsonSuccess({
|
|
297
|
+
assets: result.assets,
|
|
298
|
+
count: result.count,
|
|
299
|
+
message: result.message,
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
server.tool("summer_get_asset", `Fetch one asset by exact Summer asset ID.
|
|
303
|
+
|
|
304
|
+
Use this after a generation job returns assetId/rigAssetId/animationAssetId, or
|
|
305
|
+
after summer_list_my_assets/search results when you need the stable file URL,
|
|
306
|
+
download URL, viewer URL, metadata, license, visibility, parent chain, or
|
|
307
|
+
provider details.`, {
|
|
308
|
+
assetId: z.string().describe("Summer ArtAssets id"),
|
|
309
|
+
}, async ({ assetId }) => {
|
|
310
|
+
const result = await getAssetApi(assetId);
|
|
311
|
+
if (result.error || !result.asset) {
|
|
312
|
+
return jsonError({
|
|
313
|
+
error: result.error || "asset_not_found",
|
|
314
|
+
message: result.message || "Asset not found.",
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return jsonSuccess({ asset: result.asset });
|
|
318
|
+
});
|
|
319
|
+
server.tool("summer_get_asset_download_url", `Get a downloadable URL for a specific asset file.
|
|
320
|
+
|
|
321
|
+
Today this usually returns the Cloudinary URL plus Summer's download proxy.
|
|
322
|
+
The response shape is future-proofed for signed URLs, so prefer this tool over
|
|
323
|
+
handing users raw fileUrl when they explicitly ask to download.`, {
|
|
324
|
+
assetId: z.string().describe("Summer ArtAssets id"),
|
|
325
|
+
role: z
|
|
326
|
+
.enum(["primary", "thumbnail"])
|
|
327
|
+
.default("primary")
|
|
328
|
+
.describe("Which file to download"),
|
|
329
|
+
}, async ({ assetId, role }) => {
|
|
330
|
+
const result = await getAssetDownloadUrlApi(assetId, role);
|
|
331
|
+
if (result.error) {
|
|
332
|
+
return jsonError({ error: result.error, message: result.message });
|
|
333
|
+
}
|
|
334
|
+
return jsonSuccess(result.data);
|
|
335
|
+
});
|
|
336
|
+
server.tool("summer_import_asset_by_id", `Import an exact Summer asset ID into the current project.
|
|
337
|
+
|
|
338
|
+
Use this when the asset was just generated, selected from my assets, or returned
|
|
339
|
+
by search. Unlike summer_import_asset, this does not search or guess: it fetches
|
|
340
|
+
the asset by ID, downloads it through Summer Engine, and runs Godot's import
|
|
341
|
+
pipeline. For 3D models, pass parent to instantiate it in the open scene after
|
|
342
|
+
import.`, {
|
|
343
|
+
assetId: z.string().describe("Summer ArtAssets id"),
|
|
344
|
+
path: z
|
|
345
|
+
.string()
|
|
346
|
+
.optional()
|
|
347
|
+
.describe("Optional target res:// path. If omitted, a path is inferred from asset type and filename."),
|
|
348
|
+
parent: z
|
|
349
|
+
.string()
|
|
350
|
+
.optional()
|
|
351
|
+
.describe("Optional parent node path. For 3D models only; imports and instantiates under this node."),
|
|
352
|
+
name: z
|
|
353
|
+
.string()
|
|
354
|
+
.optional()
|
|
355
|
+
.describe("Optional scene node name when parent is provided."),
|
|
356
|
+
}, async ({ assetId, path, parent, name }) => {
|
|
357
|
+
const fetched = await getAssetApi(assetId);
|
|
358
|
+
if (fetched.error || !fetched.asset) {
|
|
359
|
+
return jsonError({
|
|
360
|
+
error: fetched.error || "asset_not_found",
|
|
361
|
+
message: fetched.message || "Asset not found.",
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
const imported = await importResolvedAsset({
|
|
366
|
+
asset: fetched.asset,
|
|
367
|
+
parent,
|
|
368
|
+
path,
|
|
369
|
+
name,
|
|
370
|
+
});
|
|
371
|
+
return jsonSuccess({
|
|
372
|
+
...imported,
|
|
373
|
+
message: imported.addedToScene
|
|
374
|
+
? `Imported "${fetched.asset.title}" and added it to ${parent}.`
|
|
375
|
+
: `Imported "${fetched.asset.title}" to ${imported.importedTo}.`,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
380
|
+
return jsonError({
|
|
381
|
+
error: "import_failed",
|
|
382
|
+
message: msg,
|
|
383
|
+
hint: "Make sure Summer Engine is running and the target scene is open.",
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
});
|
|
132
387
|
server.tool("summer_import_asset", `Search the asset library and import the best match into the project in one step.
|
|
133
388
|
|
|
134
389
|
Use when the user wants a specific type of asset added: "Add a tree to the scene", "Import a wooden barrel".
|
|
@@ -138,8 +393,9 @@ Requires authentication. If the user gets an auth error, they need to run 'npx s
|
|
|
138
393
|
query: z.string().describe("What to find, e.g. 'low-poly tree', 'wooden crate'"),
|
|
139
394
|
parent: z.string().optional().describe("Parent node path to add the asset under, e.g. './World'. If omitted, only imports (no scene placement)"),
|
|
140
395
|
assetType: z.enum(["2d_image", "animation", "3d_model", "audio", "music", "all"]).default("3d_model").describe("Preferred asset type"),
|
|
141
|
-
|
|
142
|
-
|
|
396
|
+
source: z.enum(["library", "my_assets", "all"]).default("all").describe("Where to search before importing"),
|
|
397
|
+
}, async ({ query, parent, assetType, source }) => {
|
|
398
|
+
const searchResult = await searchAssetsApi({ query, assetType, limit: 5, source });
|
|
143
399
|
if (searchResult.error) {
|
|
144
400
|
return {
|
|
145
401
|
content: [
|
|
@@ -176,45 +432,8 @@ Requires authentication. If the user gets an auth error, they need to run 'npx s
|
|
|
176
432
|
isError: true,
|
|
177
433
|
};
|
|
178
434
|
}
|
|
179
|
-
const fileName = fileUrl.split("/").pop()?.split("?")[0] || "asset";
|
|
180
|
-
const packSlug = best.packSlug ?? getPackSlugFromUrl(fileUrl) ?? "misc";
|
|
181
|
-
let imports;
|
|
182
|
-
if (best.type === "3d_model" && fileUrl.includes("kenney/3d/")) {
|
|
183
|
-
imports = await buildKenneyImportEntries(fileUrl, packSlug, fileName);
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
const path = best.type === "3d_model"
|
|
187
|
-
? `res://assets/models/${fileName}`
|
|
188
|
-
: `res://assets/${fileName}`;
|
|
189
|
-
imports = [{ url: fileUrl, path }];
|
|
190
|
-
}
|
|
191
|
-
const importPath = imports[imports.length - 1].path;
|
|
192
435
|
try {
|
|
193
|
-
const
|
|
194
|
-
const importResult = imports.length === 1
|
|
195
|
-
? await client.executeOps([{ op: "ImportFromUrl", url: imports[0].url, path: imports[0].path }])
|
|
196
|
-
: await client.executeOps([{ op: "ImportFromUrlBatch", imports }]);
|
|
197
|
-
const ok = importResult?.results?.[0]?.ok;
|
|
198
|
-
if (!ok) {
|
|
199
|
-
return {
|
|
200
|
-
content: [
|
|
201
|
-
{
|
|
202
|
-
type: "text",
|
|
203
|
-
text: JSON.stringify({
|
|
204
|
-
error: "Import failed",
|
|
205
|
-
message: "Could not import asset. Check engine logs.",
|
|
206
|
-
asset: best.title,
|
|
207
|
-
}, null, 2),
|
|
208
|
-
},
|
|
209
|
-
],
|
|
210
|
-
isError: true,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
if (parent && best.type === "3d_model") {
|
|
214
|
-
await client.executeOps([
|
|
215
|
-
{ op: "InstantiateScene", parent, scene: importPath, name: best.title.replace(/\s+/g, "_") },
|
|
216
|
-
]);
|
|
217
|
-
}
|
|
436
|
+
const imported = await importResolvedAsset({ asset: best, parent });
|
|
218
437
|
// 2D sprites and audio: import only; user adds to scene manually
|
|
219
438
|
return {
|
|
220
439
|
content: [
|
|
@@ -222,14 +441,15 @@ Requires authentication. If the user gets an auth error, they need to run 'npx s
|
|
|
222
441
|
type: "text",
|
|
223
442
|
text: JSON.stringify({
|
|
224
443
|
success: true,
|
|
444
|
+
assetId: best.id,
|
|
225
445
|
asset: best.title,
|
|
226
446
|
type: best.type,
|
|
227
|
-
importedTo:
|
|
228
|
-
addedToScene:
|
|
229
|
-
parent: parent
|
|
230
|
-
message:
|
|
447
|
+
importedTo: imported.importedTo,
|
|
448
|
+
addedToScene: imported.addedToScene,
|
|
449
|
+
parent: imported.parent,
|
|
450
|
+
message: imported.addedToScene
|
|
231
451
|
? `Imported "${best.title}" and added to ${parent}`
|
|
232
|
-
: `Imported "${best.title}" to ${
|
|
452
|
+
: `Imported "${best.title}" to ${imported.importedTo}`,
|
|
233
453
|
}, null, 2),
|
|
234
454
|
},
|
|
235
455
|
],
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { cloudCheckpoints, cloudConflicts, cloudInit, cloudPull, cloudPush, cloudRestore, cloudStatus, } from "../../lib/cloud/sync.js";
|
|
3
|
+
const projectArg = z.object({
|
|
4
|
+
project: z.string().optional().describe("Project root path. Defaults to the current working directory."),
|
|
5
|
+
});
|
|
6
|
+
const bootstrap = z.enum(["keep-cloud", "keep-local", "merge"]);
|
|
7
|
+
function textJson(value) {
|
|
8
|
+
return {
|
|
9
|
+
content: [
|
|
10
|
+
{
|
|
11
|
+
type: "text",
|
|
12
|
+
text: JSON.stringify(value, null, 2),
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function registerCloudTools(server) {
|
|
18
|
+
server.tool("summer_cloud_init", "Enable Summer Cloud for a project.", projectArg.shape, async (args) => textJson(await cloudInit({ project: args.project, face: "mcp" })));
|
|
19
|
+
server.tool("summer_cloud_status", "Show Summer Cloud sync status.", projectArg.shape, async (args) => textJson(await cloudStatus({ project: args.project, face: "mcp" })));
|
|
20
|
+
server.tool("summer_cloud_push", "Push local project changes to Summer Cloud.", {
|
|
21
|
+
...projectArg.shape,
|
|
22
|
+
confirmDeletes: z.boolean().default(false).describe("Required when a push would delete many cloud files."),
|
|
23
|
+
bootstrap: bootstrap.optional().describe("No-base bootstrap choice."),
|
|
24
|
+
adoptPath: z.boolean().optional().describe("Accept that the project folder moved and update the recorded path."),
|
|
25
|
+
}, async (args) => textJson(await cloudPush({ ...args, face: "mcp" })));
|
|
26
|
+
server.tool("summer_cloud_pull", "Pull Summer Cloud changes into the local project.", {
|
|
27
|
+
...projectArg.shape,
|
|
28
|
+
bootstrap: bootstrap.optional().describe("No-base bootstrap choice."),
|
|
29
|
+
adoptPath: z.boolean().optional().describe("Accept that the project folder moved and update the recorded path."),
|
|
30
|
+
}, async (args) => textJson(await cloudPull({ ...args, face: "mcp" })));
|
|
31
|
+
server.tool("summer_cloud_restore", "Restore a retained cloud version (creates a new head version server-side, then pulls), or a local pre-sync checkpoint via checkpointStamp.", {
|
|
32
|
+
...projectArg.shape,
|
|
33
|
+
version: z.number().int().positive().optional().describe("Cloud version to restore."),
|
|
34
|
+
checkpointStamp: z.string().optional().describe("Local checkpoint stamp (see summer_cloud_checkpoints)."),
|
|
35
|
+
}, async (args) => textJson(await cloudRestore({ project: args.project, version: args.version, checkpoint: args.checkpointStamp, face: "mcp" })));
|
|
36
|
+
server.tool("summer_cloud_checkpoints", "List local pre-sync checkpoints.", projectArg.shape, async (args) => textJson(await cloudCheckpoints({ project: args.project, face: "mcp" })));
|
|
37
|
+
server.tool("summer_cloud_conflicts", "List local Summer Cloud conflict sets, or restore a preserved conflict file with restorePath.", {
|
|
38
|
+
...projectArg.shape,
|
|
39
|
+
restorePath: z.string().optional().describe("Project-relative path to restore from the conflict sets."),
|
|
40
|
+
set: z.string().optional().describe("Conflict set stamp; defaults to the newest set containing the path."),
|
|
41
|
+
}, async (args) => textJson(await cloudConflicts({ project: args.project, restorePath: args.restorePath, set: args.set, face: "mcp" })));
|
|
42
|
+
}
|
|
@@ -1,12 +1,88 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { writeFile, mkdir } from "fs/promises";
|
|
3
4
|
import { tmpdir } from "os";
|
|
4
5
|
import { join } from "path";
|
|
5
6
|
import { getAuthToken } from "../../lib/auth.js";
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const { version: CLI_VERSION } = require("../../../package.json");
|
|
6
9
|
const GATEWAY_URL = process.env.SUMMER_GATEWAY_URL || "https://www.summerengine.com";
|
|
10
|
+
const TOOL_BY_ENDPOINT = {
|
|
11
|
+
"/api/mcp/generate/image": "summer_generate_image",
|
|
12
|
+
"/api/mcp/generate/audio": "summer_generate_audio",
|
|
13
|
+
"/api/mcp/generate/3d": "summer_generate_3d",
|
|
14
|
+
"/api/mcp/generate/video": "summer_generate_video",
|
|
15
|
+
"/api/mcp/generate/motion": "summer_generate_motion",
|
|
16
|
+
};
|
|
7
17
|
// ---------------------------------------------------------------------------
|
|
8
18
|
// Shared helpers
|
|
9
19
|
// ---------------------------------------------------------------------------
|
|
20
|
+
function asRecord(value) {
|
|
21
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
22
|
+
? value
|
|
23
|
+
: null;
|
|
24
|
+
}
|
|
25
|
+
function stringFrom(value) {
|
|
26
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
27
|
+
}
|
|
28
|
+
function formatValidationDetailEntry(entry) {
|
|
29
|
+
const record = asRecord(entry);
|
|
30
|
+
if (!record) {
|
|
31
|
+
return stringFrom(entry) ?? null;
|
|
32
|
+
}
|
|
33
|
+
const message = stringFrom(record.msg) ??
|
|
34
|
+
stringFrom(record.message) ??
|
|
35
|
+
stringFrom(record.error) ??
|
|
36
|
+
null;
|
|
37
|
+
if (!message)
|
|
38
|
+
return null;
|
|
39
|
+
const loc = Array.isArray(record.loc)
|
|
40
|
+
? record.loc.map((part) => String(part)).join(".")
|
|
41
|
+
: stringFrom(record.path) ?? stringFrom(record.field);
|
|
42
|
+
return loc ? `${loc}: ${message}` : message;
|
|
43
|
+
}
|
|
44
|
+
function formatValidationDetails(detail) {
|
|
45
|
+
if (Array.isArray(detail)) {
|
|
46
|
+
return detail
|
|
47
|
+
.map(formatValidationDetailEntry)
|
|
48
|
+
.filter((entry) => !!entry);
|
|
49
|
+
}
|
|
50
|
+
const formatted = formatValidationDetailEntry(detail);
|
|
51
|
+
return formatted ? [formatted] : [];
|
|
52
|
+
}
|
|
53
|
+
function formattedApiErrorMessage(status, data) {
|
|
54
|
+
const record = asRecord(data);
|
|
55
|
+
const provider = asRecord(record?.provider);
|
|
56
|
+
const directDetailMessages = formatValidationDetails(record?.detail);
|
|
57
|
+
const detailMessages = directDetailMessages.length > 0
|
|
58
|
+
? directDetailMessages
|
|
59
|
+
: Array.isArray(provider?.detailMessages)
|
|
60
|
+
? provider.detailMessages.filter((entry) => typeof entry === "string")
|
|
61
|
+
: formatValidationDetails(provider?.detail);
|
|
62
|
+
if (status === 422 && detailMessages.length > 0) {
|
|
63
|
+
return `Request validation failed (422): ${detailMessages.join("; ")}`;
|
|
64
|
+
}
|
|
65
|
+
return (stringFrom(record?.message) ??
|
|
66
|
+
stringFrom(record?.error) ??
|
|
67
|
+
`Request failed (${status})`);
|
|
68
|
+
}
|
|
69
|
+
async function readJsonResponse(res) {
|
|
70
|
+
if (typeof res.text !== "function") {
|
|
71
|
+
if (typeof res.json === "function") {
|
|
72
|
+
return res.json().catch(() => ({}));
|
|
73
|
+
}
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
const text = await res.text().catch(() => "");
|
|
77
|
+
if (!text)
|
|
78
|
+
return {};
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(text);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return { message: text.slice(0, 1000) };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
10
86
|
async function mcpGenerate(endpoint, body, timeoutMs = 120_000) {
|
|
11
87
|
const token = await getAuthToken();
|
|
12
88
|
if (!token) {
|
|
@@ -15,18 +91,30 @@ async function mcpGenerate(endpoint, body, timeoutMs = 120_000) {
|
|
|
15
91
|
status: 401,
|
|
16
92
|
};
|
|
17
93
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
94
|
+
let res;
|
|
95
|
+
try {
|
|
96
|
+
res = await fetch(`${GATEWAY_URL}${endpoint}`, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: {
|
|
99
|
+
Authorization: `Bearer ${token}`,
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
"X-Summer-Client": "summer-cli",
|
|
102
|
+
"X-Summer-Client-Version": CLI_VERSION,
|
|
103
|
+
"X-Summer-Client-Surface": "mcp",
|
|
104
|
+
"X-Summer-MCP-Endpoint": endpoint,
|
|
105
|
+
"X-Summer-MCP-Tool": TOOL_BY_ENDPOINT[endpoint] ?? "unknown",
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify(body),
|
|
108
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
113
|
+
return { error: `Generation request failed: ${msg}`, status: 0 };
|
|
114
|
+
}
|
|
115
|
+
const data = await readJsonResponse(res);
|
|
28
116
|
if (!res.ok) {
|
|
29
|
-
let message =
|
|
117
|
+
let message = formattedApiErrorMessage(res.status, data);
|
|
30
118
|
if (res.status === 402) {
|
|
31
119
|
message =
|
|
32
120
|
(data.message || "Insufficient credits.") +
|
|
@@ -37,7 +125,7 @@ async function mcpGenerate(endpoint, body, timeoutMs = 120_000) {
|
|
|
37
125
|
(data.message || "Auth token expired.") +
|
|
38
126
|
"\nRe-authenticate:\n npx summer-engine login";
|
|
39
127
|
}
|
|
40
|
-
return { error: message, data, status: res.status };
|
|
128
|
+
return { error: message, data: { ...data, status: res.status }, status: res.status };
|
|
41
129
|
}
|
|
42
130
|
return { data, status: res.status };
|
|
43
131
|
}
|
|
@@ -102,7 +190,7 @@ function errorResult(message, extra) {
|
|
|
102
190
|
content: [
|
|
103
191
|
{
|
|
104
192
|
type: "text",
|
|
105
|
-
text: JSON.stringify({ error: true,
|
|
193
|
+
text: JSON.stringify({ error: true, ...extra, message }, null, 2),
|
|
106
194
|
},
|
|
107
195
|
],
|
|
108
196
|
isError: true,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { ASSET_POLICIES, GAME_TASK_MODES, GAME_TASK_TARGETS, VERIFICATION_LEVELS, buildGameTaskPlan, } from "../../lib/game-task-plan.js";
|
|
2
3
|
import { getProjectMemorySummary } from "../../lib/project-memory.js";
|
|
3
4
|
import { withEngine } from "./with-engine.js";
|
|
4
5
|
function asRecord(value) {
|
|
@@ -87,6 +88,43 @@ function getCurrentScene(projectState, sceneState, health) {
|
|
|
87
88
|
null);
|
|
88
89
|
}
|
|
89
90
|
export function registerProjectTools(server) {
|
|
91
|
+
server.tool("summer_start_game_task", `Start here for any substantial AI game-building task.
|
|
92
|
+
|
|
93
|
+
Takes the user's goal and returns the recommended Summer workflow: skill routes,
|
|
94
|
+
MCP tool groups, host-file boundaries, asset policy, user confirmation gates,
|
|
95
|
+
and verification steps. This is the router before deep skills and before
|
|
96
|
+
mutating the project.`, {
|
|
97
|
+
goal: z.string().describe("The user's game-building goal or task."),
|
|
98
|
+
mode: z
|
|
99
|
+
.enum(GAME_TASK_MODES)
|
|
100
|
+
.default("auto")
|
|
101
|
+
.describe("Optional task mode override."),
|
|
102
|
+
target: z
|
|
103
|
+
.enum(GAME_TASK_TARGETS)
|
|
104
|
+
.default("auto")
|
|
105
|
+
.describe("Optional content/system target override."),
|
|
106
|
+
assetPolicy: z
|
|
107
|
+
.enum(ASSET_POLICIES)
|
|
108
|
+
.default("ask-before-paid-generation")
|
|
109
|
+
.describe("How aggressively to use paid asset generation."),
|
|
110
|
+
verification: z
|
|
111
|
+
.enum(VERIFICATION_LEVELS)
|
|
112
|
+
.default("full")
|
|
113
|
+
.describe("How much engine verification the agent should plan for."),
|
|
114
|
+
}, async ({ goal, mode, target, assetPolicy, verification }) => ({
|
|
115
|
+
content: [
|
|
116
|
+
{
|
|
117
|
+
type: "text",
|
|
118
|
+
text: JSON.stringify(buildGameTaskPlan({
|
|
119
|
+
goal,
|
|
120
|
+
mode,
|
|
121
|
+
target,
|
|
122
|
+
assetPolicy,
|
|
123
|
+
verification,
|
|
124
|
+
}), null, 2),
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
}));
|
|
90
128
|
server.tool("summer_get_agent_playbook", `AI-first operating guide for Summer Engine MCP.
|
|
91
129
|
|
|
92
130
|
Call this at the start of a fresh chat before touching scenes.
|
|
@@ -96,29 +134,33 @@ It returns the safe workflow, anti-patterns, and recovery steps.`, {}, async ()
|
|
|
96
134
|
type: "text",
|
|
97
135
|
text: JSON.stringify({
|
|
98
136
|
startupChecklist: [
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"Use Summer MCP
|
|
102
|
-
"Call summer_get_project_context first.",
|
|
137
|
+
"Understand the request and outline a brief plan before reaching for tools.",
|
|
138
|
+
"Default medium is host file tools: write/edit .gd/.cs/.tscn/.tres/.json/docs/config directly as text.",
|
|
139
|
+
"Use Summer MCP only when you need the LIVE engine: play/stop, diagnostics, screenshots/verification, navmesh or light bake, runtime inspect, or asset import.",
|
|
140
|
+
"Call summer_get_project_context first so you do not guess scene paths or the project language.",
|
|
103
141
|
"Use projectMemory from summer_get_project_context to decide which .summer Markdown files to read before creative/audio/dialogue/level/character work.",
|
|
104
|
-
"
|
|
105
|
-
"Call summer_get_scene_tree before structural edits.",
|
|
106
|
-
"Call summer_save_scene after edits.",
|
|
142
|
+
"After writing code, PLAY the scene you just made and read summer_get_diagnostics; iterate until it launches clean instead of waiting for the user to navigate to it.",
|
|
107
143
|
],
|
|
108
144
|
safeDefaults: [
|
|
109
|
-
"Never guess scene filenames (main.tscn/Main.tscn).",
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
"
|
|
145
|
+
"Never guess scene filenames (main.tscn/Main.tscn) -- get them from summer_get_project_context.",
|
|
146
|
+
"Edit files (including .tscn/.tres) with host file tools by default. Reach for MCP scene-ops only for live-engine needs (bake, play, runtime inspect) or when you want the editor to manage node ids / instancing.",
|
|
147
|
+
"If you hand-edit a .tscn that is OPEN in the editor, reload it there afterward -- the editor's open tab can overwrite your file (clobber).",
|
|
148
|
+
"Write GDScript by default; use C# only if the project already uses it.",
|
|
149
|
+
"Never remove multiple top-level nodes unless the user explicitly requests destructive edits.",
|
|
113
150
|
"Never change priority: locked .summer memory, voice IDs, canon, or provider bindings without explicit user confirmation.",
|
|
114
151
|
],
|
|
115
|
-
|
|
152
|
+
buildFlow: [
|
|
153
|
+
"1. Understand what the user wants (one pass, no tool spelunking first).",
|
|
154
|
+
"2. Outline the plan fast and briefly; proceed once it is clearly right.",
|
|
155
|
+
"3. Execute in pure code -- edit .gd/.tscn/.tres as text (GDScript by default, C# only if the project already uses it).",
|
|
156
|
+
"4. Play the scene/game (start with the scene you just made), read console + script/debugger errors via summer_get_diagnostics, fix, and repeat until it launches clean.",
|
|
157
|
+
],
|
|
158
|
+
liveEngineFlow: [
|
|
159
|
+
"Use this flow ONLY when you genuinely need live engine state (navmesh/light bake, instancing into an already-open scene, runtime inspect):",
|
|
116
160
|
"summer_get_project_context",
|
|
117
|
-
"read relevant .summer memory with host file tools when projectMemory shows matching files",
|
|
118
161
|
"summer_open_main_scene (if needed)",
|
|
119
162
|
"summer_get_scene_tree",
|
|
120
|
-
"
|
|
121
|
-
"write scripts/data/docs/simple config directly with host file-edit tools",
|
|
163
|
+
"summer_add_node / summer_set_prop / summer_set_resource_property",
|
|
122
164
|
"summer_save_scene",
|
|
123
165
|
"summer_get_diagnostics",
|
|
124
166
|
],
|
|
@@ -126,6 +168,7 @@ It returns the safe workflow, anti-patterns, and recovery steps.`, {}, async ()
|
|
|
126
168
|
"If you see 'no scene open': run summer_open_main_scene.",
|
|
127
169
|
"If open_scene fails: re-check mainScene from summer_get_project_context.",
|
|
128
170
|
"If save fails: verify scene is open and game is not running.",
|
|
171
|
+
"If a .tscn you wrote keeps reverting: the editor has that scene open -- reload or close the tab, then write again.",
|
|
129
172
|
],
|
|
130
173
|
}, null, 2),
|
|
131
174
|
},
|
|
@@ -163,7 +206,7 @@ Use this first in every fresh chat to avoid guessing scene filenames or editing
|
|
|
163
206
|
guidance: mainScene
|
|
164
207
|
? "Use `summer_open_scene` with `mainScene` if no scene is open."
|
|
165
208
|
: "Main scene not found in project state. Open a known scene path explicitly.",
|
|
166
|
-
fileEditingGuidance: "
|
|
209
|
+
fileEditingGuidance: "Edit files (including .gd/.cs/.tscn/.tres/.json/docs) with host file tools by default. Use MCP only for live engine state: play/stop, diagnostics, screenshots/verification, navmesh or light bake, runtime inspect, and asset import. If you edit a .tscn that is open in the editor, reload it there afterward so the editor's tab does not overwrite your changes.",
|
|
167
210
|
};
|
|
168
211
|
}));
|
|
169
212
|
server.tool("summer_open_main_scene", `Open the project's configured main scene from project settings.
|