summer-engine 2.6.6 → 2.7.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.
Files changed (56) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +3 -1
  3. package/.codex-plugin/plugin.json +5 -2
  4. package/.cursor-plugin/plugin.json +4 -1
  5. package/.opencode/plugins/summer.js +1 -1
  6. package/AGENTS.md +8 -4
  7. package/CHANGELOG.md +30 -0
  8. package/GEMINI.md +5 -4
  9. package/README.md +7 -4
  10. package/_persona/summer/SKILL.md +1 -0
  11. package/commands/summer.md +13 -0
  12. package/dist/bin/summer.js +4 -0
  13. package/dist/commands/debug.d.ts +2 -0
  14. package/dist/commands/debug.js +44 -0
  15. package/dist/commands/doctor.js +112 -8
  16. package/dist/commands/skills.js +1 -1
  17. package/dist/lib/api-client.d.ts +11 -2
  18. package/dist/lib/api-client.js +110 -9
  19. package/dist/lib/async-op-lifecycle.d.ts +7 -2
  20. package/dist/lib/async-op-lifecycle.js +32 -9
  21. package/dist/lib/debug-report.d.ts +50 -0
  22. package/dist/lib/debug-report.js +308 -0
  23. package/dist/lib/game-task-plan.js +4 -4
  24. package/dist/lib/mcp-boot-notice.d.ts +3 -0
  25. package/dist/lib/mcp-boot-notice.js +7 -0
  26. package/dist/lib/mcp-log.d.ts +3 -0
  27. package/dist/lib/mcp-log.js +72 -0
  28. package/dist/lib/skills-registry.d.ts +25 -1
  29. package/dist/lib/skills-registry.js +46 -0
  30. package/dist/mcp/server.d.ts +2 -2
  31. package/dist/mcp/server.js +79 -7
  32. package/dist/mcp/tools/asset-tools.js +33 -12
  33. package/dist/mcp/tools/cloud-tools.js +15 -15
  34. package/dist/mcp/tools/debug-tools.js +44 -1
  35. package/dist/mcp/tools/file-tools.d.ts +2 -0
  36. package/dist/mcp/tools/file-tools.js +154 -0
  37. package/dist/mcp/tools/generate-tools.js +143 -5
  38. package/dist/mcp/tools/project-tools.js +25 -20
  39. package/dist/mcp/tools/scene-tools.js +101 -24
  40. package/dist/mcp/tools/with-engine.js +4 -4
  41. package/hooks/pre-commit-doctor.sh +0 -0
  42. package/hooks/run-hook.cmd +0 -0
  43. package/hooks/session-start.sh +0 -0
  44. package/package.json +1 -1
  45. package/skills/2d-assets/create-asset-sheet/SKILL.md +17 -15
  46. package/skills/debugging/debug/SKILL.md +23 -1
  47. package/skills/deployment/remote-deploy/SKILL.md +107 -0
  48. package/skills/scene-and-project/brainstorm-game/SKILL.md +4 -4
  49. package/skills/scene-and-project/summer-cloud/SKILL.md +92 -0
  50. package/skills/scripting-patterns/gdscript-patterns/SKILL.md +1 -1
  51. package/skills/workflow/brainstorming/scripts/start-server.sh +0 -0
  52. package/skills/workflow/brainstorming/scripts/stop-server.sh +0 -0
  53. package/skills/workflow/investigating-bugs/find-polluter.sh +0 -0
  54. package/skills/workflow/mcpupdate/SKILL.md +6 -6
  55. package/skills/workflow/using-summer/SKILL.md +20 -1
  56. package/skills/workflow/writing-skills/render-graphs.js +0 -0
