summer-engine 3.7.0 → 3.8.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +4 -2
- package/.codex-plugin/plugin.json +5 -3
- package/.cursor-plugin/plugin.json +4 -2
- package/.factory-plugin/plugin.json +4 -2
- package/CHANGELOG.md +16 -0
- package/README.md +11 -1
- package/commands/summer.md +2 -1
- package/dist/core/capabilities/apps.d.ts +318 -0
- package/dist/core/capabilities/apps.js +340 -0
- package/dist/core/capabilities/enable-plugin.d.ts +27 -0
- package/dist/core/capabilities/enable-plugin.js +75 -0
- package/dist/core/capabilities/tool-dispatch.js +49 -0
- package/dist/core/capability-skew.js +4 -0
- package/dist/mcp/hosted-mount.js +13 -1
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/tools/app-tools.d.ts +2 -0
- package/dist/mcp/tools/app-tools.js +64 -0
- package/dist/mcp/tools/generate-tools.js +3 -3
- package/gemini-extension.json +3 -1
- package/library/references/mcp-tools-reference/mcp-tools-reference.md +23 -1
- package/library/skills/README.md +10 -2
- package/library/skills/sound-effect/SKILL.md +2 -0
- package/library/skills/summer-apps/SKILL.md +152 -0
- package/library/skills/summer-apps/manifest.md +57 -0
- package/library/skills/summer-apps/minimal-app.md +183 -0
- package/library/skills/summer-apps/resource.yaml +36 -0
- package/library/skills/summer-apps/sdk-reference.md +135 -0
- package/library/skills/summer-apps-design/SKILL.md +197 -0
- package/library/skills/summer-apps-design/resource.yaml +23 -0
- package/library/skills/using-summer/SKILL.md +1 -0
- package/library/tools/app-call/resource.yaml +59 -0
- package/library/tools/app-create/resource.yaml +67 -0
- package/library/tools/app-data/resource.yaml +74 -0
- package/library/tools/app-dock/resource.yaml +59 -0
- package/library/tools/app-events/resource.yaml +68 -0
- package/library/tools/app-get/resource.yaml +46 -0
- package/library/tools/app-guide/resource.yaml +55 -0
- package/library/tools/app-list/resource.yaml +48 -0
- package/library/tools/app-page/resource.yaml +74 -0
- package/library/tools/app-publish/resource.yaml +90 -0
- package/library/tools/app-report-gap/resource.yaml +58 -0
- package/library/tools/app-screenshot/resource.yaml +85 -0
- package/library/tools/app-tools/resource.yaml +46 -0
- package/library/tools/app-update/resource.yaml +68 -0
- package/library/tools/enable-plugin/resource.yaml +53 -0
- package/package.json +1 -1
- package/registry/generated/aliases.json +15 -0
- package/registry/generated/counts.json +3 -3
- package/registry/generated/gemini-extension.json +3 -1
- package/registry/generated/index.json +656 -3
- package/registry/generated/marketplace.claude.json +2 -2
- package/registry/generated/plugin.claude.json +4 -2
- package/registry/generated/plugin.codex.json +5 -3
- package/registry/generated/plugin.cursor.json +4 -2
- package/registry/generated/plugin.factory.json +4 -2
- package/registry/generated/skills-index.md +10 -2
- package/registry/generated/skills-registry.json +27 -0
- package/registry/schemas/allowed-hosts.json +4 -2
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { readdir, readFile, lstat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { getAuthToken } from "../auth.js";
|
|
5
|
+
import { resolveGatewayUrl } from "../config.js";
|
|
6
|
+
import { TOOLKIT_VERSION } from "../version.js";
|
|
7
|
+
/**
|
|
8
|
+
* Summer Apps (summer_app_*): tools a game team uses inside Summer Studio,
|
|
9
|
+
* built by agents. Every cloud tool here calls the Summer web API under
|
|
10
|
+
* /api/mcp/apps with the signed-in Summer account; summer_app_dock asks the
|
|
11
|
+
* running editor to dock an app (engine ops OpenAppDock / CloseAppDock /
|
|
12
|
+
* ListAppDocks).
|
|
13
|
+
*
|
|
14
|
+
* One implementation for both faces: the MCP tools (src/mcp/tools/app-tools.ts)
|
|
15
|
+
* and `summer tool app-*` (tool-dispatch.ts) share the schemas and functions
|
|
16
|
+
* here and pass their own client surface.
|
|
17
|
+
*/
|
|
18
|
+
export const APPS_ENDPOINT = "/api/mcp/apps";
|
|
19
|
+
/** The server's long-poll ceiling for summer_app_events. */
|
|
20
|
+
export const MAX_APP_EVENTS_WAIT_SECONDS = 50;
|
|
21
|
+
export const APP_GUIDE_SECTIONS = ["all", "overview", "manifest", "sdk", "kit", "layout", "design", "examples", "types"];
|
|
22
|
+
export const APP_DOCK_SLOTS = ["left", "right", "bottom"];
|
|
23
|
+
/** Bundle limits the server enforces (checked here first for a clear local error). */
|
|
24
|
+
const MAX_FILES = 300;
|
|
25
|
+
const MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
26
|
+
const MAX_BUNDLE_BYTES = 10 * 1024 * 1024;
|
|
27
|
+
const TEXT_EXTENSIONS = new Set([".html", ".js", ".mjs", ".css", ".json", ".txt", ".md", ".svg", ".gltf"]);
|
|
28
|
+
const appId = z.string().uuid().describe("The app id from summer_app_create or summer_app_list");
|
|
29
|
+
const jsonObject = z.record(z.unknown());
|
|
30
|
+
export const appGuideArgsSchema = z.object({
|
|
31
|
+
section: z
|
|
32
|
+
.enum(APP_GUIDE_SECTIONS)
|
|
33
|
+
.default("all")
|
|
34
|
+
.describe("One part of the guide: overview, manifest, sdk, kit, layout, design, examples, or types (the SDK's exact TypeScript types, not in all). Default all"),
|
|
35
|
+
});
|
|
36
|
+
export const appListArgsSchema = z.object({
|
|
37
|
+
workspaceId: z.string().uuid().optional().describe("Only this workspace (team). Default: every workspace the person is in"),
|
|
38
|
+
q: z.string().max(200).optional().describe("Search app names and purposes"),
|
|
39
|
+
});
|
|
40
|
+
export const appCreateArgsSchema = z.object({
|
|
41
|
+
name: z.string().min(1).max(80),
|
|
42
|
+
purpose: z.string().max(280).optional().describe("One line: what the tool does and where its data goes"),
|
|
43
|
+
format: z.enum(["code", "layout"]).default("code").describe("code: index.html + JS/CSS (default). layout: a JSON layout of kit nodes, for quick decision UIs"),
|
|
44
|
+
workspaceId: z.string().uuid().optional().describe("A workspace id. Default: the person's team (the one their Studio tab is on, else their latest team, else personal); the result's workspace says which"),
|
|
45
|
+
folder: z.string().min(1).max(80).optional(),
|
|
46
|
+
});
|
|
47
|
+
export const appPublishArgsSchema = z.object({
|
|
48
|
+
appId,
|
|
49
|
+
dir: z
|
|
50
|
+
.string()
|
|
51
|
+
.min(1)
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("A local folder holding the app (index.html at its root). Every file under it is published; dotfiles, node_modules and .d.ts files are skipped"),
|
|
54
|
+
files: z
|
|
55
|
+
.array(z.object({
|
|
56
|
+
path: z.string().min(1).max(200),
|
|
57
|
+
content: z.string(),
|
|
58
|
+
encoding: z.enum(["utf8", "base64"]).default("utf8"),
|
|
59
|
+
}))
|
|
60
|
+
.max(MAX_FILES)
|
|
61
|
+
.optional()
|
|
62
|
+
.describe("The files instead of dir: [{path, content, encoding}]"),
|
|
63
|
+
layout: jsonObject.optional().describe("Layout apps: { version: 1, title, root }"),
|
|
64
|
+
manifest: jsonObject.describe("The manifest {name, purpose, permissions, engineFiles, agentEvents, agentTools} (summer_app_guide section manifest)"),
|
|
65
|
+
note: z.string().max(500).optional().describe("What changed, shown in the version list"),
|
|
66
|
+
idempotencyKey: z.string().min(8).max(128).optional(),
|
|
67
|
+
});
|
|
68
|
+
export const appGetArgsSchema = z.object({ appId });
|
|
69
|
+
export const appEventsArgsSchema = z.object({
|
|
70
|
+
appId,
|
|
71
|
+
after: z.number().int().min(0).default(0).describe("Return events after this cursor (0: from the start). Pass the returned cursor next time"),
|
|
72
|
+
timeoutSeconds: z.number().int().min(0).max(MAX_APP_EVENTS_WAIT_SECONDS).default(25).describe("Wait up to this long (max 50) for the first new event"),
|
|
73
|
+
kinds: z.array(z.enum(["agent", "error", "feedback", "tool"])).max(4).optional().describe("Default: agent, error, feedback"),
|
|
74
|
+
});
|
|
75
|
+
export const appPageArgsSchema = z.object({
|
|
76
|
+
appId,
|
|
77
|
+
fill: z
|
|
78
|
+
.array(z.object({ field: z.string().min(1).max(200), value: z.union([z.string().max(10_000), z.number(), z.boolean()]) }))
|
|
79
|
+
.max(50)
|
|
80
|
+
.optional()
|
|
81
|
+
.describe("Values to set: [{field, value}] with field ids from a read"),
|
|
82
|
+
press: z.string().min(1).max(200).optional().describe("An action id to press after filling"),
|
|
83
|
+
});
|
|
84
|
+
export const appUpdateArgsSchema = z.object({
|
|
85
|
+
appId,
|
|
86
|
+
name: z.string().min(1).max(80).optional(),
|
|
87
|
+
purpose: z.string().max(280).optional(),
|
|
88
|
+
status: z.enum(["scratch", "kept", "archived"]).optional().describe("kept: the team keeps it. archived: hidden from the app list (restore with scratch or kept)"),
|
|
89
|
+
folder: z.string().min(1).max(80).nullable().optional().describe("null takes it out of its folder"),
|
|
90
|
+
});
|
|
91
|
+
export const appScreenshotArgsSchema = z.object({
|
|
92
|
+
appId,
|
|
93
|
+
version: z.number().int().positive().optional().describe("A published version. Default: the current one"),
|
|
94
|
+
width: z.number().int().min(320).max(1920).default(1280).describe("Viewport width in px: 390 for a phone"),
|
|
95
|
+
height: z.number().int().min(320).max(1600).default(800),
|
|
96
|
+
theme: z.enum(["light", "dark"]).default("light"),
|
|
97
|
+
fullPage: z.boolean().default(false).describe("Capture the whole scroll height (up to 4000 px)"),
|
|
98
|
+
params: z.record(z.string().max(500)).optional().describe("Launch params, as in /studio/apps/<id>?key=value"),
|
|
99
|
+
out: z.string().min(1).optional().describe("Also save the PNG to this local path"),
|
|
100
|
+
});
|
|
101
|
+
export const appDataArgsSchema = z.object({
|
|
102
|
+
appId,
|
|
103
|
+
collection: z.string().max(48).optional().describe("Read: one collection. Default: all"),
|
|
104
|
+
after: z.string().max(200).optional().describe("Read: the next cursor from the previous page"),
|
|
105
|
+
limit: z.number().int().min(1).max(500).default(200),
|
|
106
|
+
ops: z
|
|
107
|
+
.array(jsonObject)
|
|
108
|
+
.min(1)
|
|
109
|
+
.max(100)
|
|
110
|
+
.optional()
|
|
111
|
+
.describe("Write: [{op: put, collection, recordId, data}, {op: patch, collection, recordId, patch}, {op: remove, collection, recordId}]; ifRevision on an op applies it only at that record revision (0: must not exist)"),
|
|
112
|
+
as: z.enum(["agent", "seed"]).optional().describe("Write: seed for starter data that belongs to the team (no author). Default: agent"),
|
|
113
|
+
});
|
|
114
|
+
export const appToolsArgsSchema = z.object({ appId });
|
|
115
|
+
export const appCallArgsSchema = z.object({
|
|
116
|
+
appId,
|
|
117
|
+
tool: z.string().min(1).max(64).describe("The agent tool name from summer_app_tools"),
|
|
118
|
+
input: jsonObject.default({}).describe("Input matching the tool's input schema"),
|
|
119
|
+
});
|
|
120
|
+
export const appReportGapArgsSchema = z.object({
|
|
121
|
+
capability: z.string().min(1).max(200).describe('What the app needed, e.g. "read Steam reviews"'),
|
|
122
|
+
detail: z.string().max(2000).optional(),
|
|
123
|
+
method: z.string().max(100).optional().describe("The SDK method name you wished existed"),
|
|
124
|
+
appId: appId.optional(),
|
|
125
|
+
});
|
|
126
|
+
export const appDockArgsSchema = z.object({
|
|
127
|
+
action: z.enum(["open", "close", "list"]).default("open"),
|
|
128
|
+
appId: appId.optional().describe("open, close: the app"),
|
|
129
|
+
slot: z.enum(APP_DOCK_SLOTS).default("right").describe("open: left, right or bottom"),
|
|
130
|
+
});
|
|
131
|
+
/** Fetch with the Summer account token; errors come back as replies, never throws. */
|
|
132
|
+
export function appsGateway(surface, tool) {
|
|
133
|
+
return async (method, endpoint, { query, body, timeoutMs }) => {
|
|
134
|
+
const token = await getAuthToken();
|
|
135
|
+
if (!token)
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
status: 401,
|
|
139
|
+
data: { error: { code: "unauthorized", message: "Not signed in. Run: npx -y summer-engine@latest login" } },
|
|
140
|
+
};
|
|
141
|
+
const gateway = await resolveGatewayUrl();
|
|
142
|
+
const suffix = query?.toString() ? `?${query.toString()}` : "";
|
|
143
|
+
let response;
|
|
144
|
+
try {
|
|
145
|
+
response = await fetch(`${gateway}${endpoint}${suffix}`, {
|
|
146
|
+
method,
|
|
147
|
+
headers: {
|
|
148
|
+
Authorization: `Bearer ${token}`,
|
|
149
|
+
"X-Summer-Client": "summer-cli",
|
|
150
|
+
"X-Summer-Client-Version": TOOLKIT_VERSION,
|
|
151
|
+
"X-Summer-Client-Surface": surface,
|
|
152
|
+
"X-Summer-MCP-Endpoint": endpoint,
|
|
153
|
+
"X-Summer-MCP-Tool": tool,
|
|
154
|
+
...(body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
155
|
+
},
|
|
156
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
157
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
return { ok: false, status: 0, data: { error: { code: "unavailable", message: `Request failed: ${error instanceof Error ? error.message : String(error)}` } } };
|
|
162
|
+
}
|
|
163
|
+
const text = await response.text().catch(() => "");
|
|
164
|
+
let data = {};
|
|
165
|
+
try {
|
|
166
|
+
data = text ? JSON.parse(text) : {};
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
data = { message: text.slice(0, 1000) };
|
|
170
|
+
}
|
|
171
|
+
return { ok: response.ok, status: response.status, data };
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** The server's `{error: {code, message}}` as one flat result an agent can act on. */
|
|
175
|
+
function failure(reply) {
|
|
176
|
+
const error = reply.data.error;
|
|
177
|
+
const record = error && typeof error === "object" ? error : {};
|
|
178
|
+
const code = typeof record.code === "string" ? record.code : reply.status === 404 ? "not_found" : "unavailable";
|
|
179
|
+
const message = typeof record.message === "string" ? record.message : typeof reply.data.message === "string" ? reply.data.message : `Request failed (${reply.status})`;
|
|
180
|
+
const hints = {
|
|
181
|
+
unauthorized: "Run: npx -y summer-engine@latest login",
|
|
182
|
+
not_found: "Summer Apps is not available for this account yet, or the app id is wrong: summer_app_list shows the person's apps.",
|
|
183
|
+
app_not_open: "Use summer_app_data, or ask the person to open the app in Summer Studio.",
|
|
184
|
+
invalid: "Fix what the message names and try again (summer_app_guide has the rules).",
|
|
185
|
+
too_large: "Limits: 10 MB per app, 300 files, 5 MB per file.",
|
|
186
|
+
rate_limited: "Wait a minute and try again.",
|
|
187
|
+
};
|
|
188
|
+
const { error: _error, message: _message, ...rest } = reply.data;
|
|
189
|
+
return { ok: false, code, error: message, status: reply.status, ...(hints[code] ? { hint: hints[code] } : {}), ...rest, ...(record.details ? { details: record.details } : {}) };
|
|
190
|
+
}
|
|
191
|
+
const done = (reply) => (reply.ok ? { ok: true, ...reply.data } : failure(reply));
|
|
192
|
+
const params = (values) => {
|
|
193
|
+
const query = new URLSearchParams();
|
|
194
|
+
for (const [key, value] of Object.entries(values))
|
|
195
|
+
if (value !== undefined && value !== "")
|
|
196
|
+
query.set(key, value);
|
|
197
|
+
return query;
|
|
198
|
+
};
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Tools
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
export async function appGuide(args, deps) {
|
|
203
|
+
return done(await deps.request("GET", `${APPS_ENDPOINT}/guide`, { query: params({ section: args.section }), timeoutMs: 30_000 }));
|
|
204
|
+
}
|
|
205
|
+
export async function appList(args, deps) {
|
|
206
|
+
return done(await deps.request("GET", APPS_ENDPOINT, { query: params({ workspaceId: args.workspaceId, q: args.q }), timeoutMs: 30_000 }));
|
|
207
|
+
}
|
|
208
|
+
export async function appCreate(args, deps) {
|
|
209
|
+
const reply = await deps.request("POST", APPS_ENDPOINT, { body: args, timeoutMs: 30_000 });
|
|
210
|
+
if (!reply.ok)
|
|
211
|
+
return failure(reply);
|
|
212
|
+
const studio = reply.data.studio;
|
|
213
|
+
const workspace = reply.data.workspace;
|
|
214
|
+
const where = workspace?.name ? `It is in ${workspace.kind === "personal" ? "the person's personal workspace" : `the team ${workspace.name}`}: tell them. ` : "";
|
|
215
|
+
return {
|
|
216
|
+
ok: true,
|
|
217
|
+
...reply.data,
|
|
218
|
+
next: `${where}${studio?.queued ? "The person's Summer Studio tab is opening the app. " : ""}Write the app (summer_app_guide first), then summer_app_publish with appId and the files or dir.`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/** Every file under dir (dotfiles, node_modules and .d.ts type files skipped), as publish files. */
|
|
222
|
+
export async function readAppDir(dir) {
|
|
223
|
+
const root = path.resolve(dir);
|
|
224
|
+
const files = [];
|
|
225
|
+
let total = 0;
|
|
226
|
+
const walk = async (current) => {
|
|
227
|
+
for (const entry of (await readdir(current, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
228
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name.endsWith(".d.ts"))
|
|
229
|
+
continue;
|
|
230
|
+
const full = path.join(current, entry.name);
|
|
231
|
+
if (entry.isDirectory()) {
|
|
232
|
+
await walk(full);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (!entry.isFile() || (await lstat(full)).isSymbolicLink())
|
|
236
|
+
continue;
|
|
237
|
+
const bytes = await readFile(full);
|
|
238
|
+
if (bytes.length > MAX_FILE_BYTES)
|
|
239
|
+
throw new Error(`${path.relative(root, full)} is over 5 MB`);
|
|
240
|
+
total += bytes.length;
|
|
241
|
+
if (total > MAX_BUNDLE_BYTES)
|
|
242
|
+
throw new Error(`${dir} is over the 10 MB app limit`);
|
|
243
|
+
const relative = path.relative(root, full).split(path.sep).join("/");
|
|
244
|
+
const text = TEXT_EXTENSIONS.has(path.extname(entry.name).toLowerCase());
|
|
245
|
+
files.push({ path: relative, content: bytes.toString(text ? "utf8" : "base64"), encoding: text ? "utf8" : "base64" });
|
|
246
|
+
if (files.length > MAX_FILES)
|
|
247
|
+
throw new Error(`${dir} has more than ${MAX_FILES} files`);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
await walk(root);
|
|
251
|
+
return files;
|
|
252
|
+
}
|
|
253
|
+
export async function appPublish(args, deps) {
|
|
254
|
+
const sources = [args.dir, args.files, args.layout].filter((value) => value !== undefined).length;
|
|
255
|
+
if (sources !== 1)
|
|
256
|
+
return { ok: false, code: "invalid", error: "Send exactly one of dir, files or layout." };
|
|
257
|
+
let files = args.files;
|
|
258
|
+
if (args.dir) {
|
|
259
|
+
try {
|
|
260
|
+
files = await (deps.readDir ?? readAppDir)(args.dir);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
return { ok: false, code: "invalid", error: error instanceof Error ? error.message : String(error) };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const body = { ...(files ? { files } : { layout: args.layout }), manifest: args.manifest, ...(args.note ? { note: args.note } : {}), ...(args.idempotencyKey ? { idempotencyKey: args.idempotencyKey } : {}) };
|
|
267
|
+
const reply = await deps.request("POST", `${APPS_ENDPOINT}/${args.appId}/publish`, { body, timeoutMs: 120_000 });
|
|
268
|
+
if (!reply.ok)
|
|
269
|
+
return failure(reply);
|
|
270
|
+
const version = reply.data.version;
|
|
271
|
+
return {
|
|
272
|
+
ok: true,
|
|
273
|
+
...reply.data,
|
|
274
|
+
next: `Version ${version?.version ?? "?"} is live in the person's Studio. Wait for what the app sends with summer_app_events, or publish again to change it.`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
export async function appGet(args, deps) {
|
|
278
|
+
return done(await deps.request("GET", `${APPS_ENDPOINT}/${args.appId}`, { timeoutMs: 60_000 }));
|
|
279
|
+
}
|
|
280
|
+
export async function appEvents(args, deps) {
|
|
281
|
+
return done(await deps.request("GET", `${APPS_ENDPOINT}/${args.appId}/events`, {
|
|
282
|
+
query: params({ after: String(args.after), timeoutMs: String(args.timeoutSeconds * 1000), kinds: args.kinds?.join(",") }),
|
|
283
|
+
timeoutMs: (args.timeoutSeconds + 20) * 1000,
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
export async function appPage(args, deps) {
|
|
287
|
+
const { appId: id, ...body } = args;
|
|
288
|
+
return done(await deps.request("POST", `${APPS_ENDPOINT}/${id}/page`, { body, timeoutMs: 60_000 }));
|
|
289
|
+
}
|
|
290
|
+
export async function appUpdate(args, deps) {
|
|
291
|
+
const { appId: id, ...body } = args;
|
|
292
|
+
return done(await deps.request("POST", `${APPS_ENDPOINT}/${id}/update`, { body, timeoutMs: 30_000 }));
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* summer_app_screenshot: the server renders the app headless and read-only as the
|
|
296
|
+
* person and answers the PNG (base64) with its console, errors and declared page.
|
|
297
|
+
* The image moves to `image`; `out` also writes it to disk.
|
|
298
|
+
*/
|
|
299
|
+
export async function appScreenshot(args, deps) {
|
|
300
|
+
const { appId: id, out, ...body } = args;
|
|
301
|
+
const result = done(await deps.request("POST", `${APPS_ENDPOINT}/${id}/screenshot`, { body, timeoutMs: 90_000 }));
|
|
302
|
+
if (result.ok !== true)
|
|
303
|
+
return result;
|
|
304
|
+
const { image, ...report } = result;
|
|
305
|
+
if (!image)
|
|
306
|
+
return { ...report, ok: false, code: "invalid", error: "The server answered without an image" };
|
|
307
|
+
if (out)
|
|
308
|
+
await (deps.writeFile ?? ((file, data) => import("node:fs/promises").then((fs) => fs.writeFile(file, data))))(path.resolve(out), Buffer.from(image.base64, "base64"));
|
|
309
|
+
return { ...report, image, ...(out ? { savedTo: path.resolve(out) } : {}) };
|
|
310
|
+
}
|
|
311
|
+
export async function appData(args, deps) {
|
|
312
|
+
if (args.ops)
|
|
313
|
+
return done(await deps.request("POST", `${APPS_ENDPOINT}/${args.appId}/data`, { body: { ops: args.ops, ...(args.as ? { as: args.as } : {}) }, timeoutMs: 30_000 }));
|
|
314
|
+
return done(await deps.request("GET", `${APPS_ENDPOINT}/${args.appId}/data`, {
|
|
315
|
+
query: params({ collection: args.collection, after: args.after, limit: String(args.limit) }),
|
|
316
|
+
timeoutMs: 30_000,
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
export async function appTools(args, deps) {
|
|
320
|
+
return done(await deps.request("GET", `${APPS_ENDPOINT}/${args.appId}/tools`, { timeoutMs: 30_000 }));
|
|
321
|
+
}
|
|
322
|
+
export async function appCall(args, deps) {
|
|
323
|
+
const { appId: id, ...body } = args;
|
|
324
|
+
return done(await deps.request("POST", `${APPS_ENDPOINT}/${id}/call`, { body, timeoutMs: 90_000 }));
|
|
325
|
+
}
|
|
326
|
+
export async function appReportGap(args, deps) {
|
|
327
|
+
return done(await deps.request("POST", `${APPS_ENDPOINT}/report-gap`, { body: args, timeoutMs: 30_000 }));
|
|
328
|
+
}
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// summer_app_dock (engine)
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
export const APP_DOCK_FALLBACK = "This engine build cannot dock apps yet. Open the app in the Summer Studio tab instead (summer_studio_open with /studio/apps/<appId>), or update Summer Engine.";
|
|
333
|
+
/** The engine op for one dock action. */
|
|
334
|
+
export function appDockOp(args) {
|
|
335
|
+
if (args.action === "list")
|
|
336
|
+
return { op: "ListAppDocks" };
|
|
337
|
+
if (!args.appId)
|
|
338
|
+
throw new Error(`${args.action} needs appId`);
|
|
339
|
+
return args.action === "open" ? { op: "OpenAppDock", appId: args.appId, slot: args.slot } : { op: "CloseAppDock", appId: args.appId };
|
|
340
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* summer_enable_plugin: enable an editor plugin (`res://addons/<name>/`) and
|
|
4
|
+
* report what really happened. Engines with Summer Apps support run the
|
|
5
|
+
* EnableEditorPlugin op until the plugin's `_enter_tree` has run and answer
|
|
6
|
+
* `enabled` (or a failure `code`); older engines only queue the enable and
|
|
7
|
+
* answer `deferred: true`, so this tool says the result is unknown instead of
|
|
8
|
+
* claiming success. One implementation for the MCP tool and `summer tool
|
|
9
|
+
* enable-plugin`.
|
|
10
|
+
*/
|
|
11
|
+
export declare const enablePluginArgsSchema: z.ZodObject<{
|
|
12
|
+
plugin: z.ZodString;
|
|
13
|
+
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
}, "strip", z.ZodTypeAny, {
|
|
15
|
+
plugin: string;
|
|
16
|
+
timeoutMs?: number | undefined;
|
|
17
|
+
}, {
|
|
18
|
+
plugin: string;
|
|
19
|
+
timeoutMs?: number | undefined;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function enablePluginOp(args: z.output<typeof enablePluginArgsSchema>): {
|
|
22
|
+
op: string;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
};
|
|
25
|
+
export declare const QUEUED_ONLY_NOTE: string;
|
|
26
|
+
/** The engine's answer for EnableEditorPlugin, as one result an agent can trust. */
|
|
27
|
+
export declare function interpretEnablePluginResult(raw: unknown): Record<string, unknown>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { asRecord } from "../util/json.js";
|
|
3
|
+
import { extractOpError } from "./engine-receipt.js";
|
|
4
|
+
/**
|
|
5
|
+
* summer_enable_plugin: enable an editor plugin (`res://addons/<name>/`) and
|
|
6
|
+
* report what really happened. Engines with Summer Apps support run the
|
|
7
|
+
* EnableEditorPlugin op until the plugin's `_enter_tree` has run and answer
|
|
8
|
+
* `enabled` (or a failure `code`); older engines only queue the enable and
|
|
9
|
+
* answer `deferred: true`, so this tool says the result is unknown instead of
|
|
10
|
+
* claiming success. One implementation for the MCP tool and `summer tool
|
|
11
|
+
* enable-plugin`.
|
|
12
|
+
*/
|
|
13
|
+
export const enablePluginArgsSchema = z.object({
|
|
14
|
+
plugin: z
|
|
15
|
+
.string()
|
|
16
|
+
.min(1)
|
|
17
|
+
.max(200)
|
|
18
|
+
.describe("The plugin folder under res://addons/ (e.g. 'quest_tools') or its 'res://addons/<name>/plugin.cfg'"),
|
|
19
|
+
timeoutMs: z
|
|
20
|
+
.number()
|
|
21
|
+
.int()
|
|
22
|
+
.min(1000)
|
|
23
|
+
.max(120_000)
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("How long the engine waits for the plugin to load (default 30000)"),
|
|
26
|
+
});
|
|
27
|
+
export function enablePluginOp(args) {
|
|
28
|
+
return { op: "EnableEditorPlugin", pluginPath: args.plugin.trim(), ...(args.timeoutMs ? { timeoutMs: args.timeoutMs } : {}) };
|
|
29
|
+
}
|
|
30
|
+
const FAILURE_HINTS = {
|
|
31
|
+
CFG_MISSING: "No plugin.cfg there. Check the folder name under res://addons/.",
|
|
32
|
+
CFG_PARSE_FAILED: "plugin.cfg does not parse. Fix it and enable again.",
|
|
33
|
+
NO_SCRIPT_KEY: "plugin.cfg needs a script= entry under [plugin].",
|
|
34
|
+
EMPTY_SCRIPT_PATH: "plugin.cfg needs a script= entry under [plugin].",
|
|
35
|
+
SCRIPT_MISSING: "The script plugin.cfg names does not exist.",
|
|
36
|
+
SCRIPT_LOAD_FAILED: "The plugin script did not load. Read summer_get_script_errors for it.",
|
|
37
|
+
SCRIPT_ERROR: "The plugin script has errors. Read summer_get_script_errors, fix them, and enable again.",
|
|
38
|
+
NOT_EDITOR_PLUGIN: "The plugin script must extend EditorPlugin.",
|
|
39
|
+
NOT_TOOL: "The plugin script must start with @tool.",
|
|
40
|
+
TIMEOUT: "The plugin did not finish loading in time (often a filesystem scan). Try again, or raise timeoutMs.",
|
|
41
|
+
};
|
|
42
|
+
export const QUEUED_ONLY_NOTE = "This Summer Engine build only queues the enable and does not report whether the plugin loaded. " +
|
|
43
|
+
"Check summer_get_debugger_errors and summer_get_script_errors for the plugin script before relying on it, or update Summer Engine for the real result.";
|
|
44
|
+
/** The engine's answer for EnableEditorPlugin, as one result an agent can trust. */
|
|
45
|
+
export function interpretEnablePluginResult(raw) {
|
|
46
|
+
const root = asRecord(raw) ?? {};
|
|
47
|
+
const results = Array.isArray(root.results) ? root.results : [];
|
|
48
|
+
const entry = asRecord(results.find((item) => asRecord(item)?.op === "EnableEditorPlugin") ?? results[0]) ?? root;
|
|
49
|
+
const addonPath = typeof entry.addonPath === "string" ? entry.addonPath : undefined;
|
|
50
|
+
const error = extractOpError(raw) ?? (entry.ok === false ? String(entry.error ?? "The plugin was not enabled.") : null);
|
|
51
|
+
if (error) {
|
|
52
|
+
const code = typeof entry.code === "string" ? entry.code : undefined;
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
enabled: false,
|
|
56
|
+
...(addonPath ? { addonPath } : {}),
|
|
57
|
+
...(code ? { code } : {}),
|
|
58
|
+
error,
|
|
59
|
+
...(code && FAILURE_HINTS[code] ? { hint: FAILURE_HINTS[code] } : {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (entry.alreadyEnabled === true)
|
|
63
|
+
return { ok: true, enabled: true, alreadyEnabled: true, ...(addonPath ? { addonPath } : {}) };
|
|
64
|
+
if (entry.enabled === true) {
|
|
65
|
+
return {
|
|
66
|
+
ok: true,
|
|
67
|
+
enabled: true,
|
|
68
|
+
...(addonPath ? { addonPath } : {}),
|
|
69
|
+
autoloadsRegistered: Array.isArray(entry.autoloadsRegistered) ? entry.autoloadsRegistered : [],
|
|
70
|
+
...(typeof entry.elapsedMs === "number" ? { elapsedMs: entry.elapsedMs } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Older engines: { ok: true, deferred: true } means "scheduled", not "loaded".
|
|
74
|
+
return { ok: true, enabled: null, queued: true, ...(addonPath ? { addonPath } : {}), note: QUEUED_ONLY_NOTE };
|
|
75
|
+
}
|
|
@@ -76,6 +76,8 @@ import { attachToSurface, attachToSurfaceArgsSchema, connectPorts, connectPortsA
|
|
|
76
76
|
import { readLibraryEntry, readLibraryInputSchema } from "../library-read.js";
|
|
77
77
|
import { runSearchLibrary, searchLibraryInputSchema } from "../library-search.js";
|
|
78
78
|
import { TOOLKIT_VERSION as CLI_VERSION } from "../version.js";
|
|
79
|
+
import { APP_DOCK_FALLBACK, appCall, appCallArgsSchema, appCreate, appCreateArgsSchema, appData, appDataArgsSchema, appDockArgsSchema, appDockOp, appEvents, appEventsArgsSchema, appGet, appGetArgsSchema, appGuide, appGuideArgsSchema, appList, appListArgsSchema, appPage, appPageArgsSchema, appScreenshot, appScreenshotArgsSchema, appUpdate, appUpdateArgsSchema, appPublish, appPublishArgsSchema, appReportGap, appReportGapArgsSchema, appsGateway, appTools, appToolsArgsSchema, } from "./apps.js";
|
|
80
|
+
import { enablePluginArgsSchema, enablePluginOp, interpretEnablePluginResult } from "./enable-plugin.js";
|
|
79
81
|
/** Dispatch-level failure with a user-facing message (no stack needed). */
|
|
80
82
|
export class ToolDispatchError extends Error {
|
|
81
83
|
}
|
|
@@ -404,6 +406,14 @@ async function gatewayReply(call) {
|
|
|
404
406
|
return { error: error.message, status: 0 };
|
|
405
407
|
}
|
|
406
408
|
}
|
|
409
|
+
/** summer tool app-screenshot: the PNG goes to `out` (default ./summer-app-screenshot.png), not into the printed JSON. */
|
|
410
|
+
async function cliScreenshot(args, deps) {
|
|
411
|
+
const shot = await appScreenshot({ ...args, out: args.out ?? "summer-app-screenshot.png" }, deps);
|
|
412
|
+
if (shot.ok !== true || !("image" in shot))
|
|
413
|
+
return shot;
|
|
414
|
+
const { image: _image, ...report } = shot;
|
|
415
|
+
return report;
|
|
416
|
+
}
|
|
407
417
|
function sfxChoicesDeps() {
|
|
408
418
|
return {
|
|
409
419
|
post: (endpoint, body, timeoutMs) => gatewayReply(() => gatewayPost(endpoint, body, timeoutMs)),
|
|
@@ -906,6 +916,45 @@ export const TOOL_DISPATCH = [
|
|
|
906
916
|
throw new ToolResultError(result, String(result.error));
|
|
907
917
|
return result;
|
|
908
918
|
}),
|
|
919
|
+
// Summer Apps (core/capabilities/apps.ts): the same functions as the MCP face.
|
|
920
|
+
...[
|
|
921
|
+
["summer_app_guide", "Read the Summer Apps build guide (SDK, manifest, kit, layout, design, examples)", appGuideArgsSchema, appGuide],
|
|
922
|
+
["summer_app_list", "List the Summer Apps in the person's workspaces", appListArgsSchema, appList],
|
|
923
|
+
["summer_app_create", "Create a scratch Summer App and open it in Summer Studio", appCreateArgsSchema, appCreate],
|
|
924
|
+
["summer_app_publish", "Publish a new version of a Summer App from a folder or files", appPublishArgsSchema, appPublish],
|
|
925
|
+
["summer_app_get", "Read a Summer App's manifest, files and data summary", appGetArgsSchema, appGet],
|
|
926
|
+
["summer_app_update", "Rename a Summer App or keep, archive or restore it", appUpdateArgsSchema, appUpdate],
|
|
927
|
+
["summer_app_events", "Wait for events a Summer App sends to agents", appEventsArgsSchema, appEvents],
|
|
928
|
+
["summer_app_page", "Read or use the open app's declared fields and buttons", appPageArgsSchema, appPage],
|
|
929
|
+
["summer_app_data", "Read or write a Summer App's shared data", appDataArgsSchema, appData],
|
|
930
|
+
["summer_app_screenshot", "See a Summer App rendered (read-only) with its console and errors; out saves the PNG", appScreenshotArgsSchema, cliScreenshot],
|
|
931
|
+
["summer_app_tools", "List a Summer App's agent tools", appToolsArgsSchema, appTools],
|
|
932
|
+
["summer_app_call", "Run a Summer App's agent tool in the person's open app", appCallArgsSchema, appCall],
|
|
933
|
+
["summer_app_report_gap", "Report an SDK capability a Summer App needed", appReportGapArgsSchema, appReportGap],
|
|
934
|
+
].map(([name, summary, schema, run]) => entry(name, summary, false, async (args) => {
|
|
935
|
+
const result = await run(parseToolArgs(schema, args, name.replace(/^summer_/, "").replace(/_/g, "-")), { request: appsGateway("cli", name) });
|
|
936
|
+
if (result.ok !== true)
|
|
937
|
+
throw new ToolResultError(result, String(result.error));
|
|
938
|
+
return result;
|
|
939
|
+
})),
|
|
940
|
+
entry("summer_enable_plugin", "Enable an editor plugin and report whether it really loaded", true, async (args, ctx) => {
|
|
941
|
+
const parsed = parseToolArgs(enablePluginArgsSchema, args, "enable-plugin");
|
|
942
|
+
const result = interpretEnablePluginResult(await (await ctx.engine()).executeOps([enablePluginOp(parsed)]));
|
|
943
|
+
if (result.ok !== true)
|
|
944
|
+
throw new ToolResultError(result, String(result.error));
|
|
945
|
+
return result;
|
|
946
|
+
}),
|
|
947
|
+
entry("summer_app_dock", "Dock a Summer App in the editor, close its dock, or list docks", true, async (args, ctx) => {
|
|
948
|
+
const parsed = parseToolArgs(appDockArgsSchema, args, "app-dock");
|
|
949
|
+
if (parsed.action !== "list" && !parsed.appId)
|
|
950
|
+
throw new ToolDispatchError(`${parsed.action} needs appId`);
|
|
951
|
+
const op = appDockOp(parsed);
|
|
952
|
+
const client = await ctx.engine();
|
|
953
|
+
const missing = missingEngineOpResult(client, op.op, APP_DOCK_FALLBACK);
|
|
954
|
+
if (missing)
|
|
955
|
+
throw new ToolResultError({ ...missing }, missing.error);
|
|
956
|
+
return requireEngineSuccess(await client.executeOps([op]));
|
|
957
|
+
}),
|
|
909
958
|
entry("summer_generate_3d", "Generate a 3D model (optional auto-rig + animations)", false, async (args) => {
|
|
910
959
|
const options = {
|
|
911
960
|
...(args.options ?? {}),
|
|
@@ -49,6 +49,10 @@ export const CLI_KNOWN_OP_NEEDS = [
|
|
|
49
49
|
// engine's one-table op; the other three are the legacy
|
|
50
50
|
// fallbacks on engines that predate it.
|
|
51
51
|
"Navigate", "OpenResource", "FocusDock", "RevealInFileSystem",
|
|
52
|
+
// Summer Apps editor docks (summer_app_dock: core/capabilities/apps.ts)
|
|
53
|
+
"OpenAppDock", "CloseAppDock", "ListAppDocks",
|
|
54
|
+
// summer_enable_plugin (core/capabilities/enable-plugin.ts)
|
|
55
|
+
"EnableEditorPlugin",
|
|
52
56
|
// Project + input
|
|
53
57
|
"ProjectSetting", "InputMapAddAction", "InputMapBind",
|
|
54
58
|
// Files (summer_write_file / summer_replace_text / summer_create_scene)
|
package/dist/mcp/hosted-mount.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
1
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { connectHostedMcp } from "../core/hosted-mcp.js";
|
|
4
4
|
import { getStoreAccessToken, OAuthError, resolveMcpResourceUrl } from "../core/oauth.js";
|
|
@@ -79,6 +79,18 @@ export function prepareHostedMount(server, deps = defaultHostedMountDependencies
|
|
|
79
79
|
};
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
|
+
// tools/call for a name the server does not know yet: a hosted tool whose mount
|
|
83
|
+
// is still running (an agent that calls before it lists). Wait for that mount,
|
|
84
|
+
// at most the first-list cap, then dispatch; an unknown name still errors.
|
|
85
|
+
const callTool = handlers.get("tools/call");
|
|
86
|
+
if (callTool) {
|
|
87
|
+
server.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
88
|
+
const name = request.params?.name;
|
|
89
|
+
if (typeof name === "string" && !(name in registries._registeredTools))
|
|
90
|
+
await waitForMount(firstListWaitMs);
|
|
91
|
+
return (await callTool(request, extra));
|
|
92
|
+
});
|
|
93
|
+
}
|
|
82
94
|
let statusTool = null;
|
|
83
95
|
let running = null;
|
|
84
96
|
/** Wait for a mount in progress, at most ms; returns at once when none runs. */
|
package/dist/mcp/server.js
CHANGED
|
@@ -16,6 +16,7 @@ import { registerEventTools } from "./tools/event-tools.js";
|
|
|
16
16
|
import { registerFabricateTools } from "./tools/fabricate-tools.js";
|
|
17
17
|
import { registerUiTools } from "./tools/ui-tools.js";
|
|
18
18
|
import { registerRuntimeTools } from "./tools/runtime-tools.js";
|
|
19
|
+
import { registerAppTools } from "./tools/app-tools.js";
|
|
19
20
|
import { recordToolCall, registrationHasInputSchema, trajectoryArgsFor, } from "../core/trajectory.js";
|
|
20
21
|
import { registerFileTools } from "./tools/file-tools.js";
|
|
21
22
|
import { registerAssetTools } from "./tools/asset-tools.js";
|
|
@@ -328,6 +329,7 @@ export function createMcpServer() {
|
|
|
328
329
|
registerFabricateTools(server);
|
|
329
330
|
registerUiTools(server);
|
|
330
331
|
registerRuntimeTools(server);
|
|
332
|
+
registerAppTools(server);
|
|
331
333
|
// The playbook is also an MCP prompt so prompt-surfacing hosts get it
|
|
332
334
|
// natively (same content as the summer_get_agent_playbook tool).
|
|
333
335
|
registerPlaybookPrompt(server);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { MAX_APP_EVENTS_WAIT_SECONDS, APP_DOCK_FALLBACK, appCall, appCallArgsSchema, appCreate, appCreateArgsSchema, appData, appDataArgsSchema, appDockArgsSchema, appDockOp, appEvents, appEventsArgsSchema, appGet, appGetArgsSchema, appGuide, appGuideArgsSchema, appList, appListArgsSchema, appPage, appPageArgsSchema, appPublish, appPublishArgsSchema, appReportGap, appReportGapArgsSchema, appScreenshot, appScreenshotArgsSchema, appUpdate, appUpdateArgsSchema, appsGateway, appTools, appToolsArgsSchema, } from "../../core/capabilities/apps.js";
|
|
2
|
+
import { enablePluginArgsSchema, enablePluginOp, interpretEnablePluginResult } from "../../core/capabilities/enable-plugin.js";
|
|
3
|
+
import { missingEngineOpResult, withEngine } from "./with-engine.js";
|
|
4
|
+
/**
|
|
5
|
+
* Summer Apps over MCP: tools a game team uses inside Summer Studio, built by
|
|
6
|
+
* the agent. Shared implementation: core/capabilities/apps.ts.
|
|
7
|
+
*/
|
|
8
|
+
const CLOUD = "Cloud tool: runs on Summer's servers and works without the Summer Engine app open. Requires sign-in: npx -y summer-engine@latest login.";
|
|
9
|
+
function result(value) {
|
|
10
|
+
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }], ...(value.ok === false ? { isError: true } : {}) };
|
|
11
|
+
}
|
|
12
|
+
/** The MCP handler of one cloud tool: validate with the shared schema, call the web API. */
|
|
13
|
+
function cloud(name, schema, run) {
|
|
14
|
+
return async (args) => {
|
|
15
|
+
const parsed = schema.safeParse(args ?? {});
|
|
16
|
+
if (!parsed.success)
|
|
17
|
+
return result({ ok: false, code: "invalid", error: parsed.error.issues[0]?.message ?? "Invalid arguments" });
|
|
18
|
+
return result(await run(parsed.data, { request: appsGateway("mcp", name) }));
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function registerAppTools(server) {
|
|
22
|
+
server.tool("summer_app_guide", `Read this before building or changing a Summer App (a tool for the person's team inside Summer Studio, live on their assets, game data and engine): when to build one, Layout vs code, the build loop, manifest schema, SDK reference, UI kit, layout format, design rules and four worked examples. section returns one part.\n\n${CLOUD}`, appGuideArgsSchema.shape, cloud("summer_app_guide", appGuideArgsSchema, appGuide));
|
|
23
|
+
server.tool("summer_app_list", `The Summer Apps in the person's workspaces: id, name, purpose, folder, status (scratch, kept, archived), version, format.\n\n${CLOUD}`, appListArgsSchema.shape, cloud("summer_app_list", appListArgsSchema, appList));
|
|
24
|
+
server.tool("summer_app_create", `Create a new scratch Summer App; their open Summer Studio tab opens it. Without workspaceId it goes to the person's team (the one their Studio tab is on, else their latest team, else personal); the result's workspace says which: tell the person. Read summer_app_guide first. format code (index.html + JS/CSS) or layout (a JSON layout of kit nodes for a quick decision UI). Then summer_app_publish.\n\n${CLOUD}`, appCreateArgsSchema.shape, cloud("summer_app_create", appCreateArgsSchema, appCreate));
|
|
25
|
+
server.tool("summer_app_publish", `Publish a new immutable version of a Summer App: dir (a local folder with index.html at its root) or files [{path, content, encoding}] for code apps, layout for layout apps, plus the manifest. Limits: 10 MB, 300 files, 5 MB per file. The person's Studio tab boots it. An error names the file or rule to fix: fix it and publish again.\n\n${CLOUD}`, appPublishArgsSchema.shape, cloud("summer_app_publish", appPublishArgsSchema, appPublish));
|
|
26
|
+
server.tool("summer_app_get", `A Summer App's manifest, its current version's files (or layout) and a summary of its shared data. Use it before changing an app.\n\n${CLOUD}`, appGetArgsSchema.shape, cloud("summer_app_get", appGetArgsSchema, appGet));
|
|
27
|
+
server.tool("summer_app_update", `Rename a Summer App, change its purpose or folder, or set its status: kept (the team keeps it), archived (hidden from the app list; restore with scratch or kept), scratch. Archive the scratch apps you made that the person did not keep.\n\n${CLOUD}`, appUpdateArgsSchema.shape, cloud("summer_app_update", appUpdateArgsSchema, appUpdate));
|
|
28
|
+
server.tool("summer_app_events", `Wait for what a Summer App sends you: summer.agent.send events (kind agent), errors in the app (kind error), feedback (kind feedback), agent tool calls (kind tool). Waits up to timeoutSeconds (max ${MAX_APP_EVENTS_WAIT_SECONDS}) for the first event after the cursor; pass the returned cursor as after next time.\n\n${CLOUD}`, appEventsArgsSchema.shape, cloud("summer_app_events", appEventsArgsSchema, appEvents));
|
|
29
|
+
server.tool("summer_app_page", `Read or use the fields and buttons a Summer App declared while it is open in the person's Studio tab or editor dock: no fill and no press reads the page; fill [{field, value}] and press an action id use it. Critical actions (delete, publish, pay, send) are pressed only by the person. app_not_open when the app is not open.\n\n${CLOUD}`, appPageArgsSchema.shape, cloud("summer_app_page", appPageArgsSchema, appPage));
|
|
30
|
+
server.tool("summer_app_screenshot", `See a Summer App without the person: Summer renders its current version (or version) headless with the person's data and returns the picture plus what to fix: console lines, uncaught errors, refused calls, whether the SDK connected and rendered, and the fields, buttons and agent tools it declared. Read-only: writes, sends and paid calls are refused, nothing changes. width 390 for a phone, theme dark, fullPage for the whole scroll; out also saves the PNG. Use it after every publish.\n\n${CLOUD}`, appScreenshotArgsSchema.shape, async (args) => {
|
|
31
|
+
const parsed = appScreenshotArgsSchema.safeParse(args ?? {});
|
|
32
|
+
if (!parsed.success)
|
|
33
|
+
return result({ ok: false, code: "invalid", error: parsed.error.issues[0]?.message ?? "Invalid arguments" });
|
|
34
|
+
const shot = await appScreenshot(parsed.data, { request: appsGateway("mcp", "summer_app_screenshot") });
|
|
35
|
+
if (shot.ok !== true || !("image" in shot))
|
|
36
|
+
return result(shot);
|
|
37
|
+
const { image, ...report } = shot;
|
|
38
|
+
return { content: [{ type: "image", data: image.base64, mimeType: image.mimeType }, { type: "text", text: JSON.stringify(report, null, 2) }] };
|
|
39
|
+
});
|
|
40
|
+
server.tool("summer_app_data", `Read or write a Summer App's shared data, its API: works with no app open, and everyone in the app sees writes live. Without ops it reads records (paged with after) plus declaredCollections, the record shape the app declares; with ops it writes them in one revision: {op: put|patch|remove, collection, recordId, data|patch, ifRevision?}. Writes show as yours (actor kind agent); as: seed writes starter data for the team (kind seed, no author).\n\n${CLOUD}`, appDataArgsSchema.shape, cloud("summer_app_data", appDataArgsSchema, appData));
|
|
41
|
+
server.tool("summer_app_tools", `The agent tools (name, description, input JSON Schema) and the data collections (record fields and types) a Summer App declares. Run a tool with summer_app_call; read or write the collections with summer_app_data.\n\n${CLOUD}`, appToolsArgsSchema.shape, cloud("summer_app_tools", appToolsArgsSchema, appTools));
|
|
42
|
+
server.tool("summer_app_call", `Run one of a Summer App's agent tools inside the person's open app (Studio tab or editor dock) with their rights; waits up to 60 s. app_not_open when the app is not open: use summer_app_data or ask the person to open it. May spend the person's credits when the tool generates.\n\n${CLOUD}`, appCallArgsSchema.shape, cloud("summer_app_call", appCallArgsSchema, appCall));
|
|
43
|
+
server.tool("summer_app_report_gap", `Tell Summer about an SDK capability a Summer App needed and did not have, so it can be built for everyone.\n\n${CLOUD}`, appReportGapArgsSchema.shape, cloud("summer_app_report_gap", appReportGapArgsSchema, appReportGap));
|
|
44
|
+
server.tool("summer_app_dock", `Dock a Summer App as a panel in the running Summer Engine editor (left, right or bottom), close its dock, or list docked apps. Docks persist per project. Engine builds without app docks return engine_lacks_op: open the app in the Summer Studio tab instead.`, appDockArgsSchema.shape, async (args) => {
|
|
45
|
+
const parsed = appDockArgsSchema.safeParse(args ?? {});
|
|
46
|
+
if (!parsed.success)
|
|
47
|
+
return result({ ok: false, code: "invalid", error: parsed.error.issues[0]?.message ?? "Invalid arguments" });
|
|
48
|
+
if (parsed.data.action !== "list" && !parsed.data.appId)
|
|
49
|
+
return result({ ok: false, code: "invalid", error: `${parsed.data.action} needs appId` });
|
|
50
|
+
const op = appDockOp(parsed.data);
|
|
51
|
+
return withEngine(async (client) => {
|
|
52
|
+
const missing = missingEngineOpResult(client, op.op, APP_DOCK_FALLBACK);
|
|
53
|
+
if (missing)
|
|
54
|
+
return missing;
|
|
55
|
+
return client.executeOps([op]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
server.tool("summer_enable_plugin", `Enable an editor plugin in the open project (res://addons/<name>/, for example one paired with a Summer App) and report the real result: enabled with the autoloads it registered, or why it failed (code SCRIPT_ERROR, NOT_TOOL, NOT_EDITOR_PLUGIN, CFG_MISSING, TIMEOUT, ...). Older Summer Engine builds only queue the enable; then the result says queued: true and enabled: null, and you check summer_get_debugger_errors before relying on the plugin.`, enablePluginArgsSchema.shape, async (args) => {
|
|
59
|
+
const parsed = enablePluginArgsSchema.safeParse(args ?? {});
|
|
60
|
+
if (!parsed.success)
|
|
61
|
+
return result({ ok: false, code: "invalid", error: parsed.error.issues[0]?.message ?? "Invalid arguments" });
|
|
62
|
+
return withEngine(async (client) => interpretEnablePluginResult(await client.executeOps([enablePluginOp(parsed.data)])));
|
|
63
|
+
});
|
|
64
|
+
}
|