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,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",
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EngineApiClient } from "../lib/api-client.js";
|
|
2
|
-
import {
|
|
3
|
-
export declare
|
|
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>;
|
package/dist/mcp/server.js
CHANGED
|
@@ -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
|
|
17
|
-
export
|
|
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
|
-
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
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.
|
|
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
|
|
349
|
-
|
|
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
|
|
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
|
|
20
|
-
server.tool("summer_cloud_push", "
|
|
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
|
|
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", "
|
|
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", "
|
|
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
|
|
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.
|
|
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
|
}
|