@@ -0,0 +1,154 @@
1
+ import { z } from "zod";
2
+ import { withEngine } from "./with-engine.js";
3
+ // MCP clients may issue tool calls concurrently. Keep the complete read->write
4
+ // transaction ordered for one project file while allowing unrelated files to
5
+ // proceed in parallel. The tail is always released in `finally`, so a rejected
6
+ // tool call cannot wedge later edits.
7
+ const fileMutationTails = new Map();
8
+ async function withFileMutationTurn(projectIdHash, path, fn) {
9
+ // Windows project paths are case-insensitive. Canonicalize only the lock key;
10
+ // preserve the caller's exact safe path in the operation sent to the engine.
11
+ const canonicalPathKey = path.replace(/\\/g, "/").toLowerCase();
12
+ const key = `${projectIdHash ?? "unbound"}\n${canonicalPathKey}`;
13
+ const previous = fileMutationTails.get(key) ?? Promise.resolve();
14
+ let release;
15
+ const gate = new Promise((resolve) => {
16
+ release = resolve;
17
+ });
18
+ const current = previous.catch(() => undefined).then(() => gate);
19
+ fileMutationTails.set(key, current);
20
+ await previous.catch(() => undefined);
21
+ try {
22
+ return await fn();
23
+ }
24
+ finally {
25
+ release();
26
+ if (fileMutationTails.get(key) === current) {
27
+ fileMutationTails.delete(key);
28
+ }
29
+ }
30
+ }
31
+ function asRecord(value) {
32
+ return value && typeof value === "object" && !Array.isArray(value)
33
+ ? value
34
+ : null;
35
+ }
36
+ function safeProjectPath(path) {
37
+ const normalized = path.trim().replace(/\\/g, "/");
38
+ if (!normalized.startsWith("res://") || normalized.includes("..")) {
39
+ throw new Error("File path must be a traversal-free res:// project path.");
40
+ }
41
+ return normalized;
42
+ }
43
+ function validSha256(value) {
44
+ return typeof value === "string" && /^[0-9a-f]{64}$/i.test(value);
45
+ }
46
+ function readTextPayload(result) {
47
+ const root = asRecord(result);
48
+ const data = asRecord(root?.data);
49
+ if (root?.ok === false) {
50
+ throw new Error(String(root.error ?? "Engine could not read the file."));
51
+ }
52
+ if (data?.encoding === "binary" || typeof data?.content !== "string") {
53
+ throw new Error("Safe text mutation refused: the engine did not return text content.");
54
+ }
55
+ if (data.truncated === true) {
56
+ throw new Error("Safe text mutation refused: the file exceeds the 1 MB read limit.");
57
+ }
58
+ const sha256 = typeof data.sha256 === "string" ? data.sha256 : undefined;
59
+ if (!validSha256(sha256)) {
60
+ throw new Error("Safe text mutation refused: the engine did not return a full-file sha256 receipt.");
61
+ }
62
+ return {
63
+ content: data.content,
64
+ sha256,
65
+ size: typeof data.size === "number" ? data.size : undefined,
66
+ };
67
+ }
68
+ function occurrenceCount(content, needle) {
69
+ let count = 0;
70
+ let offset = 0;
71
+ while (true) {
72
+ const index = content.indexOf(needle, offset);
73
+ if (index < 0)
74
+ return count;
75
+ count++;
76
+ offset = index + needle.length;
77
+ }
78
+ }
79
+ export function registerFileTools(server) {
80
+ server.tool("summer_read_file", `Read a text file from the engine-bound project and return its full-file sha256 receipt.
81
+
82
+ Use the returned sha256 as expected_sha256 when overwriting an existing file. Paths must begin with res://.`, {
83
+ path: z.string().describe("Project path, e.g. res://scripts/player.gd"),
84
+ max_bytes: z.number().int().positive().max(1_000_000).default(200_000),
85
+ }, async ({ path, max_bytes }) => withEngine(async (client) => client.readProjectFile(safeProjectPath(path), max_bytes)));
86
+ server.tool("summer_write_file", `Create or safely overwrite a complete text file through the identity-bound engine.
87
+
88
+ For a new file, set create_only:true. For an existing file, first call summer_read_file and pass its sha256 as expected_sha256. Exactly one guard is required; unguarded writes fail closed. Supports scripts, .tscn scenes, .tres resources, JSON, docs, and project config.`, {
89
+ path: z.string().describe("Project path, e.g. res://scenes/player.tscn"),
90
+ content: z.string().describe("Complete new UTF-8 file content."),
91
+ create_only: z.boolean().optional().describe("Required for a new file; refuses if the path already exists."),
92
+ expected_sha256: z.string().optional().describe("Required for overwriting an existing file; obtain from summer_read_file."),
93
+ }, async ({ path, content, create_only, expected_sha256 }) => withEngine(async (client) => {
94
+ const safePath = safeProjectPath(path);
95
+ const guardedCreate = create_only === true;
96
+ const guardedOverwrite = validSha256(expected_sha256);
97
+ if (expected_sha256 !== undefined && !guardedOverwrite) {
98
+ throw new Error("Safe write refused: expected_sha256 must be a 64-character hexadecimal sha256 from summer_read_file. Nothing was written.");
99
+ }
100
+ if (guardedCreate === guardedOverwrite) {
101
+ throw new Error("Safe write requires exactly one guard: create_only:true for a new file, or a 64-character expected_sha256 from summer_read_file for an existing file. Nothing was written.");
102
+ }
103
+ const op = {
104
+ op: "WriteFile",
105
+ path: safePath,
106
+ content,
107
+ };
108
+ if (guardedCreate)
109
+ op.mustNotExist = true;
110
+ if (guardedOverwrite)
111
+ op.expectedSha256 = expected_sha256.toLowerCase();
112
+ return withFileMutationTurn(client.getBoundProjectIdHash(), safePath, () => client.executeIdentityBoundOps([op]));
113
+ }));
114
+ server.tool("summer_replace_text", `Safely replace text in an existing project file through the identity-bound engine.
115
+
116
+ The MCP server reads the complete file, requires a unique match by default, computes the new content, and submits a sha256-guarded WriteFile. Set replace_all:true only when every exact occurrence should change.`, {
117
+ path: z.string().describe("Project path, e.g. res://scripts/player.gd"),
118
+ old_text: z.string().min(1).describe("Exact text to replace."),
119
+ new_text: z.string().describe("Replacement text; may be empty."),
120
+ replace_all: z.boolean().default(false).describe("Replace every exact occurrence instead of requiring one unique match."),
121
+ }, async ({ path, old_text, new_text, replace_all }) => withEngine(async (client) => {
122
+ const safePath = safeProjectPath(path);
123
+ return withFileMutationTurn(client.getBoundProjectIdHash(), safePath, async () => {
124
+ const current = readTextPayload(await client.readProjectFile(safePath, 1_000_000));
125
+ const matches = occurrenceCount(current.content, old_text);
126
+ if (matches === 0) {
127
+ throw new Error("Safe replace refused: old_text was not found. Nothing was written.");
128
+ }
129
+ if (!replace_all && matches !== 1) {
130
+ throw new Error(`Safe replace refused: old_text matched ${matches} times. Provide a unique span or set replace_all:true. Nothing was written.`);
131
+ }
132
+ const content = replace_all
133
+ ? current.content.split(old_text).join(new_text)
134
+ : current.content.replace(old_text, new_text);
135
+ if (content === current.content) {
136
+ return {
137
+ ok: true,
138
+ noOp: true,
139
+ path: safePath,
140
+ sha256: current.sha256,
141
+ matches,
142
+ };
143
+ }
144
+ return client.executeIdentityBoundOps([
145
+ {
146
+ op: "WriteFile",
147
+ path: safePath,
148
+ content,
149
+ expectedSha256: current.sha256,
150
+ },
151
+ ]);
152
+ });
153
+ }));
154
+ }
@@ -13,6 +13,8 @@ const TOOL_BY_ENDPOINT = {
13
13
  "/api/mcp/generate/3d": "summer_generate_3d",
14
14
  "/api/mcp/generate/video": "summer_generate_video",
15
15
  "/api/mcp/generate/motion": "summer_generate_motion",
16
+ "/api/mcp/generate/slice-asset-sheet": "summer_slice_asset_sheet",
17
+ "/api/mcp/workflows": "summer_get_studio_workflow",
16
18
  };
