summer-engine 3.3.0 → 3.4.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 (68) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +4 -2
  3. package/.codex-plugin/plugin.json +5 -3
  4. package/.cursor-plugin/plugin.json +4 -2
  5. package/.factory-plugin/plugin.json +4 -2
  6. package/CHANGELOG.md +36 -0
  7. package/README.md +2 -1
  8. package/assets/api-docs.json.gz +0 -0
  9. package/dist/cli/commands/install.d.ts +10 -0
  10. package/dist/cli/commands/install.js +55 -10
  11. package/dist/cli/commands/tool.d.ts +7 -0
  12. package/dist/cli/commands/tool.js +36 -1
  13. package/dist/core/capabilities/capture-gameplay.d.ts +69 -0
  14. package/dist/core/capabilities/capture-gameplay.js +283 -0
  15. package/dist/core/capabilities/creator.js +1 -1
  16. package/dist/core/capabilities/doctor.d.ts +9 -1
  17. package/dist/core/capabilities/doctor.js +47 -4
  18. package/dist/core/capabilities/engine-run.d.ts +4 -0
  19. package/dist/core/capabilities/engine-run.js +33 -5
  20. package/dist/core/capabilities/export-engine-version.d.ts +15 -0
  21. package/dist/core/capabilities/export-engine-version.js +24 -0
  22. package/dist/core/capabilities/export-game.d.ts +9 -1
  23. package/dist/core/capabilities/export-game.js +112 -36
  24. package/dist/core/capabilities/image-generation.d.ts +9 -0
  25. package/dist/core/capabilities/image-generation.js +19 -1
  26. package/dist/core/capabilities/publish-build.d.ts +14 -1
  27. package/dist/core/capabilities/publish-build.js +74 -22
  28. package/dist/core/capabilities/tool-dispatch.js +10 -0
  29. package/dist/core/config.js +2 -2
  30. package/dist/core/engine-install.d.ts +16 -0
  31. package/dist/core/engine-install.js +32 -2
  32. package/dist/core/hosted-mcp.d.ts +115 -0
  33. package/dist/core/hosted-mcp.js +164 -0
  34. package/dist/core/launch-posture.d.ts +3 -0
  35. package/dist/core/launch-posture.js +15 -2
  36. package/dist/mcp/hosted-mount.d.ts +13 -59
  37. package/dist/mcp/hosted-mount.js +24 -30
  38. package/dist/mcp/server.js +3 -2
  39. package/dist/mcp/tools/creator-tools.js +13 -3
  40. package/dist/mcp/tools/generate-tools.js +12 -2
  41. package/gemini-extension.json +3 -1
  42. package/library/references/mcp-tools-reference/mcp-tools-reference.md +3 -2
  43. package/library/skills/README.md +6 -4
  44. package/library/skills/bounded-engine-runs/SKILL.md +96 -0
  45. package/library/skills/bounded-engine-runs/resource.yaml +29 -0
  46. package/library/skills/compatibility-renderer-traps/SKILL.md +78 -0
  47. package/library/skills/compatibility-renderer-traps/resource.yaml +28 -0
  48. package/library/skills/concept-art/SKILL.md +2 -2
  49. package/library/skills/export-and-ship/SKILL.md +2 -0
  50. package/library/skills/headless-scripting/resource.yaml +1 -0
  51. package/library/skills/in-game-purchases/SKILL.md +123 -118
  52. package/library/skills/in-game-purchases/resource.yaml +2 -2
  53. package/library/skills/web-compatibility-look/resource.yaml +1 -0
  54. package/library/tools/capture-gameplay/resource.yaml +73 -0
  55. package/library/tools/generate-image/resource.yaml +15 -1
  56. package/library/tools/publish-build/resource.yaml +1 -1
  57. package/package.json +1 -1
  58. package/registry/generated/aliases.json +1 -0
  59. package/registry/generated/counts.json +3 -3
  60. package/registry/generated/gemini-extension.json +3 -1
  61. package/registry/generated/index.json +125 -12
  62. package/registry/generated/marketplace.claude.json +2 -2
  63. package/registry/generated/plugin.claude.json +4 -2
  64. package/registry/generated/plugin.codex.json +5 -3
  65. package/registry/generated/plugin.cursor.json +4 -2
  66. package/registry/generated/plugin.factory.json +4 -2
  67. package/registry/generated/skills-index.md +6 -4
  68. package/registry/generated/skills-registry.json +28 -1
