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.
Files changed (83) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +5 -4
  3. package/.codex-plugin/plugin.json +5 -4
  4. package/.cursor-plugin/plugin.json +4 -3
  5. package/.factory-plugin/plugin.json +3 -3
  6. package/AGENTS.md +6 -4
  7. package/CHANGELOG.md +20 -2
  8. package/GEMINI.md +3 -3
  9. package/README.md +113 -100
  10. package/dist/bin/summer.js +6 -1
  11. package/dist/commands/cloud.d.ts +2 -0
  12. package/dist/commands/cloud.js +80 -0
  13. package/dist/commands/doctor.js +19 -0
  14. package/dist/commands/login.js +4 -1
  15. package/dist/commands/logout.js +1 -1
  16. package/dist/commands/open.js +1 -1
  17. package/dist/commands/plan.d.ts +2 -0
  18. package/dist/commands/plan.js +60 -0
  19. package/dist/commands/run.js +132 -35
  20. package/dist/lib/api-client.js +4 -1
  21. package/dist/lib/auth.d.ts +2 -0
  22. package/dist/lib/auth.js +13 -0
  23. package/dist/lib/cloud/api.d.ts +79 -0
  24. package/dist/lib/cloud/api.js +107 -0
  25. package/dist/lib/cloud/apply.d.ts +32 -0
  26. package/dist/lib/cloud/apply.js +294 -0
  27. package/dist/lib/cloud/checkpoint.d.ts +26 -0
  28. package/dist/lib/cloud/checkpoint.js +142 -0
  29. package/dist/lib/cloud/conflicts.d.ts +55 -0
  30. package/dist/lib/cloud/conflicts.js +131 -0
  31. package/dist/lib/cloud/containment.d.ts +8 -0
  32. package/dist/lib/cloud/containment.js +52 -0
  33. package/dist/lib/cloud/diff.d.ts +39 -0
  34. package/dist/lib/cloud/diff.js +138 -0
  35. package/dist/lib/cloud/engine-bridge.d.ts +21 -0
  36. package/dist/lib/cloud/engine-bridge.js +105 -0
  37. package/dist/lib/cloud/hash-cache.d.ts +23 -0
  38. package/dist/lib/cloud/hash-cache.js +44 -0
  39. package/dist/lib/cloud/hash.d.ts +10 -0
  40. package/dist/lib/cloud/hash.js +111 -0
  41. package/dist/lib/cloud/http.d.ts +17 -0
  42. package/dist/lib/cloud/http.js +47 -0
  43. package/dist/lib/cloud/lock-path.d.ts +1 -0
  44. package/dist/lib/cloud/lock-path.js +5 -0
  45. package/dist/lib/cloud/lock.d.ts +14 -0
  46. package/dist/lib/cloud/lock.js +143 -0
  47. package/dist/lib/cloud/manifest.d.ts +6 -0
  48. package/dist/lib/cloud/manifest.js +41 -0
  49. package/dist/lib/cloud/paths.d.ts +20 -0
  50. package/dist/lib/cloud/paths.js +90 -0
  51. package/dist/lib/cloud/project-godot-merge.d.ts +32 -0
  52. package/dist/lib/cloud/project-godot-merge.js +185 -0
  53. package/dist/lib/cloud/rules.d.ts +39 -0
  54. package/dist/lib/cloud/rules.js +159 -0
  55. package/dist/lib/cloud/sync.d.ts +21 -0
  56. package/dist/lib/cloud/sync.js +518 -0
  57. package/dist/lib/cloud/transfer.d.ts +23 -0
  58. package/dist/lib/cloud/transfer.js +184 -0
  59. package/dist/lib/cloud/types.d.ts +62 -0
  60. package/dist/lib/cloud/types.js +1 -0
  61. package/dist/lib/cloud/validate-path.d.ts +23 -0
  62. package/dist/lib/cloud/validate-path.js +103 -0
  63. package/dist/lib/engine.d.ts +6 -0
  64. package/dist/lib/engine.js +36 -1
  65. package/dist/lib/game-task-plan.d.ts +42 -0
  66. package/dist/lib/game-task-plan.js +376 -0
  67. package/dist/mcp/server.js +2 -0
  68. package/dist/mcp/tools/asset-tools.js +265 -45
  69. package/dist/mcp/tools/cloud-tools.d.ts +2 -0
  70. package/dist/mcp/tools/cloud-tools.js +42 -0
  71. package/dist/mcp/tools/generate-tools.js +101 -13
  72. package/dist/mcp/tools/project-tools.js +59 -16
  73. package/gemini-extension.json +3 -3
  74. package/package.json +16 -5
  75. package/skills/3d-assets/character-model/SKILL.md +27 -5
  76. package/skills/3d-assets/environment-kit/SKILL.md +7 -2
  77. package/skills/3d-assets/organic-model/SKILL.md +4 -3
  78. package/skills/3d-assets/prop-model/SKILL.md +16 -8
  79. package/skills/3d-assets/vehicle-model/SKILL.md +4 -3
  80. package/skills/animation/animation-tree/SKILL.md +2 -0
  81. package/skills/animation/generate-motion/SKILL.md +25 -4
  82. package/skills/asset-pipeline/asset-strategy/SKILL.md +3 -2
  83. 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
- }, async ({ query, parent, assetType }) => {
142
- const searchResult = await searchAssetsApi({ query, assetType, limit: 5 });
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 client = await getClient();
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: importPath,
228
- addedToScene: parent ? true : false,
229
- parent: parent || null,
230
- message: parent
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 ${importPath}`,
452
+ : `Imported "${best.title}" to ${imported.importedTo}`,
233
453
  }, null, 2),
234
454
  },
235
455
  ],
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerCloudTools(server: McpServer): void;
@@ -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
- const res = await fetch(`${GATEWAY_URL}${endpoint}`, {
19
- method: "POST",
20
- headers: {
21
- Authorization: `Bearer ${token}`,
22
- "Content-Type": "application/json",
23
- },
24
- body: JSON.stringify(body),
25
- signal: AbortSignal.timeout(timeoutMs),
26
- });
27
- const data = await res.json();
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 = data.message || data.error || "Request failed";
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, message, ...extra }, null, 2),
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
- "Decide first: file edit or engine operation.",
100
- "Write .gd/.cs/.json/docs/simple config with host file-edit tools directly.",
101
- "Use Summer MCP for live scene state, inspector/node edits, imports, play/stop, diagnostics, screenshots/verification.",
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
- "If no scene is open, call summer_open_main_scene.",
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
- "Do not route ordinary script, JSON, docs, or simple config edits through MCP when direct file tools are available.",
111
- "Never remove multiple top-level nodes unless user explicitly requests destructive edits.",
112
- "Use MCP for scene-tree mutations and inspector-visible values instead of hand-editing live scene state.",
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
- preferredFlow: [
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
- "edit via summer_add_node / summer_set_prop / summer_set_resource_property",
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: "Write .gd/.cs/.json/docs/simple config directly with host file tools. Use MCP for live scene state, inspector/node edits, imports, play/stop, diagnostics, and visual verification.",
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.