17
19
  // ---------------------------------------------------------------------------
18
20
  // Shared helpers
@@ -129,6 +131,37 @@ async function mcpGenerate(endpoint, body, timeoutMs = 120_000) {
129
131
  }
130
132
  return { data, status: res.status };
131
133
  }
134
+ async function mcpGet(endpoint, searchParams, timeoutMs = 30_000) {
135
+ const token = await getAuthToken();
136
+ const suffix = searchParams?.size ? `?${searchParams.toString()}` : "";
137
+ let res;
138
+ try {
139
+ res = await fetch(`${GATEWAY_URL}${endpoint}${suffix}`, {
140
+ headers: {
141
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
142
+ "X-Summer-Client": "summer-cli",
143
+ "X-Summer-Client-Version": CLI_VERSION,
144
+ "X-Summer-Client-Surface": "mcp",
145
+ "X-Summer-MCP-Endpoint": endpoint,
146
+ "X-Summer-MCP-Tool": TOOL_BY_ENDPOINT[endpoint] ?? "unknown",
147
+ },
148
+ signal: AbortSignal.timeout(timeoutMs),
149
+ });
150
+ }
151
+ catch (err) {
152
+ const msg = err instanceof Error ? err.message : String(err);
153
+ return { error: `Request failed: ${msg}`, status: 0 };
154
+ }
155
+ const data = await readJsonResponse(res);
156
+ if (!res.ok) {
157
+ return {
158
+ error: formattedApiErrorMessage(res.status, data),
159
+ data: { ...data, status: res.status },
160
+ status: res.status,
161
+ };
162
+ }
163
+ return { data, status: res.status };
164
+ }
132
165
  /**
133
166
  * Download an image URL to a temp file so the AI can Read it and show the user.
134
167
  */
