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,72 @@
1
+ import { appendFileSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { getSummerDir } from "./auth.js";
5
+ const MAX_LOG_BYTES = 512 * 1024;
6
+ const RETAIN_LOG_BYTES = 256 * 1024;
7
+ export function getMcpLogPath() {
8
+ return join(getSummerDir(), "mcp.log");
9
+ }
10
+ function redact(value) {
11
+ if (Array.isArray(value))
12
+ return value.map((item) => redact(item));
13
+ if (!value || typeof value !== "object")
14
+ return value;
15
+ const out = {};
16
+ for (const [key, nested] of Object.entries(value)) {
17
+ if (/token|secret|password|authorization|cookie|api[-_]?key/i.test(key)) {
18
+ out[key] = "[REDACTED]";
19
+ }
20
+ else {
21
+ out[key] = redact(nested);
22
+ }
23
+ }
24
+ return out;
25
+ }
26
+ function rotateIfNeeded(path) {
27
+ try {
28
+ if (statSync(path).size < MAX_LOG_BYTES)
29
+ return;
30
+ const text = readFileSync(path, "utf-8");
31
+ const retained = text.slice(-RETAIN_LOG_BYTES);
32
+ writeFileSync(path, `{"event":"mcp:log_rotated"}\n${retained}`, {
33
+ encoding: "utf-8",
34
+ mode: 0o600,
35
+ });
36
+ }
37
+ catch {
38
+ // No existing log, or not readable. The next append will recreate it.
39
+ }
40
+ }
41
+ export function appendMcpLogEvent(event, details = {}) {
42
+ try {
43
+ const dir = getSummerDir();
44
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
45
+ const path = getMcpLogPath();
46
+ rotateIfNeeded(path);
47
+ const safeDetails = redact(details);
48
+ const payload = {
49
+ ts: new Date().toISOString(),
50
+ event,
51
+ ...safeDetails,
52
+ };
53
+ appendFileSync(path, `${JSON.stringify(payload)}\n`, {
54
+ encoding: "utf-8",
55
+ mode: 0o600,
56
+ });
57
+ }
58
+ catch {
59
+ // Logging must never affect MCP behavior.
60
+ }
61
+ }
62
+ export async function readRecentMcpLogLines(maxLines = 120) {
63
+ try {
64
+ const text = await readFile(getMcpLogPath(), "utf-8");
65
+ if (!text.trim())
66
+ return [];
67
+ return text.trimEnd().split(/\r?\n/).slice(-maxLines);
68
+ }
69
+ catch {
70
+ return [];
71
+ }
72
+ }
@@ -23,7 +23,7 @@ export declare const SKILL_REGISTRY: readonly [{
23
23
  readonly public: true;
24
24
  readonly clients: readonly ["summer", "codex", "claude-code", "cursor", "windsurf", "cline", "roo-code", "kilo-code", "gemini", "github-copilot", "vscode-copilot", "opencode"];
25
25
  readonly recommended: true;
26
- readonly requiresMcpTools: readonly ["summer_get_console", "summer_get_debugger_errors", "summer_get_script_errors", "summer_get_diagnostics", "summer_clear_console", "summer_play", "summer_stop", "summer_inspect_node"];
26
+ readonly requiresMcpTools: readonly ["summer_create_debug_report", "summer_get_console", "summer_get_debugger_errors", "summer_get_script_errors", "summer_get_diagnostics", "summer_clear_console", "summer_play", "summer_stop", "summer_inspect_node"];
27
27
  readonly testScenario: "Triage a runtime crash end-to-end: read errors, locate code, propose fix, apply on approval, verify.";
28
28
  }, {
29
29
  readonly name: "play";
@@ -49,6 +49,14 @@ export declare const SKILL_REGISTRY: readonly [{
49
49
  readonly recommended: true;
50
50
  readonly requiresMcpTools: readonly ["summer_create_scene", "summer_open_scene", "summer_get_scene_tree", "summer_add_node", "summer_instantiate_scene", "summer_save_scene"];
51
51
  readonly testScenario: "Build a small reusable player scene, instantiate it in a main scene, and save both scenes through Summer tools.";
52
+ }, {
53
+ readonly name: "summer-cloud";
54
+ readonly category: "scene-and-project";
55
+ readonly public: true;
56
+ readonly clients: readonly ["summer", "codex", "claude-code", "cursor", "windsurf", "cline", "roo-code", "kilo-code", "gemini", "github-copilot", "vscode-copilot", "opencode"];
57
+ readonly recommended: true;
58
+ readonly requiresMcpTools: readonly ["summer_cloud_init", "summer_cloud_status", "summer_cloud_push", "summer_cloud_pull", "summer_cloud_restore", "summer_cloud_checkpoints", "summer_cloud_conflicts"];
59
+ readonly testScenario: "Enable Cloud for a project, push it, then pull it onto a second machine, taking the bootstrap choice and surfacing the delete count before any guarded deletion.";
52
60
  }, {
53
61
  readonly name: "fps-controller";
54
62
  readonly category: "character-controllers";
@@ -201,6 +209,14 @@ export declare const SKILL_REGISTRY: readonly [{
201
209
  readonly recommended: true;
202
210
  readonly requiresMcpTools: readonly ["summer_get_scene_tree", "summer_inspect_node", "summer_project_setting", "summer_get_console", "summer_get_diagnostics"];
203
211
  readonly testScenario: "Run the Steam pre-flight checklist (Windows + Mac + Linux), validate icons/banners/screenshots/build config, and produce a release build only when green.";
212
+ }, {
213
+ readonly name: "remote-deploy";
214
+ readonly category: "deployment";
215
+ readonly public: true;
216
+ readonly clients: readonly ["summer", "codex", "claude-code", "cursor", "windsurf", "cline", "roo-code", "kilo-code", "gemini", "github-copilot", "vscode-copilot", "opencode"];
217
+ readonly recommended: true;
218
+ readonly requiresMcpTools: readonly ["summer_project_setting", "summer_get_diagnostics", "summer_get_console", "summer_get_script_errors"];
219
+ readonly testScenario: "User says \"deploy the game to my Android phone\" — skill walks the prerequisites (runnable export preset, installed export templates, USB-debugging device), checks diagnostics are clean, and points them at the topnav Remote Deploy button to deploy a debug build with on-device remote debugging.";
204
220
  }, {
205
221
  readonly name: "concept-art";
206
222
  readonly category: "2d-assets";
@@ -249,6 +265,14 @@ export declare const SKILL_REGISTRY: readonly [{
249
265
  readonly recommended: false;
250
266
  readonly requiresMcpTools: readonly ["summer_generate_image", "summer_import_from_url", "summer_set_resource_property", "summer_add_node", "summer_set_prop"];
251
267
  readonly testScenario: "User requests a '4-frame walk cycle for the goblin' — skill generates per-frame at locked anchor, builds SpriteFrames resource, wires AnimatedSprite2D.";
268
+ }, {
269
+ readonly name: "create-asset-sheet";
270
+ readonly category: "2d-assets";
271
+ readonly public: true;
272
+ readonly clients: readonly ["summer", "codex", "claude-code", "cursor", "windsurf", "cline", "roo-code", "kilo-code", "gemini", "github-copilot", "vscode-copilot", "opencode"];
273
+ readonly recommended: false;
274
+ readonly requiresMcpTools: readonly ["summer_get_studio_workflow", "summer_generate_image", "summer_slice_asset_sheet", "summer_import_asset_by_id"];
275
+ readonly testScenario: "User requests a Japanese village asset pack — skill loads the Guided asset-pack recipe, generates one coherent sheet, slices it through the shared cloud pipeline, and imports selected results.";
252
276
  }, {
253
277
  readonly name: "skybox-panorama";
254
278
  readonly category: "2d-assets";
@@ -53,6 +53,7 @@ export const SKILL_REGISTRY = [
53
53
  clients: ALL_CLIENTS,
54
54
  recommended: true,
55
55
  requiresMcpTools: [
56
+ "summer_create_debug_report",
56
57
  "summer_get_console",
57
58
  "summer_get_debugger_errors",
58
59
  "summer_get_script_errors",
@@ -108,6 +109,23 @@ export const SKILL_REGISTRY = [
108
109
  ],
109
110
  testScenario: "Build a small reusable player scene, instantiate it in a main scene, and save both scenes through Summer tools.",
110
111
  },
112
+ {
113
+ name: "summer-cloud",
114
+ category: "scene-and-project",
115
+ public: true,
116
+ clients: ALL_CLIENTS,
117
+ recommended: true,
118
+ requiresMcpTools: [
119
+ "summer_cloud_init",
120
+ "summer_cloud_status",
121
+ "summer_cloud_push",
122
+ "summer_cloud_pull",
123
+ "summer_cloud_restore",
124
+ "summer_cloud_checkpoints",
125
+ "summer_cloud_conflicts",
126
+ ],
127
+ testScenario: "Enable Cloud for a project, push it, then pull it onto a second machine, taking the bootstrap choice and surfacing the delete count before any guarded deletion.",
128
+ },
111
129
  {
112
130
  name: "fps-controller",
113
131
  category: "character-controllers",
@@ -413,6 +431,20 @@ export const SKILL_REGISTRY = [
413
431
  ],
414
432
  testScenario: "Run the Steam pre-flight checklist (Windows + Mac + Linux), validate icons/banners/screenshots/build config, and produce a release build only when green.",
415
433
  },
434
+ {
435
+ name: "remote-deploy",
436
+ category: "deployment",
437
+ public: true,
438
+ clients: ALL_CLIENTS,
439
+ recommended: true,
440
+ requiresMcpTools: [
441
+ "summer_project_setting",
442
+ "summer_get_diagnostics",
443
+ "summer_get_console",
444
+ "summer_get_script_errors",
445
+ ],
446
+ testScenario: "User says \"deploy the game to my Android phone\" — skill walks the prerequisites (runnable export preset, installed export templates, USB-debugging device), checks diagnostics are clean, and points them at the topnav Remote Deploy button to deploy a debug build with on-device remote debugging.",
447
+ },
416
448
  {
417
449
  name: "concept-art",
418
450
  category: "2d-assets",
@@ -494,6 +526,20 @@ export const SKILL_REGISTRY = [
494
526
  ],
495
527
  testScenario: "User requests a '4-frame walk cycle for the goblin' — skill generates per-frame at locked anchor, builds SpriteFrames resource, wires AnimatedSprite2D.",
496
528
  },
529
+ {
530
+ name: "create-asset-sheet",
531
+ category: "2d-assets",
532
+ public: true,
533
+ clients: ALL_CLIENTS,
534
+ recommended: false,
535
+ requiresMcpTools: [
536
+ "summer_get_studio_workflow",
537
+ "summer_generate_image",
538
+ "summer_slice_asset_sheet",
539
+ "summer_import_asset_by_id",
540
+ ],
541
+ testScenario: "User requests a Japanese village asset pack — skill loads the Guided asset-pack recipe, generates one coherent sheet, slices it through the shared cloud pipeline, and imports selected results.",
542
+ },
497
543
  {
498
544
  name: "skybox-panorama",
499
545
  category: "2d-assets",
@@ -1,6 +1,6 @@
1
1
  import { EngineApiClient } from "../lib/api-client.js";
2
- import { type BootDriftNotice } from "../lib/version-check.js";
3
- export declare function getCachedBootDriftNotice(): BootDriftNotice | null;
2
+ import { getCachedBootDriftNotice as getCachedNotice } from "../lib/mcp-boot-notice.js";
3
+ export declare const getCachedBootDriftNotice: typeof getCachedNotice;
4
4
  export declare function getClient(): Promise<EngineApiClient>;
5
5
  export declare function resetClient(): void;
6
6
  export declare function startMcpServer(): Promise<void>;
@@ -7,16 +7,17 @@ import { registerDebugTools } from "./tools/debug-tools.js";
7
7
  import { registerVisualTools } from "./tools/visual-tools.js";
8
8
  import { WITH_ENGINE_META } from "./tools/with-engine.js";
9
9
  import { registerProjectTools } from "./tools/project-tools.js";
10
+ import { registerFileTools } from "./tools/file-tools.js";
10
11
  import { registerAssetTools } from "./tools/asset-tools.js";
11
12
  import { registerGenerateTools } from "./tools/generate-tools.js";
12
13
  import { registerCloudTools } from "./tools/cloud-tools.js";
14
+ import { getCachedBootDriftNotice as getCachedNotice, setCachedBootDriftNotice, } from "../lib/mcp-boot-notice.js";
15
+ import { appendMcpLogEvent } from "../lib/mcp-log.js";
13
16
  import { buildBootDriftNotice, fetchLatestRegistryVersion, } from "../lib/version-check.js";
14
17
  const require = createRequire(import.meta.url);
15
18
  const { version } = require("../../package.json");
16
- let cachedBootDriftNotice = null;
17
- export function getCachedBootDriftNotice() {
18
- return cachedBootDriftNotice;
19
- }
19
+ let processDiagnosticsInstalled = false;
20
+ export const getCachedBootDriftNotice = getCachedNotice;
20
21
  /**
21
22
  * Fire-and-forget probe of the npm registry on MCP boot. Caches the result for
22
23
  * the session. We never await this in startup so a slow / offline registry
@@ -27,7 +28,7 @@ async function probeBootDrift() {
27
28
  const registry = await fetchLatestRegistryVersion();
28
29
  // Agent is unknown at the MCP layer; use a generic placeholder. Doctor still
29
30
  // prescribes the correct agent-aware command via setup recommendations.
30
- cachedBootDriftNotice = buildBootDriftNotice(version, registry);
31
+ setCachedBootDriftNotice(buildBootDriftNotice(version, registry));
31
32
  }
32
33
  let cachedClient = null;
33
34
  export async function getClient() {
@@ -69,8 +70,56 @@ export function resetClient() {
69
70
  * out to produce pathological payloads in practice, the right fix is
70
71
  * adding range/depth/filter parameters to that tool — not a blanket cap.
71
72
  */
73
+ function errorDetails(error) {
74
+ if (error instanceof Error) {
75
+ return {
76
+ name: error.name,
77
+ message: error.message,
78
+ stack: error.stack,
79
+ };
80
+ }
81
+ return { message: String(error) };
82
+ }
83
+ function writeMcpStderr(event, details = {}) {
84
+ process.stderr.write(`[summer-mcp] ${JSON.stringify({ event, ...details })}\n`);
85
+ }
86
+ function installMcpProcessDiagnostics() {
87
+ if (processDiagnosticsInstalled)
88
+ return;
89
+ processDiagnosticsInstalled = true;
90
+ process.once("uncaughtException", (error) => {
91
+ const details = errorDetails(error);
92
+ appendMcpLogEvent("mcp:uncaught_exception", details);
93
+ writeMcpStderr("mcp:uncaught_exception", {
94
+ message: details.message,
95
+ log: "~/.summer/mcp.log",
96
+ });
97
+ process.exit(1);
98
+ });
99
+ process.once("unhandledRejection", (reason) => {
100
+ const details = errorDetails(reason);
101
+ appendMcpLogEvent("mcp:unhandled_rejection", details);
102
+ writeMcpStderr("mcp:unhandled_rejection", {
103
+ message: details.message,
104
+ log: "~/.summer/mcp.log",
105
+ });
106
+ process.exit(1);
107
+ });
108
+ process.once("SIGTERM", () => {
109
+ appendMcpLogEvent("mcp:sigterm", { pid: process.pid });
110
+ process.exit(0);
111
+ });
112
+ process.once("SIGINT", () => {
113
+ appendMcpLogEvent("mcp:sigint", { pid: process.pid });
114
+ process.exit(0);
115
+ });
116
+ process.once("exit", (code) => {
117
+ appendMcpLogEvent("mcp:exit", { pid: process.pid, code });
118
+ });
119
+ }
72
120
  function installResultSizeLogger(server) {
73
121
  const original = server.tool.bind(server);
122
+ let registeredToolCount = 0;
74
123
  server.tool = function patchedTool(...args) {
75
124
  if (args.length < 2)
76
125
  return original(...args);
@@ -81,6 +130,7 @@ function installResultSizeLogger(server) {
81
130
  const handler = args[lastIdx];
82
131
  if (typeof handler !== "function")
83
132
  return original(...args);
133
+ registeredToolCount += 1;
84
134
  const wrapped = async (...handlerArgs) => {
85
135
  const startedAt = Date.now();
86
136
  const result = await handler(...handlerArgs);
@@ -133,6 +183,11 @@ function installResultSizeLogger(server) {
133
183
  chars,
134
184
  bytes,
135
185
  });
186
+ appendMcpLogEvent("mcp:result_size", {
187
+ toolName: name,
188
+ chars,
189
+ bytes,
190
+ });
136
191
  process.stderr.write(`[summer-mcp] ${line}\n`);
137
192
  }
138
193
  }
@@ -146,25 +201,42 @@ function installResultSizeLogger(server) {
146
201
  newArgs[lastIdx] = wrapped;
147
202
  return original(...newArgs);
148
203
  };
204
+ return () => registeredToolCount;
149
205
  }
150
206
  export async function startMcpServer() {
207
+ installMcpProcessDiagnostics();
208
+ appendMcpLogEvent("mcp:start", {
209
+ version,
210
+ pid: process.pid,
211
+ node: process.version,
212
+ cwd: process.cwd(),
213
+ });
151
214
  const server = new McpServer({
152
215
  name: "summer-engine",
153
216
  version,
154
217
  });
155
218
  // Passive observability: log result-size to stderr but do not modify
156
219
  // results. See installResultSizeLogger above.
157
- installResultSizeLogger(server);
220
+ const getRegisteredToolCount = installResultSizeLogger(server);
158
221
  registerSceneTools(server);
159
222
  registerDebugTools(server);
160
223
  registerVisualTools(server);
161
224
  registerProjectTools(server);
225
+ registerFileTools(server);
162
226
  registerAssetTools(server);
163
227
  registerGenerateTools(server);
164
228
  registerCloudTools(server);
165
229
  // Fire-and-forget — never block tool registration on the npm registry.
166
- void probeBootDrift();
230
+ void probeBootDrift().catch((error) => {
231
+ setCachedBootDriftNotice(null);
232
+ appendMcpLogEvent("mcp:boot_drift_probe_failed", errorDetails(error));
233
+ });
167
234
  const transport = new StdioServerTransport();
168
235
  await server.connect(transport);
236
+ appendMcpLogEvent("mcp:ready", {
237
+ version,
238
+ pid: process.pid,
239
+ toolCount: getRegisteredToolCount(),
240
+ });
169
241
  process.stderr.write(`[summer-mcp] MCP server running v${version}.\n`);
170
242
  }
@@ -168,7 +168,10 @@ async function buildImportEntriesForAsset(asset, targetPath) {
168
168
  return { imports: [{ url: fileUrl, path }], importPath: path };
169
169
  }
170
170
  async function importResolvedAsset(args) {
171
- const { asset, parent, path, name } = args;
171
+ const { asset, parent, scenePath, path, name } = args;
172
+ if (parent && !scenePath) {
173
+ throw new Error("scenePath is required when importing an asset into a scene");
174
+ }
172
175
  const { imports, importPath } = await buildImportEntriesForAsset(asset, path);
173
176
  const client = await getClient();
174
177
  const importResult = imports.length === 1
@@ -176,20 +179,30 @@ async function importResolvedAsset(args) {
176
179
  { op: "ImportFromUrl", url: imports[0].url, path: imports[0].path },
177
180
  ])
178
181
  : 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
+ const importReceipts = importResult?.results ?? [];
183
+ const importFailure = importReceipts.find((receipt) => receipt?.ok === false);
184
+ if (importResult?.status === "error" || importFailure) {
185
+ throw new Error(importFailure?.error || "Could not import asset. Check engine logs.");
182
186
  }
183
187
  let addedToScene = false;
188
+ let sceneReceipt = null;
184
189
  if (parent && asset.type === "3d_model") {
185
- await client.executeOps([
190
+ sceneReceipt = await client.executeIdentityBoundOps([
186
191
  {
187
192
  op: "InstantiateScene",
188
193
  parent,
189
194
  scene: importPath,
190
195
  name: sanitizeNodeName(name || asset.title),
191
196
  },
192
- ]);
197
+ { op: "SaveScene" },
198
+ ], { scenePath });
199
+ const placementReceipts = sceneReceipt?.results ?? [];
200
+ const placementFailure = placementReceipts.find((receipt) => receipt?.ok !== true);
201
+ if (sceneReceipt?.status === "error" ||
202
+ placementReceipts.length !== 2 ||
203
+ placementFailure) {
204
+ throw new Error(placementFailure?.error || `Could not add asset to ${scenePath}`);
205
+ }
193
206
  addedToScene = true;
194
207
  }
195
208
  return {
@@ -200,6 +213,8 @@ async function importResolvedAsset(args) {
200
213
  importedTo: importPath,
201
214
  addedToScene,
202
215
  parent: parent || null,
216
+ scenePath: scenePath || null,
217
+ sceneReceipt,
203
218
  };
204
219
  }
205
220
  function jsonSuccess(data) {
@@ -345,8 +360,8 @@ Cloud tool — works WITHOUT the Summer Engine app open.`, {
345
360
  Use this when the asset was just generated, selected from my assets, or returned
346
361
  by search. Unlike summer_import_asset, this does not search or guess: it fetches
347
362
  the asset by ID, downloads it through Summer Engine, and runs Godot's import
348
- pipeline. For 3D models, pass parent to instantiate it in the open scene after
349
- import.
363
+ pipeline. For 3D models, pass parent and scenePath to instantiate it in that
364
+ exact scene; it does not need to be the active editor tab.
350
365
 
351
366
  Requires the Summer Engine app to be open with the project loaded (generation
352
367
  itself does not — only this import step does).`, {
@@ -359,11 +374,15 @@ itself does not — only this import step does).`, {
359
374
  .string()
360
375
  .optional()
361
376
  .describe("Optional parent node path. For 3D models only; imports and instantiates under this node."),
377
+ scenePath: z
378
+ .string()
379
+ .optional()
380
+ .describe("Required with parent. Exact res:// scene that receives the 3D model."),
362
381
  name: z
363
382
  .string()
364
383
  .optional()
365
384
  .describe("Optional scene node name when parent is provided."),
366
- }, async ({ assetId, path, parent, name }) => {
385
+ }, async ({ assetId, path, parent, scenePath, name }) => {
367
386
  const fetched = await getAssetApi(assetId);
368
387
  if (fetched.error || !fetched.asset) {
369
388
  return jsonError({
@@ -375,6 +394,7 @@ itself does not — only this import step does).`, {
375
394
  const imported = await importResolvedAsset({
376
395
  asset: fetched.asset,
377
396
  parent,
397
+ scenePath,
378
398
  path,
379
399
  name,
380
400
  });
@@ -390,7 +410,7 @@ itself does not — only this import step does).`, {
390
410
  return jsonError({
391
411
  error: "import_failed",
392
412
  message: msg,
393
- hint: "Make sure Summer Engine is running and the target scene is open.",
413
+ hint: "Make sure Summer Engine is running and pass the exact scenePath when parent is set.",
394
414
  });
395
415
  }
396
416
  });
@@ -402,9 +422,10 @@ Searches, picks the top result, downloads and imports it, then optionally adds i
402
422
  Requires authentication. If the user gets an auth error, they need to run 'npx summer-engine login' in their terminal first. Summer Engine must be running.`, {
403
423
  query: z.string().describe("What to find, e.g. 'low-poly tree', 'wooden crate'"),
404
424
  parent: z.string().optional().describe("Parent node path to add the asset under, e.g. './World'. If omitted, only imports (no scene placement)"),
425
+ scenePath: z.string().optional().describe("Required with parent. Exact res:// target scene path."),
405
426
  assetType: z.enum(["2d_image", "animation", "3d_model", "audio", "music", "all"]).default("3d_model").describe("Preferred asset type"),
406
427
  source: z.enum(["library", "my_assets", "all"]).default("all").describe("Where to search before importing"),
407
- }, async ({ query, parent, assetType, source }) => {
428
+ }, async ({ query, parent, scenePath, assetType, source }) => {
408
429
  const searchResult = await searchAssetsApi({ query, assetType, limit: 5, source });
409
430
  if (searchResult.error) {
410
431
  return {
@@ -443,7 +464,7 @@ Requires authentication. If the user gets an auth error, they need to run 'npx s
443
464
  };
444
465
  }
445
466
  try {
446
- const imported = await importResolvedAsset({ asset: best, parent });
467
+ const imported = await importResolvedAsset({ asset: best, parent, scenePath });
447
468
  // 2D sprites and audio: import only; user adds to scene manually
448
469
  return {
449
470
  content: [
@@ -15,28 +15,28 @@ function textJson(value) {
15
15
  };
16
16
  }
17
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.", {
18
+ server.tool("summer_cloud_init", "Enable Summer Cloud (content-addressed whole-project sync) for a project, writing the git-tracked summer-cloud.json binding. Use once per project before the first push, or after deleting the binding to fork the project into a new cloud line.", projectArg.shape, async (args) => textJson(await cloudInit({ project: args.project, face: "mcp" })));
19
+ server.tool("summer_cloud_status", "Show what a sync would move: file counts, paths queued to push or pull, and any waiting conflict sets. Always call this before push or pull so you can report the delete and change counts to the user; never sync blind.", projectArg.shape, async (args) => textJson(await cloudStatus({ project: args.project, face: "mcp" })));
20
+ server.tool("summer_cloud_push", "Upload local project changes to Summer Cloud as a new restorable version (use to back up big assets, move to another machine, or snapshot before a risky bulk op). confirmDeletes gates large deletions (more than 10 files, or >=20% of the tree, or the whole tree): run summer_cloud_status first, show the exact delete count to the user, and only set it once they confirm. Never pass confirmDeletes blindly.", {
21
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."),
22
+ confirmDeletes: z.boolean().default(false).describe("Gate for large cloud deletions. Required when a push would delete more than 10 files, at least 20% of the tracked tree, or the entire tree. Surface the delete count from summer_cloud_status to the user before setting this; never set it blindly."),
23
+ bootstrap: bootstrap.optional().describe("No-base bootstrap choice when local and cloud both have files: keep-cloud, keep-local, or merge. Present the choice to the user."),
24
+ adoptPath: z.boolean().optional().describe("Accept that the project folder moved and update the recorded path. Only set after confirming the move is intentional, not a copy."),
25
25
  }, async (args) => textJson(await cloudPush({ ...args, face: "mcp" })));
26
- server.tool("summer_cloud_pull", "Pull Summer Cloud changes into the local project.", {
26
+ server.tool("summer_cloud_pull", "Download Summer Cloud changes into the local project (use to fetch a teammate's bytes or hydrate on a second machine). A local checkpoint is written before any destructive apply, and concurrent edits become recoverable conflict sets rather than silent overwrites. Run summer_cloud_status first.", {
27
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."),
28
+ bootstrap: bootstrap.optional().describe("No-base bootstrap choice when local and cloud both have files: keep-cloud, keep-local, or merge. Present the choice to the user."),
29
+ adoptPath: z.boolean().optional().describe("Accept that the project folder moved and update the recorded path. Only set after confirming the move is intentional, not a copy."),
30
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.", {
31
+ server.tool("summer_cloud_restore", "Roll a project back to an earlier state. Pass version to restore a retained cloud version (creates a new head version server-side, then pulls); pass checkpointStamp to restore a local pre-sync checkpoint for unpushed work. The restore restores bytes but does not delete files a later sync added, so review the extraneous-file list it returns.", {
32
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)."),
33
+ version: z.number().int().positive().optional().describe("Cloud version to restore (see summer_cloud_status / version history)."),
34
+ checkpointStamp: z.string().optional().describe("Local checkpoint stamp (see summer_cloud_checkpoints). Use for unpushed local-only work."),
35
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.", {
36
+ server.tool("summer_cloud_checkpoints", "List the local pre-sync checkpoints taken before destructive applies. Use to find a checkpoint stamp to pass to summer_cloud_restore when recovering local-only work.", projectArg.shape, async (args) => textJson(await cloudCheckpoints({ project: args.project, face: "mcp" })));
37
+ server.tool("summer_cloud_conflicts", "List preserved Summer Cloud conflict sets (the losing bytes from concurrent edits, never discarded), or recover one by passing restorePath to re-enter it as a fresh edit. Use after a push or pull reports conflicts so the user can choose what to keep.", {
38
38
  ...projectArg.shape,
39
- restorePath: z.string().optional().describe("Project-relative path to restore from the conflict sets."),
39
+ restorePath: z.string().optional().describe("Project-relative path to restore from the conflict sets, re-entered as a fresh edit."),
40
40
  set: z.string().optional().describe("Conflict set stamp; defaults to the newest set containing the path."),
41
41
  }, async (args) => textJson(await cloudConflicts({ project: args.project, restorePath: args.restorePath, set: args.set, face: "mcp" })));
42
42
  }
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { withEngine } from "./with-engine.js";
3
3
  import { shapeEngineLogResponse } from "../../lib/log-filters.js";
4
+ import { createDebugReportArtifact } from "../../lib/debug-report.js";
4
5
  export function registerDebugTools(server) {
5
6
  server.tool("summer_get_diagnostics", `Quick overview of all errors and warnings from both the editor console and the runtime debugger. Returns error counts and a guidance message.
6
7
 
@@ -92,11 +93,53 @@ After starting, use summer_get_diagnostics to check for runtime errors.
92
93
  You can run a specific scene instead of the main scene — useful for testing individual levels or UI screens.`, {
93
94
  scene: z.string().optional().describe("Scene to run instead of main scene, e.g. 'res://levels/test_level.tscn'"),
94
95
  }, async ({ scene }) => withEngine(async (client) => client.play(scene)));
95
- server.tool("summer_stop", "Stop the running game. Call this before making scene changes — some operations require the game to not be running.", {}, async () => withEngine(async (client) => client.stop()));
96
+ server.tool("summer_stop", "Stop the running game. Use after runtime verification or when you intentionally need to restart the running instance; ordinary editor scene mutations do not require a blanket stop.", {}, async () => withEngine(async (client) => client.stop()));
96
97
  server.tool("summer_is_running", "Check if the game is currently running. Returns the active scene path if running.", {}, async () => withEngine(async (client) => client.executeOps([{ op: "IsGameRunning" }])));
97
98
  server.tool("summer_get_script_errors", `Check a GDScript file for parse/compile errors without running the game.
98
99
 
99
100
  Use after writing or editing a .gd file to verify it compiles. Returns line numbers, error messages, and severity. Much faster than running the game to discover script errors.`, {
100
101
  path: z.string().describe("Script path, e.g. 'res://scripts/player.gd' or 'res://player_controller.gd'"),
101
102
  }, async ({ path }) => withEngine(async (client) => client.getScriptErrors(path)));
103
+ server.tool("summer_create_debug_report", `Create a support-ready Markdown report for /summer debug.
104
+
105
+ Use this when the user says "/summer debug", asks to send Summer a bug report,
106
+ or needs a portable artifact from a failing Codex/cloud/agent session. The
107
+ report includes Summer doctor checks, engine health, diagnostics, console
108
+ output, debugger errors/warnings, and an agent handoff prompt. It omits auth
109
+ tokens and project file contents, but the user should still review it before
110
+ sending because local paths and stack traces may appear.`, {
111
+ issue: z.string().optional().describe("User-visible issue or repro summary."),
112
+ output_path: z.string().optional().describe("Where to write the Markdown report. Defaults to the open project root, or the current working directory if no project is open."),
113
+ include_play_session: z.boolean().optional().default(false).describe("Launch the game briefly and collect post-play diagnostics."),
114
+ play_wait_ms: z.number().optional().default(2500).describe("Milliseconds to wait after launching the game when include_play_session is true."),
115
+ max_console_lines: z.number().optional().default(200).describe("Console lines to include after filtering/deduping."),
116
+ max_debugger_entries: z.number().optional().default(100).describe("Debugger error/warning entries to include after filtering/deduping."),
117
+ include_doctor: z.boolean().optional().default(true).describe("Include summer doctor checks in the report."),
118
+ }, async ({ issue, output_path, include_play_session, play_wait_ms, max_console_lines, max_debugger_entries, include_doctor, }) => {
119
+ const artifact = await createDebugReportArtifact({
120
+ issue,
121
+ outputPath: output_path,
122
+ includePlaySession: include_play_session,
123
+ playWaitMs: play_wait_ms,
124
+ maxConsoleLines: max_console_lines,
125
+ maxDebuggerEntries: max_debugger_entries,
126
+ includeDoctor: include_doctor,
127
+ });
128
+ return {
129
+ content: [
130
+ {
131
+ type: "text",
132
+ text: JSON.stringify({
133
+ ok: true,
134
+ path: artifact.path,
135
+ engineConnected: artifact.report.engine.connected,
136
+ generatedAt: artifact.report.generatedAt,
137
+ issue: artifact.report.issue,
138
+ doctorSummary: artifact.report.doctor?.summary ?? null,
139
+ reviewNote: "Report omits auth tokens, but the user should review local paths and stack traces before sending.",
140
+ }, null, 2),
141
+ },
142
+ ],
143
+ };
144
+ });
102
145
  }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerFileTools(server: McpServer): void;