@@ -0,0 +1,115 @@
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ /** The part of an MCP client the mount and the CLI use (a fake in tests). */
3
+ export interface HostedClient {
4
+ listTools(params?: {
5
+ cursor?: string;
6
+ }): Promise<{
7
+ tools: Tool[];
8
+ nextCursor?: string;
9
+ }>;
10
+ callTool(params: {
11
+ name: string;
12
+ arguments?: Record<string, unknown>;
13
+ }): Promise<Record<string, unknown>>;
14
+ listPrompts(params?: {
15
+ cursor?: string;
16
+ }): Promise<{
17
+ prompts: Array<{
18
+ name: string;
19
+ title?: string;
20
+ description?: string;
21
+ arguments?: Array<{
22
+ name: string;
23
+ description?: string;
24
+ required?: boolean;
25
+ }>;
26
+ }>;
27
+ nextCursor?: string;
28
+ }>;
29
+ getPrompt(params: {
30
+ name: string;
31
+ arguments?: Record<string, string>;
32
+ }): Promise<Record<string, unknown>>;
33
+ listResources(params?: {
34
+ cursor?: string;
35
+ }): Promise<{
36
+ resources: Array<{
37
+ uri: string;
38
+ name: string;
39
+ title?: string;
40
+ description?: string;
41
+ mimeType?: string;
42
+ }>;
43
+ nextCursor?: string;
44
+ }>;
45
+ readResource(params: {
46
+ uri: string;
47
+ }): Promise<Record<string, unknown>>;
48
+ close?(): Promise<void>;
49
+ }
50
+ export interface HostedConnectDependencies {
51
+ /** The hosted Summer Engine MCP URL (the store sign-in's resource). */
52
+ url: () => string;
53
+ token: () => Promise<string>;
54
+ connect: (url: string, token: () => Promise<string>) => Promise<HostedClient>;
55
+ }
56
+ export declare function connectHostedMcp(url: string, token: () => Promise<string>): Promise<HostedClient>;
57
+ export declare const defaultHostedConnectDependencies: HostedConnectDependencies;
58
+ /** True when this machine has a store sign-in on disk (no network). */
59
+ export declare function hasStoreSignIn(): Promise<boolean>;
60
+ export declare function listAllHostedTools(client: HostedClient): Promise<Tool[]>;
61
+ /** Connect with the store sign-in, run fn, close. Throws OAuthError when not signed in. */
62
+ export declare function withHostedMcp<T>(fn: (client: HostedClient) => Promise<T>, deps?: HostedConnectDependencies): Promise<T>;
63
+ /** The text of a tool result, parsed as JSON when it is JSON. */
64
+ export declare function hostedResultPayload(result: Record<string, unknown>): {
65
+ isError: boolean;
66
+ text: string;
67
+ json: Record<string, unknown> | null;
68
+ };
69
+ export type StoreAccessResult = {
70
+ status: "not_signed_in";
71
+ message: string;
72
+ } | {
73
+ status: "ok";
74
+ games: number | null;
75
+ } | {
76
+ status: "refused";
77
+ code: string | null;
78
+ message: string;
79
+ requestId?: string;
80
+ } | {
81
+ status: "unreachable";
82
+ message: string;
83
+ };
84
+ /**
85
+ * Ask the store for the creator's games through the hosted MCP
86
+ * (summer_store_list_games): proves the store accepts this machine's sign-in.
87
+ */
88
+ export declare function checkStoreAccess(timeoutMs?: number, deps?: HostedConnectDependencies): Promise<StoreAccessResult>;
89
+ /** The hosted tool a CLI name means: the exact name, or a slug ("store-list-games" -> summer_store_list_games). */
90
+ export declare function matchHostedTool(name: string, tools: readonly Tool[]): Tool | null;
91
+ export type HostedToolsListing = {
92
+ status: "listed";
93
+ tools: Tool[];
94
+ } | {
95
+ status: "not_signed_in";
96
+ } | {
97
+ status: "unavailable";
98
+ message: string;
99
+ };
100
+ /** The hosted store and publishing tools, when this machine has a store sign-in. */
101
+ export declare function listHostedToolsForCli(deps?: HostedConnectDependencies, signedIn?: () => Promise<boolean>): Promise<HostedToolsListing>;
102
+ export type HostedCallOutcome = {
103
+ status: "called";
104
+ tool: string;
105
+ result: Record<string, unknown>;
106
+ } | {
107
+ status: "not_found";
108
+ } | {
109
+ status: "not_signed_in";
110
+ } | {
111
+ status: "unavailable";
112
+ message: string;
113
+ };
114
+ /** Call a hosted tool by its name or slug with the store sign-in (one connection). */
115
+ export declare function callHostedToolForCli(name: string, args: Record<string, unknown>, deps?: HostedConnectDependencies, signedIn?: () => Promise<boolean>): Promise<HostedCallOutcome>;
@@ -0,0 +1,164 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
+ import { getStoreAccessToken, OAuthError, readStoredOAuthToken, resolveMcpResourceUrl } from "./oauth.js";
4
+ import { TOOLKIT_VERSION } from "./version.js";
5
+ /**
6
+ * A client for the hosted Summer Engine MCP (mcp.summerengine.com), signed in
7
+ * with the store sign-in ("summer login --store"). The local MCP mounts its
8
+ * tools (src/mcp/hosted-mount.ts); `summer tool` and `summer doctor` call it
9
+ * directly through the helpers below, so all three faces reach the same
10
+ * store and publishing tools.
11
+ */
12
+ const CALL_TIMEOUT_MS = 120_000;
13
+ export async function connectHostedMcp(url, token) {
14
+ const transport = new StreamableHTTPClientTransport(new URL(url), {
15
+ // A fresh (refreshed when near expiry) token on every request.
16
+ fetch: async (input, init) => {
17
+ const headers = new Headers(init?.headers);
18
+ headers.set("authorization", `Bearer ${await token()}`);
19
+ return fetch(input, { ...init, headers });
20
+ },
21
+ });
22
+ const client = new Client({ name: "summer-engine-local", version: TOOLKIT_VERSION });
23
+ await client.connect(transport);
24
+ const timeout = { timeout: CALL_TIMEOUT_MS };
25
+ return {
26
+ listTools: (params) => client.listTools(params),
27
+ callTool: (params) => client.callTool(params, undefined, timeout),
28
+ listPrompts: (params) => client.listPrompts(params),
29
+ getPrompt: (params) => client.getPrompt(params),
30
+ listResources: (params) => client.listResources(params),
31
+ readResource: (params) => client.readResource(params),
32
+ close: () => client.close(),
33
+ };
34
+ }
35
+ export const defaultHostedConnectDependencies = {
36
+ url: () => resolveMcpResourceUrl(),
37
+ token: () => getStoreAccessToken(),
38
+ connect: connectHostedMcp,
39
+ };
40
+ /** True when this machine has a store sign-in on disk (no network). */
41
+ export async function hasStoreSignIn() {
42
+ return (await readStoredOAuthToken()) !== null;
43
+ }
44
+ export async function listAllHostedTools(client) {
45
+ const out = [];
46
+ let cursor;
47
+ for (let pages = 0; pages < 20; pages++) {
48
+ const result = await client.listTools(cursor ? { cursor } : undefined);
49
+ out.push(...(result.tools ?? []));
50
+ if (!result.nextCursor)
51
+ break;
52
+ cursor = result.nextCursor;
53
+ }
54
+ return out;
55
+ }
56
+ /** Connect with the store sign-in, run fn, close. Throws OAuthError when not signed in. */
57
+ export async function withHostedMcp(fn, deps = defaultHostedConnectDependencies) {
58
+ const url = deps.url();
59
+ await deps.token();
60
+ const client = await deps.connect(url, deps.token);
61
+ try {
62
+ return await fn(client);
63
+ }
64
+ finally {
65
+ await client.close?.().catch(() => undefined);
66
+ }
67
+ }
68
+ /** The text of a tool result, parsed as JSON when it is JSON. */
69
+ export function hostedResultPayload(result) {
70
+ const content = Array.isArray(result.content) ? result.content : [];
71
+ const text = content
72
+ .filter((part) => part.type === "text" && typeof part.text === "string")
73
+ .map((part) => part.text)
74
+ .join("\n");
75
+ let json = null;
76
+ try {
77
+ const parsed = JSON.parse(text);
78
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
79
+ json = parsed;
80
+ }
81
+ catch {
82
+ json = null;
83
+ }
84
+ if (!json && result.structuredContent && typeof result.structuredContent === "object") {
85
+ json = result.structuredContent;
86
+ }
87
+ return { isError: result.isError === true, text, json };
88
+ }
89
+ /**
90
+ * Ask the store for the creator's games through the hosted MCP
91
+ * (summer_store_list_games): proves the store accepts this machine's sign-in.
92
+ */
93
+ export async function checkStoreAccess(timeoutMs = 8_000, deps = defaultHostedConnectDependencies) {
94
+ let timer;
95
+ const timeout = new Promise((resolve) => {
96
+ timer = setTimeout(() => resolve({ status: "unreachable", message: `no answer within ${Math.round(timeoutMs / 1000)} s` }), timeoutMs);
97
+ timer.unref?.();
98
+ });
99
+ const attempt = (async () => {
100
+ try {
101
+ const result = await withHostedMcp((client) => client.callTool({ name: "summer_store_list_games", arguments: {} }), deps);
102
+ const payload = hostedResultPayload(result);
103
+ if (payload.isError || payload.json?.ok === false) {
104
+ const code = typeof payload.json?.code === "string" ? payload.json.code : typeof payload.json?.error === "string" ? payload.json.error : null;
105
+ const message = typeof payload.json?.message === "string" ? payload.json.message : payload.text.slice(0, 300) || "the store refused the call";
106
+ const requestId = typeof payload.json?.requestId === "string" ? payload.json.requestId : undefined;
107
+ return { status: "refused", code, message, ...(requestId ? { requestId } : {}) };
108
+ }
109
+ const games = Array.isArray(payload.json?.games) ? payload.json.games.length : null;
110
+ return { status: "ok", games };
111
+ }
112
+ catch (error) {
113
+ if (error instanceof OAuthError && error.code === "store_login_required") {
114
+ return { status: "not_signed_in", message: error.message.replace(` ${error.recovery}`, "") };
115
+ }
116
+ if (error instanceof OAuthError)
117
+ return { status: "refused", code: error.code, message: error.message };
118
+ return { status: "unreachable", message: error instanceof Error ? error.message : String(error) };
119
+ }
120
+ })();
121
+ try {
122
+ return await Promise.race([attempt, timeout]);
123
+ }
124
+ finally {
125
+ clearTimeout(timer);
126
+ }
127
+ }
128
+ /** The hosted tool a CLI name means: the exact name, or a slug ("store-list-games" -> summer_store_list_games). */
129
+ export function matchHostedTool(name, tools) {
130
+ const needle = name.trim();
131
+ const asAlias = needle.startsWith("summer_") ? needle : `summer_${needle.replace(/-/g, "_")}`;
132
+ return tools.find((tool) => tool.name === needle) ?? tools.find((tool) => tool.name === asAlias) ?? null;
133
+ }
134
+ /** The hosted store and publishing tools, when this machine has a store sign-in. */
135
+ export async function listHostedToolsForCli(deps = defaultHostedConnectDependencies, signedIn = hasStoreSignIn) {
136
+ if (!(await signedIn()))
137
+ return { status: "not_signed_in" };
138
+ try {
139
+ return { status: "listed", tools: await withHostedMcp(listAllHostedTools, deps) };
140
+ }
141
+ catch (error) {
142
+ if (error instanceof OAuthError && error.code === "store_login_required")
143
+ return { status: "not_signed_in" };
144
+ return { status: "unavailable", message: error instanceof Error ? error.message : String(error) };
145
+ }
146
+ }
147
+ /** Call a hosted tool by its name or slug with the store sign-in (one connection). */
148
+ export async function callHostedToolForCli(name, args, deps = defaultHostedConnectDependencies, signedIn = hasStoreSignIn) {
149
+ if (!(await signedIn()))
150
+ return { status: "not_signed_in" };
151
+ try {
152
+ return await withHostedMcp(async (client) => {
153
+ const tool = matchHostedTool(name, await listAllHostedTools(client));
154
+ if (!tool)
155
+ return { status: "not_found" };
156
+ return { status: "called", tool: tool.name, result: await client.callTool({ name: tool.name, arguments: args }) };
157
+ }, deps);
158
+ }
159
+ catch (error) {
160
+ if (error instanceof OAuthError && error.code === "store_login_required")
161
+ return { status: "not_signed_in" };
162
+ return { status: "unavailable", message: error instanceof Error ? error.message : String(error) };
163
+ }
164
+ }
@@ -96,6 +96,9 @@ export type BackgroundLaunchSupport = {
96
96
  reason: "version_unknown";
97
97
  version: null;
98
98
  };