@@ -153,7 +186,7 @@ async function downloadToTemp(url, prefix, ext = "png") {
153
186
  * Poll a job until completed, failed, or timeout.
154
187
  * Returns the final job status object.
155
188
  */
156
- async function pollJob(jobId, maxWaitMs = 300_000, intervalMs = 5_000) {
189
+ async function pollJob(jobId, maxWaitMs = 600_000, intervalMs = 5_000) {
157
190
  const token = await getAuthToken();
158
191
  if (!token)
159
192
  return { error: "Not signed in" };
@@ -210,6 +243,28 @@ function successResult(data) {
210
243
  // Registration
211
244
  // ---------------------------------------------------------------------------
212
245
  export function registerGenerateTools(server) {
246
+ // ========================================================================
247
+ // summer_get_studio_workflow
248
+ // ========================================================================
249
+ server.tool("summer_get_studio_workflow", `Discover the same Guided workflow recipes shown in Summer Engine Studio.
250
+
251
+ Call without workflowId to list every workflow and its support level. Call with
252
+ an id to get the exact starter prompt, ordered steps, required MCP tools, and
253
+ honest limitations. A "manual" support level means Studio still has a visual
254
+ handoff that MCP cannot perform automatically; do not pretend that stage ran.`, {
255
+ workflowId: z
256
+ .string()
257
+ .optional()
258
+ .describe("Guided workflow id, e.g. sprite-sheet, character-pack, asset-pack, or component-compiler"),
259
+ }, async ({ workflowId }) => {
260
+ const params = new URLSearchParams();
261
+ if (workflowId)
262
+ params.set("id", workflowId);
263
+ const result = await mcpGet("/api/mcp/workflows", params);
264
+ if (result.error)
265
+ return errorResult(result.error, result.data);
266
+ return successResult(result.data);
267
+ });
213
268
  // ========================================================================
214
269
  // summer_generate_image
215
270
  // ========================================================================
@@ -277,6 +332,35 @@ Requires authentication: run 'npx summer-engine login' first.`, {
277
332
  });
278
333
  });
279
334
  // ========================================================================
335
+ // summer_slice_asset_sheet
336
+ // ========================================================================
337
+ server.tool("summer_slice_asset_sheet", `Detect and crop every distinct game asset from an existing image asset sheet.
338
+
339
+ Use the assetId returned by summer_generate_image (or another Summer image asset).
340
+ The same guided workflow used in Studio removes the sheet background when needed,
341
+ detects named/category-aware bounding boxes, crops each item, and uploads the slices.
342
+
343
+ Returns source dimensions plus the generated slice URLs, names, categories, bounding
344
+ boxes, and widget metadata when the sheet contains UI controls.
345
+
346
+ Cloud tool — runs on Summer's servers and works WITHOUT the Summer Engine app open.
347
+ Requires authentication: run 'npx summer-engine login' first.`, {
348
+ assetId: z
349
+ .string()
350
+ .describe("Summer image asset ID to slice, usually returned by summer_generate_image"),
351
+ }, async ({ assetId }) => {
352
+ const result = await mcpGenerate("/api/mcp/generate/slice-asset-sheet", { assetId }, 300_000);
353
+ if (result.error) {
354
+ return errorResult(result.error, result.data);
355
+ }
356
+ return successResult({
357
+ ...result.data,
358
+ sliceCount: Array.isArray(result.data?.slices)
359
+ ? result.data.slices.length
360
+ : 0,
361
+ });
362
+ });
363
+ // ========================================================================
280
364
  // summer_generate_audio
281
365
  // ========================================================================
282
366
  server.tool("summer_generate_audio", `Generate audio using AI providers via Summer Engine Studio.
@@ -377,7 +461,16 @@ Example:
377
461
  })
378
462
  // Returns jobId; poll until result includes { assetId, rigAssetId }.
379
463
 
380
- By default, waits for completion (up to 5 min) and returns the result directly.
464
+ Single-image generation automatically assesses whether the source is suitable
465
+ for 3D. If needed, Summer prepares an isolated object view or rig-safe character
466
+ pose before conversion. Set referencePreparation="always" or "never" only when
467
+ you intentionally want to override that shared Studio behavior.
468
+
469
+ For a complete animated humanoid package, pass rig=true plus animationNames or
470
+ actionIds. This routes through the shared character pipeline instead of a
471
+ one-off mesh job.
472
+
473
+ By default, waits for completion (up to 10 min) and returns the result directly.
381
474
  Set wait=false to get the jobId immediately and poll manually with summer_check_job.
382
475
 
383
476
  Cloud tool — runs on Summer's servers and works WITHOUT the Summer Engine app open.
@@ -398,21 +491,66 @@ Requires authentication: run 'npx summer-engine login' first.`, {
398
491
  .string()
399
492
  .optional()
400
493
  .describe("Source image URL for image-to-3d or texture"),
494
+ imageUrls: z
495
+ .array(z.string())
496
+ .max(4)
497
+ .optional()
498
+ .describe("Up to four views for multi-image-to-3d"),
499
+ title: z.string().optional().describe("Asset or character title"),
500
+ idempotencyKey: z
501
+ .string()
502
+ .optional()
503
+ .describe("Stable retry key so the same logical request is not billed or queued twice"),
504
+ assetIntent: z
505
+ .enum(["character", "object"])
506
+ .optional()
507
+ .describe("Controls whether hidden reference preparation targets a rig-safe character pose or isolated object view"),
508
+ referencePreparation: z
509
+ .enum(["auto", "always", "never"])
510
+ .default("auto")
511
+ .describe("Shared Studio reference-preparation policy; auto is recommended"),
512
+ rig: z.boolean().default(false).describe("Auto-rig the generated model"),
513
+ animationNames: z
514
+ .array(z.string())
515
+ .optional()
516
+ .describe("Animation names for the shared animated-character pipeline, e.g. Idle, Walk, Run, Jump"),
517
+ actionIds: z
518
+ .array(z.number().int().min(0).max(696))
519
+ .optional()
520
+ .describe("Exact Meshy action ids for the shared animated-character pipeline"),
521
+ riggingHeightMeters: z
522
+ .number()
523
+ .positive()
524
+ .max(10)
525
+ .optional()
526
+ .describe("Character height used by auto-rigging"),
401
527
  wait: z
402
528
  .boolean()
403
529
  .default(true)
404
- .describe("Wait for completion (default true, up to 5 min). Set false to get jobId immediately."),
530
+ .describe("Wait for completion (default true, up to 10 min). Set false to get jobId immediately."),
405
531
  options: z
406
532
  .record(z.any())
407
533
  .optional()
408
534
  .describe("Provider-specific params (target_polycount, topology, art_style, etc.)"),
409
- }, async ({ prompt, kind, model, imageUrl, wait, options }) => {
535
+ }, async ({ prompt, kind, model, imageUrl, imageUrls, title, idempotencyKey, assetIntent, referencePreparation, rig, animationNames, actionIds, riggingHeightMeters, wait, options, }) => {
536
+ const mergedOptions = {
537
+ ...(options ?? {}),
538
+ ...(imageUrls ? { imageUrls } : {}),
539
+ ...(assetIntent ? { assetIntent } : {}),
540
+ referencePreparation,
541
+ ...(rig ? { rig: true } : {}),
542
+ ...(animationNames ? { animationNames } : {}),
543
+ ...(actionIds ? { actionIds } : {}),
544
+ ...(riggingHeightMeters !== undefined ? { riggingHeightMeters } : {}),
545
+ };
410
546
  const result = await mcpGenerate("/api/mcp/generate/3d", {
411
547
  prompt,
412
548
  kind,
413
549
  model,
414
550
  imageUrl,
415
- options,
551
+ title,
552
+ idempotencyKey,
553
+ options: mergedOptions,
416
554
  });
417
555
  if (result.error) {
418
556
  return errorResult(result.error, result.data);
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { ASSET_POLICIES, GAME_TASK_MODES, GAME_TASK_TARGETS, VERIFICATION_LEVELS, buildGameTaskPlan, } from "../../lib/game-task-plan.js";
3
+ import { getCachedBootDriftNotice } from "../../lib/mcp-boot-notice.js";
3
4
  import { getProjectMemorySummary } from "../../lib/project-memory.js";
4
5
  import { withEngine } from "./with-engine.js";
5
6
  function asRecord(value) {
@@ -134,18 +135,19 @@ anti-patterns, and recovery steps.`, {}, async () => ({
134
135
  {
135
136
  type: "text",
136
137
  text: JSON.stringify({
138
+ summerUpdateNotice: getCachedBootDriftNotice()?.text ?? null,
137
139
  startupChecklist: [
138
140
  "Understand the request and outline a brief plan before reaching for tools.",
139
- "Default medium is host file tools: write/edit .gd/.cs/.tscn/.tres/.json/docs/config directly as text.",
140
- "Use Summer MCP only when you need the LIVE engine: play/stop, diagnostics, screenshots/verification, navmesh or light bake, runtime inspect, or asset import.",
141
+ "Use summer_read_file, summer_write_file, and summer_replace_text for project text files so reads and guarded writes stay bound to the open engine project.",
142
+ "Use Summer MCP for project mutations and live-engine work: scenes, resources, play/stop, diagnostics, screenshots/verification, bake, runtime inspection, and asset import.",
141
143
  "Call summer_get_project_context first so you do not guess scene paths or the project language. It also BINDS this session to the currently-open project (see projectBinding below).",
142
144
  "Use projectMemory from summer_get_project_context to decide which .summer Markdown files to read before creative/audio/dialogue/level/character work.",
143
145
  "After writing code, VERIFY through the engine (see verificationLadder) instead of waiting for the user to navigate to it.",
144
146
  ],
145
147
  safeDefaults: [
146
148
  "Never guess scene filenames (main.tscn/Main.tscn) -- get them from summer_get_project_context.",
147
- "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.",
148
- "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).",
149
+ "Use summer_replace_text for existing project text and summer_write_file with create_only:true for new files; overwrites require the sha256 from summer_read_file.",
150
+ "For live scene hierarchy and inspector changes, prefer scene tools. Guarded text writes support .tscn/.tres, and the engine schedules editor reloads after they land.",
149
151
  "Write GDScript by default; use C# only if the project already uses it.",
150
152
  "Never remove multiple top-level nodes unless the user explicitly requests destructive edits.",
151
153
  "Never change priority: locked .summer memory, voice IDs, canon, or provider bindings without explicit user confirmation.",
@@ -153,7 +155,7 @@ anti-patterns, and recovery steps.`, {}, async () => ({
153
155
  buildFlow: [
154
156
  "1. Understand what the user wants (one pass, no tool spelunking first).",
155
157
  "2. Outline the plan fast and briefly; proceed once it is clearly right.",
156
- "3. Execute in pure code -- edit .gd/.tscn/.tres as text (GDScript by default, C# only if the project already uses it).",
158
+ "3. Execute through identity-bound MCP mutations -- guarded text tools for files, scene tools for live hierarchy/inspector work (GDScript by default, C# only if the project already uses it).",
157
159
  "4. Verify through the engine using the verificationLadder below, fix, and repeat until it launches clean.",
158
160
  ],
159
161
  // The MCP-native verification ladder. Climb only as high as the
@@ -172,11 +174,10 @@ anti-patterns, and recovery steps.`, {}, async () => ({
172
174
  ],
173
175
  "3_runs": [
174
176
  "Compose the run-and-check yourself — there is no single 'verify' tool:",
175
- "summer_clear_console -> clean slate",
176
177
  "summer_play [scene] -> boot the game (or a specific scene)",
177
178
  "summer_get_debugger_errors -> runtime errors (null refs, missing nodes, physics)",
178
179
  "summer_screenshot target:'game' -> optional visual of the live frame",
179
- "summer_stop -> ALWAYS stop; scene mutations are refused while the game runs",
180
+ "summer_stop -> stop when runtime verification is finished; editor scene mutations are not categorically blocked by a running game, but an existing game instance may need a restart to observe them",
180
181
  ],
181
182
  "4_interactive": [
182
183
  "To prove input-driven behavior (does jump/move/shoot actually work), you have two routes when the engine build supports them (both go through summer_batch as raw ops — see rawOpsViaBatch):",
@@ -204,12 +205,12 @@ anti-patterns, and recovery steps.`, {}, async () => ({
204
205
  "To intentionally follow the switch, call summer_get_project_context again to rebind, then retry.",
205
206
  ],
206
207
  liveEngineFlow: [
207
- "Use this flow ONLY when you genuinely need live engine state (navmesh/light bake, instancing into an already-open scene, runtime inspect):",
208
+ "Use this flow when you genuinely need live engine state (navmesh/light bake, scene mutation, runtime inspect):",
208
209
  "summer_get_project_context",
209
- "summer_open_main_scene (if needed)",
210
- "summer_get_scene_tree",
211
- "summer_add_node / summer_set_prop / summer_set_resource_property",
212
- "summer_save_scene",
210
+ "Choose the exact res:// scenePath. OpenScene is a user-visible tab action, not a mutation prerequisite.",
211
+ "summer_get_scene_tree with scenePath when that exact scene is already loaded; omit it only for the visible tab.",
212
+ "summer_add_node / summer_set_prop / summer_set_resource_property with scenePath",
213
+ "Mutation tools append one final SaveScene; use summer_save_scene only for a standalone save/save-as.",
213
214
  "summer_get_diagnostics",
214
215
  ],
215
216
  // summer_batch forwards each {op, ...} verbatim to the engine, so
@@ -221,11 +222,12 @@ anti-patterns, and recovery steps.`, {}, async () => ({
221
222
  "These are runtime ops, not scene mutations — the batch undo group is a harmless no-op for them.",
222
223
  ],
223
224
  recovery: [
224
- "If you see 'no scene open': run summer_open_main_scene.",
225
+ "If a scene mutation reports missing_scene_path: pass the exact res:// scenePath and retry.",
226
+ "If a scene target cannot load: repair the named missing/invalid dependency, then retry the same scenePath.",
225
227
  "If open_scene fails: re-check mainScene from summer_get_project_context.",
226
- "If save fails: verify scene is open and game is not running (summer_is_running / summer_stop).",
228
+ "If save fails: use the returned scenePath/error to repair the exact cause. A running game alone is not a generic scene-mutation blocker.",
227
229
  "If a mutation is rejected with identity_mismatch: the engine switched projects — call summer_get_project_context to rebind (only if you meant to follow it), then retry.",
228
- "If a .tscn you wrote keeps reverting: the editor has that scene open -- reload or close the tab, then write again.",
230
+ "If a guarded file mutation is rejected with content mismatch: call summer_read_file again, review the new content, and retry with its new sha256 only if the edit is still valid.",
229
231
  "If a run op or SimulateInput returns 'unsupported' / an unknown-op error: this engine build predates it — fall back to summer_play + summer_get_debugger_errors, or ask the user to interact.",
230
232
  ],
231
233
  debugging: [
@@ -270,10 +272,11 @@ Use this first in every fresh chat to avoid guessing scene filenames or editing
270
272
  mainScene,
271
273
  boundProjectIdHash,
272
274
  projectMemory: getProjectMemorySummary(projectPath),
275
+ summerUpdateNotice: getCachedBootDriftNotice()?.text ?? null,
273
276
  guidance: mainScene
274
277
  ? "Use `summer_open_scene` with `mainScene` if no scene is open."
275
278
  : "Main scene not found in project state. Open a known scene path explicitly.",
276
- 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.",
279
+ fileEditingGuidance: "Use summer_read_file plus summer_replace_text or guarded summer_write_file for project files, including .gd/.cs/.tscn/.tres/.json/docs. New files require create_only:true; overwrites require the sha256 receipt from summer_read_file. Prefer scene tools for live hierarchy/inspector changes.",
277
280
  };
278
281
  }));
279
282
  server.tool("summer_open_main_scene", `Open the project's configured main scene from project settings.
@@ -315,10 +318,12 @@ Example: Bind jump to Space and W:
315
318
  ];
316
319
  return client.executeOps(ops);
317
320
  }));
318
- server.tool("summer_get_scene_tree", `Get the full scene tree structure of the currently open scene.
319
-
320
- Use this before structural edits (add/remove/replace).
321
- If you get "no edited scene", call summer_open_main_scene first.`, {}, async () => withEngine(async (client) => client.getSceneState()));
321
+ server.tool("summer_get_scene_tree", `Get a scene tree. Pass scenePath to read that exact in-memory/open scene;
322
+ omit it only when you intentionally want the currently visible editor scene.
323
+ Scene mutations load their explicit target, so a follow-up targeted read does
324
+ not require OpenScene.`, {
325
+ scenePath: z.string().optional().describe("Exact res:// scene path to inspect"),
326
+ }, async ({ scenePath }) => withEngine(async (client) => client.getSceneState(scenePath)));
322
327
  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.
323
328
 
324
329
  Use this for: