summer-engine 2.6.3 → 2.6.5

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.
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "summer",
4
4
  "version": "2.5.1",
5
- "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 44-tool MCP bridge to the local desktop app.",
5
+ "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 52-tool MCP bridge to the local desktop app.",
6
6
  "author": {
7
7
  "name": "Summer Engine",
8
8
  "email": "founders@summerengine.com",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "summer",
3
3
  "version": "2.5.1",
4
- "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 44-tool MCP bridge. Build games by talking.",
4
+ "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 52-tool MCP bridge. Build games by talking.",
5
5
  "author": {
6
6
  "name": "Summer Engine",
7
7
  "email": "founders@summerengine.com",
@@ -92,7 +92,7 @@
92
92
  "interface": {
93
93
  "displayName": "Summer",
94
94
  "shortDescription": "Superpowers for AI game dev. Build games by talking.",
95
- "longDescription": "Summer turns Codex into a game-dev studio for Summer Engine, the AI-native game engine. It adds game-dev skills, a using-summer workflow skill, lifecycle hooks, and a 44-tool MCP bridge for scene editing, asset import, play mode, diagnostics, and generation. File, git, shell, and grep stay with the host agent. Summer Engine is compatible with Godot 4.5. Write code in GDScript and drive the engine through MCP. Session-start orients the session. Pre-commit doctor (opt-in) blocks bad commits.",
95
+ "longDescription": "Summer turns Codex into a game-dev studio for Summer Engine, the AI-native game engine. It adds game-dev skills, a using-summer workflow skill, lifecycle hooks, and a 52-tool MCP bridge for scene editing, asset import, play mode, diagnostics, and generation. File, git, shell, and grep stay with the host agent. Summer Engine is compatible with Godot 4.5. Write code in GDScript and drive the engine through MCP. Session-start orients the session. Pre-commit doctor (opt-in) blocks bad commits.",
96
96
  "developerName": "Summer Engine",
97
97
  "category": "Coding",
98
98
  "capabilities": ["Interactive", "Read", "Write"],
@@ -3,7 +3,7 @@
3
3
  "name": "summer",
4
4
  "displayName": "Summer",
5
5
  "version": "2.5.1",
6
- "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 44-tool MCP bridge to the local desktop app.",
6
+ "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 52-tool MCP bridge to the local desktop app.",
7
7
  "author": { "name": "Summer Engine", "email": "founders@summerengine.com" },
8
8
  "publisher": "summer-engine",
9
9
  "homepage": "https://summerengine.com",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "summer",
3
- "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 44-tool MCP bridge to the local desktop app.",
3
+ "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 52-tool MCP bridge to the local desktop app.",
4
4
  "version": "2.5.1",
5
5
  "author": {
6
6
  "name": "Summer Engine",
package/AGENTS.md CHANGED
@@ -22,11 +22,12 @@ This file is for any AI agent that loads context from `AGENTS.md` (Codex CLI, Fa
22
22
 
23
23
  ## MCP tool palette (engine on `localhost:6550`)
24
24
 
25
- 44 tools total. Categories:
25
+ 52 tools total. Categories:
26
26
 
27
27
  - Scene: `summer_get_scene_tree`, `summer_open_scene`, `summer_create_scene`, `summer_add_node`, `summer_set_prop`, `summer_set_resource_property`, `summer_remove_node`, `summer_save_scene`, `summer_instantiate_scene`, `summer_replace_node`, `summer_select_node`, `summer_inspect_node`, `summer_inspect_resource`, `summer_connect_signal`, `summer_batch`.
28
28
  - Diagnostics: `summer_get_script_errors`, `summer_get_diagnostics`, `summer_get_console`, `summer_clear_console`, `summer_get_debugger_errors`, `summer_get_debugger_warnings`.
29
29
  - Runtime: `summer_play`, `summer_stop`, `summer_is_running`.
30
+ - Visual: `summer_screenshot` (capture the editor viewport or running game as an image to verify it).
30
31
  - Project: `summer_get_project_context`, `summer_open_main_scene`, `summer_project_setting`, `summer_input_map_bind`, `summer_get_agent_playbook`.
31
32
  - Assets: `summer_search_assets`, `summer_list_my_assets`, `summer_get_asset`, `summer_get_asset_download_url`, `summer_import_asset`, `summer_import_asset_by_id`, `summer_import_from_url`, `summer_import_from_url_batch`.
32
33
  - Generation: `summer_generate_image`, `summer_generate_3d`, `summer_generate_audio`, `summer_generate_video`, `summer_generate_motion`, `summer_check_job`.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  All notable changes to summer-engine will be documented here. Following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## [2.6.5] — 2026-07-04 — "Cloud tools don't need the engine"
6
+
7
+ ### Fixed
8
+ - Tool descriptions now say explicitly which tools run in Summer's cloud and work WITHOUT the engine open (`summer_generate_*`, `summer_search_assets`, `summer_list_my_assets`, `summer_get_asset`, `summer_get_asset_download_url`, `summer_check_job`) and which need the engine (imports, scene ops). Agents were misreading a missing `npx summer-engine login` as "MCP requires the engine".
9
+ - The "Summer Engine is not running" error now tells the agent that cloud tools still work without the engine.
10
+
11
+ ## [2.6.4] — 2026-07-03 — "See-Work + project binding" (unpublished, ships with 2.6.5)
12
+
13
+ ### Added
14
+ - MCP session binds to its project; the engine rejects wrong-project writes (`identity_mismatch`) instead of applying them to whatever project is open.
15
+ - Structured per-tool-call stderr logging; agent playbook rewritten around the MCP verification ladder; honest game-capture failure states and identity-stamped reads.
16
+
17
+ ## [2.6.3] — 2026-06-30 — "Agent vision"
18
+
19
+ ### Added
20
+ - `summer_screenshot` MCP tool: capture the editor viewport or the running game as an image the agent sees directly (`target: "viewport" | "game"`, viewport by default). Lets the agent visually verify scene layout, asset placement, scale, framing, and runtime state — the client reads the actual frame, with no description step in between. Total MCP tool surface is now 52.
21
+
22
+ ### Fixed
23
+ - MCP/CLI session now reconnects automatically after a transient engine restart (the engine rotates its api-token and can move its port on relaunch), instead of surfacing as a "disconnected" error.
24
+
5
25
  ## [2.6.0] — 2026-06-10 — "Summer Cloud"
6
26
 
7
27
  ### Added
package/GEMINI.md CHANGED
@@ -20,12 +20,13 @@ The single most important skill to know is `summer:using-summer` — it explains
20
20
 
21
21
  ## MCP tools (when the engine is running on `localhost:6550`)
22
22
 
23
- The `summer-engine` MCP server exposes 44 focused tools. The most important:
23
+ The `summer-engine` MCP server exposes 52 focused tools. The most important:
24
24
 
25
25
  - **Scene mutation**: `summer_add_node`, `summer_set_prop`, `summer_set_resource_property`, `summer_remove_node`, `summer_save_scene`, `summer_replace_node`, `summer_batch`.
26
26
  - **Scene inspection**: `summer_get_scene_tree`, `summer_inspect_node`, `summer_inspect_resource`.
27
27
  - **Diagnostics**: `summer_get_script_errors` (cheapest), `summer_get_diagnostics`, `summer_get_console`, `summer_get_debugger_errors`, `summer_get_debugger_warnings`.
28
28
  - **Run game**: `summer_play`, `summer_stop`, `summer_is_running`.
29
+ - **Visual**: `summer_screenshot` (capture the viewport or running game as an image you can see).
29
30
  - **Project**: `summer_get_project_context`, `summer_open_main_scene`, `summer_project_setting`, `summer_input_map_bind`.
30
31
  - **Assets**: `summer_search_assets`, `summer_import_asset`, `summer_import_from_url`, `summer_generate_image`, `summer_generate_3d`, `summer_generate_audio`, `summer_generate_video`, `summer_check_job`.
31
32
 
package/README.md CHANGED
@@ -18,7 +18,7 @@ It just works. Open your agent, say *"let's make an FPS in Summer Engine,"* and
18
18
  Three names, one npm package (`summer-engine`):
19
19
 
20
20
  - **Summer CLI** (`npx -y summer-engine@latest`): installs the engine, signs you in, scaffolds and runs projects, and writes your agent's config. See [www.summerengine.com/cli](https://www.summerengine.com/cli).
21
- - **Summer MCP**: the local MCP server that gives your agent 44 engine tools to build, run, and debug a real game. See [www.summerengine.com/mcp](https://www.summerengine.com/mcp).
21
+ - **Summer MCP**: the local MCP server that gives your agent 52 engine tools to build, run, and debug a real game. See [www.summerengine.com/mcp](https://www.summerengine.com/mcp).
22
22
  - **Summer agent layer**: the game-dev skills, hooks, and plugin manifests that give your AI agent judgment, not just a chat box.
23
23
 
24
24
  All MIT, all free to use. One paste sets up all three.
@@ -269,13 +269,15 @@ Two pieces, plus the glue.
269
269
 
270
270
  Skills don't list steps. They encode the **order of operations**: diagnose before editing, scope before building, ask before guessing. [Agent Skills](https://agentskills.io) format, so any conformant tool picks them up.
271
271
 
272
- **MCP bridge.** The `summer-engine` MCP server gives the agent 44 tools that talk to your local engine on `localhost:6550`:
272
+ **MCP bridge.** The `summer-engine` MCP server gives the agent 52 tools that talk to your local engine on `localhost:6550`:
273
273
 
274
274
  | | |
275
275
  |---|---|
276
276
  | Scene | `summer_add_node`, `summer_set_prop`, `summer_instantiate_scene`, `summer_replace_node`, `summer_get_scene_tree`, `summer_inspect_node`, `summer_batch` |
277
277
  | Diagnostics | `summer_get_script_errors`, `summer_get_diagnostics`, `summer_get_console`, `summer_get_debugger_errors`, `summer_get_debugger_warnings` |
278
278
  | Runtime | `summer_play`, `summer_stop`, `summer_is_running` |
279
+ | Visual | `summer_screenshot` (see the editor viewport, an offscreen scene render, or the running game) |
280
+ | Interactive | `SimulateInput` / `RunVerification` raw ops via `summer_batch` — drive the running game or run a hidden GDScript probe (engine-build dependent) |
279
281
  | Project | `summer_get_project_context`, `summer_open_main_scene`, `summer_project_setting`, `summer_input_map_bind` |
280
282
  | Assets | `summer_search_assets`, `summer_import_asset`, `summer_import_from_url`, `summer_generate_image`, `summer_generate_3d`, `summer_generate_audio`, `summer_generate_video` |
281
283
 
@@ -285,6 +287,36 @@ File ops, git, shell, and grep already belong to your agent. We don't shadow the
285
287
 
286
288
  For the full architecture, see [docs/OVERVIEW.md](docs/OVERVIEW.md).
287
289
 
290
+ ### Using Summer via MCP: the verification loop
291
+
292
+ There is no single "verify" tool. You **compose** the checks, climbing only as high as the change demands. Call `summer_get_agent_playbook` for the full ladder; the short version:
293
+
294
+ **1. Does it compile?** After writing a `.gd`, call `summer_get_script_errors res://scripts/player.gd` — no game boot. For a project-wide sweep, `summer_get_diagnostics` (it tells you whether to then read `summer_get_console` / `summer_get_debugger_errors`).
295
+
296
+ **2. Does it look right?** `summer_screenshot` returns the actual pixels — you see them, no description layer. Pick the target on purpose:
297
+ - `viewport` (default) — the editor's current tab, as it looks now.
298
+ - `scene` — an offscreen render of a scene *file* (`scenePath`). No boot; **static at t=0**. Confesses if the scene has no `Camera3D` (renders grey/black when played) or no light — read those warnings.
299
+ - `game` — a frame from the **running** game (`summer_play` first). Over plain local HTTP this returns an honest failure until the desktop bridge is present; when it fails, drop to `viewport`/`scene` or ask the user.
300
+
301
+ **3. Does it run?** Compose the loop:
302
+
303
+ ```
304
+ summer_clear_console # clean slate
305
+ summer_play # boot the game (or a specific scene)
306
+ summer_get_debugger_errors # runtime errors: null refs, missing nodes, physics
307
+ summer_screenshot target:game # optional: look at the live frame
308
+ summer_stop # ALWAYS stop — scene edits are refused while running
309
+ ```
310
+
311
+ **4. Does the interaction work?** To prove input-driven behavior (jump/move/shoot), send a raw op through `summer_batch` (engine-build dependent — a `failure_reason:"unsupported"` comes back verbatim on older builds):
312
+ - `SimulateInput` — inject into the **running** game: `summer_batch ops:[{op:"SimulateInput", type:"action", action:"jump", pressed:true}]`, then re-check screenshot/debugger.
313
+ - `RunVerification` — spawn a hidden, disposable game instance that runs a GDScript probe (press inputs, read state, save frames) and dies, never touching the editor: `summer_batch ops:[{op:"RunVerification", probe_source:"…", max_seconds:20}]` → `{ok, results, frames, out_dir}`.
314
+ - If neither is available on this build, **ask the user to try it** and report what they see — don't fake it.
315
+
316
+ **Honesty.** Never describe an image you didn't receive; a failed capture is a *result* — report it and climb down (`scene`→`viewport`) or ask the user. Pass structured failures (`failure_reason`, `terminalState`, `identity_mismatch`) through verbatim. `summer_get_project_context` binds the session to the open project; a `projectMismatch` warning on a screenshot means the engine switched projects and the frame may be from the wrong one — rebind before trusting it.
317
+
318
+ **Debugging the MCP itself.** Set `SUMMER_MCP_DEBUG=1` in the server's environment to log a structured stderr line per tool call (`tool`, `ok`, `durationMs`, `terminalState`, `errorClass`, `failureReason`, `retried`). With the flag off, only failures are logged.
319
+
288
320
  ---
289
321
 
290
322
  ## The basic workflow
@@ -16,12 +16,43 @@ export type EngineSnapshot = {
16
16
  bytes?: number;
17
17
  error?: string;
18
18
  metadata?: Record<string, unknown>;
19
+ /** Structured failure classifier the engine returns on a game snapshot over
20
+ * local HTTP (409 `bridge_required`) — surfaced verbatim so the tool can give
21
+ * an honest, actionable message instead of a truncated generic 409 string.
22
+ * When P4.4 lands (game snapshots answer 200/202 over HTTP) this simply
23
+ * never fires and the tool works. */
24
+ failureReason?: string;
25
+ /** Scene-preview confession fields (P4.3). Populated only for target:"scene".
26
+ * A scene with no Camera3D renders grey/black when played; the engine reports
27
+ * whether it had to synthesize a camera/light so the model can warn honestly. */
28
+ sceneHasCamera?: boolean;
29
+ sceneHadLight?: boolean;
30
+ usedSyntheticCamera?: boolean;
31
+ /** Set true when the bound projectIdHash no longer matches the engine's live
32
+ * health hash at capture time — the frame may be from the WRONG project
33
+ * (item 4, client-side drift check). */
34
+ projectMismatch?: boolean;
19
35
  };
20
36
  export declare class EngineApiClient {
21
37
  private port;
22
38
  private token;
23
- constructor(port: number, token: string);
39
+ private boundProjectIdHash?;
40
+ constructor(port: number, token: string, boundProjectIdHash?: string);
24
41
  static connect(): Promise<EngineApiClient>;
42
+ /** The projectIdHash this session is bound to (undefined if none was reported
43
+ * at connect — e.g. no project open yet). */
44
+ getBoundProjectIdHash(): string | undefined;
45
+ /**
46
+ * Re-read health and rebind to the currently-open project. Called by
47
+ * summer_get_project_context so the agent can INTENTIONALLY follow a project
48
+ * switch (the deliberate escape hatch after an identity_mismatch). Returns the
49
+ * new bound hash. Reads carry no identity, so this always reaches the engine
50
+ * even when a mutation would be rejected.
51
+ */
52
+ rebind(): Promise<string | undefined>;
53
+ /** Identity to attach to a MUTATING command's options so the engine can reject
54
+ * a wrong-project write. Empty when unbound (engine then skips the check). */
55
+ private identityOptions;
25
56
  private request;
26
57
  /** Raw fetch with auth + timeout — returns the Response so callers can inspect
27
58
  * the status (202 queued vs 200 legacy). */
@@ -51,8 +82,34 @@ export declare class EngineApiClient {
51
82
  getSelection(): Promise<unknown>;
52
83
  private writeSnapshotFile;
53
84
  private snapshot;
85
+ /** Shared: turn an ops/snapshot response envelope into an EngineSnapshot.
86
+ * Used by viewport/game snapshots and by scenePreview. */
87
+ private _parseSnapshotResponse;
88
+ /**
89
+ * Probe /api/snapshot/game for the structural 409 `bridge_required` shape
90
+ * (tool_net_thread.cpp:495-503). Returns a structured failure only when the
91
+ * engine actually answers 409 with `bridge_required` / `unsupported_transport`;
92
+ * ANY other status (200/202/other) returns null so the caller proceeds to the
93
+ * normal queued path — so the moment the engine starts answering over HTTP
94
+ * (P4.4), game capture just works with no code change here.
95
+ */
96
+ private _detectBridgeRequired;
54
97
  viewportSnapshot(): Promise<EngineSnapshot>;
55
98
  gameSnapshot(): Promise<EngineSnapshot>;
99
+ /**
100
+ * Offscreen scene render via the `ScenePreview` op over /api/ops (no game
101
+ * boot; physics/animations are static). Mirrors the web previewScene op input
102
+ * (snapshot-tools.ts): { op:'ScenePreview', scene_path?, framing?, size?,
103
+ * node_path? } — scene_path optional, engine defaults to the open scene. The
104
+ * result carries image_base64 + mime (+ width/height) plus the P4.3 confession
105
+ * fields (scene_has_camera / scene_had_light / used_synthetic_camera).
106
+ */
107
+ scenePreview(input?: {
108
+ scenePath?: string;
109
+ framing?: "auto" | "top" | "front" | "iso";
110
+ size?: [number, number];
111
+ nodePath?: string;
112
+ }): Promise<EngineSnapshot>;
56
113
  getPort(): number;
57
114
  /**
58
115
  * Cheap drift probe for the MCP client cache. The engine mints a fresh
@@ -14,6 +14,9 @@ function stringFrom(value) {
14
14
  function numberFrom(value) {
15
15
  return typeof value === "number" ? value : undefined;
16
16
  }
17
+ function boolFrom(value) {
18
+ return typeof value === "boolean" ? value : undefined;
19
+ }
17
20
  function findSnapshotPayload(value) {
18
21
  const record = asRecord(value);
19
22
  if (!record)
@@ -21,7 +24,8 @@ function findSnapshotPayload(value) {
21
24
  if (typeof record.base64 === "string" ||
22
25
  typeof record.image_base64 === "string" ||
23
26
  record.op === "ViewportSnapshot" ||
24
- record.op === "GameSnapshot") {
27
+ record.op === "GameSnapshot" ||
28
+ record.op === "ScenePreview") {
25
29
  return record;
26
30
  }
27
31
  if (Array.isArray(record.results)) {
@@ -63,9 +67,17 @@ function withoutImageData(payload) {
63
67
  export class EngineApiClient {
64
68
  port;
65
69
  token;
66
- constructor(port, token) {
70
+ // The project this session is BOUND to — the projectIdHash reported by the
71
+ // engine when this client first connected. Sent on every mutating op so the
72
+ // engine's identity guard (local_api_server.cpp ~271-302) atomically rejects a
73
+ // write aimed at a DIFFERENT project (e.g. after the user switched projects
74
+ // in-place). Health is the only identity signal the MCP has — /api/state/project
75
+ // carries no path and /api/health exposes only projectIdHash, not the raw id.
76
+ boundProjectIdHash;
77
+ constructor(port, token, boundProjectIdHash) {
67
78
  this.port = port;
68
79
  this.token = token;
80
+ this.boundProjectIdHash = boundProjectIdHash;
69
81
  }
70
82
  static async connect() {
71
83
  const port = await getApiPort();
@@ -77,7 +89,36 @@ export class EngineApiClient {
77
89
  if (!health) {
78
90
  throw new Error(`Summer Engine is not responding on port ${port}. Make sure it's open.`);
79
91
  }
80
- return new EngineApiClient(port, token);
92
+ // Bind to whatever project is open right now. On a genuine engine restart the
93
+ // token rotates and getClient() rebuilds this client, so a restart naturally
94
+ // rebinds to the current project. An in-place project switch keeps the same
95
+ // token, so the cached client retains its original binding and the engine
96
+ // rejects mismatched mutations until the agent explicitly rebinds.
97
+ return new EngineApiClient(port, token, health.projectIdHash);
98
+ }
99
+ /** The projectIdHash this session is bound to (undefined if none was reported
100
+ * at connect — e.g. no project open yet). */
101
+ getBoundProjectIdHash() {
102
+ return this.boundProjectIdHash;
103
+ }
104
+ /**
105
+ * Re-read health and rebind to the currently-open project. Called by
106
+ * summer_get_project_context so the agent can INTENTIONALLY follow a project
107
+ * switch (the deliberate escape hatch after an identity_mismatch). Returns the
108
+ * new bound hash. Reads carry no identity, so this always reaches the engine
109
+ * even when a mutation would be rejected.
110
+ */
111
+ async rebind() {
112
+ const health = await checkEngineHealth(this.port);
113
+ if (health) {
114
+ this.boundProjectIdHash = health.projectIdHash;
115
+ }
116
+ return this.boundProjectIdHash;
117
+ }
118
+ /** Identity to attach to a MUTATING command's options so the engine can reject
119
+ * a wrong-project write. Empty when unbound (engine then skips the check). */
120
+ identityOptions() {
121
+ return this.boundProjectIdHash ? { projectIdHash: this.boundProjectIdHash } : {};
81
122
  }
82
123
  async request(method, path, body) {
83
124
  const url = `http://127.0.0.1:${this.port}${path}`;
@@ -150,7 +191,11 @@ export class EngineApiClient {
150
191
  // Ops may include long-running work (ImportFromUrlBatch, GitCommit); the
151
192
  // engine keeps it "running" and we poll. 120s budget. Resolves legacy-200 or
152
193
  // async-202 (Block E).
153
- return this._requestQueued("POST", "/api/ops", { ops, options }, 120_000);
194
+ // Attach the bound project identity so the engine rejects a write aimed at a
195
+ // different project (identity_mismatch, atomic — before any op applies).
196
+ const merged = { ...this.identityOptions(), ...(options ?? {}) };
197
+ const body = Object.keys(merged).length ? { ops, options: merged } : { ops };
198
+ return this._requestQueued("POST", "/api/ops", body, 120_000);
154
199
  }
155
200
  async getSceneState() {
156
201
  return this.request("GET", "/api/state/scene");
@@ -174,11 +219,14 @@ export class EngineApiClient {
174
219
  // Cold-load on large projects can take 25-40s. 60s budget.
175
220
  // The engine reads play params from body.options (tool_net_thread.cpp:503),
176
221
  // and the play handler reads options["scene"] — a top-level { scene } is
177
- // dropped, so the scene MUST be nested inside options.
178
- return this._requestQueued("POST", "/api/play", scene ? { options: { scene } } : {}, 60_000);
222
+ // dropped, so the scene MUST be nested inside options. The bound identity
223
+ // rides in the same options dict so play is refused on a mismatched project.
224
+ const options = { ...this.identityOptions(), ...(scene ? { scene } : {}) };
225
+ return this._requestQueued("POST", "/api/play", Object.keys(options).length ? { options } : {}, 60_000);
179
226
  }
180
227
  async stop() {
181
- return this._requestQueued("POST", "/api/stop", undefined, 15_000);
228
+ const options = this.identityOptions();
229
+ return this._requestQueued("POST", "/api/stop", Object.keys(options).length ? { options } : undefined, 15_000);
182
230
  }
183
231
  async readFile(path, maxBytes) {
184
232
  const params = new URLSearchParams({ path });
@@ -211,6 +259,18 @@ export class EngineApiClient {
211
259
  // The terminal result is the apply dict; the image rides as base64 inside it
212
260
  // (no raw-binary channel). Legacy/dormant engines answer 200 synchronously
213
261
  // with the same payload shape — _requestQueued resolves both.
262
+ //
263
+ // Game capture over local HTTP structurally 409s today with a STRUCTURED
264
+ // reason (`failure_reason:"unsupported_transport"`, `bridge_required:true`,
265
+ // tool_net_thread.cpp:495-503). Detect that specific shape and return it
266
+ // verbatim so the tool can give an honest message — do NOT hardcode "game
267
+ // always fails": once the engine answers 200/202 (P4.4), the normal path
268
+ // below just works.
269
+ if (kind === "game") {
270
+ const bridge = await this._detectBridgeRequired(`/api/snapshot/${kind}`);
271
+ if (bridge)
272
+ return bridge;
273
+ }
214
274
  let response;
215
275
  try {
216
276
  response = await this._requestQueued("GET", `/api/snapshot/${kind}`, undefined, 60_000);
@@ -218,11 +278,16 @@ export class EngineApiClient {
218
278
  catch (err) {
219
279
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
220
280
  }
281
+ return this._parseSnapshotResponse(response, kind);
282
+ }
283
+ /** Shared: turn an ops/snapshot response envelope into an EngineSnapshot.
284
+ * Used by viewport/game snapshots and by scenePreview. */
285
+ async _parseSnapshotResponse(response, kind) {
221
286
  const payload = findSnapshotPayload(response);
222
287
  if (!payload) {
223
288
  return {
224
289
  ok: false,
225
- error: "Snapshot response did not include a viewport/game payload.",
290
+ error: "Snapshot response did not include an image payload.",
226
291
  };
227
292
  }
228
293
  const base64 = stringFrom(payload.base64) ?? stringFrom(payload.image_base64);
@@ -230,12 +295,35 @@ export class EngineApiClient {
230
295
  return {
231
296
  ok: false,
232
297
  error: stringFrom(payload.error) ?? "Snapshot response did not include image data.",
298
+ failureReason: stringFrom(payload.failure_reason),
299
+ sceneHasCamera: boolFrom(payload.scene_has_camera),
300
+ sceneHadLight: boolFrom(payload.scene_had_light),
301
+ usedSyntheticCamera: boolFrom(payload.used_synthetic_camera),
233
302
  metadata: withoutImageData(payload),
234
303
  };
235
304
  }
236
305
  const { format, mime, ext } = snapshotFormat(payload);
237
306
  const buffer = Buffer.from(base64, "base64");
238
307
  const localPath = await this.writeSnapshotFile(kind, buffer, ext);
308
+ // Client-side identity drift check (item 4). The engine's identity guard only
309
+ // acts on options.projectIdHash inside a queued command; a GET snapshot from
310
+ // `fetch` carries no reliable body, so we cannot count on the engine to reject
311
+ // a drifted read. Re-read health and compare to the bound hash — if the engine
312
+ // has switched projects since we bound, this frame may be from the WRONG
313
+ // project. Best-effort: a failed health read never blocks the (already
314
+ // captured) image.
315
+ let projectMismatch;
316
+ if (this.boundProjectIdHash) {
317
+ try {
318
+ const health = await checkEngineHealth(this.port);
319
+ if (health?.projectIdHash && health.projectIdHash !== this.boundProjectIdHash) {
320
+ projectMismatch = true;
321
+ }
322
+ }
323
+ catch {
324
+ // ignore — never fail a capture on a transient health read
325
+ }
326
+ }
239
327
  return {
240
328
  ok: payload.ok !== false,
241
329
  localPath,
@@ -249,15 +337,86 @@ export class EngineApiClient {
249
337
  op: stringFrom(payload.op),
250
338
  provenance: payload.provenance,
251
339
  bytes: buffer.byteLength,
340
+ sceneHasCamera: boolFrom(payload.scene_has_camera),
341
+ sceneHadLight: boolFrom(payload.scene_had_light),
342
+ usedSyntheticCamera: boolFrom(payload.used_synthetic_camera),
343
+ projectMismatch,
252
344
  metadata: withoutImageData(payload),
253
345
  };
254
346
  }
347
+ /**
348
+ * Probe /api/snapshot/game for the structural 409 `bridge_required` shape
349
+ * (tool_net_thread.cpp:495-503). Returns a structured failure only when the
350
+ * engine actually answers 409 with `bridge_required` / `unsupported_transport`;
351
+ * ANY other status (200/202/other) returns null so the caller proceeds to the
352
+ * normal queued path — so the moment the engine starts answering over HTTP
353
+ * (P4.4), game capture just works with no code change here.
354
+ */
355
+ async _detectBridgeRequired(path) {
356
+ let res;
357
+ try {
358
+ res = await this._fetchRaw("GET", path, undefined, 15_000);
359
+ }
360
+ catch {
361
+ // Transport error — let the normal path surface it uniformly.
362
+ return null;
363
+ }
364
+ if (res.status !== 409)
365
+ return null;
366
+ const body = (await res.json().catch(() => ({})));
367
+ const reason = stringFrom(body.failure_reason);
368
+ const bridge = body.bridge_required === true;
369
+ if (!bridge && reason !== "unsupported_transport") {
370
+ // A 409 we don't recognize — surface it as a plain error rather than
371
+ // silently claiming "bridge required".
372
+ return {
373
+ ok: false,
374
+ error: stringFrom(body.error) ?? "Engine returned 409 for game snapshot.",
375
+ };
376
+ }
377
+ return {
378
+ ok: false,
379
+ failureReason: reason ?? "bridge_required",
380
+ error: stringFrom(body.error) ?? "Game snapshots require the desktop bridge async transport.",
381
+ };
382
+ }
255
383
  async viewportSnapshot() {
256
384
  return this.snapshot("viewport");
257
385
  }
258
386
  async gameSnapshot() {
259
387
  return this.snapshot("game");
260
388
  }
389
+ /**
390
+ * Offscreen scene render via the `ScenePreview` op over /api/ops (no game
391
+ * boot; physics/animations are static). Mirrors the web previewScene op input
392
+ * (snapshot-tools.ts): { op:'ScenePreview', scene_path?, framing?, size?,
393
+ * node_path? } — scene_path optional, engine defaults to the open scene. The
394
+ * result carries image_base64 + mime (+ width/height) plus the P4.3 confession
395
+ * fields (scene_has_camera / scene_had_light / used_synthetic_camera).
396
+ */
397
+ async scenePreview(input) {
398
+ const opInput = { op: "ScenePreview" };
399
+ const trimmed = input?.scenePath?.trim();
400
+ if (trimmed && trimmed !== "." && trimmed !== "./")
401
+ opInput.scene_path = trimmed;
402
+ if (input?.framing)
403
+ opInput.framing = input.framing;
404
+ if (input?.size)
405
+ opInput.size = input.size;
406
+ if (input?.nodePath)
407
+ opInput.node_path = input.nodePath;
408
+ let response;
409
+ try {
410
+ // executeOps stamps the bound identity so a drifted project is rejected
411
+ // (identity_mismatch) before rendering — ScenePreview reads no game state
412
+ // but still targets a project's resources.
413
+ response = await this.executeOps([opInput]);
414
+ }
415
+ catch (err) {
416
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
417
+ }
418
+ return this._parseSnapshotResponse(response, "scene");
419
+ }
261
420
  getPort() {
262
421
  return this.port;
263
422
  }
@@ -308,10 +308,14 @@ function buildMcpToolPlan(mode, target, assetPolicy, verification, goal = "") {
308
308
  assets.push("summer_search_assets", "summer_import_asset");
309
309
  }
310
310
  if (verification !== "none") {
311
- verify.push("summer_get_script_errors", "summer_get_diagnostics");
311
+ // Rung 1-2 of the ladder: does it compile / does it look right.
312
+ verify.push("summer_get_script_errors", "summer_get_diagnostics", "summer_screenshot");
312
313
  }
313
314
  if (verification === "full") {
314
- verify.push("summer_clear_console", "summer_play", "summer_get_debugger_errors", "summer_stop");
315
+ // Rung 3: run it and read runtime errors. Ordered as the composed loop —
316
+ // clear -> play -> read runtime errors -> look at the live frame -> stop.
317
+ // (There is no single "verify" tool; the agent composes these.)
318
+ verify.push("summer_clear_console", "summer_play", "summer_get_debugger_errors", "summer_screenshot", "summer_stop");
315
319
  }
316
320
  return { start, mutate, assets, verify };
317
321
  }
@@ -5,6 +5,7 @@ import { EngineApiClient } from "../lib/api-client.js";
5
5
  import { registerSceneTools } from "./tools/scene-tools.js";
6
6
  import { registerDebugTools } from "./tools/debug-tools.js";
7
7
  import { registerVisualTools } from "./tools/visual-tools.js";
8
+ import { WITH_ENGINE_META } from "./tools/with-engine.js";
8
9
  import { registerProjectTools } from "./tools/project-tools.js";
9
10
  import { registerAssetTools } from "./tools/asset-tools.js";
10
11
  import { registerGenerateTools } from "./tools/generate-tools.js";
@@ -49,7 +50,10 @@ export async function getClient() {
49
50
  }
50
51
  catch {
51
52
  cachedClient = null;
52
- throw new Error("Summer Engine is not running. Open it first, or run: npx summer-engine run");
53
+ throw new Error("Summer Engine is not running. Open it first, or run: npx summer-engine run\n" +
54
+ "Note: only tools that touch the local project need the engine. Cloud tools " +
55
+ "(summer_generate_*, summer_search_assets, summer_list_my_assets, summer_get_asset, " +
56
+ "summer_check_job) work right now without it — they only need 'npx summer-engine login'.");
53
57
  }
54
58
  }
55
59
  export function resetClient() {
@@ -78,30 +82,59 @@ function installResultSizeLogger(server) {
78
82
  if (typeof handler !== "function")
79
83
  return original(...args);
80
84
  const wrapped = async (...handlerArgs) => {
85
+ const startedAt = Date.now();
81
86
  const result = await handler(...handlerArgs);
87
+ const durationMs = Date.now() - startedAt;
82
88
  try {
83
89
  if (result &&
84
90
  typeof result === "object" &&
85
91
  "content" in result &&
86
92
  Array.isArray(result.content)) {
87
- let chars = 0;
88
- for (const entry of result
89
- .content) {
90
- if (entry && entry.type === "text" && typeof entry.text === "string") {
91
- chars += entry.text.length;
93
+ const debug = process.env.SUMMER_MCP_DEBUG === "1";
94
+ const isError = result.isError === true;
95
+ // withEngine stamps failure classifiers + retried + bound hash on a
96
+ // non-enumerable symbol so we can log them here without every tool
97
+ // threading its own name through withEngine.
98
+ const meta = result[WITH_ENGINE_META];
99
+ // Structured per-call line. Verbose behind SUMMER_MCP_DEBUG=1; when the
100
+ // flag is off, log ONLY failures — the success path stays quiet.
101
+ if (debug || isError) {
102
+ const callLine = JSON.stringify({
103
+ event: "mcp:tool_call",
104
+ tool: name,
105
+ ok: !isError,
106
+ isError,
107
+ durationMs,
108
+ ...(meta?.terminalState ? { terminalState: meta.terminalState } : {}),
109
+ ...(meta?.errorClass ? { errorClass: meta.errorClass } : {}),
110
+ ...(meta?.failureReason ? { failureReason: meta.failureReason } : {}),
111
+ ...(meta?.retried ? { retried: true } : {}),
112
+ ...(meta?.boundProjectIdHash
113
+ ? { boundProjectIdHash: meta.boundProjectIdHash }
114
+ : {}),
115
+ });
116
+ process.stderr.write(`[summer-mcp] ${callLine}\n`);
117
+ }
118
+ if (debug) {
119
+ let chars = 0;
120
+ for (const entry of result
121
+ .content) {
122
+ if (entry && entry.type === "text" && typeof entry.text === "string") {
123
+ chars += entry.text.length;
124
+ }
92
125
  }
126
+ const bytes = Buffer.byteLength(result.content
127
+ .filter((e) => e && e.type === "text" && typeof e.text === "string")
128
+ .map((e) => e.text)
129
+ .join(""), "utf8");
130
+ const line = JSON.stringify({
131
+ event: "mcp:result_size",
132
+ tool_name: name,
133
+ chars,
134
+ bytes,
135
+ });
136
+ process.stderr.write(`[summer-mcp] ${line}\n`);
93
137
  }
94
- const bytes = Buffer.byteLength(result.content
95
- .filter((e) => e && e.type === "text" && typeof e.text === "string")
96
- .map((e) => e.text)
97
- .join(""), "utf8");
98
- const line = JSON.stringify({
99
- event: "mcp:result_size",
100
- tool_name: name,
101
- chars,
102
- bytes,
103
- });
104
- process.stderr.write(`[summer-mcp] ${line}\n`);
105
138
  }
106
139
  }
107
140
  catch {