99
+ /** Whether `version` is `minimum` or newer; null when either is missing or
100
+ * not x.y.z (an unknown version is never "too old" and never "new enough"). */
101
+ export declare function isVersionAtLeast(version: string | null | undefined, minimum: string): boolean | null;
99
102
  /**
100
103
  * Whether the installed engine honours the background flag. The `--help`
101
104
  * probe answer (true/false) is definitive when it ran; a probe that could not
@@ -61,7 +61,7 @@
61
61
  * --help probe below runs against THAT binary (cache key: path + mtime + size).
62
62
  */
63
63
  import { spawn } from "node:child_process";
64
- import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
64
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
65
65
  import { basename, dirname, join } from "node:path";
66
66
  import { platform } from "node:os";
67
67
  import { getSummerDir } from "./auth.js";
@@ -100,6 +100,15 @@ function compareVersions(a, b) {
100
100
  return a.minor - b.minor;
101
101
  return a.patch - b.patch;
102
102
  }
103
+ /** Whether `version` is `minimum` or newer; null when either is missing or
104
+ * not x.y.z (an unknown version is never "too old" and never "new enough"). */
105
+ export function isVersionAtLeast(version, minimum) {
106
+ const parsed = version ? parseVersion(version) : null;
107
+ const min = parseVersion(minimum);
108
+ if (!parsed || !min)
109
+ return null;
110
+ return compareVersions(parsed, min) >= 0;
111
+ }
103
112
  /**
104
113
  * Whether the installed engine honours the background flag. The `--help`
105
114
  * probe answer (true/false) is definitive when it ran; a probe that could not
@@ -282,12 +291,16 @@ export function parseVelopackVersion(sqVersionText) {
282
291
  const match = sqVersionText.match(/<version>\s*([^<\s]+)\s*<\/version>/i);
283
292
  return match ? match[1].trim() : null;
284
293
  }
285
- /** Walk up from `.../Summer.app/Contents/MacOS/Summer` to the bundle. */
294
+ /** Walk up from `.../Summer.app/Contents/MacOS/Summer` to the bundle. A
295
+ * bundle unpacked without its `.app` folder (`<dir>/Contents/MacOS/Summer`)
296
+ * reads as `<dir>`. */
286
297
  function macBundlePathFor(binary) {
287
298
  let current = dirname(binary);
288
299
  for (let depth = 0; depth < 4; depth++) {
289
300
  if (basename(current).endsWith(".app"))
290
301
  return current;
302
+ if (basename(current) === "Contents" && existsSync(join(current, "Info.plist")))
303
+ return dirname(current);
291
304
  const parent = dirname(current);
292
305
  if (parent === current)
293
306
  break;
@@ -1,5 +1,6 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { type Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import { type HostedClient, type HostedConnectDependencies } from "../core/hosted-mcp.js";
3
+ export type { HostedClient };
3
4
  /**
4
5
  * One Summer Engine MCP. The cloud tools a creator's agent needs to publish
5
6
  * (projects, store page, build upload, submit for the owner's approval, the
@@ -11,65 +12,16 @@ import { type Tool } from "@modelcontextprotocol/sdk/types.js";
11
12
  * that server). A name the local MCP already has stays local (the engine and
12
13
  * generation tools keep their CLI-token path).
13
14
  *
14
- * Mounting is lazy: the server answers with the engine tools at once, and the
15
- * hosted tools appear when the mount finishes (tools/list_changed). Without a
16
- * store sign-in one tool, summer_store_tools, says how to sign in and mounts
17
- * on the next call.
15
+ * Mounting runs after connect. The first tools/list waits for a mount in
16
+ * progress (up to FIRST_LIST_WAIT_MS), so a host that lists tools once still
17
+ * sees the store tools; a slower mount adds them later (tools/list_changed).
18
+ * Without a store sign-in the mount fails at once, and one tool,
19
+ * summer_store_tools, says how to sign in and mounts on the next call.
18
20
  */
19
21
  export declare const STORE_TOOLS_STATUS = "summer_store_tools";
20
- /** The part of an MCP client the mount uses (a fake in tests). */
21
- export interface HostedClient {
22
- listTools(params?: {
23
- cursor?: string;
24
- }): Promise<{
25
- tools: Tool[];
26
- nextCursor?: string;
27
- }>;
28
- callTool(params: {
29
- name: string;
30
- arguments?: Record<string, unknown>;
31
- }): Promise<Record<string, unknown>>;
32
- listPrompts(params?: {
33
- cursor?: string;
34
- }): Promise<{
35
- prompts: Array<{
36
- name: string;
37
- title?: string;
38
- description?: string;
39
- arguments?: Array<{
40
- name: string;
41
- description?: string;
42
- required?: boolean;
43
- }>;
44
- }>;
45
- nextCursor?: string;
46
- }>;
47
- getPrompt(params: {
48
- name: string;
49
- arguments?: Record<string, string>;
50
- }): Promise<Record<string, unknown>>;
51
- listResources(params?: {
52
- cursor?: string;
53
- }): Promise<{
54
- resources: Array<{
55
- uri: string;
56
- name: string;
57
- title?: string;
58
- description?: string;
59
- mimeType?: string;
60
- }>;
61
- nextCursor?: string;
62
- }>;
63
- readResource(params: {
64
- uri: string;
65
- }): Promise<Record<string, unknown>>;
66
- }
67
- export interface HostedMountDependencies {
68
- /** The hosted Summer Engine MCP URL (the store sign-in's resource). */
69
- url: () => string;
70
- token: () => Promise<string>;
71
- connect: (url: string, token: () => Promise<string>) => Promise<HostedClient>;
72
- }
22
+ /** How long the first tools/list waits for a mount in progress, so hosts that list once see the store tools. */
23
+ export declare const FIRST_LIST_WAIT_MS = 5000;
24
+ export type HostedMountDependencies = HostedConnectDependencies;
73
25
  export declare const defaultHostedMountDependencies: HostedMountDependencies;
74
26
  export interface HostedMountState {
75
27
  status: "idle" | "mounting" | "mounted" | "unavailable";
@@ -90,7 +42,9 @@ export interface HostedMountState {
90
42
  * reports each mounted tool's own JSON Schema rather than the passthrough one.
91
43
  * Returns mount(), which callers run after connect without awaiting it.
92
44
  */
93
- export declare function prepareHostedMount(server: McpServer, deps?: HostedMountDependencies): {
45
+ export declare function prepareHostedMount(server: McpServer, deps?: HostedMountDependencies, options?: {
46
+ firstListWaitMs?: number;
47
+ }): {
94
48
  mount: () => Promise<HostedMountState>;
95
49
  state: HostedMountState;
96
50
  };
@@ -1,10 +1,8 @@
1
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
1
  import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
2
  import { z } from "zod";
3
+ import { connectHostedMcp } from "../core/hosted-mcp.js";
5
4
  import { getStoreAccessToken, OAuthError, resolveMcpResourceUrl } from "../core/oauth.js";
6
5
  import { appendMcpLogEvent } from "../core/mcp-log.js";
7
- import { TOOLKIT_VERSION } from "../core/version.js";
8
6
  /**
9
7
  * One Summer Engine MCP. The cloud tools a creator's agent needs to publish
10
8
  * (projects, store page, build upload, submit for the owner's approval, the
@@ -16,38 +14,20 @@ import { TOOLKIT_VERSION } from "../core/version.js";
16
14
  * that server). A name the local MCP already has stays local (the engine and
17
15
  * generation tools keep their CLI-token path).
18
16
  *
19
- * Mounting is lazy: the server answers with the engine tools at once, and the
20
- * hosted tools appear when the mount finishes (tools/list_changed). Without a
21
- * store sign-in one tool, summer_store_tools, says how to sign in and mounts
22
- * on the next call.
17
+ * Mounting runs after connect. The first tools/list waits for a mount in
18
+ * progress (up to FIRST_LIST_WAIT_MS), so a host that lists tools once still
19
+ * sees the store tools; a slower mount adds them later (tools/list_changed).
20
+ * Without a store sign-in the mount fails at once, and one tool,
21
+ * summer_store_tools, says how to sign in and mounts on the next call.
23
22
  */
24
23
  export const STORE_TOOLS_STATUS = "summer_store_tools";
25
- const CALL_TIMEOUT_MS = 120_000;
24
+ /** How long the first tools/list waits for a mount in progress, so hosts that list once see the store tools. */
25
+ export const FIRST_LIST_WAIT_MS = 5_000;
26
26
  const passthroughArgs = z.object({}).passthrough();
27
27
  export const defaultHostedMountDependencies = {
28
28
  url: () => resolveMcpResourceUrl(),
29
29
  token: () => getStoreAccessToken(),
30
- connect: async (url, token) => {
31
- const transport = new StreamableHTTPClientTransport(new URL(url), {
32
- // A fresh (refreshed when near expiry) token on every request.
33
- fetch: async (input, init) => {
34
- const headers = new Headers(init?.headers);
35
- headers.set("authorization", `Bearer ${await token()}`);
36
- return fetch(input, { ...init, headers });
37
- },
38
- });
39
- const client = new Client({ name: "summer-engine-local", version: TOOLKIT_VERSION });
40
- await client.connect(transport);
41
- const timeout = { timeout: CALL_TIMEOUT_MS };
42
- return {
43
- listTools: (params) => client.listTools(params),
44
- callTool: (params) => client.callTool(params, undefined, timeout),
45
- listPrompts: (params) => client.listPrompts(params),
46
- getPrompt: (params) => client.getPrompt(params),
47
- listResources: (params) => client.listResources(params),
48
- readResource: (params) => client.readResource(params),
49
- };
50
- },
30
+ connect: connectHostedMcp,
51
31
  };
52
32
  async function listAll(page, key) {
53
33
  const out = [];
@@ -70,7 +50,9 @@ function errorText(error) {
70
50
  * reports each mounted tool's own JSON Schema rather than the passthrough one.
71
51
  * Returns mount(), which callers run after connect without awaiting it.
72
52
  */
73
- export function prepareHostedMount(server, deps = defaultHostedMountDependencies) {
53
+ export function prepareHostedMount(server, deps = defaultHostedMountDependencies, options = {}) {
54
+ const firstListWaitMs = options.firstListWaitMs ?? FIRST_LIST_WAIT_MS;
55
+ let listed = false;
74
56
  const state = { status: "idle", tools: [], prompts: [], resources: [], collisions: [] };
75
57
  const schemas = new Map();
76
58
  const registries = server;
@@ -82,6 +64,10 @@ export function prepareHostedMount(server, deps = defaultHostedMountDependencies
82
64
  const listTools = handlers.get("tools/list");
83
65
  if (listTools) {
84
66
  server.server.setRequestHandler(ListToolsRequestSchema, async (request, extra) => {
67
+ if (!listed) {
68
+ listed = true;
69
+ await waitForMount(firstListWaitMs);
70
+ }
85
71
  const result = await listTools(request, extra);
86
72
  return {
87
73
  ...result,
@@ -94,6 +80,14 @@ export function prepareHostedMount(server, deps = defaultHostedMountDependencies
94
80
  }
95
81
  let statusTool = null;
96
82
  let running = null;
83
+ /** Wait for a mount in progress, at most ms; returns at once when none runs. */
84
+ async function waitForMount(ms) {
85
+ if (!running || ms <= 0)
86
+ return;
87
+ let timer;
88
+ await Promise.race([running, new Promise((done) => (timer = setTimeout(done, ms)))]);
89
+ clearTimeout(timer);
90
+ }
97
91
  const showStatusTool = () => {
98
92
  if (statusTool || STORE_TOOLS_STATUS in registries._registeredTools)
99
93
  return;
@@ -356,8 +356,9 @@ export async function startMcpServer(options = {}) {
356
356
  appendMcpLogEvent("mcp:boot_drift_probe_failed", errorDetails(error));
357
357
  });
358
358
  // Hosted Summer Engine MCP tools (store, publishing, board, Grow): ready
359
- // before connect, mounted after it without blocking the engine tools.
360
- const hosted = prepareHostedMount(server);
359
+ // before connect, mounted after it. The first tools/list waits a few
360
+ // seconds for the mount (not under summer doctor, which times the list).
361
+ const hosted = prepareHostedMount(server, undefined, process.env.SUMMER_MCP_DOCTOR === "1" ? { firstListWaitMs: 0 } : {});
361
362
  const transport = new StdioServerTransport();
362
363
  await server.connect(transport);
363
364
  void hosted.mount();
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { CreatorOperationError, listCreatorReleases, publishCreator, } from "../../core/capabilities/creator.js";
3
+ import { captureGameplay } from "../../core/capabilities/capture-gameplay.js";
3
4
  import { exportGame } from "../../core/capabilities/export-game.js";
4
5
  import { STORE_TARGETS } from "../../core/capabilities/export-presets.js";
5
6
  import { exportTemplates, TEMPLATE_PLATFORMS } from "../../core/capabilities/export-templates.js";
@@ -39,7 +40,7 @@ async function creatorResult(operation) {
39
40
  }
40
41
  }
41
42
  export function registerCreatorTools(server) {
42
- server.tool("summer_export_game", "Export the game in the exact file the Summer Games store takes, with the installed Summer Engine headless (no window). format \"bundle\" (default): the summer.games .zip the Summer apps run on iPhone, Android, macOS and Windows; targets picks the platforms (a game without a server: ios and android only); upload it with summer_publish_build. format \"download\" with one target: web (an HTML5 .zip on Summer's WebGPU Forward+ web template, played on summer.games), macos (.app zip, macos-universal) or windows (.exe zip, windows-x64); these need the export template from summer_export_templates and are uploaded as store versions. Works without the editor running. Returns path, sha256, size, the manifest or store platform, and warnings.", {
43
+ server.tool("summer_export_game", "Export the game in the exact file the Summer Games store takes, with the installed Summer Engine headless (no window). format \"bundle\" (default): the summer.games .zip the Summer apps run on iPhone, Android, macOS and Windows; targets picks the platforms (a game without a server: ios and android only); upload it with summer_publish_build. format \"download\" with one target: web (an HTML5 .zip on Summer's WebGPU Forward+ web template, played on summer.games), macos (.app zip, macos-universal) or windows (.exe zip, windows-x64); these need the export template from summer_export_templates and are uploaded as store versions. Needs Summer Engine 0.7.0 or newer: an older engine is refused before anything is written (code engine_too_old). Works without the editor running. Returns path, sha256, size, the manifest or store platform, and warnings.", {
43
44
  project: z.string().optional().describe("Project folder with project.godot. Defaults to the MCP's bound project, then the working directory."),
44
45
  out: z.string().optional().describe("Output .zip path. Defaults to <project>/.summer/exports/."),
45
46
  targets: z
@@ -65,8 +66,17 @@ export function registerCreatorTools(server) {
65
66
  .optional()
66
67
  .describe("The installed Summer Engine version (Help > About, e.g. 0.7.1). Read automatically on macOS; needed on Windows and Linux."),
67
68
  }, async ({ action, platforms, includeDebug, summerVersion }) => creatorResult(() => exportTemplates({ action, platforms, includeDebug, summerVersion })));
68
- server.tool("summer_publish_build", "Upload an export from summer_export_game to the creator's game on summer.games, through the same store upload Studio uses, straight from disk. A store bundle (format bundle): declare, upload parts, seal, wait until Summer makes the Build, name its client pack, and with publish=true approve it. A web build or native download (format download): upload it as the game's store version for its platform (web, macos-universal, windows-x64, linux-x64; read from the last export) and wait until Summer has checked it. First call with confirm=false and show the user the returned target; set confirm only after they approve. Needs \"summer login --store\". A retry with the same file and clientVersion continues the same upload. Nothing here makes a game live: the owner approves publishing (summer_store_submit).", {
69
- gameId: z.string().optional().describe("The store game id (Studio store page URL). Omit to get the list of your games."),
69
+ server.tool("summer_capture_gameplay", "Capture real gameplay frames without a running editor: starts the game in Summer Engine's offscreen verify instance (a real renderer, a window parked offscreen with no focus, never on screen), lets it run waitSeconds, and saves PNG frames of what the game draws, HUD included. Use for store screenshots (1920x1080 landscape or 1080x1920 portrait). With an editor open, summer_screenshot target game also works. Returns each frame's path, width and height; look at them before using them.", {
70
+ project: z.string().optional().describe("Project folder with project.godot. Defaults to the MCP's bound project, then the working directory."),
71
+ scene: z.string().optional().describe("Scene to start (res://... or uid://...). Default: the project's main scene, which is often a title menu; pass the gameplay scene for gameplay frames."),
72
+ resolution: z.string().optional().describe("Window size WIDTHxHEIGHT (default 1920x1080; 1080x1920 for portrait)."),
73
+ frames: z.number().int().min(1).max(10).optional().describe("Frames to save (default 1)."),
74
+ waitSeconds: z.number().min(0).max(120).optional().describe("Seconds the game runs before the first frame (default 3)."),
75
+ intervalSeconds: z.number().min(0.1).max(60).optional().describe("Seconds between frames (default 1)."),
76
+ out: z.string().optional().describe("Output folder. Defaults to <project>/.summer/captures/<time>/ (ignored by git)."),
77
+ }, async (args) => creatorResult(() => captureGameplay(args)));
78
+ server.tool("summer_publish_build", "Upload an export from summer_export_game to the creator's game on summer.games, through the same store upload Studio uses, straight from disk. A store bundle (format bundle): declare, upload parts, seal, wait until Summer makes the Build, name its client pack, and with publish=true approve it. A web build or native download (format download): upload it as the game's store version for its platform (web, macos-universal, windows-x64, linux-x64; read from the last export) and wait until Summer has checked it. First call with confirm=false: it checks that the game exists for this account and warns when its store page does not list the export's platforms, then returns the target (game name and id, file, digest, size, version) to show the user; set confirm only after they approve. Needs \"summer login --store\". A retry with the same file and clientVersion continues the same upload. Nothing here makes a game live: the owner approves publishing (summer_store_submit).", {
79
+ gameId: z.string().optional().describe("The store game id (from summer_store_list_games, or the Studio store page URL). Without it the call fails with game_required and lists your store games, or says why the store did not list them."),
70
80
  file: z.string().optional().describe("Exported .zip. Defaults to the last summer_export_game result."),
71
81
  clientVersion: z.string().describe("Build version, vMAJOR.MINOR.PATCH (e.g. v1.0.0); a new upload needs a new version."),
72
82
  publish: z.boolean().default(false).describe("Bundles only: also approve the Build for players. A game that has not passed review keeps it as a preview; an agent needs the owner's approval."),
@@ -297,19 +297,29 @@ Style presets: "realistic" (default), "cartoon", "anime", "none"
297
297
  For an alpha PNG, set removeBackground: true. Prompt wording alone does not
298
298
  guarantee transparency; background removal runs server-side after generation.
299
299
 
300
- The 'options' object is passed directly to the AI provider for full control.
300
+ Size: aspectRatio (1:1, 16:9, 9:16, 4:3, 3:4, ...) or width + height (the
301
+ smallest size you need). For store art ask width 1920 height 1080 (key art)
302
+ or width 1080 height 1920 (tall cover): the server picks a model that reaches
303
+ that size, or refuses a named model that cannot, before anything is spent.
304
+ Each result reports the real width and height and the model that ran.
305
+
306
+ 'options' carries extra model params (seed, negative_prompt, ...); the result
307
+ lists any the server did not use in ignoredOptions.
301
308
 
302
309
  Returns the asset with fileUrl (hosted) and localPath (temp file on disk).
303
310
  Use the Read tool on localPath to show the image to the user for approval.
304
311
 
305
312
  Cloud tool — runs on Summer's servers and works WITHOUT the Summer Engine app open.
306
- Requires authentication: run 'npx -y summer-engine@latest login' first.`, imageGenerationArgsSchema.shape, async ({ prompt, model, style, referenceImageUrl, removeBackground, options }) => {
313
+ Requires authentication: run 'npx -y summer-engine@latest login' first.`, imageGenerationArgsSchema.shape, async ({ prompt, model, style, referenceImageUrl, removeBackground, aspectRatio, width, height, options }) => {
307
314
  const result = await mcpGenerate("/api/mcp/generate/image", {
308
315
  prompt,
309
316
  model,
310
317
  style,
311
318
  referenceImageUrl,
312
319
  removeBackground,
320
+ aspectRatio,
321
+ width,
322
+ height,
313
323
  options,
314
324
  });
315
325
  if (result.error) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_generated": "GENERATED from integrations/gemini — do not edit; npm run generate:registry",
3
3
  "name": "summer",
4
- "version": "3.3.0",
4
+ "version": "3.4.0",
5
5
  "description": "Agent tooling for Summer Engine: MCP bridge, context primer, and game-dev skills. Use with the npm CLI for skill files on disk.",
6
6
  "contextFileName": "GEMINI.md",
7
7
  "mcpServers": {
@@ -35,6 +35,7 @@
35
35
  "./library/skills/asset-strategy/",
36
36
  "./library/skills/audio-direction/",
37
37
  "./library/skills/auto-fire-targeting/",
38
+ "./library/skills/bounded-engine-runs/",
38
39
  "./library/skills/brainstorm-game/",
39
40
  "./library/skills/brainstorming/",
40
41
  "./library/skills/browse-templates/",
@@ -46,6 +47,7 @@
46
47
  "./library/skills/character-portrait/",
47
48
  "./library/skills/cinematic-cutscene/",
48
49
  "./library/skills/combat-basics/",
50
+ "./library/skills/compatibility-renderer-traps/",
49
51
  "./library/skills/concept-art/",
50
52
  "./library/skills/create-asset-sheet/",
51
53
  "./library/skills/debug/",