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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.cursor-plugin/plugin.json +1 -1
- package/.factory-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +20 -0
- package/GEMINI.md +2 -1
- package/README.md +34 -2
- package/dist/lib/api-client.d.ts +58 -1
- package/dist/lib/api-client.js +167 -8
- package/dist/lib/game-task-plan.js +6 -2
- package/dist/mcp/server.js +50 -17
- package/dist/mcp/tools/asset-tools.js +14 -4
- package/dist/mcp/tools/generate-tools.js +6 -0
- package/dist/mcp/tools/project-tools.js +72 -5
- package/dist/mcp/tools/scene-tools.js +6 -2
- package/dist/mcp/tools/visual-tools.js +86 -11
- package/dist/mcp/tools/with-engine.d.ts +32 -7
- package/dist/mcp/tools/with-engine.js +91 -54
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/lib/api-client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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 {
|