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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -1
- package/.codex-plugin/plugin.json +5 -2
- package/.cursor-plugin/plugin.json +4 -1
- package/.opencode/plugins/summer.js +1 -1
- package/AGENTS.md +8 -4
- package/CHANGELOG.md +30 -0
- package/GEMINI.md +5 -4
- package/README.md +7 -4
- package/_persona/summer/SKILL.md +1 -0
- package/commands/summer.md +13 -0
- package/dist/bin/summer.js +4 -0
- package/dist/commands/debug.d.ts +2 -0
- package/dist/commands/debug.js +44 -0
- package/dist/commands/doctor.js +112 -8
- package/dist/commands/skills.js +1 -1
- package/dist/lib/api-client.d.ts +11 -2
- package/dist/lib/api-client.js +110 -9
- package/dist/lib/async-op-lifecycle.d.ts +7 -2
- package/dist/lib/async-op-lifecycle.js +32 -9
- package/dist/lib/debug-report.d.ts +50 -0
- package/dist/lib/debug-report.js +308 -0
- package/dist/lib/game-task-plan.js +4 -4
- package/dist/lib/mcp-boot-notice.d.ts +3 -0
- package/dist/lib/mcp-boot-notice.js +7 -0
- package/dist/lib/mcp-log.d.ts +3 -0
- package/dist/lib/mcp-log.js +72 -0
- package/dist/lib/skills-registry.d.ts +25 -1
- package/dist/lib/skills-registry.js +46 -0
- package/dist/mcp/server.d.ts +2 -2
- package/dist/mcp/server.js +79 -7
- package/dist/mcp/tools/asset-tools.js +33 -12
- package/dist/mcp/tools/cloud-tools.js +15 -15
- package/dist/mcp/tools/debug-tools.js +44 -1
- package/dist/mcp/tools/file-tools.d.ts +2 -0
- package/dist/mcp/tools/file-tools.js +154 -0
- package/dist/mcp/tools/generate-tools.js +143 -5
- package/dist/mcp/tools/project-tools.js +25 -20
- package/dist/mcp/tools/scene-tools.js +101 -24
- package/dist/mcp/tools/with-engine.js +4 -4
- package/hooks/pre-commit-doctor.sh +0 -0
- package/hooks/run-hook.cmd +0 -0
- package/hooks/session-start.sh +0 -0
- package/package.json +1 -1
- package/skills/2d-assets/create-asset-sheet/SKILL.md +17 -15
- package/skills/debugging/debug/SKILL.md +23 -1
- package/skills/deployment/remote-deploy/SKILL.md +107 -0
- package/skills/scene-and-project/brainstorm-game/SKILL.md +4 -4
- package/skills/scene-and-project/summer-cloud/SKILL.md +92 -0
- package/skills/scripting-patterns/gdscript-patterns/SKILL.md +1 -1
- package/skills/workflow/brainstorming/scripts/start-server.sh +0 -0
- package/skills/workflow/brainstorming/scripts/stop-server.sh +0 -0
- package/skills/workflow/investigating-bugs/find-polluter.sh +0 -0
- package/skills/workflow/mcpupdate/SKILL.md +6 -6
- package/skills/workflow/using-summer/SKILL.md +20 -1
- 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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
140
|
-
"Use Summer MCP
|
|
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
|
-
"
|
|
148
|
-
"
|
|
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
|
|
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 ->
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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:
|
|
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
|
|
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: "
|
|
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
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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:
|