summer-engine 1.2.0 → 1.2.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.
@@ -0,0 +1,118 @@
1
+ export const AGENT_CLIENTS = [
2
+ "summer",
3
+ "codex",
4
+ "claude-code",
5
+ "cursor",
6
+ "windsurf",
7
+ ];
8
+ const ALL_CLIENTS = AGENT_CLIENTS;
9
+ export const SKILL_REGISTRY = [
10
+ {
11
+ name: "gdscript-patterns",
12
+ public: true,
13
+ category: "scripting",
14
+ clients: ALL_CLIENTS,
15
+ recommended: true,
16
+ requiresMcpTools: [
17
+ "summer_set_prop",
18
+ "summer_connect_signal",
19
+ "summer_input_map_bind",
20
+ "summer_get_script_errors",
21
+ ],
22
+ testScenario: "Create a typed GDScript with a signal, attach it to a node, connect the signal, and verify script errors.",
23
+ },
24
+ {
25
+ name: "scene-composition",
26
+ public: true,
27
+ category: "scene",
28
+ clients: ALL_CLIENTS,
29
+ recommended: true,
30
+ requiresMcpTools: [
31
+ "summer_create_scene",
32
+ "summer_open_scene",
33
+ "summer_get_scene_tree",
34
+ "summer_add_node",
35
+ "summer_instantiate_scene",
36
+ "summer_save_scene",
37
+ ],
38
+ testScenario: "Build a small reusable player scene, instantiate it in a main scene, and save both scenes through Summer tools.",
39
+ },
40
+ {
41
+ name: "fps-controller",
42
+ public: true,
43
+ category: "gameplay",
44
+ clients: ALL_CLIENTS,
45
+ recommended: true,
46
+ requiresMcpTools: [
47
+ "summer_add_node",
48
+ "summer_set_prop",
49
+ "summer_set_resource_property",
50
+ "summer_input_map_bind",
51
+ ],
52
+ testScenario: "Create a CharacterBody3D player with collision, camera, input actions, and a host-edited controller script.",
53
+ },
54
+ {
55
+ name: "3d-lighting",
56
+ public: true,
57
+ category: "lighting",
58
+ clients: ALL_CLIENTS,
59
+ recommended: true,
60
+ requiresMcpTools: [
61
+ "summer_add_node",
62
+ "summer_set_prop",
63
+ "summer_set_resource_property",
64
+ ],
65
+ testScenario: "Add sun, fill lighting, WorldEnvironment, and shadow settings to a simple 3D scene.",
66
+ },
67
+ {
68
+ name: "ui-basics",
69
+ public: true,
70
+ category: "ui",
71
+ clients: ALL_CLIENTS,
72
+ recommended: true,
73
+ requiresMcpTools: [
74
+ "summer_add_node",
75
+ "summer_set_prop",
76
+ "summer_connect_signal",
77
+ ],
78
+ testScenario: "Create a HUD CanvasLayer with labels, a ProgressBar, and a connected Button signal.",
79
+ },
80
+ {
81
+ name: "asset-strategy",
82
+ public: true,
83
+ category: "assets",
84
+ clients: ALL_CLIENTS,
85
+ recommended: true,
86
+ requiresMcpTools: [
87
+ "summer_search_assets",
88
+ "summer_import_asset",
89
+ "summer_import_from_url",
90
+ "summer_generate_image",
91
+ "summer_generate_3d",
92
+ "summer_generate_audio",
93
+ ],
94
+ testScenario: "Choose library, generated, or primitive assets for a small level and import one asset into the project.",
95
+ },
96
+ {
97
+ name: "make-game",
98
+ public: true,
99
+ category: "workflow",
100
+ clients: ALL_CLIENTS,
101
+ recommended: false,
102
+ requiresMcpTools: [
103
+ "summer_get_agent_playbook",
104
+ "summer_get_project_context",
105
+ "summer_create_scene",
106
+ "summer_open_scene",
107
+ "summer_save_scene",
108
+ "summer_add_node",
109
+ "summer_set_prop",
110
+ "summer_set_resource_property",
111
+ "summer_instantiate_scene",
112
+ "summer_play",
113
+ "summer_stop",
114
+ "summer_get_diagnostics",
115
+ ],
116
+ testScenario: "Build one narrow playable prototype from requirements through scene setup, host-edited scripts, and diagnostics.",
117
+ },
118
+ ];
@@ -1,18 +1,83 @@
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)
3
+ function asRecord(value) {
4
+ return value && typeof value === "object" && !Array.isArray(value)
5
+ ? value
6
+ : null;
7
+ }
8
+ function stringFrom(value) {
9
+ return typeof value === "string" && value.length > 0 ? value : null;
10
+ }
11
+ function pickString(record, keys) {
12
+ if (!record)
7
13
  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;
14
+ for (const key of keys) {
15
+ const value = stringFrom(record[key]);
16
+ if (value)
17
+ return value;
12
18
  }
13
- catch {
19
+ return null;
20
+ }
21
+ function getProjectData(projectState) {
22
+ return asRecord(asRecord(projectState)?.data);
23
+ }
24
+ function getProjectSetting(projectState, keys) {
25
+ const data = getProjectData(projectState);
26
+ const entries = data?.entries;
27
+ if (!Array.isArray(entries))
14
28
  return null;
29
+ for (const entry of entries) {
30
+ const item = asRecord(entry);
31
+ const key = stringFrom(item?.key);
32
+ if (key && keys.includes(key)) {
33
+ return stringFrom(item?.value);
34
+ }
15
35
  }
36
+ return null;
37
+ }
38
+ function getProjectPath(projectState) {
39
+ const root = asRecord(projectState);
40
+ const data = getProjectData(projectState);
41
+ return (pickString(data, [
42
+ "projectPath",
43
+ "project_path",
44
+ "projectRoot",
45
+ "project_root",
46
+ "rootPath",
47
+ "root_path",
48
+ ]) ??
49
+ pickString(root, ["projectPath", "project_path", "projectRoot", "project_root"]) ??
50
+ null);
51
+ }
52
+ function getProjectName(projectState) {
53
+ const root = asRecord(projectState);
54
+ const data = getProjectData(projectState);
55
+ return (pickString(data, ["projectName", "project_name", "name"]) ??
56
+ pickString(root, ["projectName", "project_name", "name"]) ??
57
+ getProjectSetting(projectState, ["application/config/name", "config/name"]));
58
+ }
59
+ function getMainScene(projectState) {
60
+ const root = asRecord(projectState);
61
+ const data = getProjectData(projectState);
62
+ return (pickString(data, ["mainScene", "main_scene", "mainScenePath", "main_scene_path"]) ??
63
+ pickString(root, ["mainScene", "main_scene", "mainScenePath", "main_scene_path"]) ??
64
+ getProjectSetting(projectState, [
65
+ "application/run/main_scene",
66
+ "run/main_scene",
67
+ ]));
68
+ }
69
+ function getCurrentScene(projectState, sceneState) {
70
+ const sceneRoot = asRecord(sceneState);
71
+ const sceneData = asRecord(sceneRoot?.data);
72
+ const sceneProvenance = asRecord(sceneRoot?.provenance);
73
+ const projectRoot = asRecord(projectState);
74
+ const projectData = getProjectData(projectState);
75
+ const projectProvenance = asRecord(projectRoot?.provenance);
76
+ return (pickString(sceneProvenance, ["scenePath", "scene_path"]) ??
77
+ pickString(sceneData, ["scenePath", "scene_path", "currentScene", "current_scene"]) ??
78
+ pickString(projectData, ["currentScene", "current_scene", "scenePath", "scene_path"]) ??
79
+ pickString(projectProvenance, ["scenePath", "scene_path"]) ??
80
+ null);
16
81
  }
17
82
  export function registerProjectTools(server) {
18
83
  server.tool("summer_get_agent_playbook", `AI-first operating guide for Summer Engine MCP.
@@ -25,7 +90,8 @@ It returns the safe workflow, anti-patterns, and recovery steps.`, {}, async ()
25
90
  text: JSON.stringify({
26
91
  startupChecklist: [
27
92
  "Decide first: file edit or engine operation.",
28
- "For normal code/content edits (.gd/.tscn/.tres/.ts), use host file-edit tools directly.",
93
+ "Write .gd/.cs/.json/docs/simple config with host file-edit tools directly.",
94
+ "Use Summer MCP for live scene state, inspector/node edits, imports, play/stop, diagnostics, screenshots/verification.",
29
95
  "Call summer_get_project_context first.",
30
96
  "If no scene is open, call summer_open_main_scene.",
31
97
  "Call summer_get_scene_tree before structural edits.",
@@ -33,15 +99,16 @@ It returns the safe workflow, anti-patterns, and recovery steps.`, {}, async ()
33
99
  ],
34
100
  safeDefaults: [
35
101
  "Never guess scene filenames (main.tscn/Main.tscn).",
36
- "Do not route ordinary code edits through MCP when direct file tools are available.",
102
+ "Do not route ordinary script, JSON, docs, or simple config edits through MCP when direct file tools are available.",
37
103
  "Never remove multiple top-level nodes unless user explicitly requests destructive edits.",
38
- "Never write .tscn/.scn with file write tools; use scene ops.",
104
+ "Use MCP for scene-tree mutations and inspector-visible values instead of hand-editing live scene state.",
39
105
  ],
40
106
  preferredFlow: [
41
107
  "summer_get_project_context",
42
108
  "summer_open_main_scene (if needed)",
43
109
  "summer_get_scene_tree",
44
110
  "edit via summer_add_node / summer_set_prop / summer_set_resource_property",
111
+ "write scripts/data/docs/simple config directly with host file-edit tools",
45
112
  "summer_save_scene",
46
113
  "summer_get_diagnostics",
47
114
  ],
@@ -56,35 +123,45 @@ It returns the safe workflow, anti-patterns, and recovery steps.`, {}, async ()
56
123
  }));
57
124
  server.tool("summer_get_project_context", `Get essential project context before editing. Returns:
58
125
  - engine health/status
59
- - project name and path
126
+ - project name and project path when exposed by project state
60
127
  - current scene path (if available)
61
- - main scene path from project.godot
128
+ - main scene path from project settings
62
129
 
63
130
  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([
131
+ const [health, projectState, sceneState] = await Promise.all([
65
132
  client.health(),
66
133
  client.getProjectState(),
134
+ client.getSceneState().catch((err) => ({
135
+ ok: false,
136
+ error: err instanceof Error ? err.message : String(err),
137
+ })),
67
138
  ]);
68
- const healthObj = (health ?? {});
69
- const mainScene = await readMainSceneFromProject(typeof healthObj.project_path === "string" ? healthObj.project_path : undefined);
139
+ const projectPath = getProjectPath(projectState);
140
+ const projectName = getProjectName(projectState);
141
+ const mainScene = getMainScene(projectState);
142
+ const currentScene = getCurrentScene(projectState, sceneState);
70
143
  return {
71
144
  health,
72
145
  project: projectState,
146
+ scene: sceneState,
147
+ projectName,
148
+ projectPath,
149
+ currentScene,
73
150
  mainScene,
74
151
  guidance: mainScene
75
152
  ? "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.",
153
+ : "Main scene not found in project state. Open a known scene path explicitly.",
154
+ fileEditingGuidance: "Write .gd/.cs/.json/docs/simple config directly with host file tools. Use MCP for live scene state, inspector/node edits, imports, play/stop, diagnostics, and visual verification.",
77
155
  };
78
156
  }));
79
- server.tool("summer_open_main_scene", `Open the project's configured main scene from project.godot.
157
+ server.tool("summer_open_main_scene", `Open the project's configured main scene from project settings.
80
158
 
81
159
  Safer than guessing scene names like main.tscn/Main.tscn.
82
160
  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);
161
+ const projectState = await client.getProjectState();
162
+ const mainScene = getMainScene(projectState);
86
163
  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.");
164
+ throw new Error("Could not resolve application/run/main_scene from project state. Call `summer_get_project_context` and open a scene explicitly.");
88
165
  }
89
166
  return client.executeOps([{ op: "OpenScene", path: mainScene }]);
90
167
  }));
@@ -1,10 +1,10 @@
1
1
  import { getClient, resetClient } from "../server.js";
2
- import { getAuthToken } from "../../lib/auth.js";
3
- const GATEWAY_URL = process.env.SUMMER_GATEWAY_URL || "https://www.summerengine.com";
4
2
  function extractOpError(result) {
5
3
  if (!result || typeof result !== "object")
6
4
  return null;
7
5
  const op = result;
6
+ if (op.ok === false && op.error)
7
+ return op.error;
8
8
  if (op.status === "error" && op.error)
9
9
  return op.error;
10
10
  const firstFailed = op.results?.find((r) => r.ok === false && r.error);
@@ -19,62 +19,12 @@ function buildActionHint(message) {
19
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
20
  }
21
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.";
22
+ return "Write .gd/.cs/.json/docs/simple config files with normal file-edit tools. Use Summer MCP for live scene state, inspector/node edits, imports, play/stop, diagnostics, and visual verification.";
23
23
  }
24
24
  return null;
25
25
  }
26
- async function checkMcpQuota() {
27
- const token = await getAuthToken();
28
- if (!token) {
29
- return {
30
- allowed: false,
31
- message: "Not signed in. The user needs to run this in their terminal:\n npx summer-engine login\nOr open: https://www.summerengine.com/login\nMCP tools require a Summer Engine account (free to create).",
32
- };
33
- }
34
- try {
35
- const res = await fetch(`${GATEWAY_URL}/api/mcp/log-local-call`, {
36
- method: "POST",
37
- headers: {
38
- Authorization: `Bearer ${token}`,
39
- "Content-Type": "application/json",
40
- },
41
- body: JSON.stringify({}),
42
- signal: AbortSignal.timeout(10000),
43
- });
44
- const data = (await res.json().catch(() => ({})));
45
- // Hard enforcement errors should block tool execution.
46
- if ([401, 402, 403, 429].includes(res.status)) {
47
- return {
48
- allowed: false,
49
- message: data.message || "Quota check failed. Try again.",
50
- };
51
- }
52
- // Soft-fail on service/gateway issues so local tools keep working.
53
- if (!res.ok) {
54
- return { allowed: true };
55
- }
56
- if (data.allowed === false) {
57
- return {
58
- allowed: false,
59
- message: data.message || "MCP call limit reached. Upgrade at https://summerengine.com/pricing",
60
- };
61
- }
62
- return { allowed: true };
63
- }
64
- catch (err) {
65
- // Network/transient errors should not brick local MCP usage.
66
- return { allowed: true };
67
- }
68
- }
69
26
  export async function withEngine(fn) {
70
27
  try {
71
- const quota = await checkMcpQuota();
72
- if (!quota.allowed) {
73
- return {
74
- content: [{ type: "text", text: quota.message || "Quota exceeded" }],
75
- isError: true,
76
- };
77
- }
78
28
  const client = await getClient();
79
29
  const result = await fn(client);
80
30
  const opError = extractOpError(result);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "summer-engine",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "CLI and MCP tools for Summer Engine — the AI-native game engine",
5
5
  "keywords": [
6
6
  "game-engine",
@@ -31,6 +31,8 @@
31
31
  "build": "tsc",
32
32
  "dev": "tsc --watch",
33
33
  "summer": "node dist/bin/summer.js",
34
+ "test": "vitest run",
35
+ "test:watch": "vitest",
34
36
  "prepublishOnly": "npm run build"
35
37
  },
36
38
  "engines": {
@@ -48,6 +50,7 @@
48
50
  "devDependencies": {
49
51
  "@types/diff": "^7.0.0",
50
52
  "@types/node": "^20.0.0",
51
- "typescript": "^5.5.0"
53
+ "typescript": "^5.5.0",
54
+ "vitest": "^3.2.4"
52
55
  }
53
56
  }
@@ -89,8 +89,8 @@ var siblings = get_parent().get_children()
89
89
 
90
90
  For health systems, input handling, and state machines, see [reference.md](reference.md).
91
91
 
92
- ## MCP Integration
92
+ ## Scene Integration
93
93
 
94
- When the AI adds a script via `summer_add_node` and attaches a script, the script path is set via `summer_set_prop(path, "script", "res://path/to/script.gd")`. The script file must exist—use `summer_write_file` to create it first.
94
+ When the AI adds a node with Summer MCP tools and attaches a script, the script path is set via `summer_set_prop(path, "script", "res://path/to/script.gd")`. The script file must exist first. Create or edit `.gd` files with the host agent's normal file-editing tools, then use Summer MCP only to attach the script and connect scene signals.
95
95
 
96
96
  For signal connections, the receiver must have the handler method. Create the script with the method stub before calling `summer_connect_signal`.
@@ -12,7 +12,7 @@ compatibility:
12
12
 
13
13
  Step-by-step workflow for building a complete game. Follow phases 0-6 in order. Each phase must complete before moving to the next.
14
14
 
15
- **Rule**: Scene operations (.tscn) use MCP tools. Script files (.gd) use host file-edit tools (Write/Edit). Never cross the streams.
15
+ **Rule**: Scene operations (.tscn) use Summer MCP tools. Script files (.gd) use the host agent's normal file-editing tools. Never cross the streams.
16
16
 
17
17
  ---
18
18
 
@@ -218,7 +218,7 @@ summer_save_scene()
218
218
 
219
219
  ## Phase 4: Scripting
220
220
 
221
- **Write .gd files with host file-edit tools (Write/Edit). Attach via MCP.**
221
+ **Write .gd files with the host agent's normal file-editing tools. Attach via MCP.**
222
222
 
223
223
  Workflow:
224
224
  1. Write the script file to disk
@@ -395,7 +395,7 @@ After first playable, ask the user what to improve. Common next steps:
395
395
 
396
396
  ## Anti-Patterns
397
397
 
398
- - Never write .tscn files with host file tools -- always use MCP scene tools
398
+ - Never write .tscn files with host agent file tools -- always use Summer MCP scene tools
399
399
  - Never guess node paths -- call `summer_get_scene_tree()` first
400
400
  - Never edit scenes while game is running -- call `summer_stop()` first
401
401
  - Never skip `summer_save_scene()` -- unsaved changes are editor-only
@@ -79,7 +79,7 @@ Use `./` for paths relative to the scene root:
79
79
 
80
80
  1. Build the hierarchy in the main scene
81
81
  2. Select the root of what you want to extract
82
- 3. Save as scene: `summer_save_scene(path="res://scenes/player.tscn")` — but note: SaveScene saves the *current* scene. To save a branch, you typically save the open scene first, then in the editor you'd use "Save Branch as Scene." MCP doesn't have a direct "save branch" op—so for now, create the sub-scene as its own file via `summer_write_file` or build it in a separate scene and save.
82
+ 3. Save as scene: `summer_save_scene(path="res://scenes/player.tscn")`. SaveScene saves the current open scene, so build reusable scenes in their own open scene and save them there. Do not handwrite `.tscn` files as the preferred path.
83
83
  4. In the main scene, add it with `summer_instantiate_scene(parent="./World", scene="res://scenes/player.tscn", name="Player")`
84
84
 
85
85
  **Practical approach:** Create reusable scenes (player.tscn, enemy.tscn) as separate scene files, then instantiate them into levels.