summer-engine 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp/server.js
CHANGED
|
@@ -36,5 +36,5 @@ export async function startMcpServer() {
|
|
|
36
36
|
registerAssetTools(server);
|
|
37
37
|
const transport = new StdioServerTransport();
|
|
38
38
|
await server.connect(transport);
|
|
39
|
-
process.stderr.write(`[summer-mcp] MCP server running v${version}
|
|
39
|
+
process.stderr.write(`[summer-mcp] MCP server running v${version}.\n`);
|
|
40
40
|
}
|
|
@@ -1,6 +1,93 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { withEngine } from "./with-engine.js";
|
|
3
|
+
import { readFile } from "fs/promises";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
async function readMainSceneFromProject(projectPath) {
|
|
6
|
+
if (!projectPath)
|
|
7
|
+
return null;
|
|
8
|
+
try {
|
|
9
|
+
const text = await readFile(join(projectPath, "project.godot"), "utf-8");
|
|
10
|
+
const match = text.match(/run\/main_scene="([^"]+)"/);
|
|
11
|
+
return match?.[1] ?? null;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
3
17
|
export function registerProjectTools(server) {
|
|
18
|
+
server.tool("summer_get_agent_playbook", `AI-first operating guide for Summer Engine MCP.
|
|
19
|
+
|
|
20
|
+
Call this at the start of a fresh chat before touching scenes.
|
|
21
|
+
It returns the safe workflow, anti-patterns, and recovery steps.`, {}, async () => ({
|
|
22
|
+
content: [
|
|
23
|
+
{
|
|
24
|
+
type: "text",
|
|
25
|
+
text: JSON.stringify({
|
|
26
|
+
startupChecklist: [
|
|
27
|
+
"Decide first: file edit or engine operation.",
|
|
28
|
+
"For normal code/content edits (.gd/.tscn/.tres/.ts), use host file-edit tools directly.",
|
|
29
|
+
"Call summer_get_project_context first.",
|
|
30
|
+
"If no scene is open, call summer_open_main_scene.",
|
|
31
|
+
"Call summer_get_scene_tree before structural edits.",
|
|
32
|
+
"Call summer_save_scene after edits.",
|
|
33
|
+
],
|
|
34
|
+
safeDefaults: [
|
|
35
|
+
"Never guess scene filenames (main.tscn/Main.tscn).",
|
|
36
|
+
"Do not route ordinary code edits through MCP when direct file tools are available.",
|
|
37
|
+
"Never remove multiple top-level nodes unless user explicitly requests destructive edits.",
|
|
38
|
+
"Never write .tscn/.scn with file write tools; use scene ops.",
|
|
39
|
+
],
|
|
40
|
+
preferredFlow: [
|
|
41
|
+
"summer_get_project_context",
|
|
42
|
+
"summer_open_main_scene (if needed)",
|
|
43
|
+
"summer_get_scene_tree",
|
|
44
|
+
"edit via summer_add_node / summer_set_prop / summer_set_resource_property",
|
|
45
|
+
"summer_save_scene",
|
|
46
|
+
"summer_get_diagnostics",
|
|
47
|
+
],
|
|
48
|
+
recovery: [
|
|
49
|
+
"If you see 'no scene open': run summer_open_main_scene.",
|
|
50
|
+
"If open_scene fails: re-check mainScene from summer_get_project_context.",
|
|
51
|
+
"If save fails: verify scene is open and game is not running.",
|
|
52
|
+
],
|
|
53
|
+
}, null, 2),
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
}));
|
|
57
|
+
server.tool("summer_get_project_context", `Get essential project context before editing. Returns:
|
|
58
|
+
- engine health/status
|
|
59
|
+
- project name and path
|
|
60
|
+
- current scene path (if available)
|
|
61
|
+
- main scene path from project.godot
|
|
62
|
+
|
|
63
|
+
Use this first in every fresh chat to avoid guessing scene filenames or editing the wrong scene.`, {}, async () => withEngine(async (client) => {
|
|
64
|
+
const [health, projectState] = await Promise.all([
|
|
65
|
+
client.health(),
|
|
66
|
+
client.getProjectState(),
|
|
67
|
+
]);
|
|
68
|
+
const healthObj = (health ?? {});
|
|
69
|
+
const mainScene = await readMainSceneFromProject(typeof healthObj.project_path === "string" ? healthObj.project_path : undefined);
|
|
70
|
+
return {
|
|
71
|
+
health,
|
|
72
|
+
project: projectState,
|
|
73
|
+
mainScene,
|
|
74
|
+
guidance: mainScene
|
|
75
|
+
? "Use `summer_open_scene` with `mainScene` if no scene is open."
|
|
76
|
+
: "Main scene not found in project.godot. Open a known scene path explicitly.",
|
|
77
|
+
};
|
|
78
|
+
}));
|
|
79
|
+
server.tool("summer_open_main_scene", `Open the project's configured main scene from project.godot.
|
|
80
|
+
|
|
81
|
+
Safer than guessing scene names like main.tscn/Main.tscn.
|
|
82
|
+
Call this when you get "no scene open".`, {}, async () => withEngine(async (client) => {
|
|
83
|
+
const health = (await client.health());
|
|
84
|
+
const projectPath = typeof health.project_path === "string" ? health.project_path : undefined;
|
|
85
|
+
const mainScene = await readMainSceneFromProject(projectPath);
|
|
86
|
+
if (!mainScene) {
|
|
87
|
+
throw new Error("Could not resolve application/run/main_scene from project.godot. Call `summer_get_project_context` and open a scene explicitly.");
|
|
88
|
+
}
|
|
89
|
+
return client.executeOps([{ op: "OpenScene", path: mainScene }]);
|
|
90
|
+
}));
|
|
4
91
|
server.tool("summer_project_setting", `Set a project setting in project.godot. Common settings:
|
|
5
92
|
- "application/config/name" — project name
|
|
6
93
|
- "application/run/main_scene" — main scene path
|
|
@@ -29,7 +116,10 @@ Example: Bind jump to Space and W:
|
|
|
29
116
|
];
|
|
30
117
|
return client.executeOps(ops);
|
|
31
118
|
}));
|
|
32
|
-
server.tool("summer_get_scene_tree",
|
|
119
|
+
server.tool("summer_get_scene_tree", `Get the full scene tree structure of the currently open scene.
|
|
120
|
+
|
|
121
|
+
Use this before structural edits (add/remove/replace).
|
|
122
|
+
If you get "no edited scene", call summer_open_main_scene first.`, {}, async () => withEngine(async (client) => client.getSceneState()));
|
|
33
123
|
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.
|
|
34
124
|
|
|
35
125
|
Use this for:
|
|
@@ -1,8 +1,79 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { withEngine } from "./with-engine.js";
|
|
3
|
+
import { readFile } from "fs/promises";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
async function readMainSceneFromProject(projectPath) {
|
|
6
|
+
if (!projectPath)
|
|
7
|
+
return null;
|
|
8
|
+
try {
|
|
9
|
+
const text = await readFile(join(projectPath, "project.godot"), "utf-8");
|
|
10
|
+
const match = text.match(/run\/main_scene="([^"]+)"/);
|
|
11
|
+
return match?.[1] ?? null;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
3
17
|
export function registerSceneTools(server) {
|
|
18
|
+
server.tool("summer_create_scene", `Create a new empty scene file safely.
|
|
19
|
+
|
|
20
|
+
IMPORTANT:
|
|
21
|
+
- This tool uses a temporary mutation strategy: it opens a template scene, removes children, saves to a new path, then restores the previous scene.
|
|
22
|
+
- To prevent accidental destructive actions, you MUST explicitly set allow_temporary_scene_mutation=true.
|
|
23
|
+
- If you don't want this strategy, stop and ask the user for manual scene creation in the editor.
|
|
24
|
+
|
|
25
|
+
Recommended workflow:
|
|
26
|
+
1) Call summer_get_project_context
|
|
27
|
+
2) Call summer_open_main_scene (optional)
|
|
28
|
+
3) Call summer_create_scene with a new path`, {
|
|
29
|
+
path: z.string().describe("New scene path, e.g. 'res://scenes/empty_level.tscn'"),
|
|
30
|
+
rootName: z.string().default("Main").describe("Root node name for the new scene"),
|
|
31
|
+
allow_temporary_scene_mutation: z
|
|
32
|
+
.boolean()
|
|
33
|
+
.default(false)
|
|
34
|
+
.describe("Safety gate. Must be true to proceed."),
|
|
35
|
+
}, async ({ path, rootName, allow_temporary_scene_mutation }) => withEngine(async (client) => {
|
|
36
|
+
if (!allow_temporary_scene_mutation) {
|
|
37
|
+
throw new Error("Refusing to create scene without explicit approval. Re-run with allow_temporary_scene_mutation=true.");
|
|
38
|
+
}
|
|
39
|
+
const health = (await client.health());
|
|
40
|
+
const currentScene = typeof health.scene === "string" && health.scene.length > 0 ? health.scene : null;
|
|
41
|
+
const projectPath = typeof health.project_path === "string" ? health.project_path : undefined;
|
|
42
|
+
const mainScene = await readMainSceneFromProject(projectPath);
|
|
43
|
+
const templateScene = currentScene || mainScene;
|
|
44
|
+
if (!templateScene) {
|
|
45
|
+
throw new Error("No scene open and could not resolve main scene. Call summer_get_project_context first, then open a known scene.");
|
|
46
|
+
}
|
|
47
|
+
await client.executeOps([{ op: "OpenScene", path: templateScene }]);
|
|
48
|
+
const tree = (await client.getSceneState());
|
|
49
|
+
const children = tree.data?.children ?? tree.children ?? [];
|
|
50
|
+
const removeOps = children
|
|
51
|
+
.map((c) => c.path)
|
|
52
|
+
.filter((p) => typeof p === "string" && p.length > 0)
|
|
53
|
+
.map((p) => ({ op: "RemoveNode", path: `./${p}` }));
|
|
54
|
+
if (removeOps.length > 0) {
|
|
55
|
+
await client.executeOps(removeOps, { groupUndo: true });
|
|
56
|
+
}
|
|
57
|
+
await client.executeOps([{ op: "SetProp", path: ".", key: "name", value: rootName }]);
|
|
58
|
+
await client.executeOps([{ op: "SaveScene", path }]);
|
|
59
|
+
if (currentScene && currentScene !== path) {
|
|
60
|
+
await client.executeOps([{ op: "OpenScene", path: currentScene }]);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
created: path,
|
|
65
|
+
rootName,
|
|
66
|
+
templateScene,
|
|
67
|
+
restoredScene: currentScene,
|
|
68
|
+
};
|
|
69
|
+
}));
|
|
4
70
|
server.tool("summer_add_node", `Add a new node to the scene tree.
|
|
5
71
|
|
|
72
|
+
Preflight (fresh chat):
|
|
73
|
+
1) summer_get_project_context
|
|
74
|
+
2) summer_open_main_scene (if no scene open)
|
|
75
|
+
3) summer_get_scene_tree
|
|
76
|
+
|
|
6
77
|
Common node types:
|
|
7
78
|
- 3D: Node3D, MeshInstance3D, CharacterBody3D, RigidBody3D, StaticBody3D, Camera3D, DirectionalLight3D, OmniLight3D, SpotLight3D, WorldEnvironment, CollisionShape3D, Area3D
|
|
8
79
|
- 2D: Node2D, Sprite2D, CharacterBody2D, RigidBody2D, StaticBody2D, Camera2D, CollisionShape2D, Area2D, TileMapLayer
|
|
@@ -52,14 +123,19 @@ Use when you need to modify a sub-property of a resource, like:
|
|
|
52
123
|
}, async ({ nodePath, resourceProperty, subProperty, value }) => withEngine(async (client) => client.executeOps([
|
|
53
124
|
{ op: "SetResourceProperty", nodePath, resourceProperty, subProperty, value },
|
|
54
125
|
])));
|
|
55
|
-
server.tool("summer_remove_node", "Remove a node from the scene tree. All children are removed too. Cannot remove the root node. Supports undo.", { path: z.string().describe("Node path to remove, e.g. './World/OldEnemy'") }, async ({ path }) => withEngine(async (client) => client.executeOps([{ op: "RemoveNode", path }])));
|
|
126
|
+
server.tool("summer_remove_node", "Remove a node from the scene tree. All children are removed too. Cannot remove the root node. Supports undo. Destructive operation: do not delete multiple top-level nodes unless the user explicitly requests destructive changes.", { path: z.string().describe("Node path to remove, e.g. './World/OldEnemy'") }, async ({ path }) => withEngine(async (client) => client.executeOps([{ op: "RemoveNode", path }])));
|
|
56
127
|
server.tool("summer_save_scene", "Save the current scene to disk. Call this after making scene changes to persist them. Without saving, changes only exist in the editor's memory.", { path: z.string().optional().describe("Save-as path for creating a new scene file, e.g. 'res://levels/level2.tscn'") }, async ({ path }) => withEngine(async (client) => {
|
|
57
128
|
const op = { op: "SaveScene" };
|
|
58
129
|
if (path)
|
|
59
130
|
op.path = path;
|
|
60
131
|
return client.executeOps([op]);
|
|
61
132
|
}));
|
|
62
|
-
server.tool("summer_open_scene",
|
|
133
|
+
server.tool("summer_open_scene", `Open a scene file in the editor. Use this to switch between scenes.
|
|
134
|
+
|
|
135
|
+
Do not guess paths. Prefer:
|
|
136
|
+
1) summer_get_project_context (read mainScene)
|
|
137
|
+
2) summer_open_main_scene (open known main scene)
|
|
138
|
+
3) summer_open_scene only when user gave an explicit path.`, { path: z.string().describe("Scene path, e.g. 'res://main.tscn' or 'res://levels/level1.tscn'") }, async ({ path }) => withEngine(async (client) => client.executeOps([{ op: "OpenScene", path }])));
|
|
63
139
|
server.tool("summer_instantiate_scene", `Add an existing scene or 3D model as a child node. Use this to:
|
|
64
140
|
- Add a .tscn prefab (reusable scene) as a child
|
|
65
141
|
- Add a .glb/.gltf 3D model into the scene
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import { getClient, resetClient } from "../server.js";
|
|
2
2
|
import { getAuthToken } from "../../lib/auth.js";
|
|
3
3
|
const GATEWAY_URL = process.env.SUMMER_GATEWAY_URL || "https://www.summerengine.com";
|
|
4
|
+
function extractOpError(result) {
|
|
5
|
+
if (!result || typeof result !== "object")
|
|
6
|
+
return null;
|
|
7
|
+
const op = result;
|
|
8
|
+
if (op.status === "error" && op.error)
|
|
9
|
+
return op.error;
|
|
10
|
+
const firstFailed = op.results?.find((r) => r.ok === false && r.error);
|
|
11
|
+
return firstFailed?.error ?? null;
|
|
12
|
+
}
|
|
13
|
+
function buildActionHint(message) {
|
|
14
|
+
const normalized = message.toLowerCase();
|
|
15
|
+
if (normalized.includes("no scene open") || normalized.includes("no edited scene")) {
|
|
16
|
+
return "No scene is currently open. Call `summer_get_project_context` first, then `summer_open_main_scene` (or `summer_open_scene` with a known .tscn path).";
|
|
17
|
+
}
|
|
18
|
+
if (normalized.includes("failed to open scene")) {
|
|
19
|
+
return "Scene path could not be opened. Call `summer_get_project_context` to get `mainScene`, then open that exact path. Avoid guessing scene filenames.";
|
|
20
|
+
}
|
|
21
|
+
if (normalized.includes("writefile cannot edit .tscn/.scn")) {
|
|
22
|
+
return "This MCP operation cannot write .tscn/.scn directly. If your host AI has normal file-edit tools, edit .gd/.tscn files there; use Summer MCP for engine/state/scene-tree operations.";
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
4
26
|
async function checkMcpQuota() {
|
|
5
27
|
const token = await getAuthToken();
|
|
6
28
|
if (!token) {
|
|
@@ -55,6 +77,12 @@ export async function withEngine(fn) {
|
|
|
55
77
|
}
|
|
56
78
|
const client = await getClient();
|
|
57
79
|
const result = await fn(client);
|
|
80
|
+
const opError = extractOpError(result);
|
|
81
|
+
if (opError) {
|
|
82
|
+
const hint = buildActionHint(opError);
|
|
83
|
+
const message = hint ? `${opError}\n\nHint: ${hint}` : opError;
|
|
84
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
85
|
+
}
|
|
58
86
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
59
87
|
}
|
|
60
88
|
catch (err) {
|
package/package.json
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "summer-engine",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "CLI and MCP tools for Summer Engine — the AI-native game engine",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"game-engine",
|
|
7
|
+
"godot",
|
|
8
|
+
"ai",
|
|
9
|
+
"mcp",
|
|
10
|
+
"gamedev",
|
|
11
|
+
"3d",
|
|
12
|
+
"summer-engine"
|
|
13
|
+
],
|
|
6
14
|
"homepage": "https://summerengine.com",
|
|
7
15
|
"repository": {
|
|
8
16
|
"type": "git",
|
|
@@ -14,7 +22,11 @@
|
|
|
14
22
|
"bin": {
|
|
15
23
|
"summer": "./dist/bin/summer.js"
|
|
16
24
|
},
|
|
17
|
-
"files": [
|
|
25
|
+
"files": [
|
|
26
|
+
"dist/",
|
|
27
|
+
"skills/",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
18
30
|
"scripts": {
|
|
19
31
|
"build": "tsc",
|
|
20
32
|
"dev": "tsc --watch",
|