From b19bf8d5cee1a9414a1c20b8a2ddbf37bd77b84e Mon Sep 17 00:00:00 2001 From: Velizar Seleznev Date: Tue, 4 Aug 2026 12:21:24 +0200 Subject: [PATCH 1/2] feat: improve MCP onboarding for local agents --- .opencode/INSTALL.md | 198 +++++++++++++++---- CHANGELOG.md | 8 + README.md | 91 +++++++-- docs/SETUP_PROMPT.md | 39 ++-- package-lock.json | 4 +- package.json | 2 +- references/antigravity.md | 91 +++++++++ references/lm-studio.md | 143 ++++++++++++++ src/commands/doctor.ts | 10 +- src/commands/mcp.ts | 39 +++- src/commands/setup.ts | 117 +++++++++-- src/commands/skills.ts | 13 +- src/lib/agent-config.test.ts | 238 +++++++++++++++++++++- src/lib/agent-config.ts | 282 ++++++++++++++++++++++++--- src/lib/skills-registry.ts | 1 + src/mcp/server.ts | 22 ++- src/mcp/tools/asset-tools.test.ts | 21 +- src/mcp/tools/asset-tools.ts | 6 +- src/mcp/tools/project-tools.test.ts | 1 + src/mcp/tools/project-tools.ts | 19 +- src/mcp/tools/scene-mutation.test.ts | 120 ++++++++++++ src/mcp/tools/scene-mutation.ts | 86 ++++++++ src/mcp/tools/scene-tools.test.ts | 97 ++++++++- src/mcp/tools/scene-tools.ts | 115 +++++++---- 24 files changed, 1575 insertions(+), 188 deletions(-) create mode 100644 references/antigravity.md create mode 100644 references/lm-studio.md create mode 100644 src/mcp/tools/scene-mutation.test.ts create mode 100644 src/mcp/tools/scene-mutation.ts diff --git a/.opencode/INSTALL.md b/.opencode/INSTALL.md index 01d9873..6387b0c 100644 --- a/.opencode/INSTALL.md +++ b/.opencode/INSTALL.md @@ -1,75 +1,191 @@ -# Installing Summer in OpenCode +# OpenCode setup -OpenCode loads plugins as JavaScript modules from `node_modules`, so installation = `npm install` of this package into your OpenCode project. +Summer Engine works in OpenCode through MCP. Installing `summer-engine` as an +OpenCode plugin is not required. -## Quick install +## Fast path for OpenCode -From your OpenCode project root, run: +From the Summer project root, configure OpenCode itself: ```bash -npm install --save-dev summer-engine +npx -y summer-engine@latest setup opencode --yes --force --project "$PWD" ``` -Then add the plugin to your `opencode.json`: +This writes project-scoped MCP config and Summer guidance, preserves unrelated +OpenCode config, and does not add or change any model provider. OpenCode +receives the same complete Summer MCP tool registry as every other client. -```json -{ - "plugin": ["summer-engine"] -} +Restart OpenCode after setup. A running OpenCode process does not discover a +new MCP server or newly installed guidance from config written mid-session. + +## Optional recipe: OpenCode with LM Studio + +1. In LM Studio, load a tool-calling model, set its context to at least 64k, + and start the local server. +2. From the Summer project root, get the exact loaded model ID: + + ```bash + curl -fsS http://127.0.0.1:1234/v1/models + ``` + +3. Configure both the LM Studio provider and Summer MCP in one command. Replace + the example model ID with the `id` returned above: + + ```bash + npx -y summer-engine@latest setup opencode --yes \ + --project "$PWD" \ + --lm-studio-model "google/gemma-4-26b-a4b-qat" \ + --lm-studio-vision \ + --json + ``` + +When OpenCode setup receives `--project` and no explicit `--scope`, it writes +`./opencode.json`. The generated config: + +- selects the loaded LM Studio model through its OpenAI-compatible endpoint; +- declares image input when `--lm-studio-vision` is present, allowing OpenCode + to pass Summer screenshot results to a vision-capable model; +- uses a 131k context and disables hidden reasoning by default so small local + models do not spend their output budget before calling a tool; +- starts Summer MCP with the complete tool registry and an absolute project + binding; +- preserves unrelated OpenCode providers, models, plugins, and MCP servers. + +When testing a local npm tarball, use the local package name rather than the +published `@latest` spec: + +```bash +npm install --save-dev ./summer-engine-2.7.0.tgz +npx summer setup opencode --yes \ + --project "$PWD" \ + --lm-studio-model "google/gemma-4-26b-a4b-qat" \ + --lm-studio-vision \ + --local-dev \ + --json ``` -OpenCode resolves `summer-engine` via the package's `main` field, which points to the Summer plugin entry. You can also pin to git for unreleased changes: +The project must have its own `package.json`. The generated MCP command will +point at that installed candidate instead of an npx cache or +`summer-engine@latest`. -```json -{ - "plugin": ["summer-engine@git+https://github.com/SummerEngine/summer.git"] -} +## OpenCode with any existing model provider + +If OpenCode already has a working model provider, configure only Summer MCP: + +```bash +npx -y summer-engine@latest setup opencode --yes \ + --project "$PWD" \ + --json ``` -Restart OpenCode. The orientation banner ("Summer Engine is loaded. N skills available…") will appear at the top of every new session, and skills will auto-discover from `node_modules/summer-engine/skills/`. +Use `--scope user` explicitly if one Summer MCP entry should be shared by all +OpenCode projects. Keep `--project` even for user scope so scene tools bind to +the intended Summer editor. + +## Verify before changing a scene + +After restarting OpenCode in the project, use this first prompt: -## What this gives you +> Call `summer_get_agent_playbook` and read the result. Then call +> `summer_get_project_context` and inspect my scene without changing it. Report +> the MCP server version, bound project, scene root, and any diagnostics. Do not claim +> success unless those Summer tools returned results. -- **24 auto-trigger skills** under the `summer:` namespace, including `using-summer`, `brainstorm-game`, `debug`, `play`, `fps-controller`, `gdscript-patterns`, `scene-composition`, `art-direction`, and more. -- **A `summer-engine` MCP server** — start it with `npx summer-engine mcp` and OpenCode will route scene/diagnostics/asset tools to your local Summer Engine running on `localhost:6550`. -- **Session-start orientation** — first user message of every session is prefixed with the using-summer primer so the model invokes skills before responding. +Only after that succeeds should the model mutate the scene. For a reversible +smoke test, ask it to add one uniquely named node, inspect it, run the project, +capture editor and game screenshots, check diagnostics, then remove that node +and verify cleanup. -## Configure the MCP server +## Manual MCP-only `opencode.json` -Add this block to your `opencode.json` so OpenCode launches the MCP server on demand: +If the CLI cannot write the config, start with this provider-neutral shape: ```json { + "$schema": "https://opencode.ai/config.json", "mcp": { "summer-engine": { - "command": "npx", - "args": ["summer-engine", "mcp"] + "type": "local", + "command": [ + "npx", + "-y", + "summer-engine@latest", + "mcp", + "--project", + "/absolute/path/to/project" + ] } } } ``` -## Verify +This is the complete OpenCode MCP setup; no provider block is required. -In a fresh OpenCode session, ask: +## Manual optional OpenCode + LM Studio recipe -> Let's make an FPS in Summer Engine. +Use this larger shape only when you explicitly want Summer setup to also add an +LM Studio provider. Replace both the model ID and project path. OpenCode +requires the MCP command as an array. -The model should auto-invoke the `summer:fps-controller` skill before writing any code. If it doesn't, the plugin isn't loaded — check `opencode.json` and your `node_modules/summer-engine/` install. +```json +{ + "$schema": "https://opencode.ai/config.json", + "model": "lmstudio/google/gemma-4-26b-a4b-qat", + "small_model": "lmstudio/google/gemma-4-26b-a4b-qat", + "provider": { + "lmstudio": { + "npm": "@ai-sdk/openai-compatible", + "name": "LM Studio (local)", + "options": { + "baseURL": "http://127.0.0.1:1234/v1" + }, + "models": { + "google/gemma-4-26b-a4b-qat": { + "name": "google/gemma-4-26b-a4b-qat (local)", + "limit": { + "context": 131072, + "output": 8192 + }, + "modalities": { + "input": ["text", "image"], + "output": ["text"] + }, + "options": { + "reasoningEffort": "none" + } + } + } + } + }, + "mcp": { + "summer-engine": { + "type": "local", + "command": [ + "npx", + "-y", + "summer-engine@latest", + "mcp", + "--project", + "/absolute/path/to/project" + ] + } + } +} +``` ## Troubleshooting -| Symptom | Fix | +| Symptom | Check | |---|---| -| No orientation banner appears | Verify `plugin` array in `opencode.json` and that `summer-engine` is installed in `node_modules/`. | -| MCP tools return "not connected" | Run `summer run` to launch the engine. The MCP server lazy-connects on the first tool call. | -| `summer` command not found | Use `npx -y summer-engine@latest ` or install the CLI globally only if you want a persistent `summer` command. | -| Skills don't auto-trigger | The using-summer skill loads on first user message; if that message is empty (e.g. a startup probe), they'll trigger on the second. | - -## Uninstall - -```bash -npm uninstall summer-engine -``` - -Remove the `plugin` and `mcp` entries from `opencode.json`. +| LM Studio model is missing | `curl -fsS http://127.0.0.1:1234/v1/models` must return its exact ID. | +| Summer tools are missing | Restart OpenCode, then inspect `opencode debug config` and `opencode mcp list`. | +| Tools exist but scene calls fail | Open the same project in Summer Engine and confirm the absolute `--project` path. | +| Model talks about tools but never calls them | Use a model trained for multi-turn tool use, raise its context to at least 64k, enable only the Summer integration for the first test, and require playbook/context results before mutation. | +| Screenshot is returned but OpenCode says the model cannot see it | Re-run setup with `--lm-studio-vision` only if the loaded model actually accepts image input, then restart OpenCode. | +| LM Studio reports `Unknown ArrayValue filter: upper` or `Unknown test: sequence` | The model's embedded Jinja tool template is incompatible with that LM Studio runtime. In **My Models → gear → Inference → Prompt Template**, replace it with the model package's bundled `chat_template.jinja`, restart OpenCode, and retry the playbook prompt. Use **Reset** to restore the prior template. | +| `npx` is not found | Put the absolute path from `command -v npx` in the MCP command array. | + +The optional npm plugin is separate from MCP. OpenCode discovers installed +Summer skills from its standard `.opencode/skills`, `.claude/skills`, or +`.agents/skills` locations; the MCP server itself exposes +`summer_get_agent_playbook` for clients that do not load skills. diff --git a/CHANGELOG.md b/CHANGELOG.md index 816d37f..463a6c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,21 @@ All notable changes to summer-engine will be documented here. Following [Keep a ## [Unreleased] +## [2.8.0] — 2026-08-03 — "One-command MCP onboarding" + ### Added - MCP discovers every live Summer editor through `~/.summer/instances/` and automatically binds local tools to the editor whose project contains the agent's current working directory. - `summer mcp --project ` and `summer mcp --instance ` provide explicit selection for hosts that do not start the MCP server from a project directory. +- OpenCode setup can configure a loaded LM Studio model alongside the unchanged complete Summer MCP tool registry with `--lm-studio-model `, with opt-in screenshot input through `--lm-studio-vision`. +- `summer setup antigravity` writes Antigravity's current project or user MCP configuration and installs Summer skills into its native `.agents` or `~/.gemini/config` directories. ### Changed - Multiple live editors are now a fail-closed state when no project can be inferred. MCP lists the non-secret project/instance choices instead of following the machine-global last-opened editor pointer. - Selected MCP sessions keep following the same project across editor restarts and validate registry identity against `/api/health` before connecting. +- OpenCode setup now treats `--project` as project scope unless `--scope user` is explicit, and the OpenCode guide includes a complete local-model configuration and verification path. +- OpenCode, direct LM Studio, and Antigravity setup are independent client targets. Plain OpenCode and Antigravity setup preserve the user's existing model provider. +- `summer_remove_node` keeps the preferred exact `path` argument and also accepts the common small-model `parent` + `name` shape for one direct child. +- `summer_batch` infers unambiguous op-less AddNode and SetProp items emitted in individual-tool form by smaller models while keeping explicit `op` as the preferred shape. ## [2.7.0] — 2026-07-24 — "Reliable project mutations" diff --git a/README.md b/README.md index b61572e..0565070 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Engine installers currently ship for macOS on Apple silicon and Windows; Steam, browser, mobile, and additional desktop distribution are planned targets, not shipping promises. -**Summer** is the MIT open-source agent layer that connects your AI coding agent to Summer Engine. It is the **Summer CLI**, the **Summer MCP** server, and the **Summer agent** skills, hooks, and plugin manifests, all in one package. First-class setup works in Claude Code, Cursor, Codex, Devin Desktop (formerly Windsurf), Cline, Roo Code, Gemini CLI, GitHub Copilot CLI, GitHub Copilot in VS Code, and OpenCode. Factory Droid uses the plugin marketplace path. +**Summer** is the MIT open-source agent layer that connects your AI coding agent to Summer Engine. It is the **Summer CLI**, the **Summer MCP** server, and the **Summer agent** skills, hooks, and plugin manifests, all in one package. First-class setup works in Claude Code, Cursor, Codex, Devin Desktop (formerly Windsurf), Cline, Roo Code, Gemini CLI, GitHub Copilot CLI, GitHub Copilot in VS Code, OpenCode, LM Studio, and Antigravity. Factory Droid uses the plugin marketplace path. - **Source:** [github.com/SummerEngine/summer-engine-agent](https://github.com/SummerEngine/summer-engine-agent) - **CLI setup:** [www.summerengine.com/cli](https://www.summerengine.com/cli) @@ -25,24 +25,27 @@ Three names, one npm package (`summer-engine`): - **Summer MCP**: the MCP server that gives your agent 62 tools spanning local engine work, cloud workflows, and creator publishing. See [www.summerengine.com/mcp](https://www.summerengine.com/mcp). - **Summer agent layer**: the game-dev skills, hooks, and plugin manifests that give your AI agent judgment, not just a chat box. -All MIT, all free to use. One paste sets up all three. +The CLI, MCP bridge, and agent layer are MIT licensed. The Summer Engine app +has its own license. MCP setup is deliberately separate from installing the +app, signing in, choosing a model, or creating a project. ## Get started: one prompt -Open your AI agent (Claude Code, Cursor, Codex, Copilot, Devin Desktop, etc.) and paste: +Open your AI agent and paste: -> Install Summer Engine and let's make a game. Read github.com/SummerEngine/summer-engine-agent, check what I already have installed, install whatever is missing, log me in, and scaffold a starter project so we can start building. +> Set up Summer MCP for this coding agent and the current project. The MCP client and model provider are separate choices. Before writing anything, run `npx -y summer-engine@latest setup --help` and confirm that the exact client target and the `--project` option are both listed. Use `opencode` for OpenCode and `antigravity` for Antigravity. If either requirement is missing, STOP and report that npm latest is older than these instructions. Do not substitute a different client, omit `--project`, change to user/global scope, or hand-edit a fallback config. Otherwise run `npx -y summer-engine@latest setup --yes --force --project "$PWD"`. Do not change my model provider, install the engine app, sign me in, or create a project. Show the config path. A doctor result such as "MCP Tools registered" proves only that the server can load; it does not prove this agent can see or call the tools. If the client needs a restart, ask me to restart it. Before any mutation, call `summer_get_agent_playbook`, `summer_get_project_context`, and `summer_get_scene_tree`, consume their results, and report the exact bound project and scene. If any tool is missing or rejected, quote the error and STOP. -That's it. The agent reads this README, detects what's already on your machine, and runs only what's missing via Bash. **No `/plugin` slash commands. No manual installs. One paste.** +That's it. The agent follows a bounded command instead of inventing an installation flow. **No `/plugin` slash commands. No provider rewrites. One paste.** -The first run will: -1. Install the `summer-engine` npm package (~3 MB) if it's not on `PATH`. -2. Install Summer's game-dev skills into `~/.claude/skills/` (or your agent's user-skill directory) so the agent loads them next session. -3. Download the Summer Engine app (~1 GB, including the engine binary, Git, and other runtime tools so users without them aren't blocked) if it's not installed. -4. Open your browser for one-click sign-in. -5. Scaffold a starter project and open the engine. +The setup command will: +1. Merge the `summer-engine` server into the selected client's MCP config. +2. Bind that server to the exact current project. +3. Install recommended Summer skills when the client supports them. +4. Run diagnostics and print the config path plus reload steps. -At that point the agent has the skills, the MCP server, and your engine running. Ask it to build something. +It will not install the engine app, sign you in, create a project, or rewrite a +model provider. Those are separate, explicit workflows. The optional OpenCode ++ LM Studio recipe is the only setup path that writes provider/model keys. --- @@ -90,7 +93,7 @@ The relevant check ids are `node-version`, `cli-version`, `cli-version-current`, npx -y summer-engine@latest setup claude-code --yes --force ``` -Replace `claude-code` with the user's actual agent: `codex`, `cursor`, `windsurf`, `cline`, `roo-code`, `kilo-code`, `gemini`, `github-copilot`, `vscode-copilot`, `opencode`, or `lm-studio`. Use `github-copilot` for Copilot CLI and `vscode-copilot` for GitHub Copilot Chat/Agent in VS Code. Factory Droid still uses the plugin marketplace path in the install section below. This writes the current Summer skill bundle to `~/.claude/skills//SKILL.md` (or the agent's equivalent user-skill directory) AND writes the MCP server config so the agent can talk to the engine. The `--force` flag wipes any stale skill content first, so re-runs always end up with the latest copy. +Replace `claude-code` with the user's actual MCP client: `codex`, `cursor`, `windsurf`, `cline`, `roo-code`, `kilo-code`, `gemini`, `github-copilot`, `vscode-copilot`, `opencode`, `lm-studio`, or `antigravity`. Use `github-copilot` for Copilot CLI and `vscode-copilot` for GitHub Copilot Chat/Agent in VS Code. A model runtime such as LM Studio or Ollama is not the client unless the user is chatting directly in that app. Factory Droid still uses the plugin marketplace path in the install section below. This writes the current Summer skill bundle to the client's native directory when it supports skills and writes the MCP server config so the client can talk to the engine. The `--force` flag wipes stale Summer skill content first, so re-runs end up with the latest copy. **First-install detection:** if `~/.claude/skills/` didn't exist before this command, Claude Code wasn't watching it and won't auto-detect the new files this session. Tell the user **once**: "Skills installed - restart your agent so they load." On subsequent installs (directory already existed), skills auto-detect mid-session and no restart is needed. **You can detect first-install vs upgrade by checking if `~/.claude/skills/` existed before Step 1; record the result before running setup.** @@ -183,7 +186,7 @@ Only skip brainstorm if the user explicitly said "skip brainstorm" or "just buil - **Don't loop `summer login` if it times out at 120s.** Re-run it once and tell the user to be quicker; loop forever and the user is stuck. - **Don't jump straight into `summer create` from a vague prompt.** Invoke `summer:brainstorm-game` first (Step 6). The build skills assume `.summer/GameSoul.md` exists. -**Using a different agent?** Replace `claude-code` with any supported agent in Step 1: `codex`, `cursor`, `windsurf`, `cline`, `roo-code`, `kilo-code`, `gemini`, `github-copilot`, `vscode-copilot`, `opencode`, or `lm-studio`. (`devin` is also accepted as an alias for `windsurf`.) Skill targets vary per agent (Cursor uses `.cursor/rules/`, Devin Desktop uses `.windsurfrules`, Cline + Roo use `.clinerules/`, Copilot uses `~/.copilot/skills` or `.github/skills`, OpenCode uses agent definitions, etc.). The CLI handles the difference. After install, **Cline and Roo Code users should restart VS Code** so the extension reloads its MCP config. **Gemini users** may need to run `gemini extensions enable summer-engine` after the first install. **VS Code Copilot users** should start the `summer-engine` MCP server from Agent mode if VS Code does not autostart it. **Factory Droid** still has its own plugin path below. +**Using a different agent?** Replace `claude-code` with any supported agent in Step 1: `codex`, `cursor`, `windsurf`, `cline`, `roo-code`, `kilo-code`, `gemini`, `github-copilot`, `vscode-copilot`, `opencode`, `lm-studio`, or `antigravity`. (`devin` is also accepted as an alias for `windsurf`.) Skill targets vary per agent (Cursor uses `.cursor/rules/`, Devin Desktop uses `.windsurfrules`, Cline + Roo use `.clinerules/`, Copilot uses `~/.copilot/skills` or `.github/skills`, Antigravity uses `.agents/skills`, etc.). The CLI handles the difference. After install, **Cline and Roo Code users should restart VS Code** so the extension reloads its MCP config. **Gemini users** may need to run `gemini extensions enable summer-engine` after the first install. **VS Code Copilot users** should start the `summer-engine` MCP server from Agent mode if VS Code does not autostart it. **Factory Droid** still has its own plugin path below. **Power-user note:** if the user specifically wants `summer` on their `PATH` for everyday terminal use outside the AI agent, a global npm install is still possible. The agent flow doesn't need it. @@ -465,18 +468,68 @@ Writes MCP config to VS Code's user `mcp.json` and skills to `~/.copilot/skills/ ### OpenCode ```bash -npx -y summer-engine@latest setup opencode --yes +npx -y summer-engine@latest setup opencode --yes --project "$PWD" ``` -Writes the MCP server entry into `opencode.json` (`~/.config/opencode/opencode.json` for user scope, `./opencode.json` for project) using the array-shaped `command: ["npx", "-y", "summer-engine@latest", "mcp"]` format. Restart OpenCode. Full guide: [`.opencode/INSTALL.md`](./.opencode/INSTALL.md). +With `--project` and no explicit scope, OpenCode setup writes `./opencode.json`, +binds MCP to that exact project, installs Summer guidance, and leaves every +model/provider setting untouched. OpenCode receives the same complete Summer +MCP tool registry as every other supported client. Restart OpenCode after setup. + +If LM Studio is the model provider, configure the provider and MCP together +using the exact ID returned by `curl http://127.0.0.1:1234/v1/models`: + +```bash +npx -y summer-engine@latest mcp setup opencode \ + --project "$PWD" \ + --lm-studio-model "your-loaded-model-id" \ + --lm-studio-vision \ + --json +``` + +This preserves unrelated OpenCode config, selects the local model, gives it a +131k context, disables hidden reasoning by default, declares image input for a +vision-capable model, and writes the array-shaped Summer MCP command. Omit +`--lm-studio-vision` for a text-only model. Full manual config, restart boundary, +verification prompt, and troubleshooting: [`.opencode/INSTALL.md`](./.opencode/INSTALL.md). + +### Antigravity + +```bash +npx -y summer-engine@latest setup antigravity --yes --project "$PWD" +``` + +This writes the current workspace MCP config to `.agents/mcp_config.json` and +recommended Summer skills to `.agents/skills/`. It preserves other MCP servers +and does not change Antigravity's model. Open **Customizations > MCP Servers**, +refresh `summer-engine`, and approve tool use when prompted. Use `--scope user` +without `--project` for the global `~/.gemini/config/` paths. See the +[Antigravity setup and verification guide](./references/antigravity.md). + +For terminal verification, start a normal interactive `agy` session in the +configured project and run `/mcp`. Antigravity CLI 1.1.10 headless print mode +(`agy -p`) can list a project MCP server yet reject its calls; the same +project-scoped configuration executes correctly in the interactive client. ### LM Studio (local models) ```bash -npx -y summer-engine@latest setup lm-studio --yes +npx -y summer-engine@latest setup lm-studio --yes --project "/absolute/path/to/your-project" ``` -Writes the MCP server entry into `~/.lmstudio/mcp.json` (app-global; there is no project scope). In LM Studio, toggle the `summer-engine` server on in the **Program** tab, and raise the loaded model's context length to **32k or higher** — MCP tool schemas overflow small contexts silently. LM Studio has no rules/skills folder; the MCP server's `summer_get_agent_playbook` tool covers in-chat guidance. Pair with a tool-calling-reliable local model (gpt-oss-20b on 12–16 GB VRAM, Qwen3-Coder-30B on 24 GB). +Detects the active LM Studio config, preserves other MCP servers, binds Summer +to the named project, and exposes the same complete Summer MCP tool registry as +other clients. In LM Studio, enable `summer-engine` under **Chat > Integrations** +(**Program** in older versions), load a multi-turn tool-calling model with at +least **64k** context, and start a fresh chat. LM Studio has no Summer skills +folder, so the model must call `summer_get_agent_playbook` itself. + +LM Studio is a model runtime and MCP host, not a full coding agent. If automatic +setup does not make the integration visible, use LM Studio's own **Edit +mcp.json** action and follow the complete [manual LM Studio setup and first +prompt](./references/lm-studio.md). Do not assume a fixed config path: current +releases may use `~/.cache/lm-studio/mcp.json`, while older releases and their +documentation use `~/.lmstudio/mcp.json`. ### Ollama (local models) @@ -519,7 +572,7 @@ npx -y summer-engine@latest doctor | `summer mcp setup ` | Write MCP config for an agent. | | `summer setup [--yes]` | One shot: MCP config + recommended skills + doctor. | -Agents: `claude-code`, `codex`, `cursor`, `windsurf`, `cline`, `roo-code`, `kilo-code`, `gemini`, `github-copilot`, `vscode-copilot`, `opencode`, `lm-studio`. (`devin` and `devin-desktop` are accepted as aliases for `windsurf`.) Scopes: `--scope user` (default), `--scope project`. +Agents: `claude-code`, `codex`, `cursor`, `windsurf`, `cline`, `roo-code`, `kilo-code`, `gemini`, `github-copilot`, `vscode-copilot`, `opencode`, `lm-studio`, `antigravity`. (`devin` and `devin-desktop` are accepted as aliases for `windsurf`.) Scopes: `--scope user` (default), `--scope project`. For OpenCode and Antigravity, passing `--project` without `--scope` defaults to project scope; use `--scope user` explicitly for a shared config. --- diff --git a/docs/SETUP_PROMPT.md b/docs/SETUP_PROMPT.md index 3ace653..fd182d1 100644 --- a/docs/SETUP_PROMPT.md +++ b/docs/SETUP_PROMPT.md @@ -1,6 +1,8 @@ # Copy-Paste Setup Prompt -Use this when a human wants to install Summer Engine from Claude Code, Cursor, Codex, Devin Desktop (formerly Windsurf), Cline, Roo Code, Gemini CLI, GitHub Copilot CLI, GitHub Copilot in VS Code, or OpenCode. +Use this when a human wants to connect Summer MCP from a supported client. The +MCP client and model provider are separate choices: OpenCode does not imply LM +Studio, and LM Studio can host MCP directly without OpenCode. Source: https://github.com/SummerEngine/summer-engine-agent MCP setup page: https://summerengine.com/mcp @@ -8,23 +10,38 @@ MCP setup page: https://summerengine.com/mcp Paste this into the AI environment: ```text -Install Summer Engine and let's make a game. +Set up Summer MCP for the agent I am using and the current project. Identify +the MCP client separately from its model provider, then run +`npx -y summer-engine@latest setup --yes --force --project "$PWD"` +with the correct supported client target. Use `opencode` for OpenCode, +`lm-studio` when chatting directly in LM Studio, and `antigravity` for +Antigravity. If the client target is uncertain, run setup help and use only a +listed target; do not invent one. Do not change my model provider, install the +engine app, sign me in, or create a project unless I explicitly ask. Run +doctor, show me the config path, and verify that `summer-engine` tools are +visible before changing the project. ``` -The agent should read the Summer README/install playbook, detect the current machine state, and run only the missing setup steps with `npx -y summer-engine@latest`. +The prompt deliberately contains the executable contract. A smaller model does +not need to infer the client/provider distinction or scrape a README first. Expected agent behavior: 1. Check Node.js 18+. -2. Run `npx -y summer-engine@latest doctor --json`. -3. Run `npx -y summer-engine@latest setup --yes --force` if skills or MCP config are missing or stale. -4. Run `npx -y summer-engine@latest install` if the engine app is missing. -5. Run `npx -y summer-engine@latest login` if the user is not signed in. -6. Create and run a starter project only after choosing a stable parent directory. -7. Use `summer:brainstorm-game` before building from a vague prompt. +2. Select the client target, never the model provider. +3. Run the exact `setup` command with an explicit project path. +4. Read the setup result and run `doctor --json`. +5. Reload the client and verify `summer-engine` plus a read-only tool call. +6. Stop and quote the exact error if tool discovery or the read-only call fails. -First-class setup targets: `claude-code`, `codex`, `cursor`, `windsurf`, `cline`, `roo-code`, `kilo-code`, `gemini`, `github-copilot`, `vscode-copilot`, `opencode`, `lm-studio`. +First-class setup targets: `claude-code`, `codex`, `cursor`, `windsurf`, `cline`, `roo-code`, `kilo-code`, `gemini`, `github-copilot`, `vscode-copilot`, `opencode`, `lm-studio`, `antigravity`. + +Common recipes are opt-in additions, not aliases for clients. For example, +OpenCode + LM Studio may add `--lm-studio-model `; plain OpenCode +setup must never write provider or model keys. Direct LM Studio setup uses the +`lm-studio` target and does not require OpenCode. Factory Droid uses its plugin marketplace path today. Other older-school or adjacent surfaces worth watching are Continue, Aider, Zed, JetBrains AI/Junie, Goose, and Amp; do not claim first-class Summer setup support for those until a real config target exists. -Manual terminal commands are still supported, but the primary onboarding path is the copy-paste prompt. This keeps users out of npm/global install details and lets their AI agent handle platform-specific setup. +Engine download, login, project creation, and provider configuration are +separate workflows. The MCP setup prompt must not silently broaden into them. diff --git a/package-lock.json b/package-lock.json index b99d16f..8637576 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "summer-engine", - "version": "2.7.0", + "version": "2.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "summer-engine", - "version": "2.7.0", + "version": "2.8.0", "license": "MIT", "dependencies": { "@ai-sdk/anthropic": "^3.0.0", diff --git a/package.json b/package.json index 2a6705d..7f04287 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "summer-engine", - "version": "2.7.0", + "version": "2.8.0", "description": "Local Summer CLI and MCP server for Summer Engine. Install and run the engine, connect Claude Code, Cursor, Codex, Gemini, and other agents, and build real games with bundled skills, hooks, and plugins.", "keywords": [ "summer-engine", diff --git a/references/antigravity.md b/references/antigravity.md new file mode 100644 index 0000000..3860174 --- /dev/null +++ b/references/antigravity.md @@ -0,0 +1,91 @@ +# Antigravity setup + +Antigravity is an MCP client. Its selected Google or third-party model is a +separate setting and Summer setup does not change it. + +## One-command project setup + +Update the terminal client, then open a terminal in the Summer project: + +```bash +agy update +``` + +Run: + +```bash +npx -y summer-engine@latest setup antigravity --yes --force --project "$PWD" +``` + +This creates or merges: + +- `.agents/mcp_config.json` with the `summer-engine` MCP server; +- `.agents/skills//SKILL.md` with recommended Summer guidance. + +Other MCP servers and unrelated JSON keys are preserved. The server command is +bound to the absolute project path, so it does not guess when several Summer +editors are open. Antigravity's model and provider settings are not changed. + +For a user-wide install, run without `--project` and add `--scope user`. The +equivalent global paths are `~/.gemini/config/mcp_config.json` and +`~/.gemini/config/skills/`. + +## Reload and verify + +In the desktop app, open Antigravity Settings > Customizations > MCP Servers. +In the terminal client, start `agy` from the configured project and run `/mcp`. + +1. Refresh or restart `summer-engine` and inspect its connection status. +2. Confirm the host itself lists Summer tools. `summer doctor` only proves the + server can register them; it does not prove Antigravity loaded them. +3. Keep MCP permissions in Ask mode for the first test. +4. Start a fresh conversation in the project and ask: + +```text +Use the summer-engine MCP server. Call summer_get_agent_playbook and read its +result. Then call summer_get_project_context and summer_get_scene_tree. Report +the exact project and scene you found. Do not change anything. If a call fails, +quote the error and stop. +``` + +Only proceed to mutations when the model can discover the server, call a tool, +consume its result, and report the correct project identity. + +In the tested `agy` 1.1.10 build, a normal interactive session loaded the +project-scoped server, called and consumed the playbook, project context, and +scene tree, then completed a reversible add, save, read-back, remove, save, and +clean-tree check through Summer MCP. + +Do not use `agy -p` (headless print mode) as the MCP readiness check in that +build. Print mode rejected the same working project server with +`tool ... is not enabled for server summer-engine`, while a normal interactive +`agy` session succeeded. If that exact error appears under `-p`, start `agy` +normally in the configured project, run `/mcp`, and test in a fresh interactive +conversation. If the normal session also rejects the call, stop before +mutations and report the error. + +## Manual configuration + +Current Antigravity versions use `.agents/mcp_config.json` for workspace setup +and `~/.gemini/config/mcp_config.json` for global setup. The file has a top-level +`mcpServers` object: + +```json +{ + "mcpServers": { + "summer-engine": { + "command": "npx", + "args": [ + "-y", + "summer-engine@latest", + "mcp", + "--project", + "/absolute/path/to/your-project" + ] + } + } +} +``` + +The older `.vscode/mcp.json` instructions describe a legacy IDE compatibility +path, not Antigravity's current native MCP configuration. Prefer `.agents`. diff --git a/references/lm-studio.md b/references/lm-studio.md new file mode 100644 index 0000000..34ef8f4 --- /dev/null +++ b/references/lm-studio.md @@ -0,0 +1,143 @@ +# LM Studio manual setup + +LM Studio can host MCP servers and run a local model, but it is not a full +coding agent. It does not read Summer skills from disk, choose a project folder, +or repair its own MCP configuration. This guide covers those manual steps. + +## Fast setup + +Open the Summer project in Summer Engine first. Then run this in a terminal: + +LM Studio Chat cannot execute this installer or repair its own MCP config. Do +not paste the coding-agent install prompt into the local model. Run the command +yourself in a normal terminal, or follow the in-app manual path below. + +```bash +npx -y summer-engine@latest setup lm-studio --yes --project "/absolute/path/to/your-project" +``` + +The setup command: + +- updates the active LM Studio `mcp.json` without replacing other servers; +- binds Summer tools to the named project; +- exposes the same complete Summer MCP tool registry used by other clients; +- prints the exact file it updated and the next LM Studio steps. + +You may omit `--project` when only one Summer editor is running. Keep it when +you regularly open more than one project. An unscoped MCP server fails closed +instead of guessing when multiple Summer editors are available. + +## Manual setup inside LM Studio + +Use this path when the setup command completed but `summer-engine` is not shown. +LM Studio has changed the physical config location between releases, so its +in-app editor is the source of truth. + +1. Open LM Studio once. +2. Open Chat > Integrations. Older releases call this the Program tab. +3. Choose Edit mcp.json. +4. Merge the `summer-engine` entry below into the existing `mcpServers` object. + Do not replace or delete other servers. +5. Replace the example project path with the absolute path to your project. + +```json +{ + "mcpServers": { + "summer-engine": { + "command": "npx", + "args": [ + "-y", + "summer-engine@latest", + "mcp", + "--project", + "/absolute/path/to/your-project" + ] + } + } +} +``` + +If LM Studio reports that `npx` cannot be found, run `command -v npx` on macOS +or Linux, or `where npx` on Windows, and use the returned absolute executable +path as `command`. + +## Load the model + +1. Load a model that supports multi-turn tool calling, not only JSON output. +2. Use at least 64k context. Small contexts can lose tool definitions or tool + results without an obvious error. +3. Enable only the `summer-engine` integration for the first test. +4. Keep tool confirmations enabled until you trust the model's behavior. + +Summer exposes one complete MCP tool registry to every client. A local model +therefore needs enough context for the tool schemas as well as the conversation; +64k is the recommended minimum. Keep only the `summer-engine` integration +enabled for the first test so unrelated MCP servers do not consume additional +context or complicate tool selection. + +## Safe first chat + +Do not begin with a mutation. Start a fresh chat with this read-only smoke test: + +```text +Use only the enabled Summer Engine tools. Call summer_get_agent_playbook once, +read the tool result, and summarize it. Then call summer_get_project_context +and summer_get_scene_tree and report the exact project and scene you found. +Do not change anything. If a tool fails, quote its error and do not guess. +``` + +Only continue when the model both calls the tools and successfully responds +after reading their results. A model that emits a tool call but cannot consume +the result is not ready to edit a project safely. + +For the first mutation test, ask it to add one `MeshInstance3D` with a +`BoxMesh`, verify that the node appears, then remove it again and verify the +removal with `summer_get_scene_tree`. Only ask for +`summer_get_script_errors` after the model has identified an actual `.gd` +file; that tool does not accept a `.tscn` scene path. This proves read, write, +save, and verification without leaving test content in the scene or baiting a +small model into an invalid diagnostic call. + +## Troubleshooting + +### The integration is missing + +Use LM Studio's in-app Edit mcp.json action and compare that file with the JSON +above. Do not assume `~/.lmstudio/mcp.json` is the active file. Current LM +Studio builds may use a runtime config under `~/.cache/lm-studio` instead. + +### Summer says multiple editors are running + +Add `--project` and the absolute project path to the MCP arguments, save +`mcp.json`, and restart or reload the integration. + +### The model talks about tools but never calls them + +Confirm that the model advertises tool use, raise context to 64k or more, start +a fresh chat, and keep only Summer enabled. A chat model that can emit JSON is +not necessarily a multi-turn tool-calling model. + +### A tool call works, then the model fails on the result + +Errors such as `Unknown test: sequence`, `Unknown ArrayValue filter: upper`, or +`Error rendering prompt with jinja template` come from the model's LM Studio +prompt template. They are not Summer operation failures. Update the model or +LM Studio, choose a model package with a tested tool template, or override the +prompt template in LM Studio's model settings. Do not keep retrying mutations: +the model cannot safely complete a tool loop in that state. + +The default prompt template shipped for the tested +`google/gemma-4-26b-a4b-qat` MLX package produced both errors in LM Studio +0.4.20. Replacing it with the compatible `chat_template.jinja` bundled in the +downloaded model fixed the tool loop; the same model then consumed Summer tool +results successfully. In the direct LM Studio smoke it also added a +`MeshInstance3D`, assigned a `BoxMesh`, verified the scene tree, removed the +node, and verified cleanup. Reducing the Summer tool count alone does not +repair a broken template. + +### Nothing changes in the engine + +Read the tool-call confirmation and result. A proposed tool call is not an +applied edit. The model must receive a successful mutation result and then +verify the scene tree. Run `npx -y summer-engine@latest doctor --json` when the +local editor connection itself is unhealthy. diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 81ca6d4..00da1a1 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -16,6 +16,7 @@ import { buildSkillsVersionCheck, defaultSkillMarkerCandidates, fetchLatestRegistryVersion, + type SkillMarkerCandidate, } from "../lib/version-check.js"; const require = createRequire(import.meta.url); @@ -47,9 +48,10 @@ export interface DoctorResult { }; } -interface DoctorOptions { +export interface DoctorOptions { json?: boolean; quiet?: boolean; + skillCandidates?: SkillMarkerCandidate[]; } const MAC_ENGINE_PATHS = [ @@ -92,7 +94,7 @@ export async function runDoctor(options: DoctorOptions = {}): Promise { }; } -function checkSkillsVersion(): DoctorCheck { +function checkSkillsVersion(candidates?: SkillMarkerCandidate[]): DoctorCheck { const result = buildSkillsVersionCheck({ installedCliVersion: version, - candidates: defaultSkillMarkerCandidates(), + candidates: candidates ?? defaultSkillMarkerCandidates(), }); return { id: "skills-version-stale", diff --git a/src/commands/mcp.ts b/src/commands/mcp.ts index 389ea72..bb057e8 100644 --- a/src/commands/mcp.ts +++ b/src/commands/mcp.ts @@ -2,7 +2,7 @@ import { Command } from "commander"; import { configureAgentMcp, parseAgent, - parseScope, + resolveAgentConfigScope, supportedAgents, } from "../lib/agent-config.js"; import { startMcpServer } from "../mcp/server.js"; @@ -27,10 +27,27 @@ export const mcpCommand = new Command("mcp") mcpCommand .command("setup ") .description("Configure an AI agent to use the Summer Engine MCP server") - .option("--scope ", "Configuration scope: user or project", "user") + .option( + "--scope ", + "Configuration scope: user or project (OpenCode + --project defaults to project)" + ) .option("--print", "Print the MCP config snippet instead of writing files") .option("--dry-run", "Show planned changes without writing files") .option("--local-dev", "Use the local built CLI instead of npx summer-engine") + .option("--project ", "Bind the MCP server entry to one Summer project") + .option( + "--lm-studio-model ", + "Configure OpenCode to use this loaded LM Studio model ID" + ) + .option( + "--lm-studio-url ", + "LM Studio OpenAI-compatible base URL", + "http://127.0.0.1:1234/v1" + ) + .option( + "--lm-studio-vision", + "Declare image input support for a vision-capable LM Studio model" + ) .option("--json", "Print the setup result as JSON") .action( async ( @@ -40,6 +57,10 @@ mcpCommand print?: boolean; dryRun?: boolean; localDev?: boolean; + project?: string; + lmStudioModel?: string; + lmStudioUrl?: string; + lmStudioVision?: boolean; json?: boolean; } ) => { @@ -48,10 +69,14 @@ mcpCommand throw new Error(`Unsupported agent. Use one of: ${supportedAgents.join(", ")}`); } - const scope = parseScope(opts.scope); + const projectPath = opts.project ?? mcpCommand.opts().project; + const scope = resolveAgentConfigScope(agent, opts.scope, projectPath); if (!scope) { throw new Error("Invalid --scope. Use user or project."); } + if (opts.lmStudioModel && agent !== "opencode") { + throw new Error("--lm-studio-model is only supported with `summer mcp setup opencode`."); + } const result = await configureAgentMcp({ agent, @@ -59,6 +84,14 @@ mcpCommand print: opts.print, dryRun: opts.dryRun, localDev: opts.localDev, + projectPath, + opencodeLmStudio: opts.lmStudioModel + ? { + modelId: opts.lmStudioModel, + baseUrl: opts.lmStudioUrl, + vision: opts.lmStudioVision, + } + : undefined, }); if (opts.json) { diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 46621e0..0af9887 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -6,7 +6,7 @@ import { SupportedAgent, configureAgentMcp, parseAgent, - parseScope, + resolveAgentConfigScope, supportedAgents, } from "../lib/agent-config.js"; import { DoctorResult, printDoctorResult, runDoctor } from "./doctor.js"; @@ -30,6 +30,7 @@ const AGENT_LABEL: Record = { "vscode-copilot": "GitHub Copilot in VS Code", opencode: "OpenCode", "lm-studio": "LM Studio", + antigravity: "Antigravity", }; interface SetupCommandOptions { @@ -40,6 +41,10 @@ interface SetupCommandOptions { yes?: boolean; json?: boolean; force?: boolean; + project?: string; + lmStudioModel?: string; + lmStudioUrl?: string; + lmStudioVision?: boolean; } interface SkillSetupResult { @@ -54,39 +59,79 @@ interface SkillInstallInvocation { command: string; args: string[]; display: string[]; + cwd?: string; } export const setupCommand = new Command("setup") .description("Configure Summer Engine for an AI agent and run diagnostics") .argument("[agent]", `Agent to configure: ${supportedAgents.join(", ")}`) .option("--agent ", `Agent to configure: ${supportedAgents.join(", ")}`) - .option("--scope ", "Configuration scope: user or project", "user") + .option( + "--scope ", + "Configuration scope: user or project (OpenCode/Antigravity + --project default to project)" + ) .option("--dry-run", "Show planned changes without writing files") .option("--print", "Print the MCP config snippet instead of writing files") .option("--yes", "Apply practical setup steps without prompting") .option("--json", "Print setup result as JSON") + .option("--project ", "Bind the MCP server entry to one Summer project") + .option( + "--lm-studio-model ", + "Configure OpenCode to use this loaded LM Studio model ID" + ) + .option( + "--lm-studio-url ", + "LM Studio OpenAI-compatible base URL", + "http://127.0.0.1:1234/v1" + ) + .option( + "--lm-studio-vision", + "Declare image input support for a vision-capable LM Studio model" + ) .option( "--force", "Overwrite existing skill content (passes --force through to skills install)" ) .action(async (agentArg: string | undefined, opts: SetupCommandOptions) => { const agent = resolveAgent(agentArg, opts.agent); - const scope = resolveScope(opts.scope); + const scope = resolveScope(agent, opts.scope, opts.project); + if (opts.lmStudioModel && agent !== "opencode") { + throw new Error("--lm-studio-model is only supported with `summer setup opencode`."); + } const config = await configureAgentMcp({ agent, scope, dryRun: opts.dryRun, print: opts.print, + projectPath: opts.project, + opencodeLmStudio: opts.lmStudioModel + ? { + modelId: opts.lmStudioModel, + baseUrl: opts.lmStudioUrl, + vision: opts.lmStudioVision, + } + : undefined, }); const skills = setupRecommendedSkills(agent, { dryRun: Boolean(opts.dryRun || opts.print), yes: Boolean(opts.yes), force: Boolean(opts.force), + scope, + projectPath: config.projectPath, }); - const doctor = await runDoctor({ quiet: true }); + const installedSkillsDir = + skills.status === "installed" ? parseSkillTargetDir(skills.stdout) : null; + const doctor = await runDoctor({ + quiet: true, + // Setup should validate the skills it just installed, not fail because an + // unrelated agent has an older global skill marker elsewhere on disk. + skillCandidates: installedSkillsDir + ? [{ agent, dir: installedSkillsDir }] + : [], + }); if (opts.json) { console.log( @@ -123,8 +168,12 @@ function resolveAgent(agentArg: string | undefined, agentOpt: string | undefined return parsed; } -function resolveScope(scopeOpt: string | undefined): ConfigScope { - const parsed = parseScope(scopeOpt); +function resolveScope( + agent: SupportedAgent, + scopeOpt: string | undefined, + projectPath: string | undefined +): ConfigScope { + const parsed = resolveAgentConfigScope(agent, scopeOpt, projectPath); if (!parsed) { throw new Error("Invalid --scope. Use user or project."); } @@ -133,7 +182,13 @@ function resolveScope(scopeOpt: string | undefined): ConfigScope { function setupRecommendedSkills( agent: SupportedAgent, - options: { dryRun: boolean; yes: boolean; force: boolean } + options: { + dryRun: boolean; + yes: boolean; + force: boolean; + scope: ConfigScope; + projectPath: string | null; + } ): SkillSetupResult { if (agent === "lm-studio") { return { @@ -143,7 +198,11 @@ function setupRecommendedSkills( }; } - const invocation = skillInstallInvocation(agent, { force: options.force }); + const invocation = skillInstallInvocation(agent, { + force: options.force, + scope: options.scope, + projectPath: options.projectPath, + }); if (!invocation) { return { @@ -164,6 +223,7 @@ function setupRecommendedSkills( const result = spawnSync(invocation.command, invocation.args, { env: process.env, encoding: "utf-8", + cwd: invocation.cwd, }); if (result.status === 0) { @@ -187,7 +247,11 @@ function setupRecommendedSkills( function skillInstallInvocation( agent: SupportedAgent, - opts: { force: boolean } = { force: false } + opts: { + force: boolean; + scope: ConfigScope; + projectPath: string | null; + } ): SkillInstallInvocation | null { const cliPath = process.argv[1]; if (!cliPath) return null; @@ -195,12 +259,24 @@ function skillInstallInvocation( const command = cliPath.endsWith(".js") ? process.execPath : cliPath; const prefix = cliPath.endsWith(".js") ? [cliPath] : []; - const baseArgs = ["skills", "install", "--recommended", "--agent", agent]; + const baseArgs = [ + "skills", + "install", + "--recommended", + "--agent", + agent, + "--scope", + opts.scope, + ]; if (opts.force) baseArgs.push("--force"); return { command, args: [...prefix, ...baseArgs], display: [cliPath, ...baseArgs], + cwd: + opts.scope === "project" && opts.projectPath + ? opts.projectPath + : undefined, }; } @@ -259,12 +335,29 @@ function printSetupResult( console.log(""); printDoctorResult(doctor); - if (doctor.ok && skills.status !== "failed" && !config.dryRun) { + if (config.agent === "lm-studio" && !config.dryRun) { console.log(""); + console.log(` ${c.bold("LM Studio next steps")}`); + for (const step of config.nextSteps) { + console.log(` - ${step}`); + } console.log( - `${c.dim("Try it:")} open ${AGENT_LABEL[config.agent] ?? config.agent} and ask: ${c.bold("\"add a DirectionalLight3D and Camera3D to my scene\"")}` + " - Manual setup and first prompt: https://github.com/SummerEngine/summer-engine-agent/blob/main/references/lm-studio.md" ); } + + if (doctor.ok && skills.status !== "failed" && !config.dryRun) { + console.log(""); + if (config.agent === "lm-studio") { + console.log( + `${c.dim("Safe first chat:")} ask: ${c.bold("\"call summer_get_agent_playbook, read its result, then inspect my project without changing it\"")}` + ); + } else { + console.log( + `${c.dim("Safe first chat:")} open ${AGENT_LABEL[config.agent] ?? config.agent} and ask: ${c.bold("\"call summer_get_agent_playbook, read it, then report my exact project and scene without changing anything\"")}` + ); + } + } } function parseInstalledSkills(stdout: string | undefined): string[] { diff --git a/src/commands/skills.ts b/src/commands/skills.ts index 876b4af..a2ac0e0 100644 --- a/src/commands/skills.ts +++ b/src/commands/skills.ts @@ -176,7 +176,8 @@ function resolveScope(agent: AgentClient, opts: InstallOptions): SkillScope { agent === "windsurf" || agent === "cline" || agent === "roo-code" || - agent === "kilo-code" + agent === "kilo-code" || + agent === "antigravity" ) { return "project"; } @@ -207,6 +208,8 @@ function agentLabel(agent: AgentClient): string { return "GitHub Copilot in VS Code"; case "opencode": return "OpenCode"; + case "antigravity": + return "Antigravity"; case "summer": return "Summer"; } @@ -290,6 +293,14 @@ function resolveInstallLocation( ? opencodeUserAgentsDir() : join(process.cwd(), ".opencode", "agents", "summer"), }; + case "antigravity": + return { + kind: "skill-dir", + path: + scope === "user" + ? join(homedir(), ".gemini", "config", "skills") + : join(process.cwd(), ".agents", "skills"), + }; case "summer": return { kind: "skill-dir", path: join(root, ".summer", "skills") }; } diff --git a/src/lib/agent-config.test.ts b/src/lib/agent-config.test.ts index 84a1f56..6dfba10 100644 --- a/src/lib/agent-config.test.ts +++ b/src/lib/agent-config.test.ts @@ -1,8 +1,13 @@ -import { mkdtempSync, readFileSync, writeFileSync } from "fs"; +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import { describe, expect, it } from "vitest"; -import { configureAgentMcp, createSummerMcpServerConfig, parseAgent } from "./agent-config.js"; +import { + configureAgentMcp, + createSummerMcpServerConfig, + parseAgent, + resolveAgentConfigScope, +} from "./agent-config.js"; function tmp(): string { return mkdtempSync(join(tmpdir(), "summer-agent-config-")); @@ -26,6 +31,32 @@ describe("parseAgent", () => { it("keeps windsurf as windsurf", () => { expect(parseAgent("windsurf")).toBe("windsurf"); }); + + it("maps current Antigravity client names", () => { + expect(parseAgent("antigravity")).toBe("antigravity"); + expect(parseAgent("antigravity-ide")).toBe("antigravity"); + expect(parseAgent("antigravity-cli")).toBe("antigravity"); + }); +}); + +describe("resolveAgentConfigScope", () => { + it("defaults OpenCode with an explicit project to project scope", () => { + expect(resolveAgentConfigScope("opencode", undefined, ".")).toBe("project"); + }); + + it("preserves an explicit OpenCode user scope", () => { + expect(resolveAgentConfigScope("opencode", "user", ".")).toBe("user"); + }); + + it("defaults Antigravity with an explicit project to project scope", () => { + expect(resolveAgentConfigScope("antigravity", undefined, ".")).toBe( + "project" + ); + }); + + it("keeps the existing user default for other agents", () => { + expect(resolveAgentConfigScope("codex", undefined, ".")).toBe("user"); + }); }); describe("createSummerMcpServerConfig", () => { @@ -34,6 +65,14 @@ describe("createSummerMcpServerConfig", () => { expect(server.command).toBe("npx"); expect(server.args).toEqual(NPX_ARGS); }); + + it("adds an explicit project when requested", () => { + const project = join(tmp(), "game"); + const server = createSummerMcpServerConfig(false, { + projectPath: project, + }); + expect(server.args).toEqual([...NPX_ARGS, "--project", project]); + }); }); describe("configureAgentMcp", () => { @@ -51,6 +90,42 @@ describe("configureAgentMcp", () => { expect(written.mcpServers["summer-engine"].args).toEqual(NPX_ARGS); }); + it("prefers the active LM Studio runtime config directory when present", async () => { + const home = tmp(); + const runtimeDir = join(home, ".cache", "lm-studio"); + mkdirSync(runtimeDir, { recursive: true }); + + const result = await configureAgentMcp({ + agent: "lm-studio", + scope: "user", + env: { HOME: home, USERPROFILE: home } as NodeJS.ProcessEnv, + }); + + expect(result.path).toBe(join(runtimeDir, "mcp.json")); + const written = JSON.parse(readFileSync(result.path, "utf-8")); + expect(written.mcpServers["summer-engine"].args).toEqual(NPX_ARGS); + }); + + it("binds an LM Studio config entry to an explicit project", async () => { + const dir = tmp(); + const path = join(dir, "mcp.json"); + const project = join(dir, "project"); + const result = await configureAgentMcp({ + agent: "lm-studio", + scope: "user", + projectPath: project, + env: { SUMMER_LM_STUDIO_CONFIG_FILE: path } as NodeJS.ProcessEnv, + }); + + const written = JSON.parse(readFileSync(path, "utf-8")); + expect(written.mcpServers["summer-engine"].args).toEqual([ + ...NPX_ARGS, + "--project", + project, + ]); + expect(result.projectPath).toBe(project); + }); + it("preserves unrelated keys when merging claude-code config", async () => { const dir = tmp(); const path = join(dir, ".claude.json"); @@ -319,5 +394,164 @@ describe("configureAgentMcp", () => { "summer-engine@latest", "mcp", ]); + expect(written.provider).toBeUndefined(); + expect(written.model).toBeUndefined(); + }); + + it("writes current Antigravity project MCP config without touching providers", async () => { + const dir = tmp(); + const project = join(dir, "game"); + mkdirSync(join(project, ".agents"), { recursive: true }); + const path = join(project, ".agents", "mcp_config.json"); + writeFileSync( + path, + JSON.stringify({ + theme: "preserve-me", + mcpServers: { other: { command: "node", args: ["other.js"] } }, + }) + ); + + const result = await configureAgentMcp({ + agent: "antigravity", + scope: "project", + cwd: dir, + projectPath: project, + env: {} as NodeJS.ProcessEnv, + }); + + expect(result.path).toBe(path); + const written = JSON.parse(readFileSync(path, "utf-8")); + expect(written.theme).toBe("preserve-me"); + expect(written.mcpServers.other.command).toBe("node"); + expect(written.mcpServers["summer-engine"].args).toEqual([ + ...NPX_ARGS, + "--project", + project, + ]); + expect(written.provider).toBeUndefined(); + expect(written.model).toBeUndefined(); + expect(result.nextSteps.join("\n")).toContain( + "normal interactive `agy` session" + ); + expect(result.nextSteps.join("\n")).toContain("`agy -p`"); + }); + + it("writes current Antigravity user MCP config path", async () => { + const home = tmp(); + const result = await configureAgentMcp({ + agent: "antigravity", + scope: "user", + env: { HOME: home, USERPROFILE: home } as NodeJS.ProcessEnv, + }); + + expect(result.path).toBe( + join(home, ".gemini", "config", "mcp_config.json") + ); + const written = JSON.parse(readFileSync(result.path, "utf-8")); + expect(written.mcpServers["summer-engine"].command).toBe("npx"); + }); + + it("configures an OpenCode LM Studio provider without changing the MCP surface", async () => { + const dir = tmp(); + const path = join(dir, "opencode.json"); + const result = await configureAgentMcp({ + agent: "opencode", + scope: "project", + cwd: dir, + projectPath: dir, + opencodeLmStudio: { + modelId: "google/gemma-4-26b-a4b-qat", + vision: true, + }, + env: { SUMMER_OPENCODE_CONFIG_FILE: path } as NodeJS.ProcessEnv, + }); + + const written = JSON.parse(readFileSync(path, "utf-8")); + expect(written.model).toBe("lmstudio/google/gemma-4-26b-a4b-qat"); + expect(written.small_model).toBe("lmstudio/google/gemma-4-26b-a4b-qat"); + expect(written.provider.lmstudio.npm).toBe("@ai-sdk/openai-compatible"); + expect(written.provider.lmstudio.options.baseURL).toBe( + "http://127.0.0.1:1234/v1" + ); + expect( + written.provider.lmstudio.models["google/gemma-4-26b-a4b-qat"] + ).toMatchObject({ + limit: { context: 131072, output: 8192 }, + modalities: { input: ["text", "image"], output: ["text"] }, + options: { reasoningEffort: "none" }, + }); + expect(written.mcp["summer-engine"].command).toEqual([ + "npx", + ...NPX_ARGS, + "--project", + dir, + ]); + }); + + it("does not advertise image input for a text-only LM Studio setup", async () => { + const dir = tmp(); + const path = join(dir, "opencode.json"); + + await configureAgentMcp({ + agent: "opencode", + scope: "project", + cwd: dir, + projectPath: dir, + opencodeLmStudio: { modelId: "text-only-model" }, + env: { SUMMER_OPENCODE_CONFIG_FILE: path } as NodeJS.ProcessEnv, + }); + + const written = JSON.parse(readFileSync(path, "utf-8")); + expect( + written.provider.lmstudio.models["text-only-model"].modalities + ).toBeUndefined(); + }); + + it("preserves unrelated OpenCode providers and LM Studio model keys", async () => { + const dir = tmp(); + const path = join(dir, "opencode.json"); + writeFileSync( + path, + JSON.stringify({ + provider: { + other: { npm: "other-provider" }, + lmstudio: { + options: { apiKey: "local-placeholder" }, + models: { + existing: { name: "Existing model" }, + "google/gemma-4-26b-a4b-qat": { + custom: true, + modalities: { input: ["text", "audio"], custom: true }, + }, + }, + }, + }, + }) + ); + + await configureAgentMcp({ + agent: "opencode", + scope: "user", + opencodeLmStudio: { + modelId: "google/gemma-4-26b-a4b-qat", + vision: true, + }, + env: { SUMMER_OPENCODE_CONFIG_FILE: path } as NodeJS.ProcessEnv, + }); + + const written = JSON.parse(readFileSync(path, "utf-8")); + expect(written.provider.other.npm).toBe("other-provider"); + expect(written.provider.lmstudio.options.apiKey).toBe("local-placeholder"); + expect(written.provider.lmstudio.models.existing.name).toBe("Existing model"); + expect( + written.provider.lmstudio.models["google/gemma-4-26b-a4b-qat"].custom + ).toBe(true); + expect( + written.provider.lmstudio.models["google/gemma-4-26b-a4b-qat"].modalities + ).toEqual({ + input: ["text", "image"], + output: ["text"], + custom: true, + }); }); }); diff --git a/src/lib/agent-config.ts b/src/lib/agent-config.ts index 0c66ac1..1740292 100644 --- a/src/lib/agent-config.ts +++ b/src/lib/agent-config.ts @@ -1,6 +1,6 @@ import { existsSync } from "fs"; import { mkdir, readFile, writeFile } from "fs/promises"; -import { dirname, join, resolve } from "path"; +import { delimiter, dirname, join, resolve } from "path"; import { fileURLToPath } from "url"; import { homedir, platform } from "os"; @@ -19,6 +19,7 @@ export const supportedAgents = [ "vscode-copilot", "opencode", "lm-studio", + "antigravity", ] as const; export type SupportedAgent = (typeof supportedAgents)[number]; @@ -30,12 +31,23 @@ export interface StdioMcpServerConfig { env?: Record; } +export interface OpencodeLmStudioConfig { + modelId: string; + baseUrl?: string; + context?: number; + output?: number; + vision?: boolean; + reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high"; +} + export interface AgentConfigOptions { agent: SupportedAgent; scope: ConfigScope; dryRun?: boolean; print?: boolean; localDev?: boolean; + projectPath?: string; + opencodeLmStudio?: OpencodeLmStudioConfig; cwd?: string; env?: NodeJS.ProcessEnv; } @@ -53,6 +65,8 @@ export interface AgentConfigResult { dryRun: boolean; print: boolean; localDev: boolean; + projectPath: string | null; + opencodeLmStudio: Required | null; warnings: string[]; nextSteps: string[]; } @@ -91,6 +105,9 @@ const agentAliases: Record = { lmstudio: "lm-studio", "lm-studio": "lm-studio", "lm_studio": "lm-studio", + antigravity: "antigravity", + "antigravity-ide": "antigravity", + "antigravity-cli": "antigravity", }; export function parseAgent(value: string | undefined): SupportedAgent | null { @@ -105,14 +122,78 @@ export function parseScope(value: string | undefined): ConfigScope | null { return null; } +export function resolveAgentConfigScope( + agent: SupportedAgent, + requestedScope: string | undefined, + projectPath: string | undefined +): ConfigScope | null { + if (requestedScope !== undefined) return parseScope(requestedScope); + if ((agent === "opencode" || agent === "antigravity") && projectPath) { + return "project"; + } + return "user"; +} + +function normalizeOpencodeLmStudio( + value: OpencodeLmStudioConfig | undefined +): Required | null { + if (!value) return null; + + const modelId = value.modelId.trim(); + if (!modelId) throw new Error("LM Studio model ID cannot be empty."); + + const baseUrl = (value.baseUrl ?? "http://127.0.0.1:1234/v1").replace(/\/$/, ""); + const parsedUrl = new URL(baseUrl); + if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") { + throw new Error("LM Studio URL must use http or https."); + } + + const context = value.context ?? 131072; + const output = value.output ?? 8192; + if (!Number.isInteger(context) || context < 65536) { + throw new Error("LM Studio context must be an integer of at least 65536."); + } + if (!Number.isInteger(output) || output < 1024) { + throw new Error("LM Studio output limit must be an integer of at least 1024."); + } + + return { + modelId, + baseUrl, + context, + output, + vision: value.vision ?? false, + reasoningEffort: value.reasoningEffort ?? "none", + }; +} + export async function configureAgentMcp( options: AgentConfigOptions ): Promise { const env = options.env ?? process.env; const cwd = resolve(options.cwd ?? process.cwd()); - const server = createSummerMcpServerConfig(Boolean(options.localDev)); - const target = resolveConfigTarget(options.agent, options.scope, cwd, env); - const snippet = renderConfigSnippet(options.agent, server); + const opencodeLmStudio = normalizeOpencodeLmStudio(options.opencodeLmStudio); + if (opencodeLmStudio && options.agent !== "opencode") { + throw new Error("LM Studio provider setup is only available for OpenCode."); + } + const projectPath = options.projectPath + ? resolve(cwd, options.projectPath) + : undefined; + const server = createSummerMcpServerConfig(Boolean(options.localDev), { + projectPath, + command: + options.agent === "lm-studio" && !options.localDev + ? findCommandOnPath("npx", env) + : undefined, + }); + const target = resolveConfigTarget( + options.agent, + options.scope, + cwd, + env, + projectPath + ); + const snippet = renderConfigSnippet(options.agent, server, opencodeLmStudio); const dryRun = Boolean(options.dryRun); const print = Boolean(options.print); const shouldWrite = !dryRun && !print; @@ -122,7 +203,7 @@ export async function configureAgentMcp( : target.format === "toml" ? await upsertCodexConfig(target.path, server, shouldWrite) : target.format === "json-opencode" - ? await upsertOpencodeConfig(target.path, server, shouldWrite) + ? await upsertOpencodeConfig(target.path, server, shouldWrite, opencodeLmStudio) : target.format === "json-copilot" ? await upsertCopilotConfig(target.path, server, shouldWrite) : target.format === "json-vscode" @@ -144,46 +225,44 @@ export async function configureAgentMcp( dryRun, print, localDev: Boolean(options.localDev), + projectPath: projectPath ?? null, + opencodeLmStudio, warnings: target.warnings, nextSteps: createNextSteps(options.agent, options.scope, target.path), }; } -export function createSummerMcpServerConfig(localDev: boolean): StdioMcpServerConfig { +export function createSummerMcpServerConfig( + localDev: boolean, + options: { projectPath?: string; command?: string } = {} +): StdioMcpServerConfig { + const args = ["mcp"]; + if (options.projectPath) args.push("--project", resolve(options.projectPath)); + if (localDev) { return { command: "node", - args: [resolveLocalCliPath(), "mcp"], + args: [resolveLocalCliPath(), ...args], }; } return { - command: "npx", - args: ["-y", "summer-engine@latest", "mcp"], + command: options.command ?? "npx", + args: ["-y", "summer-engine@latest", ...args], }; } export function renderConfigSnippet( agent: SupportedAgent, - server: StdioMcpServerConfig + server: StdioMcpServerConfig, + opencodeLmStudio: Required | null = null ): string { if (agent === "codex") { return renderCodexServerTable(server); } if (agent === "opencode") { - return ( - JSON.stringify( - { - $schema: "https://opencode.ai/config.json", - mcp: { - [SUMMER_MCP_SERVER_NAME]: opencodeServerEntry(server), - }, - }, - null, - 2 - ) + "\n" - ); + return `${JSON.stringify(opencodeConfigDocument(server, opencodeLmStudio), null, 2)}\n`; } if (agent === "github-copilot") { @@ -242,10 +321,12 @@ function resolveConfigTarget( agent: SupportedAgent, scope: ConfigScope, cwd: string, - env: NodeJS.ProcessEnv + env: NodeJS.ProcessEnv, + projectPath?: string ): { path: string; format: "json" | "toml" | "json-opencode" | "json-copilot" | "json-vscode"; warnings: string[] } { const override = getConfigPathOverride(agent, env); const warnings: string[] = []; + const projectRoot = projectPath ?? cwd; if (override) { if ( @@ -343,13 +424,14 @@ function resolveConfigTarget( } if (agent === "lm-studio") { + const path = lmStudioConfigPath(env); if (scope === "project") { warnings.push( - "LM Studio's MCP config is app-global (~/.lmstudio/mcp.json); treating as user scope." + `LM Studio's MCP config is app-global (${path}); treating as user scope. Use --project to bind the server entry to one Summer project.` ); } return { - path: join(homedir(), ".lmstudio", "mcp.json"), + path, format: "json", warnings, }; @@ -401,12 +483,23 @@ function resolveConfigTarget( path: scope === "user" ? opencodeUserConfigPath(env) - : join(cwd, "opencode.json"), + : join(projectRoot, "opencode.json"), format: "json-opencode", warnings, }; } + if (agent === "antigravity") { + return { + path: + scope === "user" + ? antigravityUserConfigPath(env) + : join(projectRoot, ".agents", "mcp_config.json"), + format: "json", + warnings, + }; + } + if (scope === "project") { warnings.push( "Devin Desktop (formerly Windsurf) documents MCP configuration as user-scoped; project scope writes .windsurf/mcp_config.json for teams that load workspace config." @@ -469,6 +562,41 @@ function vsCodeGlobalStoragePath( ); } +function lmStudioConfigPath(env: NodeJS.ProcessEnv): string { + const home = + platform() === "win32" + ? env.USERPROFILE ?? homedir() + : env.HOME ?? homedir(); + const runtimePath = join(home, ".cache", "lm-studio", "mcp.json"); + const documentedPath = join(home, ".lmstudio", "mcp.json"); + + if (existsSync(runtimePath) || existsSync(dirname(runtimePath))) { + return runtimePath; + } + if (existsSync(documentedPath) || existsSync(dirname(documentedPath))) { + return documentedPath; + } + return documentedPath; +} + +function findCommandOnPath( + command: string, + env: NodeJS.ProcessEnv +): string | undefined { + const pathValue = env.PATH; + if (!pathValue) return undefined; + const names = + platform() === "win32" ? [`${command}.cmd`, `${command}.exe`, command] : [command]; + for (const directory of pathValue.split(delimiter)) { + if (!directory) continue; + for (const name of names) { + const candidate = join(directory, name); + if (existsSync(candidate)) return candidate; + } + } + return undefined; +} + function opencodeUserConfigPath(env: NodeJS.ProcessEnv): string { const os = platform(); if (os === "win32") { @@ -479,6 +607,14 @@ function opencodeUserConfigPath(env: NodeJS.ProcessEnv): string { return join(xdg, "opencode", "opencode.json"); } +function antigravityUserConfigPath(env: NodeJS.ProcessEnv): string { + const home = + platform() === "win32" + ? env.USERPROFILE ?? homedir() + : env.HOME ?? homedir(); + return join(home, ".gemini", "config", "mcp_config.json"); +} + function vsCodeUserMcpPath(env: NodeJS.ProcessEnv): string { const os = platform(); if (os === "win32") { @@ -507,6 +643,7 @@ function getConfigPathOverride( if (agent === "github-copilot") return env.SUMMER_GITHUB_COPILOT_CONFIG_FILE; if (agent === "vscode-copilot") return env.SUMMER_VSCODE_COPILOT_CONFIG_FILE; if (agent === "opencode") return env.SUMMER_OPENCODE_CONFIG_FILE; + if (agent === "antigravity") return env.SUMMER_ANTIGRAVITY_CONFIG_FILE; return env.SUMMER_WINDSURF_MCP_CONFIG_FILE; } @@ -590,7 +727,8 @@ async function upsertCodexConfig( async function upsertOpencodeConfig( path: string, server: StdioMcpServerConfig, - write: boolean + write: boolean, + opencodeLmStudio: Required | null = null ): Promise<{ changed: boolean }> { const current = await readJsonConfig(path); const next = copyJsonObject(current); @@ -605,6 +743,10 @@ async function upsertOpencodeConfig( [SUMMER_MCP_SERVER_NAME]: opencodeServerEntry(server), }; + if (opencodeLmStudio) { + upsertOpencodeLmStudioProvider(next, opencodeLmStudio); + } + const currentRendered = renderJsonFile(current); const nextRendered = renderJsonFile(next); const changed = currentRendered !== nextRendered; @@ -698,6 +840,88 @@ function opencodeServerEntry(server: StdioMcpServerConfig): JsonObject { return entry; } +function opencodeConfigDocument( + server: StdioMcpServerConfig, + opencodeLmStudio: Required | null +): JsonObject { + const config: JsonObject = { + $schema: "https://opencode.ai/config.json", + mcp: { + [SUMMER_MCP_SERVER_NAME]: opencodeServerEntry(server), + }, + }; + if (opencodeLmStudio) { + upsertOpencodeLmStudioProvider(config, opencodeLmStudio); + } + return config; +} + +function upsertOpencodeLmStudioProvider( + config: JsonObject, + lmStudio: Required +): void { + const providers = isJsonObject(config.provider) ? config.provider : {}; + const currentProvider = isJsonObject(providers.lmstudio) ? providers.lmstudio : {}; + const currentOptions = isJsonObject(currentProvider.options) + ? currentProvider.options + : {}; + const currentModels = isJsonObject(currentProvider.models) ? currentProvider.models : {}; + const currentModelValue = currentModels[lmStudio.modelId]; + const currentModel = isJsonObject(currentModelValue) ? currentModelValue : {}; + const currentLimit = isJsonObject(currentModel.limit) ? currentModel.limit : {}; + const currentModelOptions = isJsonObject(currentModel.options) + ? currentModel.options + : {}; + const currentModalities = isJsonObject(currentModel.modalities) + ? currentModel.modalities + : {}; + + config.provider = { + ...providers, + lmstudio: { + ...currentProvider, + npm: "@ai-sdk/openai-compatible", + name: "LM Studio (local)", + options: { + ...currentOptions, + baseURL: lmStudio.baseUrl, + }, + models: { + ...currentModels, + [lmStudio.modelId]: { + ...currentModel, + name: + typeof currentModel.name === "string" + ? currentModel.name + : `${lmStudio.modelId} (local)`, + limit: { + ...currentLimit, + context: lmStudio.context, + output: lmStudio.output, + }, + ...(lmStudio.vision + ? { + modalities: { + ...currentModalities, + input: ["text", "image"], + output: ["text"], + }, + } + : {}), + options: { + ...currentModelOptions, + reasoningEffort: lmStudio.reasoningEffort, + }, + }, + }, + }, + }; + + const selectedModel = `lmstudio/${lmStudio.modelId}`; + config.model = selectedModel; + config.small_model = selectedModel; +} + function copilotServerEntry(server: StdioMcpServerConfig): JsonObject { const entry: JsonObject = { type: "local", @@ -860,7 +1084,7 @@ function createNextSteps( : agent === "kilo-code" ? "Restart VS Code so Kilo Code reloads its MCP config." : agent === "lm-studio" - ? "Open LM Studio, toggle on the summer-engine MCP server in the Program tab, and raise the loaded model's context length to 32k or higher." + ? "Open LM Studio, enable summer-engine under Chat > Integrations (called Program in older versions), and load a tool-calling model with at least 64k context. If the server is not listed, use LM Studio's in-app Edit mcp.json action and compare it with the updated path above." : agent === "gemini" ? "Run `gemini extensions enable summer-engine` (if not already enabled), then restart Gemini CLI." : agent === "github-copilot" @@ -869,6 +1093,8 @@ function createNextSteps( ? "Restart VS Code or run MCP: List Servers, then start summer-engine in Copilot Agent mode." : agent === "opencode" ? "Restart OpenCode so it reloads opencode.json." + : agent === "antigravity" + ? "For the terminal client, start a normal interactive `agy` session in this project and run /mcp; Antigravity CLI 1.1.10 print mode (`agy -p`) does not activate project MCP tools. In the desktop app, open Customizations > MCP Servers and refresh summer-engine. Keep tools in Ask mode for the first test." : "Restart Devin Desktop (formerly Windsurf) and refresh MCP servers from the agent settings."; const projectTrust = diff --git a/src/lib/skills-registry.ts b/src/lib/skills-registry.ts index 8ed3839..31450b7 100644 --- a/src/lib/skills-registry.ts +++ b/src/lib/skills-registry.ts @@ -11,6 +11,7 @@ export const AGENT_CLIENTS = [ "github-copilot", "vscode-copilot", "opencode", + "antigravity", ] as const; export type AgentClient = (typeof AGENT_CLIENTS)[number]; diff --git a/src/mcp/server.ts b/src/mcp/server.ts index cfd4389..594c279 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -259,6 +259,18 @@ export interface StartMcpServerOptions { cwd?: string; } +export function registerMcpTools(server: McpServer): void { + registerSceneTools(server); + registerDebugTools(server); + registerVisualTools(server); + registerProjectTools(server); + registerFileTools(server); + registerAssetTools(server); + registerGenerateTools(server); + registerCloudTools(server); + registerCreatorTools(server); +} + export async function startMcpServer( options: StartMcpServerOptions = {} ): Promise { @@ -293,15 +305,7 @@ export async function startMcpServer( server as unknown as { tool: (...args: unknown[]) => unknown } ); - registerSceneTools(server); - registerDebugTools(server); - registerVisualTools(server); - registerProjectTools(server); - registerFileTools(server); - registerAssetTools(server); - registerGenerateTools(server); - registerCloudTools(server); - registerCreatorTools(server); + registerMcpTools(server); // Fire-and-forget — never block tool registration on the npm registry. void probeBootDrift().catch((error) => { diff --git a/src/mcp/tools/asset-tools.test.ts b/src/mcp/tools/asset-tools.test.ts index 6f0f619..df152ac 100644 --- a/src/mcp/tools/asset-tools.test.ts +++ b/src/mcp/tools/asset-tools.test.ts @@ -152,7 +152,17 @@ describe("registerAssetTools", () => { })); globalThis.fetch = fetchMock as any; executeOpsMock.mockResolvedValue({ results: [{ ok: true }] }); - executeIdentityBoundOpsMock.mockResolvedValue({ results: [{ ok: true }, { ok: true }] }); + executeIdentityBoundOpsMock + .mockResolvedValueOnce({ + status: "ok", + terminalState: "applied", + results: [{ ok: true, op: "InstantiateScene" }], + }) + .mockResolvedValueOnce({ + status: "ok", + terminalState: "applied", + results: [{ ok: true, op: "SaveScene" }], + }); const { server, tools } = createFakeServer(); registerAssetTools(server as any); @@ -171,7 +181,8 @@ describe("registerAssetTools", () => { path: "res://assets/models/iron_sword.glb", }, ]); - expect(executeIdentityBoundOpsMock).toHaveBeenCalledWith( + expect(executeIdentityBoundOpsMock).toHaveBeenNthCalledWith( + 1, [ { op: "InstantiateScene", @@ -179,10 +190,14 @@ describe("registerAssetTools", () => { scene: "res://assets/models/iron_sword.glb", name: "HeroSword", }, - { op: "SaveScene" }, ], { scenePath: "res://main.tscn" }, ); + expect(executeIdentityBoundOpsMock).toHaveBeenNthCalledWith( + 2, + [{ op: "SaveScene" }], + { scenePath: "res://main.tscn" }, + ); expect(parseResult(result)).toMatchObject({ success: true, diff --git a/src/mcp/tools/asset-tools.ts b/src/mcp/tools/asset-tools.ts index 7c53a70..6ff70ee 100644 --- a/src/mcp/tools/asset-tools.ts +++ b/src/mcp/tools/asset-tools.ts @@ -2,6 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { getAuthToken } from "../../lib/auth.js"; import { getClient } from "../server.js"; +import { executeSceneMutation } from "./scene-mutation.js"; const GATEWAY_URL = process.env.SUMMER_GATEWAY_URL || "https://www.summerengine.com"; @@ -298,15 +299,14 @@ async function importResolvedAsset(args: { let addedToScene = false; let sceneReceipt: unknown = null; if (parent && asset.type === "3d_model") { - sceneReceipt = await client.executeIdentityBoundOps([ + sceneReceipt = await executeSceneMutation(client, scenePath!, [ { op: "InstantiateScene", parent, scene: importPath, name: sanitizeNodeName(name || asset.title), }, - { op: "SaveScene" }, - ], { scenePath }); + ]); const placementReceipts = (sceneReceipt as { results?: Array<{ ok?: boolean; error?: string }> })?.results ?? []; const placementFailure = placementReceipts.find((receipt) => receipt?.ok !== true); diff --git a/src/mcp/tools/project-tools.test.ts b/src/mcp/tools/project-tools.test.ts index f0a46c3..3032bb1 100644 --- a/src/mcp/tools/project-tools.test.ts +++ b/src/mcp/tools/project-tools.test.ts @@ -182,4 +182,5 @@ priority: locked expect(JSON.stringify(body)).toContain("projectMemory"); expect(JSON.stringify(body)).toContain("priority: locked"); }); + }); diff --git a/src/mcp/tools/project-tools.ts b/src/mcp/tools/project-tools.ts index 2ae0df7..d5377c3 100644 --- a/src/mcp/tools/project-tools.ts +++ b/src/mcp/tools/project-tools.ts @@ -226,10 +226,12 @@ anti-patterns, and recovery steps.`, "summer_stop -> stop when runtime verification is finished; editor scene mutations are not categorically blocked by a running game, but an existing game instance may need a restart to observe them", ], "4_interactive": [ - "To prove input-driven behavior (does jump/move/shoot actually work), you have two routes when the engine build supports them (both go through summer_batch as raw ops — see rawOpsViaBatch):", - "SimulateInput: inject a single action/key/mouse/axis into the RUNNING game, then re-check screenshot/debugger. Requires summer_play first.", - "RunVerification: spawn a hidden, disposable game instance that runs a GDScript probe (press inputs, read state, save frames) and dies — never touches the user's editor. Returns results.json + frames. Best for a scripted 'does X happen when I do Y' assertion.", - "If neither op is available on this engine build (failure_reason:'unsupported' or an unknown-op error comes back), do NOT fake it — ask the user to try the interaction and report what they see.", + "To prove input-driven behavior (does jump/move/shoot actually work), use RunVerification. It is the ONLY interactive route available to MCP, and it is a real one — do NOT hand this rung to the user.", + "RunVerification: spawn a hidden, disposable game instance that runs a GDScript probe (press inputs, read state, save frames) and dies — never touches the user's editor. Returns results.json + frames. Send it as a raw op through summer_batch (see rawOpsViaBatch).", + "Unlike the editor's own --headless mode, the verify instance renders REAL PIXELS, so save_frame() writes a real image and Performance.TIME_FPS reports a real number.", + "press()/key() are COROUTINES — 'await press(\"move_right\", 500)' or the hold never elapses and the input does nothing.", + "Assert on physics-frame-derived state (positions after N 'await get_tree().physics_frame'), which is reproducible. press(hold_ms) waits on wall clock, so distance-travelled jitters run to run — assert 'moved more than X', never an exact value.", + "SimulateInput is NOT reachable from MCP — see rawOpsViaBatch. Do not attempt it as a fallback.", ], }, // HONESTY — mirror the in-product agent's vision rules. A capture is @@ -263,8 +265,10 @@ anti-patterns, and recovery steps.`, // engine ops that have no dedicated tool are still reachable. rawOpsViaBatch: [ "summer_batch runs an array of raw engine ops in one undo group; each op is passed through untouched, so newer engine ops with no dedicated tool are still callable.", - "SimulateInput (drive the RUNNING game): summer_batch ops:[{op:'SimulateInput', type:'action', action:'jump', pressed:true}]. type is 'action' | 'key' | 'mouse_click' | 'axis'. summer_play first. Structured failure_reasons (incl 'unsupported' on older builds) come back verbatim — surface them.", - "RunVerification (hidden probe instance): summer_batch ops:[{op:'RunVerification', probe_source:'', max_seconds:20}]. probe_source extends SummerProbeBase and uses report()/save_frame()/press()/key()/finish(); returns {ok, results, frames, out_dir} or {ok:false, failure_reason: spawn_failed|timeout|bad_args|no_project}.", + "RunVerification (hidden probe instance): summer_batch ops:[{op:'RunVerification', probe_source:'', max_seconds:20}]. probe_source extends SummerProbeBase and uses report()/save_frame()/press()/key()/finish(); returns {ok, results, frames, out_dir} or {ok:false, failure_reason: spawn_failed|timeout|bad_args|no_project|probe_not_node}.", + "SimulateInput is NOT reachable this way, on any engine build. It needs the in-editor chat bridge's async reply channel; every queued caller (MCP, CLI) is answered with {ok:false, failure_reason:'unsupported_transport'}. Use RunVerification's press()/key() instead.", + "WriteFile and ReplaceText are rejected here by design — use summer_write_file / summer_replace_text so project identity, content guards and same-file ordering are enforced.", + "You do not need an engine op to run a shell command: your own host already has a shell. The engine binary that runs project scripts is at OS.get_executable_path() (on macOS, /Applications/Summer.app/Contents/MacOS/Summer); see the summer-cli headless-scripting skill.", "These are runtime ops, not scene mutations — the batch undo group is a harmless no-op for them.", ], recovery: [ @@ -274,7 +278,8 @@ anti-patterns, and recovery steps.`, "If save fails: use the returned scenePath/error to repair the exact cause. A running game alone is not a generic scene-mutation blocker.", "If a mutation is rejected with identity_mismatch: the engine switched projects — call summer_get_project_context to rebind (only if you meant to follow it), then retry.", "If a guarded file mutation is rejected with content mismatch: call summer_read_file again, review the new content, and retry with its new sha256 only if the edit is still valid.", - "If a run op or SimulateInput returns 'unsupported' / an unknown-op error: this engine build predates it — fall back to summer_play + summer_get_debugger_errors, or ask the user to interact.", + "If a run op returns 'unsupported' / an unknown-op error: this engine build predates it — fall back to summer_play + summer_get_debugger_errors.", + "If SimulateInput returns 'unsupported_transport': that is permanent for MCP, not a build gap. Rewrite the check as a RunVerification probe.", ], debugging: [ "Set SUMMER_MCP_DEBUG=1 in the MCP server's environment to log a structured stderr line per tool call (tool, ok, durationMs, terminalState, errorClass, failureReason, retried, boundProjectIdHash). With the flag OFF, only failures are logged. Use it to see exactly which op failed and why.", diff --git a/src/mcp/tools/scene-mutation.test.ts b/src/mcp/tools/scene-mutation.test.ts new file mode 100644 index 0000000..938c9a4 --- /dev/null +++ b/src/mcp/tools/scene-mutation.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("../server.js", () => ({ + getClient: vi.fn(), + resetClient: vi.fn(), +})); + +vi.mock("../../lib/telemetry.js", () => ({ + recordMcpSession: vi.fn(), +})); + +import { executeSceneMutation } from "./scene-mutation.js"; + +describe("executeSceneMutation", () => { + it("sends SaveScene as a separate single-op request", async () => { + const executeIdentityBoundOps = vi.fn() + .mockResolvedValueOnce({ + status: "ok", + terminalState: "applied", + appliedSeq: 10, + results: [{ ok: true, op: "AddNode" }], + }) + .mockResolvedValueOnce({ + status: "ok", + terminalState: "applied", + appliedSeq: 11, + results: [{ ok: true, op: "SaveScene" }], + }); + + const result = await executeSceneMutation( + { executeIdentityBoundOps } as never, + "res://main.tscn", + [{ op: "AddNode", parent: "./", type: "Node3D", name: "Marker" }], + { groupUndo: true }, + ) as Record; + + expect(executeIdentityBoundOps).toHaveBeenNthCalledWith(1, [ + { op: "AddNode", parent: "./", type: "Node3D", name: "Marker" }, + ], { groupUndo: true, scenePath: "res://main.tscn" }); + expect(executeIdentityBoundOps).toHaveBeenNthCalledWith(2, [ + { op: "SaveScene" }, + ], { groupUndo: true, scenePath: "res://main.tscn" }); + expect(result).toMatchObject({ + ok: true, + status: "ok", + terminalState: "applied", + appliedSeq: 11, + results: [ + { ok: true, op: "AddNode" }, + { ok: true, op: "SaveScene" }, + ], + }); + }); + + it("does not save when the mutation failed", async () => { + const failure = { + status: "error", + terminalState: "identity_mismatch", + results: [{ ok: false, op: "AddNode", error: "wrong project" }], + }; + const executeIdentityBoundOps = vi.fn().mockResolvedValue(failure); + + const result = await executeSceneMutation( + { executeIdentityBoundOps } as never, + "res://main.tscn", + [{ op: "AddNode" }], + ); + + expect(result).toBe(failure); + expect(executeIdentityBoundOps).toHaveBeenCalledTimes(1); + }); + + it("reports that the editor may contain unsaved changes when save fails", async () => { + const executeIdentityBoundOps = vi.fn() + .mockResolvedValueOnce({ + status: "ok", + terminalState: "applied", + results: [{ ok: true, op: "SetProp" }], + }) + .mockResolvedValueOnce({ + status: "error", + terminalState: "failed", + results: [{ ok: false, op: "SaveScene", error: "disk full" }], + }); + + const result = await executeSceneMutation( + { executeIdentityBoundOps } as never, + "res://main.tscn", + [{ op: "SetProp" }], + ) as Record; + + expect(result).toMatchObject({ + ok: false, + status: "error", + terminalState: "failed", + }); + expect(result.error).toContain("mutation applied"); + expect(result.error).toContain("unsaved changes"); + }); + + it("preserves an explicit save-as path in the single-op request", async () => { + const executeIdentityBoundOps = vi.fn().mockResolvedValue({ + status: "ok", + terminalState: "applied", + results: [{ ok: true, op: "SaveScene" }], + }); + + await executeSceneMutation( + { executeIdentityBoundOps } as never, + "res://main.tscn", + [{ op: "SaveScene", path: "res://levels/copy.tscn" }], + ); + + expect(executeIdentityBoundOps).toHaveBeenCalledOnce(); + expect(executeIdentityBoundOps).toHaveBeenCalledWith( + [{ op: "SaveScene", path: "res://levels/copy.tscn" }], + { scenePath: "res://main.tscn" }, + ); + }); +}); diff --git a/src/mcp/tools/scene-mutation.ts b/src/mcp/tools/scene-mutation.ts new file mode 100644 index 0000000..5d028f4 --- /dev/null +++ b/src/mcp/tools/scene-mutation.ts @@ -0,0 +1,86 @@ +import type { EngineApiClient } from "../../lib/api-client.js"; +import { extractOpError } from "./with-engine.js"; + +type Receipt = Record & { + results?: unknown[]; + terminalState?: string; + appliedSeq?: number; +}; + +function asReceipt(value: unknown): Receipt { + return value && typeof value === "object" && !Array.isArray(value) + ? value as Receipt + : {}; +} + +function combinedSuccess(mutationReceipt: Receipt | null, saveReceipt: Receipt): Receipt { + const mutationResults = Array.isArray(mutationReceipt?.results) ? mutationReceipt.results : []; + const saveResults = Array.isArray(saveReceipt.results) ? saveReceipt.results : []; + return { + ok: true, + status: "ok", + terminalState: saveReceipt.terminalState ?? mutationReceipt?.terminalState ?? "applied", + appliedSeq: saveReceipt.appliedSeq ?? mutationReceipt?.appliedSeq, + results: [...mutationResults, ...saveResults], + mutationReceipt, + saveReceipt, + }; +} + +/** + * Apply scene edits and persist them using the editor's transport contract. + * + * Current editors require SaveScene to be submitted as a single operation so + * it cannot block the editor thread. Older MCP clients batched it after the + * mutation, which makes the whole request fail before anything is applied. + */ +export async function executeSceneMutation( + client: EngineApiClient, + scenePath: string, + ops: Record[], + options?: Record, +): Promise { + const saveIndexes = ops + .map((op, index) => op.op === "SaveScene" ? index : -1) + .filter((index) => index >= 0); + if (saveIndexes.length > 1) { + throw new Error("A scene mutation batch may contain only one SaveScene"); + } + if (saveIndexes.length === 1 && saveIndexes[0] !== ops.length - 1) { + throw new Error("SaveScene must be the final operation in a scene mutation batch"); + } + + const saveOp = saveIndexes.length === 1 + ? ops[saveIndexes[0]!]! + : { op: "SaveScene" }; + const mutationOps = saveIndexes.length === 1 ? ops.slice(0, -1) : ops; + const scopedOptions = { ...(options ?? {}), scenePath }; + + let mutationReceipt: Receipt | null = null; + if (mutationOps.length > 0) { + mutationReceipt = asReceipt( + await client.executeIdentityBoundOps(mutationOps, scopedOptions), + ); + if (extractOpError(mutationReceipt)) return mutationReceipt; + } + + const saveReceipt = asReceipt( + await client.executeIdentityBoundOps([saveOp], scopedOptions), + ); + const saveError = extractOpError(saveReceipt); + if (saveError) { + if (!mutationReceipt) return saveReceipt; + return { + ...saveReceipt, + ok: false, + status: "error", + error: + `Scene mutation applied in the editor, but saving ${scenePath} failed: ${saveError} ` + + "The scene may contain unsaved changes; inspect or undo before retrying.", + mutationReceipt, + saveReceipt, + }; + } + + return combinedSuccess(mutationReceipt, saveReceipt); +} diff --git a/src/mcp/tools/scene-tools.test.ts b/src/mcp/tools/scene-tools.test.ts index 0a169d4..4dfca19 100644 --- a/src/mcp/tools/scene-tools.test.ts +++ b/src/mcp/tools/scene-tools.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from "vitest"; +import { z } from "zod"; vi.mock("../server.js", () => ({ getClient: vi.fn(), @@ -14,27 +15,32 @@ import { registerSceneTools } from "./scene-tools.js"; type RegisteredTool = { name: string; + schema: Record; handler: (args: Record) => Promise; }; -function batchTool(): RegisteredTool { +function sceneTool(toolName: string): RegisteredTool { const registered: RegisteredTool[] = []; registerSceneTools({ tool( name: string, _description: string, - _schema: Record, + schema: Record, handler: (args: Record) => Promise, ) { - registered.push({ name, handler }); + registered.push({ name, schema, handler }); return { name }; }, } as never); - const found = registered.find((candidate) => candidate.name === "summer_batch"); - if (!found) throw new Error("summer_batch was not registered"); + const found = registered.find((candidate) => candidate.name === toolName); + if (!found) throw new Error(`${toolName} was not registered`); return found; } +function batchTool(): RegisteredTool { + return sceneTool("summer_batch"); +} + describe("summer_batch file mutation boundary", () => { it.each(["WriteFile", "ReplaceText"])( "rejects raw %s so guarded dedicated tools cannot be bypassed", @@ -56,3 +62,84 @@ describe("summer_batch file mutation boundary", () => { }, ); }); + +describe("summer_batch weak-model compatibility", () => { + it("infers AddNode and SetProp for unambiguous individual-tool-shaped ops", async () => { + const executeIdentityBoundOps = vi.fn().mockResolvedValue({ + ok: true, + status: "ok", + terminalState: "applied", + results: [{ ok: true }, { ok: true }], + }); + vi.mocked(getClient).mockResolvedValue({ + getBoundProjectIdHash: () => "hash-a", + executeIdentityBoundOps, + } as never); + + const tool = sceneTool("summer_batch"); + const args = z.object(tool.schema).parse({ + scenePath: "res://main.tscn", + ops: [ + { + scenePath: "res://main.tscn", + parent: "./", + type: "MeshInstance3D", + name: "Cube", + }, + { + scenePath: "res://main.tscn", + path: "./Cube", + key: "mesh", + value: "BoxMesh", + }, + ], + }); + + await tool.handler(args); + + expect(executeIdentityBoundOps).toHaveBeenNthCalledWith( + 1, + [ + { op: "AddNode", parent: "./", type: "MeshInstance3D", name: "Cube" }, + { op: "SetProp", path: "./Cube", key: "mesh", value: "BoxMesh" }, + ], + { groupUndo: true, scenePath: "res://main.tscn" }, + ); + expect(executeIdentityBoundOps).toHaveBeenNthCalledWith( + 2, + [{ op: "SaveScene" }], + { groupUndo: true, scenePath: "res://main.tscn" }, + ); + }); +}); + +describe("summer_remove_node weak-model compatibility", () => { + it("accepts add-node-shaped name and parent aliases and resolves one exact path", async () => { + const executeIdentityBoundOps = vi.fn().mockResolvedValue({ + ok: true, + status: "ok", + terminalState: "applied", + results: [{ ok: true }], + }); + vi.mocked(getClient).mockResolvedValue({ + getBoundProjectIdHash: () => "hash-a", + executeIdentityBoundOps, + } as never); + + const tool = sceneTool("summer_remove_node"); + const args = z.object(tool.schema).parse({ + scenePath: "res://main.tscn", + parent: "./", + name: "Marker", + type: "Node3D", + }); + + await tool.handler(args); + + expect(executeIdentityBoundOps).toHaveBeenNthCalledWith( + 1, + [{ op: "RemoveNode", path: "./Marker" }], + { scenePath: "res://main.tscn" }, + ); + }); +}); diff --git a/src/mcp/tools/scene-tools.ts b/src/mcp/tools/scene-tools.ts index 12070c1..3dd34df 100644 --- a/src/mcp/tools/scene-tools.ts +++ b/src/mcp/tools/scene-tools.ts @@ -1,37 +1,9 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { withEngine } from "./with-engine.js"; +import { executeSceneMutation } from "./scene-mutation.js"; import { readFile } from "fs/promises"; import { join } from "path"; -import type { EngineApiClient } from "../../lib/api-client.js"; - -function sceneMutationOps(ops: Record[]): Record[] { - const saveIndexes = ops - .map((op, index) => op.op === "SaveScene" ? index : -1) - .filter((index) => index >= 0); - if (saveIndexes.length > 1) { - throw new Error("A scene mutation batch may contain only one SaveScene"); - } - if (saveIndexes.length === 1) { - if (saveIndexes[0] !== ops.length - 1) { - throw new Error("SaveScene must be the final operation in a scene mutation batch"); - } - return ops; - } - return [...ops, { op: "SaveScene" }]; -} - -function executeSceneMutation( - client: EngineApiClient, - scenePath: string, - ops: Record[], - options?: Record, -): Promise { - return client.executeIdentityBoundOps(sceneMutationOps(ops), { - ...(options ?? {}), - scenePath, - }); -} async function readMainSceneFromProject(projectPath?: string): Promise { if (!projectPath) return null; @@ -60,6 +32,59 @@ function requireSuccessfulOps(result: unknown, context: string): Record): Record { + if (typeof op.op === "string" && op.op.trim()) return op; + + const args = { ...op }; + delete args.scenePath; + + if ( + typeof args.parent === "string" && + typeof args.type === "string" && + typeof args.name === "string" + ) { + return { op: "AddNode", ...args }; + } + if ( + typeof args.path === "string" && + typeof args.key === "string" && + Object.hasOwn(args, "value") + ) { + return { op: "SetProp", ...args }; + } + + throw new Error( + "Each summer_batch item requires an op discriminator. " + + "For small-model compatibility, unambiguous AddNode {parent,type,name} and " + + "SetProp {path,key,value} items are inferred automatically.", + ); +} + export function registerSceneTools(server: McpServer): void { server.tool( "summer_create_scene", @@ -235,13 +260,23 @@ Use when you need to modify a sub-property of a resource, like: server.tool( "summer_remove_node", - "Remove a node from the scene tree. All children are removed too. Cannot remove the root node. Supports undo. Destructive operation: do not delete multiple top-level nodes unless the user explicitly requests destructive changes.", + `Remove a node from the scene tree. All children are removed too. Cannot remove the root node. Supports undo. + +Preferred arguments: { scenePath: "res://main.tscn", path: "./World/OldEnemy" }. +Compatibility arguments for small models: { scenePath, parent: "./World", name: "OldEnemy" } resolves to the same path. +Destructive operation: do not delete multiple top-level nodes unless the user explicitly requests destructive changes.`, { scenePath: z.string().describe("Target scene path, e.g. 'res://main.tscn'"), - path: z.string().describe("Node path to remove, e.g. './World/OldEnemy'"), + path: z.string().optional().describe("Preferred exact node path to remove, e.g. './World/OldEnemy'"), + parent: z.string().optional().describe("Compatibility alias: parent path used with name, e.g. './World'"), + name: z.string().optional().describe("Compatibility alias: direct child name used with parent, e.g. 'OldEnemy'"), }, - async ({ scenePath, path }) => - withEngine(async (client) => executeSceneMutation(client, scenePath, [{ op: "RemoveNode", path }])) + async ({ scenePath, path, parent, name }) => { + const resolvedPath = resolveRemoveNodePath(path, parent, name); + return withEngine(async (client) => + executeSceneMutation(client, scenePath, [{ op: "RemoveNode", path: resolvedPath }]) + ); + } ); server.tool( @@ -394,6 +429,11 @@ Each op in the array uses the same format as the individual tools: - {"op": "SetProp", "path": "Floor", "key": "mesh", "value": "PlaneMesh"} - {"op": "SetResourceProperty", "nodePath": "Floor", "resourceProperty": "mesh", "subProperty": "size", "value": "Vector2(20, 20)"} +The op discriminator is preferred and required for every other shape. For small +models, unambiguous individual-tool-shaped AddNode (parent, type, name) and +SetProp (path, key, value) items are inferred when op is omitted; a repeated +per-item scenePath is ignored in favor of the batch-level target. + RAW RUNTIME OPS (interactive verification — engine-build dependent; structured failure_reason incl "unsupported" passes through verbatim): - SimulateInput — drive the RUNNING game (summer_play first): {"op": "SimulateInput", "type": "action", "action": "jump", "pressed": true}. type is "action" | "key" | "mouse_click" | "axis". - RunVerification — spawn a hidden, disposable game instance that runs a GDScript probe and dies (never touches the editor): {"op": "RunVerification", "probe_source": "extends SummerProbeBase\\nfunc _ready(): await super._ready(); report('ok', true); finish()", "max_seconds": 20}. Returns {ok, results, frames, out_dir}. Probe API: report()/save_frame()/press()/key()/finish(). @@ -410,7 +450,8 @@ must appear exactly once and be the final operation.`, }, async ({ scenePath, ops }) => withEngine(async (client) => { - const rawFileMutation = ops.find((op) => { + const normalizedOps = ops.map(normalizeBatchOp); + const rawFileMutation = normalizedOps.find((op) => { const kind = String(op.op ?? ""); return kind === "WriteFile" || kind === "ReplaceText"; }); @@ -425,14 +466,14 @@ must appear exactly once and be the final operation.`, "SetProp", "SetResourceProperty", "ConnectSignal", "DisconnectSignal", "InstantiateScene", "SaveScene", "Undo", ]); - const needsScenePath = ops.some((op) => sceneMutations.has(String(op.op ?? ""))); + const needsScenePath = normalizedOps.some((op) => sceneMutations.has(String(op.op ?? ""))); if (needsScenePath && !scenePath) { throw new Error("summer_batch requires scenePath when ops contains scene mutations"); } const options = { groupUndo: true, ...(scenePath ? { scenePath } : {}) }; return needsScenePath - ? executeSceneMutation(client, scenePath!, ops as Record[], options) - : client.executeOps(ops as Record[], options); + ? executeSceneMutation(client, scenePath!, normalizedOps, options) + : client.executeOps(normalizedOps, options); }) ); } From fa6772f513e808a44e42ebe42692fe0608ef963e Mon Sep 17 00:00:00 2001 From: Velizar Seleznev Date: Thu, 6 Aug 2026 10:14:38 +0200 Subject: [PATCH 2/2] docs: reconcile 2.8 MCP registry --- AGENTS.md | 3 +- CHANGELOG.md | 2 ++ README.md | 7 +++++ references/mcp-tools-reference.md | 9 +++++- src/lib/public-product-language.test.ts | 41 +++++++++++++++++++------ 5 files changed, 51 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5a89ed0..a17cf3b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,8 +40,9 @@ technical compatibility reference for version-sensitive upstream APIs. - Project: `summer_get_project_context`, `summer_open_main_scene`, `summer_project_setting`, `summer_input_map_bind`, `summer_get_agent_playbook`. - Files: `summer_read_file`, `summer_write_file`, `summer_replace_text` (identity-bound; create-only or sha256-guarded writes). - 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`. -- Generation: `summer_generate_image`, `summer_generate_3d`, `summer_generate_audio`, `summer_generate_video`, `summer_generate_motion`, `summer_check_job`. +- Generation: `summer_get_studio_workflow`, `summer_generate_image`, `summer_slice_asset_sheet`, `summer_generate_3d`, `summer_generate_audio`, `summer_generate_video`, `summer_generate_motion`, `summer_check_job`. - Meta: `summer_start_game_task`. +- Cloud: `summer_cloud_init`, `summer_cloud_status`, `summer_cloud_push`, `summer_cloud_pull`, `summer_cloud_restore`, `summer_cloud_checkpoints`, `summer_cloud_conflicts`. - Creator: `summer_creator_publish`, `summer_creator_releases`, `summer_creator_logs`, `summer_creator_config`. Git, shell, and grep are not exposed. Project file reads and writes are exposed through identity-bound Summer tools; do not bypass them with host writes when MCP is available. External host tools cannot be technically blocked, so the agent must follow this rule. diff --git a/CHANGELOG.md b/CHANGELOG.md index 463a6c5..768b0c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,14 @@ All notable changes to summer-engine will be documented here. Following [Keep a ## [2.8.0] — 2026-08-03 — "One-command MCP onboarding" ### Added +- The complete MCP registry grows from the 58 tools registered by the published 2.7.0 package to 62 tools. The only new registrations are `summer_creator_publish`, `summer_creator_releases`, `summer_creator_logs`, and `summer_creator_config`. - MCP discovers every live Summer editor through `~/.summer/instances/` and automatically binds local tools to the editor whose project contains the agent's current working directory. - `summer mcp --project ` and `summer mcp --instance ` provide explicit selection for hosts that do not start the MCP server from a project directory. - OpenCode setup can configure a loaded LM Studio model alongside the unchanged complete Summer MCP tool registry with `--lm-studio-model `, with opt-in screenshot input through `--lm-studio-vision`. - `summer setup antigravity` writes Antigravity's current project or user MCP configuration and installs Summer skills into its native `.agents` or `~/.gemini/config` directories. ### Changed +- The onboarding and routing work in this release does not add or remove MCP registrations relative to its 2.8 development base: both the base and this change expose the same 62-tool registry. It changes client setup, editor selection, compatibility handling, and documentation. - Multiple live editors are now a fail-closed state when no project can be inferred. MCP lists the non-secret project/instance choices instead of following the machine-global last-opened editor pointer. - Selected MCP sessions keep following the same project across editor restarts and validate registry identity against `/api/health` before connecting. - OpenCode setup now treats `--project` as project scope unless `--scope user` is explicit, and the OpenCode guide includes a complete local-model configuration and verification path. diff --git a/README.md b/README.md index 0565070..d4800ec 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,13 @@ requiring the editor: | Assets | `summer_search_assets`, `summer_import_asset`, `summer_import_from_url`, `summer_generate_image`, `summer_generate_3d`, `summer_generate_audio`, `summer_generate_video` | | Creator | `summer_creator_publish`, `summer_creator_releases`, `summer_creator_logs`, `summer_creator_config` — confirmed immutable publishing, real release history, explicit unsupported logs, and non-secret shared configuration | +The 62 total is the complete `tools/list` surface of the `summer-engine` 2.8 candidate, +not the smaller local-operation registry exposed by a particular desktop engine +build. The published 2.7.0 package registers 58 tools; 2.8 adds exactly four: +`summer_creator_publish`, `summer_creator_releases`, `summer_creator_logs`, and +`summer_creator_config`. The 2.8 onboarding and editor-routing changes do not +add further tools. + Git, shell, and grep remain host-native. Project file reads and writes use Summer's identity-bound tools so compatible engine builds can reject wrong-project and stale-content mutations. A host agent can still bypass these safeguards with its own file tools; the package cannot technically intercept that external process. When several Summer editors are open, MCP automatically selects the one whose diff --git a/references/mcp-tools-reference.md b/references/mcp-tools-reference.md index bedd0f3..8f794fe 100644 --- a/references/mcp-tools-reference.md +++ b/references/mcp-tools-reference.md @@ -103,11 +103,18 @@ | `summer_import_asset` | Search, choose the top match, download, run Godot import, and optionally instantiate 3D models. | | `summer_import_asset_by_id` | Import one exact Summer asset ID. Use after generation jobs or when the user selects a specific asset. | -### Asset generation (5 — metered) +### Studio workflows (1) + +| Tool | Use | +|---|---| +| `summer_get_studio_workflow` | List Studio's guided workflow recipes, or return the exact steps, required tools, support level, and limitations for one workflow. | + +### Asset generation and processing (6 — generation is metered) | Tool | Use | |---|---| | `summer_generate_image` | AI image gen. | +| `summer_slice_asset_sheet` | Detect, crop, name, and upload the distinct assets in an existing Summer image asset sheet. | | `summer_generate_3d` | Image-to-3D. | | `summer_generate_audio` | SFX / music gen. | | `summer_generate_video` | Video gen. | diff --git a/src/lib/public-product-language.test.ts b/src/lib/public-product-language.test.ts index f3c5ed5..454f569 100644 --- a/src/lib/public-product-language.test.ts +++ b/src/lib/public-product-language.test.ts @@ -18,6 +18,28 @@ const PUBLIC_ROOTS = [ ] as const; const TEXT_EXTENSIONS = new Set([".md", ".json", ".ts"]); +async function registeredMcpToolNames(): Promise { + const toolsDir = join(ROOT, "src/mcp/tools"); + const toolFiles = (await readdir(toolsDir)) + .filter((name) => name.endsWith("-tools.ts") && !name.endsWith(".test.ts")); + const names = new Set(); + for (const file of toolFiles) { + const source = await readFile(join(toolsDir, file), "utf8"); + for (const match of source.matchAll( + /\bserver\.tool\(\s*["'](summer_[a-z0-9_]+)["']/g + )) { + names.add(match[1]); + } + } + return [...names].sort(); +} + +function documentedMcpToolNames(text: string): string[] { + return [...new Set( + [...text.matchAll(/`(summer_[a-z0-9_]+)`/g)].map((match) => match[1]) + )].sort(); +} + async function publicTextFiles(path: string): Promise { const absolute = join(ROOT, path); const entries = await readdir(absolute, { withFileTypes: true }).catch(() => []); @@ -74,15 +96,8 @@ describe("Summer-first public product language", () => { }); it("keeps the public MCP total aligned with registered source tools", async () => { - const toolsDir = join(ROOT, "src/mcp/tools"); - const toolFiles = (await readdir(toolsDir)) - .filter((name) => name.endsWith("-tools.ts") && !name.endsWith(".test.ts")); - let registered = 0; - for (const file of toolFiles) { - const source = await readFile(join(toolsDir, file), "utf8"); - registered += source.match(/\bserver\.tool\(/g)?.length ?? 0; - } - expect(registered).toBe(62); + const registered = await registeredMcpToolNames(); + expect(registered).toHaveLength(62); for (const path of [ "README.md", @@ -98,4 +113,12 @@ describe("Summer-first public product language", () => { expect(text, path).not.toMatch(/\b(?:56|60)(?: tools|-tool)/); } }); + + it("keeps canonical tool inventories identical to registered source names", async () => { + const registered = await registeredMcpToolNames(); + for (const path of ["AGENTS.md", "references/mcp-tools-reference.md"]) { + const text = await readFile(join(ROOT, path), "utf8"); + expect(documentedMcpToolNames(text), path).toEqual(registered); + } + }); });