summer-engine 3.5.1 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +3 -2
  3. package/.codex-plugin/plugin.json +4 -3
  4. package/.cursor-plugin/plugin.json +3 -2
  5. package/.factory-plugin/plugin.json +3 -2
  6. package/CHANGELOG.md +22 -1
  7. package/commands/summer.md +1 -0
  8. package/dist/core/capabilities/sfx-choices.d.ts +91 -0
  9. package/dist/core/capabilities/sfx-choices.js +291 -0
  10. package/dist/core/capabilities/tool-dispatch.js +35 -0
  11. package/dist/mcp/hosted-mount.js +2 -1
  12. package/dist/mcp/provider-safe-schema.d.ts +10 -0
  13. package/dist/mcp/provider-safe-schema.js +96 -0
  14. package/dist/mcp/server.js +3 -0
  15. package/dist/mcp/tools/generate-tools.js +70 -3
  16. package/gemini-extension.json +2 -1
  17. package/library/references/mcp-tools-reference/mcp-tools-reference.md +5 -3
  18. package/library/references/mcp-tools-reference/resource.yaml +1 -1
  19. package/library/skills/README.md +4 -3
  20. package/library/skills/ambient-bed/SKILL.md +3 -1
  21. package/library/skills/ambient-bed/resource.yaml +1 -1
  22. package/library/skills/audio-direction/SKILL.md +5 -5
  23. package/library/skills/audio-direction/resource.yaml +1 -1
  24. package/library/skills/export-and-ship/SKILL.md +4 -0
  25. package/library/skills/make-game/SKILL.md +21 -2
  26. package/library/skills/make-game/resource.yaml +1 -0
  27. package/library/skills/multiplayer/SKILL.md +3 -1
  28. package/library/skills/multiplayer/resource.yaml +1 -0
  29. package/library/skills/multiplayer-movement/SKILL.md +1 -1
  30. package/library/skills/multiplayer-movement/files.md +2 -3
  31. package/library/skills/multiplayer-project/SKILL.md +20 -4
  32. package/library/skills/multiplayer-project/files.md +2 -3
  33. package/library/skills/multiplayer-publish/SKILL.md +90 -5
  34. package/library/skills/multiplayer-publish/resource.yaml +1 -0
  35. package/library/skills/multiplayer-state/SKILL.md +1 -1
  36. package/library/skills/multiplayer-state/files.md +2 -3
  37. package/library/skills/multiplayer-testing/SKILL.md +13 -0
  38. package/library/skills/multiplayer-testing/resource.yaml +1 -0
  39. package/library/skills/publish-your-game/SKILL.md +2 -0
  40. package/library/skills/release-readiness/SKILL.md +437 -0
  41. package/library/skills/release-readiness/resource.yaml +44 -0
  42. package/library/skills/sound-effect/SKILL.md +83 -158
  43. package/library/skills/sound-effect/resource.yaml +8 -2
  44. package/library/skills/summer-parties/SKILL.md +1 -1
  45. package/library/skills/ui-basics/SKILL.md +1 -0
  46. package/library/tools/generate-audio/resource.yaml +6 -2
  47. package/library/tools/generate-sfx-choices/resource.yaml +69 -0
  48. package/library/tools/get-choices/resource.yaml +50 -0
  49. package/package.json +1 -1
  50. package/registry/generated/aliases.json +2 -0
  51. package/registry/generated/counts.json +3 -3
  52. package/registry/generated/gemini-extension.json +2 -1
  53. package/registry/generated/index.json +163 -24
  54. package/registry/generated/marketplace.claude.json +2 -2
  55. package/registry/generated/plugin.claude.json +3 -2
  56. package/registry/generated/plugin.codex.json +4 -3
  57. package/registry/generated/plugin.cursor.json +3 -2
  58. package/registry/generated/plugin.factory.json +3 -2
  59. package/registry/generated/skills-index.md +4 -3
  60. package/registry/generated/skills-registry.json +15 -1
@@ -11,8 +11,8 @@
11
11
  {
12
12
  "name": "summer",
13
13
  "source": "./",
14
- "version": "3.5.1",
15
- "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 105-tool MCP bridge to the local desktop app.",
14
+ "version": "3.6.0",
15
+ "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 107-tool MCP bridge to the local desktop app.",
16
16
  "author": {
17
17
  "name": "Summer Engine",
18
18
  "email": "founders@summerengine.com"
@@ -2,8 +2,8 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "_generated": "GENERATED from integrations/claude — do not edit; npm run generate:registry",
4
4
  "name": "summer",
5
- "version": "3.5.1",
6
- "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 105-tool MCP bridge to the local desktop app.",
5
+ "version": "3.6.0",
6
+ "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 107-tool MCP bridge to the local desktop app.",
7
7
  "author": {
8
8
  "name": "Summer Engine",
9
9
  "email": "founders@summerengine.com",
@@ -96,6 +96,7 @@
96
96
  "./library/skills/psx-retro-rendering/",
97
97
  "./library/skills/publish-your-game/",
98
98
  "./library/skills/realtime-wet-surfaces/",
99
+ "./library/skills/release-readiness/",
99
100
  "./library/skills/remote-deploy/",
100
101
  "./library/skills/retarget/",
101
102
  "./library/skills/running-in-the-cloud/",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "_generated": "GENERATED from integrations/codex — do not edit; npm run generate:registry",
3
3
  "name": "summer",
4
- "version": "3.5.1",
5
- "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 105-tool MCP bridge. Build games by talking.",
4
+ "version": "3.6.0",
5
+ "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 107-tool MCP bridge. Build games by talking.",
6
6
  "author": {
7
7
  "name": "Summer Engine",
8
8
  "email": "founders@summerengine.com",
@@ -91,6 +91,7 @@
91
91
  "./library/skills/psx-retro-rendering/",
92
92
  "./library/skills/publish-your-game/",
93
93
  "./library/skills/realtime-wet-surfaces/",
94
+ "./library/skills/release-readiness/",
94
95
  "./library/skills/remote-deploy/",
95
96
  "./library/skills/retarget/",
96
97
  "./library/skills/running-in-the-cloud/",
@@ -150,7 +151,7 @@
150
151
  "interface": {
151
152
  "displayName": "Summer",
152
153
  "shortDescription": "Superpowers for AI game dev. Build games by talking.",
153
- "longDescription": "Summer turns Codex into a game-dev studio for Summer Engine. Users make Summer games with the Summer SDK and GDScript. The plugin adds game-dev skills, lifecycle hooks, and a 105-tool MCP bridge for guarded project files, scene editing, asset import, play mode, diagnostics, generation, and creator workflows. Git, shell, and grep stay with the host agent. Summer follows its upstream technical base continuously; version-sensitive work uses the bundled compatibility reference instead of a fixed onboarding version. Session-start orients the session. Pre-commit doctor (opt-in) blocks bad commits.",
154
+ "longDescription": "Summer turns Codex into a game-dev studio for Summer Engine. Users make Summer games with the Summer SDK and GDScript. The plugin adds game-dev skills, lifecycle hooks, and a 107-tool MCP bridge for guarded project files, scene editing, asset import, play mode, diagnostics, generation, and creator workflows. Git, shell, and grep stay with the host agent. Summer follows its upstream technical base continuously; version-sensitive work uses the bundled compatibility reference instead of a fixed onboarding version. Session-start orients the session. Pre-commit doctor (opt-in) blocks bad commits.",
154
155
  "developerName": "Summer Engine",
155
156
  "category": "Coding",
156
157
  "capabilities": [
@@ -3,8 +3,8 @@
3
3
  "_generated": "GENERATED from integrations/cursor — do not edit; npm run generate:registry",
4
4
  "name": "summer",
5
5
  "displayName": "Summer",
6
- "version": "3.5.1",
7
- "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 105-tool MCP bridge to the local desktop app.",
6
+ "version": "3.6.0",
7
+ "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 107-tool MCP bridge to the local desktop app.",
8
8
  "author": {
9
9
  "name": "Summer Engine",
10
10
  "email": "founders@summerengine.com"
@@ -103,6 +103,7 @@
103
103
  "./library/skills/psx-retro-rendering/",
104
104
  "./library/skills/publish-your-game/",
105
105
  "./library/skills/realtime-wet-surfaces/",
106
+ "./library/skills/release-readiness/",
106
107
  "./library/skills/remote-deploy/",
107
108
  "./library/skills/retarget/",
108
109
  "./library/skills/running-in-the-cloud/",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "_generated": "GENERATED from integrations/factory — do not edit; npm run generate:registry",
3
3
  "name": "summer",
4
- "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 105-tool MCP bridge to the local desktop app.",
5
- "version": "3.5.1",
4
+ "description": "Agent tooling for Summer Engine: game-dev skills, lifecycle hooks, and a 107-tool MCP bridge to the local desktop app.",
5
+ "version": "3.6.0",
6
6
  "author": {
7
7
  "name": "Summer Engine",
8
8
  "email": "founders@summerengine.com",
@@ -88,6 +88,7 @@
88
88
  "./library/skills/psx-retro-rendering/",
89
89
  "./library/skills/publish-your-game/",
90
90
  "./library/skills/realtime-wet-surfaces/",
91
+ "./library/skills/release-readiness/",
91
92
  "./library/skills/remote-deploy/",
92
93
  "./library/skills/retarget/",
93
94
  "./library/skills/running-in-the-cloud/",
package/CHANGELOG.md CHANGED
@@ -2,7 +2,28 @@
2
2
 
3
3
  All notable changes to summer-engine will be documented here. Following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/).
4
4
 
5
- ## [Unreleased]
5
+ ## [3.6.0] - 2026-10-12
6
+
7
+ Pick your sound effects by ear, and hosted games: agents check that players can actually join before a release.
8
+
9
+ - **Three versions of a sound effect, picked in Summer Studio.** The best sound-effect model changes a lot from one sound to the next. The first time you ask for a sound effect in a chat, the `sound-effect` skill now asks whether you want three versions from three models. If you say yes, the new `summer_generate_sfx_choices` makes them and shows them on a Summer Studio page: in your open Studio tab when you are signed in to the store, else in your browser, else as a link. You listen and approve one or more; `summer_get_choices` tells the agent which ones, and it imports and wires them. The model you approve becomes your default for later sound effects.
10
+ - `summer_generate_audio` sound effects take `modelId` (`eleven_text_to_sound_v2`, `sonilo-sfx-1.1`, `stable-audio-3-sfx`; left out, your approved default runs, else ElevenLabs) and `loop` for seamless loops (ElevenLabs only). Existing calls work as before.
11
+ - The `sound-effect` skill has prompt shapes that held up in testing: a Foley one-shot with the materials, the attack and the tail; exact event times for sequences such as a rifle reload; "air movement only" for whooshes; and the minimum length of each model. `ambient-bed` names the looping model, and `audio-direction` makes its first sounds through `sound-effect`.
12
+
13
+ - **`release-readiness` check 10: a signed-in player keeps playing for three minutes.** A released game closed for every player about a minute in, with "That link can't be opened", and left them signed out: a Summer service refused the player's sign-in and the app ended play. The fault was Summer's, and no check on the game's files could see it. The check asks the user to play the released version for 3 minutes in each Summer Games app, through an analytics event, and to report a close or sign-out to Summer with the time. `multiplayer-publish` step 6 points to it.
14
+ - **`release-readiness` checks 8 and 9: every file the game reads is in the pack, and the export is on the apps' Summer version line.** A release's uploaded pack left out a font file the game read with `FileAccess`; it fell back to `SystemFont`, which draws every glyph as a box on iPhone, while desktop looked right. Check 8 traces every by-path read, checks the summer.games preset's non-resource filter, lists `client.pck` with a short `--script` probe, bans `SystemFont` for phone text and asks for a look at the text on an iPhone. Another release was a 0.6 export, and a Summer Games phone app that carries 0.7 refused it with "The game could not start" while desktop ran it. Check 9 compares the export's MAJOR.MINOR with the current Summer release. `export-and-ship`, `ui-basics` and `multiplayer-publish` point to both.
15
+ - **`release-readiness` check 7: export with a released Summer, and prove the scripts compile on the runtime players get.** Players' apps run a bundle on the released Summer of the version its export stamp names. A development build that still stamps an older released version can use APIs that release lacks; its scripts then fail to compile on every player's device (`Parse Error: Static function "…" not found`, `Failed to load script`) and the game opens to an empty window, while every upload check passes. The check reads the stamp from `summer-bundle.json`, compares `engineSha` with the release's, and runs `client.pck` on that released Summer with `--headless --verbose --main-pack`, failing on `SCRIPT ERROR`, `Parse Error` or `Failed to load script`. `multiplayer-publish` adds the run as step 4e, and `export-and-ship` says to export summer.games bundles with the released editor only.
16
+
17
+ - **New `release-readiness` skill.** Before a version reaches players, it runs six checks on the summer.games `.zip` you upload: the bundle's `server.pck`, started with no scene, loads the authority scene; a real online join with the released version reaches the game; online play is reachable by default; the server saves online progress through player data; players can get back to the title; and `Summer.initialize` passes no hard-coded Game ID. It reports pass, fail, not applicable or not run for each, and grows by one check after every release that fails in a way none of them caught. The same checklist is in the docs at `/publishing/release-readiness`.
18
+ - **The multiplayer skills call `Summer.initialize()` with no Game ID.** The summer.games bundle leaves `gameId` out, so an ID in code (or a fallback when it is missing) fails on the hosted server with `game_identity_mismatch` and every online join ends in `world_start_timeout`.
19
+ - `multiplayer-publish` explains why the server preset needs the `summer_authority` custom feature, adds a pre-upload step that runs the bundle's `server.pck` with no scene, a real online join after release, and what an empty server log does and doesn't mean. `multiplayer-testing` lists what Local Play can't catch; `make-game` plans online play reachable by default, server-saved progress and a way back to the title, and runs `release-readiness` before shipping.
20
+
21
+ ## [3.5.2] - 2026-10-10
22
+
23
+ Summer works with Kimi Code and other agents whose model checks tool schemas strictly.
24
+
25
+ - Kimi Code no longer fails every prompt with `400 tools.function.parameters is not a valid moonshot flavored json schema ... items must be an object` when Summer is connected. Ten tools (`summer_align_distribute_3d`, `summer_raycast`, `summer_instantiate_scene`, `summer_capture_gameplay`, `summer_test_placement`, `summer_snap_to_surface`, `summer_navigation_probe`, `summer_attach_to_surface`, `summer_repeat_along`, `summer_game_input`) described their `[x, y, z]` and `[x, y]` vectors in a form Moonshot rejects. Every tool now lists its parameters in the JSON Schema subset strict providers accept: a vector is an array of numbers with an exact length, and no schema points into another with `$ref`. The hosted store tools get the same treatment. The tools still reject a vector of the wrong length.
26
+ - If you added Summer tools to Kimi's `disabledTools` to work around this, remove them after updating, then restart Kimi Code.
6
27
 
7
28
  ## [3.5.1] - 2026-10-10
8
29
 
@@ -47,6 +47,7 @@ Skills are invoked by their bare slug, such as `brainstorm-game`. There is no `s
47
47
  | add an animation to [character] | `generate-motion` |
48
48
  | wire up a state machine for [character] | `animation-tree` |
49
49
  | the game feels mushy / add juice | `game-feel` |
50
+ | is it ready to release / check before publishing / online joins fail with `world_start_timeout` | `release-readiness` |
50
51
  | ship a trailer | `trailer-shot` |
51
52
  | cutscene for [X] | `cinematic-cutscene` |
52
53
  | let me play it | `play` |
@@ -0,0 +1,91 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Sound-effect choices (summer_generate_sfx_choices + summer_get_choices).
4
+ *
5
+ * Sound-effect models differ a lot from one sound to the next, so the agent
6
+ * can make one sound with several models and let the person pick by ear:
7
+ * POST /api/mcp/generate/audio/choices makes the versions and a choice set,
8
+ * the person listens and approves one or more on the Studio page
9
+ * /studio/choices/<id>, and GET /api/mcp/choices/<id> reads the decision.
10
+ * An approval makes that model the person's default for later
11
+ * summer_generate_audio sound_effects calls without a modelId.
12
+ *
13
+ * One implementation for both faces: the MCP tools (generate-tools.ts) and
14
+ * `summer tool generate-sfx-choices` / `get-choices` (tool-dispatch.ts) pass
15
+ * their own gateway request functions and share everything else here.
16
+ */
17
+ export declare const SFX_MODEL_IDS: readonly ["eleven_text_to_sound_v2", "sonilo-sfx-1.1", "stable-audio-3-sfx"];
18
+ export type SfxModelId = (typeof SFX_MODEL_IDS)[number];
19
+ export declare const SFX_CHOICES_ENDPOINT = "/api/mcp/generate/audio/choices";
20
+ export declare const CHOICES_ENDPOINT_PREFIX = "/api/mcp/choices/";
21
+ /** Three models run in parallel on the server; the slowest decides. */
22
+ export declare const SFX_CHOICES_TIMEOUT_MS = 240000;
23
+ export declare const MAX_CHOICES_WAIT_SECONDS = 50;
24
+ export declare const sfxChoicesArgsSchema: z.ZodObject<{
25
+ text: z.ZodString;
26
+ durationSeconds: z.ZodOptional<z.ZodNumber>;
27
+ loop: z.ZodOptional<z.ZodBoolean>;
28
+ models: z.ZodOptional<z.ZodArray<z.ZodEnum<["eleven_text_to_sound_v2", "sonilo-sfx-1.1", "stable-audio-3-sfx"]>, "many">>;
29
+ open: z.ZodDefault<z.ZodBoolean>;
30
+ }, "strip", z.ZodTypeAny, {
31
+ text: string;
32
+ open: boolean;
33
+ durationSeconds?: number | undefined;
34
+ loop?: boolean | undefined;
35
+ models?: ("eleven_text_to_sound_v2" | "sonilo-sfx-1.1" | "stable-audio-3-sfx")[] | undefined;
36
+ }, {
37
+ text: string;
38
+ open?: boolean | undefined;
39
+ durationSeconds?: number | undefined;
40
+ loop?: boolean | undefined;
41
+ models?: ("eleven_text_to_sound_v2" | "sonilo-sfx-1.1" | "stable-audio-3-sfx")[] | undefined;
42
+ }>;
43
+ export type SfxChoicesArgs = z.input<typeof sfxChoicesArgsSchema>;
44
+ export declare const getChoicesArgsSchema: z.ZodObject<{
45
+ choiceSetId: z.ZodString;
46
+ waitSeconds: z.ZodOptional<z.ZodNumber>;
47
+ }, "strip", z.ZodTypeAny, {
48
+ choiceSetId: string;
49
+ waitSeconds?: number | undefined;
50
+ }, {
51
+ choiceSetId: string;
52
+ waitSeconds?: number | undefined;
53
+ }>;
54
+ export type GetChoicesArgs = z.input<typeof getChoicesArgsSchema>;
55
+ /** A gateway reply as both faces' request helpers produce it. */
56
+ export interface GatewayReply {
57
+ data?: any;
58
+ error?: string;
59
+ status: number;
60
+ }
61
+ export type StudioOpenOutcome = {
62
+ ok: true;
63
+ } | {
64
+ ok: false;
65
+ reason: string;
66
+ };
67
+ export interface SfxChoicesDeps {
68
+ post(endpoint: string, body: Record<string, unknown>, timeoutMs: number): Promise<GatewayReply>;
69
+ get(endpoint: string, params: URLSearchParams, timeoutMs: number): Promise<GatewayReply>;
70
+ /** Move the person's open Studio tab to a /studio path (reviewUrl: the same page as a full URL). */
71
+ studioOpen(path: string, reviewUrl: string): Promise<StudioOpenOutcome>;
72
+ openUrl(url: string): Promise<unknown>;
73
+ canOpenBrowser(): boolean;
74
+ }
75
+ export type ShownIn = "studio_tab" | "browser" | "not_opened";
76
+ /** Same rule as summer_open: a browser only ever opens summerengine.com, its subdomains, or loopback. */
77
+ export declare function canOpenReviewUrl(url: string): boolean;
78
+ /** False where a browser launch cannot reach the person: CI, or Linux with no display. */
79
+ export declare function defaultCanOpenBrowser(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
80
+ /**
81
+ * Move the person's open Studio tab through the hosted Summer Engine MCP's
82
+ * summer_studio_open (needs the store sign-in). Only for the production site:
83
+ * the hosted MCP moves tabs on summerengine.com, so a review page on another
84
+ * origin (a preview deploy, local web dev) is opened in the browser instead.
85
+ */
86
+ export declare function openInStudioTab(path: string, reviewUrl: string): Promise<StudioOpenOutcome>;
87
+ export declare function sfxChoicesRequestBody(args: z.output<typeof sfxChoicesArgsSchema>): Record<string, unknown>;
88
+ export declare function generateSfxChoices(args: z.output<typeof sfxChoicesArgsSchema>, deps: SfxChoicesDeps): Promise<Record<string, unknown>>;
89
+ export declare function choicesEndpoint(choiceSetId: string): string;
90
+ export declare function choicesTimeoutMs(waitSeconds: number | undefined): number;
91
+ export declare function getChoices(args: z.output<typeof getChoicesArgsSchema>, deps: Pick<SfxChoicesDeps, "get">): Promise<Record<string, unknown>>;
@@ -0,0 +1,291 @@
1
+ import { z } from "zod";
2
+ import { hasStoreSignIn, hostedResultPayload, withHostedMcp } from "../hosted-mcp.js";
3
+ import { resolveMcpResourceUrl } from "../oauth.js";
4
+ /**
5
+ * Sound-effect choices (summer_generate_sfx_choices + summer_get_choices).
6
+ *
7
+ * Sound-effect models differ a lot from one sound to the next, so the agent
8
+ * can make one sound with several models and let the person pick by ear:
9
+ * POST /api/mcp/generate/audio/choices makes the versions and a choice set,
10
+ * the person listens and approves one or more on the Studio page
11
+ * /studio/choices/<id>, and GET /api/mcp/choices/<id> reads the decision.
12
+ * An approval makes that model the person's default for later
13
+ * summer_generate_audio sound_effects calls without a modelId.
14
+ *
15
+ * One implementation for both faces: the MCP tools (generate-tools.ts) and
16
+ * `summer tool generate-sfx-choices` / `get-choices` (tool-dispatch.ts) pass
17
+ * their own gateway request functions and share everything else here.
18
+ */
19
+ export const SFX_MODEL_IDS = ["eleven_text_to_sound_v2", "sonilo-sfx-1.1", "stable-audio-3-sfx"];
20
+ export const SFX_CHOICES_ENDPOINT = "/api/mcp/generate/audio/choices";
21
+ export const CHOICES_ENDPOINT_PREFIX = "/api/mcp/choices/";
22
+ /** Three models run in parallel on the server; the slowest decides. */
23
+ export const SFX_CHOICES_TIMEOUT_MS = 240_000;
24
+ export const MAX_CHOICES_WAIT_SECONDS = 50;
25
+ const STUDIO_OPEN_TIMEOUT_MS = 20_000;
26
+ export const sfxChoicesArgsSchema = z.object({
27
+ text: z
28
+ .string()
29
+ .min(1)
30
+ .max(1000)
31
+ .describe("The sound to make, written as a Foley description (what hits what, the attack and the tail, close-mic, dry)"),
32
+ durationSeconds: z
33
+ .number()
34
+ .positive()
35
+ .max(30)
36
+ .optional()
37
+ .describe("Exact length in seconds. One-shots 0.5-2; animation-synced sounds match the animation. stable-audio-3-sfx needs >= 1, sonilo-sfx-1.1 >= 0.5"),
38
+ loop: z
39
+ .boolean()
40
+ .optional()
41
+ .describe("Ask for a seamless loop (eleven_text_to_sound_v2 only; say 'seamless loop' in text too)"),
42
+ models: z
43
+ .array(z.enum(SFX_MODEL_IDS))
44
+ .min(2)
45
+ .max(4)
46
+ .optional()
47
+ .describe("2-4 model ids to compare. Default: eleven_text_to_sound_v2, sonilo-sfx-1.1, stable-audio-3-sfx"),
48
+ open: z
49
+ .boolean()
50
+ .default(true)
51
+ .describe("Show the choices page to the person (their open Studio tab, else the browser). false: only return the URL"),
52
+ });
53
+ export const getChoicesArgsSchema = z.object({
54
+ choiceSetId: z.string().min(1).max(200).describe("The choiceSetId from summer_generate_sfx_choices"),
55
+ waitSeconds: z
56
+ .number()
57
+ .int()
58
+ .min(0)
59
+ .max(MAX_CHOICES_WAIT_SECONDS)
60
+ .optional()
61
+ .describe("Wait up to this many seconds (max 50) for the person to decide before answering. 0 or omitted: answer now"),
62
+ });
63
+ // ---------------------------------------------------------------------------
64
+ // Showing the page
65
+ // ---------------------------------------------------------------------------
66
+ function hostOf(url) {
67
+ try {
68
+ return new URL(url).hostname.toLowerCase();
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
74
+ function isSummerHost(host) {
75
+ return host === "summerengine.com" || (host?.endsWith(".summerengine.com") ?? false);
76
+ }
77
+ function isLoopback(host) {
78
+ return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1";
79
+ }
80
+ /** Same rule as summer_open: a browser only ever opens summerengine.com, its subdomains, or loopback. */
81
+ export function canOpenReviewUrl(url) {
82
+ const host = hostOf(url);
83
+ return isSummerHost(host) || isLoopback(host);
84
+ }
85
+ /** False where a browser launch cannot reach the person: CI, or Linux with no display. */
86
+ export function defaultCanOpenBrowser(env = process.env, platform = process.platform) {
87
+ if (env.CI)
88
+ return false;
89
+ if (platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY)
90
+ return false;
91
+ return true;
92
+ }
93
+ /**
94
+ * Move the person's open Studio tab through the hosted Summer Engine MCP's
95
+ * summer_studio_open (needs the store sign-in). Only for the production site:
96
+ * the hosted MCP moves tabs on summerengine.com, so a review page on another
97
+ * origin (a preview deploy, local web dev) is opened in the browser instead.
98
+ */
99
+ export async function openInStudioTab(path, reviewUrl) {
100
+ if (!isSummerHost(hostOf(reviewUrl)))
101
+ return { ok: false, reason: "review_page_not_on_summerengine_com" };
102
+ let resource;
103
+ try {
104
+ resource = resolveMcpResourceUrl();
105
+ }
106
+ catch {
107
+ return { ok: false, reason: "hosted_mcp_url_invalid" };
108
+ }
109
+ if (!isSummerHost(hostOf(resource)))
110
+ return { ok: false, reason: "hosted_mcp_not_summerengine_com" };
111
+ if (!(await hasStoreSignIn()))
112
+ return { ok: false, reason: "no_store_sign_in" };
113
+ let timer;
114
+ const timeout = new Promise((resolve) => {
115
+ timer = setTimeout(() => resolve({ ok: false, reason: "studio_tab_timeout" }), STUDIO_OPEN_TIMEOUT_MS);
116
+ timer.unref?.();
117
+ });
118
+ const attempt = (async () => {
119
+ try {
120
+ const result = await withHostedMcp((client) => client.callTool({ name: "summer_studio_open", arguments: { path } }));
121
+ const payload = hostedResultPayload(result);
122
+ if (payload.isError || payload.json?.ok === false) {
123
+ const code = typeof payload.json?.error === "string" ? payload.json.error : "studio_open_failed";
124
+ return { ok: false, reason: code };
125
+ }
126
+ return { ok: true };
127
+ }
128
+ catch (error) {
129
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
130
+ }
131
+ })();
132
+ try {
133
+ return await Promise.race([attempt, timeout]);
134
+ }
135
+ finally {
136
+ clearTimeout(timer);
137
+ }
138
+ }
139
+ async function showChoicesPage(reviewUrl, studioPath, deps) {
140
+ let studioTab;
141
+ if (studioPath) {
142
+ const moved = await deps.studioOpen(studioPath, reviewUrl);
143
+ if (moved.ok)
144
+ return { shown: "studio_tab" };
145
+ studioTab = moved.reason;
146
+ }
147
+ if (!canOpenReviewUrl(reviewUrl))
148
+ return { shown: "not_opened", studioTab, browser: "not_a_summer_url" };
149
+ if (!deps.canOpenBrowser())
150
+ return { shown: "not_opened", studioTab, browser: "no_browser_here" };
151
+ try {
152
+ await deps.openUrl(reviewUrl);
153
+ return { shown: "browser", studioTab };
154
+ }
155
+ catch (error) {
156
+ return { shown: "not_opened", studioTab, browser: error instanceof Error ? error.message : String(error) };
157
+ }
158
+ }
159
+ // ---------------------------------------------------------------------------
160
+ // summer_generate_sfx_choices
161
+ // ---------------------------------------------------------------------------
162
+ export function sfxChoicesRequestBody(args) {
163
+ const body = { capability: "sound_effects", text: args.text };
164
+ if (args.durationSeconds !== undefined)
165
+ body.durationSeconds = args.durationSeconds;
166
+ if (args.loop !== undefined)
167
+ body.loop = args.loop;
168
+ if (args.models)
169
+ body.models = args.models;
170
+ return body;
171
+ }
172
+ function clipSummary(clip) {
173
+ return {
174
+ clipId: clip.clipId,
175
+ modelId: clip.modelId,
176
+ modelName: clip.modelName,
177
+ audioUrl: clip.audioUrl,
178
+ assetId: clip.assetId,
179
+ durationSeconds: clip.durationSeconds,
180
+ };
181
+ }
182
+ export async function generateSfxChoices(args, deps) {
183
+ const reply = await deps.post(SFX_CHOICES_ENDPOINT, sfxChoicesRequestBody(args), SFX_CHOICES_TIMEOUT_MS);
184
+ if (reply.error) {
185
+ return { ok: false, error: reply.error, ...(reply.data ? { details: reply.data } : {}) };
186
+ }
187
+ const data = reply.data ?? {};
188
+ const clips = Array.isArray(data.clips) ? data.clips : [];
189
+ const failed = Array.isArray(data.failed) ? data.failed : [];
190
+ const choiceSetId = typeof data.choiceSetId === "string" ? data.choiceSetId : null;
191
+ const reviewUrl = typeof data.reviewUrl === "string" ? data.reviewUrl : null;
192
+ const studioPath = typeof data.studioPath === "string" ? data.studioPath : undefined;
193
+ if (!choiceSetId || !reviewUrl || clips.length === 0) {
194
+ return {
195
+ ok: false,
196
+ error: clips.length === 0 ? "No model made a version of this sound." : "The server did not return a choice set.",
197
+ failed,
198
+ hint: "Read failed[] for each model's error. Fix the request (a longer durationSeconds, a plainer description) or call summer_generate_audio with one modelId.",
199
+ };
200
+ }
201
+ const shown = args.open
202
+ ? await showChoicesPage(reviewUrl, studioPath, deps)
203
+ : { shown: "not_opened", browser: "open_false" };
204
+ const count = clips.length;
205
+ const where = shown.shown === "studio_tab"
206
+ ? "I opened them in your Summer Studio tab"
207
+ : shown.shown === "browser"
208
+ ? "I opened them in your browser"
209
+ : `Open ${reviewUrl} to hear them`;
210
+ const say = `I made ${count} versions of this sound with ${count} different models. ${where}. Listen, approve the ones you like (one or more), then tell me. The model you approve becomes your default for sound effects.`;
211
+ return {
212
+ ok: true,
213
+ choiceSetId,
214
+ prompt: typeof data.prompt === "string" ? data.prompt : args.text,
215
+ reviewUrl,
216
+ ...(studioPath ? { studioPath } : {}),
217
+ shown: shown.shown,
218
+ ...(shown.studioTab || shown.browser
219
+ ? { notShownBecause: { ...(shown.studioTab ? { studioTab: shown.studioTab } : {}), ...(shown.browser ? { browser: shown.browser } : {}) } }
220
+ : {}),
221
+ clips: clips.map(clipSummary),
222
+ failed,
223
+ say,
224
+ next: `Tell the person the "say" text (include reviewUrl when shown is "not_opened"). Do not pick for them. Then call summer_get_choices with choiceSetId "${choiceSetId}" and waitSeconds ${MAX_CHOICES_WAIT_SECONDS}; call it again while status is "pending".`,
225
+ };
226
+ }
227
+ // ---------------------------------------------------------------------------
228
+ // summer_get_choices
229
+ // ---------------------------------------------------------------------------
230
+ function fileExtension(url) {
231
+ if (!url)
232
+ return "wav";
233
+ try {
234
+ const match = /\.(wav|mp3|ogg)$/i.exec(new URL(url).pathname);
235
+ return match ? match[1].toLowerCase() : "wav";
236
+ }
237
+ catch {
238
+ return "wav";
239
+ }
240
+ }
241
+ export function choicesEndpoint(choiceSetId) {
242
+ return `${CHOICES_ENDPOINT_PREFIX}${encodeURIComponent(choiceSetId)}`;
243
+ }
244
+ export function choicesTimeoutMs(waitSeconds) {
245
+ return ((waitSeconds ?? 0) + 20) * 1000;
246
+ }
247
+ export async function getChoices(args, deps) {
248
+ const params = new URLSearchParams();
249
+ if (args.waitSeconds)
250
+ params.set("waitSeconds", String(args.waitSeconds));
251
+ const reply = await deps.get(choicesEndpoint(args.choiceSetId), params, choicesTimeoutMs(args.waitSeconds));
252
+ if (reply.error) {
253
+ return { ok: false, error: reply.error, ...(reply.data ? { details: reply.data } : {}) };
254
+ }
255
+ const data = reply.data ?? {};
256
+ const clips = Array.isArray(data.clips) ? data.clips : [];
257
+ const status = data.status === "decided" ? "decided" : "pending";
258
+ const approved = clips
259
+ .filter((clip) => clip.approved === true)
260
+ .map((clip) => ({
261
+ ...clipSummary(clip),
262
+ ...(clip.note ? { note: clip.note } : {}),
263
+ fileExtension: fileExtension(clip.audioUrl),
264
+ }));
265
+ let next;
266
+ if (status === "pending") {
267
+ next = `The person has not decided yet. Call summer_get_choices again with waitSeconds ${MAX_CHOICES_WAIT_SECONDS}, or wait until they say they picked. Do not pick for them.`;
268
+ }
269
+ else if (approved.length === 0) {
270
+ next = "They approved none. Ask what to change (the description, the length, the models) and run summer_generate_sfx_choices again.";
271
+ }
272
+ else {
273
+ next =
274
+ "Import each approved clip into the project: summer_import_from_url({ url: audioUrl, path: \"res://audio/sfx/<name>.<fileExtension>\" }) " +
275
+ "or summer_import_asset_by_id({ assetId, path }). Read each clip's note. Then wire it (sound-effect skill, step Wire). " +
276
+ (typeof data.preferredModelId === "string"
277
+ ? `Their default sound-effect model is now ${data.preferredModelId}: later summer_generate_audio sound_effects calls without modelId use it.`
278
+ : "");
279
+ }
280
+ return {
281
+ ok: true,
282
+ choiceSetId: typeof data.choiceSetId === "string" ? data.choiceSetId : args.choiceSetId,
283
+ kind: data.kind,
284
+ prompt: data.prompt,
285
+ status,
286
+ preferredModelId: data.preferredModelId ?? null,
287
+ approved,
288
+ clips: clips.map((clip) => ({ ...clipSummary(clip), approved: clip.approved === true, ...(clip.note ? { note: clip.note } : {}) })),
289
+ next: next.trim(),
290
+ };
291
+ }
@@ -30,6 +30,7 @@ import { lookupApiDocs } from "./api-docs.js";
30
30
  import { BOARD_ENDPOINT, boardArgsSchema, boardQuery } from "./board.js";
31
31
  import { IMAGE_GENERATION_TIMEOUT_MS, imageGenerationArgsSchema, saveGeneratedImage } from "./image-generation.js";
32
32
  import { buildMotionRequestBody, motionGenerationArgsSchema } from "./motion-generation.js";
33
+ import { defaultCanOpenBrowser, generateSfxChoices, getChoices, getChoicesArgsSchema, openInStudioTab, sfxChoicesArgsSchema, } from "./sfx-choices.js";
33
34
  import { ImportHdriError, importHdriArgsSchema, importPolyHavenHdri } from "./hdri-import.js";
34
35
  import { FABRICATE_FALLBACK, buildFabricateMeshOp, fabricateArgsSchema } from "./fabricate-mesh.js";
35
36
  import { buildUiActionsOp, buildUiActivateOp, buildUiScreenshotOp, buildUiTreeOp, executeUiOp, uiActionsArgsSchema, uiActivateArgsSchema, uiScreenshotArgsSchema, uiScreenshotCaption, uiScreenshotImage, uiTreeArgsSchema, withUiFailureDetails, } from "./ui-control.js";
@@ -393,6 +394,26 @@ function optStr(args, key) {
393
394
  }
394
395
  /** Validate CLI args against the SAME zod schema the MCP face uses, so the
395
396
  * two surfaces reject the same inputs with a readable message. */
397
+ /** The gateway helpers as the shared sfx-choices module expects them: a reply, never a throw. */
398
+ async function gatewayReply(call) {
399
+ try {
400
+ return { data: await call(), status: 200 };
401
+ }
402
+ catch (error) {
403
+ if (!(error instanceof ToolDispatchError))
404
+ throw error;
405
+ return { error: error.message, status: 0 };
406
+ }
407
+ }
408
+ function sfxChoicesDeps() {
409
+ return {
410
+ post: (endpoint, body, timeoutMs) => gatewayReply(() => gatewayPost(endpoint, body, timeoutMs)),
411
+ get: (endpoint, params, timeoutMs) => gatewayReply(() => gatewayGet(endpoint, params, timeoutMs)),
412
+ studioOpen: (path, reviewUrl) => openInStudioTab(path, reviewUrl),
413
+ openUrl: (url) => open(url),
414
+ canOpenBrowser: () => defaultCanOpenBrowser(),
415
+ };
416
+ }
396
417
  function parseToolArgs(schema, args, tool) {
397
418
  const parsed = schema.safeParse(args);
398
419
  if (parsed.success)
@@ -917,12 +938,26 @@ export const TOOL_DISPATCH = [
917
938
  }
918
939
  if (typeof args.durationSeconds === "number")
919
940
  body.durationSeconds = args.durationSeconds;
941
+ if (typeof args.loop === "boolean")
942
+ body.loop = args.loop;
920
943
  if (Array.isArray(args.inputs))
921
944
  body.inputs = args.inputs;
922
945
  if (args.options)
923
946
  body.options = args.options;
924
947
  return gatewayPost("/api/mcp/generate/audio", body);
925
948
  }),
949
+ entry("summer_generate_sfx_choices", "Make one sound effect with several models and show them for the person to pick", false, async (args) => {
950
+ const result = await generateSfxChoices(parseToolArgs(sfxChoicesArgsSchema, args, "generate-sfx-choices"), sfxChoicesDeps());
951
+ if (result.ok !== true)
952
+ throw new ToolResultError(result, String(result.error));
953
+ return result;
954
+ }),
955
+ entry("summer_get_choices", "Read which sound-effect versions the person approved", false, async (args) => {
956
+ const result = await getChoices(parseToolArgs(getChoicesArgsSchema, args, "get-choices"), sfxChoicesDeps());
957
+ if (result.ok !== true)
958
+ throw new ToolResultError(result, String(result.error));
959
+ return result;
960
+ }),
926
961
  entry("summer_generate_3d", "Generate a 3D model (optional auto-rig + animations)", false, async (args) => {
927
962
  const options = {
928
963
  ...(args.options ?? {}),
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { connectHostedMcp } from "../core/hosted-mcp.js";
4
4
  import { getStoreAccessToken, OAuthError, resolveMcpResourceUrl } from "../core/oauth.js";
5
5
  import { appendMcpLogEvent } from "../core/mcp-log.js";
6
+ import { providerSafeSchema } from "./provider-safe-schema.js";
6
7
  /**
7
8
  * One Summer Engine MCP. The cloud tools a creator's agent needs to publish
8
9
  * (projects, store page, build upload, submit for the owner's approval, the
@@ -73,7 +74,7 @@ export function prepareHostedMount(server, deps = defaultHostedMountDependencies
73
74
  ...result,
74
75
  tools: result.tools.map((tool) => {
75
76
  const hosted = schemas.get(tool.name);
76
- return hosted ? { ...tool, inputSchema: hosted.inputSchema, ...(hosted.annotations ? { annotations: hosted.annotations } : {}), ...(hosted.title ? { title: hosted.title } : {}) } : tool;
77
+ return hosted ? { ...tool, inputSchema: providerSafeSchema(hosted.inputSchema), ...(hosted.annotations ? { annotations: hosted.annotations } : {}), ...(hosted.title ? { title: hosted.title } : {}) } : tool;
77
78
  }),
78
79
  };
79
80
  });
@@ -0,0 +1,10 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /** String formats OpenAI strict mode accepts; other providers accept no more. */
3
+ export declare const STRICT_PROVIDER_FORMATS: Set<string>;
4
+ export declare function providerSafeSchema<T>(schema: T): T;
5
+ /**
6
+ * Wrap the SDK's tools/list so every tool's inputSchema leaves the server in
7
+ * the provider-safe form. Call after the first registerTool (the SDK installs
8
+ * its tools/list handler then).
9
+ */
10
+ export declare function serveProviderSafeToolSchemas(server: McpServer): void;