applaunchflow 0.3.1
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/README.md +110 -0
- package/build/catalog.js +5 -0
- package/build/catalog.test.js +13 -0
- package/build/cli-core.js +20 -0
- package/build/cli-core.test.js +24 -0
- package/build/cli.js +71 -0
- package/build/client/api.js +341 -0
- package/build/http.js +178 -0
- package/build/http.test.js +111 -0
- package/build/index.js +127 -0
- package/build/prompts/register.js +66 -0
- package/build/read-receipt.test.js +36 -0
- package/build/resources/data.js +723 -0
- package/build/resources/register.js +115 -0
- package/build/social-template-previews.js +84 -0
- package/build/template-previews.js +83 -0
- package/build/tool-metadata.js +112 -0
- package/build/tool-metadata.test.js +32 -0
- package/build/tools/assets.js +311 -0
- package/build/tools/graphics.js +475 -0
- package/build/tools/keywords.js +132 -0
- package/build/tools/layouts.js +283 -0
- package/build/tools/localization.js +59 -0
- package/build/tools/mockups.js +324 -0
- package/build/tools/projects.js +113 -0
- package/build/tools/promovideo.js +199 -0
- package/build/tools/screenshots.js +307 -0
- package/build/tools/templates.js +195 -0
- package/build/tools/utils.js +212 -0
- package/build/tools/variants.js +71 -0
- package/package.json +33 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { listPublicTemplateIds } from "../catalog.js";
|
|
3
|
+
import { buildSocialTemplateGalleryUrl, decorateSocialTemplatePayload, SOCIAL_FORMATS, } from "../social-template-previews.js";
|
|
4
|
+
import { createHostedReadReceipt, fail, hostedMcpEnabled, ok, openUrl, startProgressHeartbeat, verifyHostedReadReceipt, } from "./utils.js";
|
|
5
|
+
function buildGraphicsEditorUrl(client, args) {
|
|
6
|
+
const params = new URLSearchParams({ projectId: args.generationId });
|
|
7
|
+
if (args.variantId) {
|
|
8
|
+
params.set("variantId", args.variantId);
|
|
9
|
+
}
|
|
10
|
+
if (args.format) {
|
|
11
|
+
params.set("format", args.format);
|
|
12
|
+
}
|
|
13
|
+
return `${client.credentials.baseUrl}/graphics?${params.toString()}`;
|
|
14
|
+
}
|
|
15
|
+
export function registerGraphicsTools(server, client) {
|
|
16
|
+
server.registerTool("prepare_social_graphics_styles", {
|
|
17
|
+
title: "Prepare Personalized Social Graphics Styles",
|
|
18
|
+
description: "Generate or reuse the project's personalized social-graphics style catalog before the user chooses a style. " +
|
|
19
|
+
"This prepares every social template across OG image, social post, Instagram story, Play Store feature graphic, X banner, and LinkedIn banner in one AI call and returns a catalogKey. " +
|
|
20
|
+
"Next call browse_social_templates with the returned templateIds, generationId, and catalogKey, then apply_social_graphics_style. Repeating the same app context and screenshot paths reuses the cache.",
|
|
21
|
+
inputSchema: {
|
|
22
|
+
generationId: z.string().uuid(),
|
|
23
|
+
selectedScreenshotPaths: z
|
|
24
|
+
.array(z.string().min(1))
|
|
25
|
+
.min(3)
|
|
26
|
+
.max(7)
|
|
27
|
+
.refine((paths) => new Set(paths).size === paths.length, {
|
|
28
|
+
message: "Screenshot paths must be unique",
|
|
29
|
+
})
|
|
30
|
+
.describe("3-7 unique project-relative paths from list_source_screenshots, in the desired story order."),
|
|
31
|
+
primaryFormat: z
|
|
32
|
+
.enum(SOCIAL_FORMATS)
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Format to preview first. Defaults to og."),
|
|
35
|
+
},
|
|
36
|
+
}, async ({ generationId, selectedScreenshotPaths, primaryFormat = "og" }, extra) => {
|
|
37
|
+
const stopHeartbeat = startProgressHeartbeat(extra, "Preparing personalized social graphics across every format…");
|
|
38
|
+
try {
|
|
39
|
+
const result = await client.generateGraphics({
|
|
40
|
+
generationId,
|
|
41
|
+
selectedScreenshotPaths,
|
|
42
|
+
primaryFormat,
|
|
43
|
+
previewAllTemplates: true,
|
|
44
|
+
});
|
|
45
|
+
const templateIds = listPublicTemplateIds(result.templatePayloads);
|
|
46
|
+
if (!result.catalogKey || templateIds.length === 0) {
|
|
47
|
+
throw new Error("The social graphics catalog response did not include a cache key and templates");
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
content: [
|
|
51
|
+
{
|
|
52
|
+
type: "text",
|
|
53
|
+
text: [
|
|
54
|
+
result.cacheHit
|
|
55
|
+
? "Reused the existing personalized social-graphics catalog."
|
|
56
|
+
: "Prepared personalized social graphics across all six formats.",
|
|
57
|
+
`Catalog key: ${result.catalogKey}`,
|
|
58
|
+
`Available template ids: ${templateIds.join(", ")}`,
|
|
59
|
+
"Next: call browse_social_templates with these templateIds, generationId, and this catalog key so the gallery shows the personalized results; then apply_social_graphics_style with the selected id.",
|
|
60
|
+
].join("\n"),
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
structuredContent: {
|
|
64
|
+
success: true,
|
|
65
|
+
data: {
|
|
66
|
+
generationId,
|
|
67
|
+
catalogKey: result.catalogKey,
|
|
68
|
+
cacheHit: result.cacheHit === true,
|
|
69
|
+
templateIds,
|
|
70
|
+
formats: [...SOCIAL_FORMATS],
|
|
71
|
+
primaryFormat,
|
|
72
|
+
selectedScreenshotPaths,
|
|
73
|
+
},
|
|
74
|
+
message: result.cacheHit
|
|
75
|
+
? "Reused personalized social graphics styles"
|
|
76
|
+
: "Prepared personalized social graphics styles",
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return fail(error);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
stopHeartbeat();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
server.registerTool("apply_social_graphics_style", {
|
|
88
|
+
title: "Apply Personalized Social Graphics Style",
|
|
89
|
+
description: "Create a new social-graphics variant from a previously prepared personalized catalog without another AI generation. " +
|
|
90
|
+
"Use after prepare_social_graphics_styles and browse_social_templates. The new variant contains all six formats and opens in the graphics editor.",
|
|
91
|
+
inputSchema: {
|
|
92
|
+
generationId: z.string().uuid(),
|
|
93
|
+
catalogKey: z.string().min(1).max(128),
|
|
94
|
+
templateId: z.string().min(1),
|
|
95
|
+
primaryFormat: z
|
|
96
|
+
.enum(SOCIAL_FORMATS)
|
|
97
|
+
.optional()
|
|
98
|
+
.describe("Format to show first in the editor. Defaults to og."),
|
|
99
|
+
paletteMode: z
|
|
100
|
+
.enum(["v1", "v2"])
|
|
101
|
+
.optional()
|
|
102
|
+
.describe("Color palette variant. v1 is the original palette; v2 uses stronger color separation. Defaults to v1."),
|
|
103
|
+
},
|
|
104
|
+
}, async ({ generationId, catalogKey, templateId, primaryFormat = "og", paletteMode = "v1", }, extra) => {
|
|
105
|
+
try {
|
|
106
|
+
const result = await client.applyGraphicsTemplate({
|
|
107
|
+
generationId,
|
|
108
|
+
catalogKey,
|
|
109
|
+
templateId,
|
|
110
|
+
primaryFormat,
|
|
111
|
+
paletteMode,
|
|
112
|
+
});
|
|
113
|
+
const editorUrl = buildGraphicsEditorUrl(client, {
|
|
114
|
+
generationId,
|
|
115
|
+
variantId: result.variantId,
|
|
116
|
+
format: primaryFormat,
|
|
117
|
+
});
|
|
118
|
+
await openUrl(server, editorUrl, "Opening the selected personalized social graphics style in the editor.", { signal: extra.signal });
|
|
119
|
+
return {
|
|
120
|
+
content: [
|
|
121
|
+
{
|
|
122
|
+
type: "text",
|
|
123
|
+
text: [
|
|
124
|
+
`Applied social graphics style ${templateId} across all six formats without another AI generation.`,
|
|
125
|
+
`Editor URL: ${editorUrl}`,
|
|
126
|
+
"IMPORTANT: Paste this exact editor URL in the reply so the user can open it.",
|
|
127
|
+
].join("\n"),
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
structuredContent: {
|
|
131
|
+
success: true,
|
|
132
|
+
data: {
|
|
133
|
+
generationId,
|
|
134
|
+
variantId: result.variantId,
|
|
135
|
+
templateId,
|
|
136
|
+
primaryFormat,
|
|
137
|
+
formats: [...SOCIAL_FORMATS],
|
|
138
|
+
editorUrl,
|
|
139
|
+
},
|
|
140
|
+
message: "Applied personalized social graphics style",
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
return fail(error);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
server.registerTool("browse_social_templates", {
|
|
149
|
+
title: "Browse & Select Social Template",
|
|
150
|
+
description: "Use this visual style gallery after prepare_social_graphics_styles, restricted to its returned templateIds, so the user can choose which prepared result to apply. It can also be used independently for static style discovery. Returns the selected template id. Never offer social templates via text or AskUserQuestion.",
|
|
151
|
+
inputSchema: {
|
|
152
|
+
format: z
|
|
153
|
+
.enum(SOCIAL_FORMATS)
|
|
154
|
+
.optional()
|
|
155
|
+
.describe("Which social format the gallery should preview first. Defaults to 'og'."),
|
|
156
|
+
templateIds: z
|
|
157
|
+
.array(z.string())
|
|
158
|
+
.optional()
|
|
159
|
+
.describe("Optional subset of social template ids to show."),
|
|
160
|
+
selectedTemplateId: z
|
|
161
|
+
.string()
|
|
162
|
+
.optional()
|
|
163
|
+
.describe("Optional template id to highlight in the gallery."),
|
|
164
|
+
title: z
|
|
165
|
+
.string()
|
|
166
|
+
.optional()
|
|
167
|
+
.describe("Optional gallery heading, for example the project name."),
|
|
168
|
+
generationId: z
|
|
169
|
+
.string()
|
|
170
|
+
.uuid()
|
|
171
|
+
.optional()
|
|
172
|
+
.describe("Project UUID from prepare_social_graphics_styles. Pass together with catalogKey to show the real personalized previews."),
|
|
173
|
+
catalogKey: z
|
|
174
|
+
.string()
|
|
175
|
+
.min(1)
|
|
176
|
+
.max(128)
|
|
177
|
+
.optional()
|
|
178
|
+
.describe("Catalog key from prepare_social_graphics_styles. Pass together with generationId."),
|
|
179
|
+
},
|
|
180
|
+
}, async ({ format = "og", templateIds, selectedTemplateId, title, generationId, catalogKey, }) => {
|
|
181
|
+
try {
|
|
182
|
+
const payload = decorateSocialTemplatePayload(await client.listSocialTemplates(), client.credentials.baseUrl);
|
|
183
|
+
const availableIds = new Set(payload.templates.map((t) => t.id));
|
|
184
|
+
const filteredTemplateIds = templateIds?.filter((id) => availableIds.has(id)) || [];
|
|
185
|
+
const droppedTemplateIds = templateIds?.filter((id) => !availableIds.has(id)) || [];
|
|
186
|
+
if (droppedTemplateIds.length > 0) {
|
|
187
|
+
console.error(`[browse_social_templates] Ignoring unknown template ids not in the registry: ${droppedTemplateIds.join(", ")}`);
|
|
188
|
+
}
|
|
189
|
+
// Restricting to ids that all turn out to be unknown would fall back to
|
|
190
|
+
// showing every template — misleading for a prepared catalog. Fail
|
|
191
|
+
// loudly so the caller re-prepares instead.
|
|
192
|
+
if (templateIds &&
|
|
193
|
+
templateIds.length > 0 &&
|
|
194
|
+
filteredTemplateIds.length === 0) {
|
|
195
|
+
return fail(new Error(`None of the requested social template ids match the available templates (${droppedTemplateIds.join(", ")}). ` +
|
|
196
|
+
"The prepared catalog and the social template registry may be out of sync — re-run prepare_social_graphics_styles and pass its returned templateIds."));
|
|
197
|
+
}
|
|
198
|
+
const galleryUrl = buildSocialTemplateGalleryUrl(client.credentials.baseUrl, {
|
|
199
|
+
format,
|
|
200
|
+
templateIds: filteredTemplateIds.length > 0 ? filteredTemplateIds : undefined,
|
|
201
|
+
selectedTemplateId: selectedTemplateId && availableIds.has(selectedTemplateId)
|
|
202
|
+
? selectedTemplateId
|
|
203
|
+
: undefined,
|
|
204
|
+
title,
|
|
205
|
+
generationId,
|
|
206
|
+
catalogKey,
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
content: [
|
|
210
|
+
{
|
|
211
|
+
type: "text",
|
|
212
|
+
text: [
|
|
213
|
+
"Paste this exact gallery URL into the user-visible reply.",
|
|
214
|
+
`Social template gallery URL: ${galleryUrl}`,
|
|
215
|
+
"After you pick a template, reply with the template name or id.",
|
|
216
|
+
].join("\n"),
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
type: "resource_link",
|
|
220
|
+
uri: galleryUrl,
|
|
221
|
+
name: "Open Social Template Gallery",
|
|
222
|
+
mimeType: "text/html",
|
|
223
|
+
description: "Hosted gallery for browsing social-graphics template previews.",
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
structuredContent: {
|
|
227
|
+
success: true,
|
|
228
|
+
data: {
|
|
229
|
+
galleryUrl,
|
|
230
|
+
userFacingUrl: galleryUrl,
|
|
231
|
+
format,
|
|
232
|
+
templateIds: filteredTemplateIds,
|
|
233
|
+
},
|
|
234
|
+
message: "Prepared social template gallery",
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
return fail(error);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
server.registerTool("generate_graphics", {
|
|
243
|
+
title: "Generate Social Graphics",
|
|
244
|
+
description: "Legacy direct generation for one chosen template. For the normal style chooser, prefer prepare_social_graphics_styles → browse_social_templates → apply_social_graphics_style so all personalized previews are generated once and the chosen style is applied from cache. " +
|
|
245
|
+
"Generate AI social graphics for all six formats (OG, X post, Instagram story, Play Store feature, X header, LinkedIn banner) using a chosen social template. Omit variantId to create a fresh variant — never overwrite an existing one. " +
|
|
246
|
+
"After generation, the graphics editor opens automatically.",
|
|
247
|
+
inputSchema: {
|
|
248
|
+
generationId: z.string().uuid(),
|
|
249
|
+
templateId: z
|
|
250
|
+
.string()
|
|
251
|
+
.min(1)
|
|
252
|
+
.describe("Social template id (e.g. 'social-clean')."),
|
|
253
|
+
primaryFormat: z
|
|
254
|
+
.enum(SOCIAL_FORMATS)
|
|
255
|
+
.optional()
|
|
256
|
+
.describe("Default format the editor highlights. Defaults to 'og'."),
|
|
257
|
+
variantId: z
|
|
258
|
+
.string()
|
|
259
|
+
.uuid()
|
|
260
|
+
.optional()
|
|
261
|
+
.describe("DO NOT pass this. Always omit so a new variant is created. Never overwrite existing variants."),
|
|
262
|
+
},
|
|
263
|
+
}, async (args, extra) => {
|
|
264
|
+
try {
|
|
265
|
+
const result = await client.generateGraphics(args);
|
|
266
|
+
const variantId = result?.variantId || args.variantId || "";
|
|
267
|
+
const editorUrl = buildGraphicsEditorUrl(client, {
|
|
268
|
+
generationId: args.generationId,
|
|
269
|
+
variantId,
|
|
270
|
+
});
|
|
271
|
+
await openUrl(server, editorUrl, "Opening the generated social graphics in the editor.", { signal: extra.signal });
|
|
272
|
+
return {
|
|
273
|
+
content: [
|
|
274
|
+
{
|
|
275
|
+
type: "text",
|
|
276
|
+
text: [
|
|
277
|
+
"Generated social graphics successfully.",
|
|
278
|
+
`Editor URL: ${editorUrl}`,
|
|
279
|
+
"IMPORTANT: Paste this exact editor URL in the reply so the user can open it.",
|
|
280
|
+
].join("\n"),
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
structuredContent: {
|
|
284
|
+
success: true,
|
|
285
|
+
data: { ...result, editorUrl },
|
|
286
|
+
message: "Generated social graphics",
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
return fail(error);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
const graphicsReadReceipts = new Set();
|
|
295
|
+
const graphicsReceiptKey = (args) => [args.generationId, args.variantId || "active", args.format].join("::");
|
|
296
|
+
server.registerTool("get_graphics", {
|
|
297
|
+
title: "Get Social Graphics",
|
|
298
|
+
description: "Fetch the current social graphics layouts (one per format) for overview or metadata inspection. " +
|
|
299
|
+
"For direct edits, use get_graphics_format instead.",
|
|
300
|
+
inputSchema: {
|
|
301
|
+
generationId: z.string().uuid(),
|
|
302
|
+
variantId: z.string().uuid().optional(),
|
|
303
|
+
},
|
|
304
|
+
}, async ({ generationId, variantId }) => {
|
|
305
|
+
try {
|
|
306
|
+
const result = await client.getGraphics(generationId, variantId);
|
|
307
|
+
const editorUrl = buildGraphicsEditorUrl(client, {
|
|
308
|
+
generationId,
|
|
309
|
+
variantId,
|
|
310
|
+
});
|
|
311
|
+
return {
|
|
312
|
+
content: [
|
|
313
|
+
{
|
|
314
|
+
type: "text",
|
|
315
|
+
text: [
|
|
316
|
+
"Fetched social graphics.",
|
|
317
|
+
`Editor URL: ${editorUrl}`,
|
|
318
|
+
"Use get_graphics_format before a direct one-format edit.",
|
|
319
|
+
].join("\n"),
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
structuredContent: {
|
|
323
|
+
success: true,
|
|
324
|
+
data: { ...result, editorUrl, readBeforeEditSatisfied: false },
|
|
325
|
+
message: "Fetched social graphics",
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
return fail(error);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
server.registerTool("get_graphics_format", {
|
|
334
|
+
title: "Get One Social Graphics Format",
|
|
335
|
+
description: "Fetch exactly one social graphics layout for a project. " +
|
|
336
|
+
"Use this before direct edits so the next save_graphics_format call works from the current state of that same format. " +
|
|
337
|
+
"If the user did not request a specific format, use the variant's primary format from an earlier get_graphics response or inspect the editor URL. " +
|
|
338
|
+
"The returned layout is the same Layout shape screenshots use — one screen, canvas sized to the format. Read the resource applaunchflow://schema/layout for every node type's fields and valid ranges.",
|
|
339
|
+
inputSchema: {
|
|
340
|
+
generationId: z.string().uuid(),
|
|
341
|
+
variantId: z.string().uuid().optional(),
|
|
342
|
+
format: z.enum(SOCIAL_FORMATS),
|
|
343
|
+
},
|
|
344
|
+
}, async ({ generationId, variantId, format }) => {
|
|
345
|
+
try {
|
|
346
|
+
const result = await client.getGraphicsFormat(generationId, format, variantId);
|
|
347
|
+
const editorUrl = buildGraphicsEditorUrl(client, {
|
|
348
|
+
generationId,
|
|
349
|
+
variantId,
|
|
350
|
+
format,
|
|
351
|
+
});
|
|
352
|
+
graphicsReadReceipts.add(graphicsReceiptKey({ generationId, variantId, format }));
|
|
353
|
+
const receiptKey = graphicsReceiptKey({ generationId, variantId, format });
|
|
354
|
+
const readReceipt = hostedMcpEnabled()
|
|
355
|
+
? createHostedReadReceipt(receiptKey, client.credentials.token)
|
|
356
|
+
: undefined;
|
|
357
|
+
return {
|
|
358
|
+
content: [
|
|
359
|
+
{
|
|
360
|
+
type: "text",
|
|
361
|
+
text: [
|
|
362
|
+
`Fetched social graphics format ${format}.`,
|
|
363
|
+
`Editor URL: ${editorUrl}`,
|
|
364
|
+
"A fresh same-format read receipt was recorded and can be used for one save_graphics_format call.",
|
|
365
|
+
].join("\n"),
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
structuredContent: {
|
|
369
|
+
success: true,
|
|
370
|
+
data: {
|
|
371
|
+
...result,
|
|
372
|
+
editorUrl,
|
|
373
|
+
readBeforeEditSatisfied: true,
|
|
374
|
+
readReceipt,
|
|
375
|
+
},
|
|
376
|
+
message: "Fetched one social graphics format",
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
return fail(error);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
server.registerTool("save_graphics", {
|
|
385
|
+
title: "Save Social Graphics",
|
|
386
|
+
description: "Persist a complete social graphics payload (template id, primary format, all per-format layouts). " +
|
|
387
|
+
"Prefer save_graphics_format when editing a single format. Each layout uses the same shape as screenshot layouts; see the resource applaunchflow://schema/layout.",
|
|
388
|
+
inputSchema: {
|
|
389
|
+
generationId: z.string().uuid(),
|
|
390
|
+
variantId: z.string().uuid().optional(),
|
|
391
|
+
socialTemplateId: z.string().min(1),
|
|
392
|
+
socialPrimaryFormat: z.enum(SOCIAL_FORMATS),
|
|
393
|
+
graphics: z
|
|
394
|
+
.array(z.object({
|
|
395
|
+
format: z.enum(SOCIAL_FORMATS),
|
|
396
|
+
layout: z
|
|
397
|
+
.record(z.any())
|
|
398
|
+
.describe("Complete Layout object for this format — the SAME shape screenshot layouts use, with exactly one entry in screens[] and canvasWidth/canvasHeight matching the format. Full field reference: applaunchflow://schema/layout."),
|
|
399
|
+
}))
|
|
400
|
+
.min(1),
|
|
401
|
+
},
|
|
402
|
+
}, async (args) => {
|
|
403
|
+
try {
|
|
404
|
+
return ok(await client.saveGraphics(args), "Saved social graphics");
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
return fail(error);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
server.registerTool("save_graphics_format", {
|
|
411
|
+
title: "Save One Social Graphics Format",
|
|
412
|
+
description: "Persist exactly one social graphics format after reading the latest same-format layout with get_graphics_format. " +
|
|
413
|
+
"ENFORCED: each call requires a fresh get_graphics_format for the same generationId/variantId/format immediately beforehand. " +
|
|
414
|
+
"Read the current layouts, mutate only the requested format in memory, then save that single layout here.",
|
|
415
|
+
inputSchema: {
|
|
416
|
+
generationId: z.string().uuid(),
|
|
417
|
+
variantId: z.string().uuid().optional(),
|
|
418
|
+
format: z.enum(SOCIAL_FORMATS),
|
|
419
|
+
layout: z
|
|
420
|
+
.record(z.any())
|
|
421
|
+
.describe("Complete Layout object for this one format — the SAME shape screenshot layouts use, with exactly one entry in screens[] and canvasWidth/canvasHeight matching the format. " +
|
|
422
|
+
"Full field reference: read the resource applaunchflow://schema/layout."),
|
|
423
|
+
readReceipt: z
|
|
424
|
+
.string()
|
|
425
|
+
.optional()
|
|
426
|
+
.describe("Hosted connector only: pass the readReceipt returned by the immediately preceding get_graphics_format call."),
|
|
427
|
+
},
|
|
428
|
+
}, async (args) => {
|
|
429
|
+
try {
|
|
430
|
+
const receiptKey = graphicsReceiptKey({
|
|
431
|
+
generationId: args.generationId,
|
|
432
|
+
variantId: args.variantId,
|
|
433
|
+
format: args.format,
|
|
434
|
+
});
|
|
435
|
+
const hasReceipt = hostedMcpEnabled()
|
|
436
|
+
? verifyHostedReadReceipt(args.readReceipt, receiptKey, client.credentials.token)
|
|
437
|
+
: graphicsReadReceipts.has(receiptKey);
|
|
438
|
+
if (!hasReceipt) {
|
|
439
|
+
return fail(new Error("Call get_graphics_format first for this generation/variant/format before save_graphics_format. Direct editing is locked until the current same-format state has been read."));
|
|
440
|
+
}
|
|
441
|
+
const { readReceipt: _readReceipt, ...saveArgs } = args;
|
|
442
|
+
const result = await client.saveGraphicsFormat(saveArgs);
|
|
443
|
+
graphicsReadReceipts.delete(receiptKey);
|
|
444
|
+
const editorUrl = buildGraphicsEditorUrl(client, {
|
|
445
|
+
generationId: args.generationId,
|
|
446
|
+
variantId: args.variantId,
|
|
447
|
+
format: args.format,
|
|
448
|
+
});
|
|
449
|
+
return {
|
|
450
|
+
content: [
|
|
451
|
+
{
|
|
452
|
+
type: "text",
|
|
453
|
+
text: [
|
|
454
|
+
`Saved social graphics format ${args.format}.`,
|
|
455
|
+
`Editor URL (already open — do NOT run \`open\` again): ${editorUrl}`,
|
|
456
|
+
"This save consumed the current same-format read receipt. Call get_graphics_format again before the next direct edit.",
|
|
457
|
+
].join("\n"),
|
|
458
|
+
},
|
|
459
|
+
],
|
|
460
|
+
structuredContent: {
|
|
461
|
+
success: true,
|
|
462
|
+
data: {
|
|
463
|
+
result,
|
|
464
|
+
editorUrl,
|
|
465
|
+
nextEditRequiresFreshRead: true,
|
|
466
|
+
},
|
|
467
|
+
message: "Saved one social graphics format",
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
return fail(error);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fail, ok } from "./utils.js";
|
|
3
|
+
const storeProviderSchema = z
|
|
4
|
+
.enum(["app_store", "google_play"])
|
|
5
|
+
.optional()
|
|
6
|
+
.describe("Which store to read. Defaults to the project's primary store provider.");
|
|
7
|
+
export function registerKeywordTools(server, client) {
|
|
8
|
+
server.registerTool("list_keywords", {
|
|
9
|
+
title: "List Tracked Keywords",
|
|
10
|
+
description: "Fetch the keywords currently tracked for a project, including current rank, 7d/30d deltas, difficulty/traffic estimates, sparkline, competitor positions, and a summary (tracked count, ranked count, average position, top-10 share). " +
|
|
11
|
+
"Use this as the first read for any keyword/ASO conversation.",
|
|
12
|
+
inputSchema: {
|
|
13
|
+
projectId: z.string().uuid(),
|
|
14
|
+
storeProvider: storeProviderSchema,
|
|
15
|
+
},
|
|
16
|
+
}, async ({ projectId, storeProvider }) => {
|
|
17
|
+
try {
|
|
18
|
+
return ok(await client.listKeywords({ projectId, storeProvider }), "Fetched tracked keywords");
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
return fail(error);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
server.registerTool("list_keyword_competitors", {
|
|
25
|
+
title: "List Keyword Competitors",
|
|
26
|
+
description: "List the competitor apps configured for keyword tracking on a project (name, developer, icon) plus the user's plan-based competitor limit. " +
|
|
27
|
+
"These are the apps shown alongside the user's app in the keyword monitor's competitor columns.",
|
|
28
|
+
inputSchema: {
|
|
29
|
+
projectId: z.string().uuid(),
|
|
30
|
+
storeProvider: storeProviderSchema,
|
|
31
|
+
},
|
|
32
|
+
}, async ({ projectId, storeProvider }) => {
|
|
33
|
+
try {
|
|
34
|
+
return ok(await client.listKeywordCompetitors({ projectId, storeProvider }), "Fetched keyword competitors");
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return fail(error);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
server.registerTool("add_keywords", {
|
|
41
|
+
title: "Add Tracked Keywords",
|
|
42
|
+
description: "Add one or more keywords to track for a project's linked app. Up to 50 keywords per call. " +
|
|
43
|
+
"If appId or storeProvider is omitted, both are resolved from the project (primaryStoreProvider + appleAppId / googlePlayPackageName). " +
|
|
44
|
+
"Returns 402 with error \"keyword_limit\" if the user's plan limit would be exceeded.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
projectId: z.string().uuid(),
|
|
47
|
+
keywords: z
|
|
48
|
+
.array(z.string().min(1))
|
|
49
|
+
.min(1)
|
|
50
|
+
.max(50)
|
|
51
|
+
.describe("Keyword strings to track. Normalized server-side."),
|
|
52
|
+
appId: z
|
|
53
|
+
.string()
|
|
54
|
+
.min(1)
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("Store app id (Apple numeric id or Google package name). Auto-resolved from the project when omitted."),
|
|
57
|
+
storeProvider: z
|
|
58
|
+
.enum(["app_store", "google_play"])
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("Store to track in. Defaults to the project's primaryStoreProvider."),
|
|
61
|
+
country: z
|
|
62
|
+
.string()
|
|
63
|
+
.length(2)
|
|
64
|
+
.optional()
|
|
65
|
+
.describe("ISO country code, lowercase. Defaults to \"us\"."),
|
|
66
|
+
lang: z
|
|
67
|
+
.string()
|
|
68
|
+
.min(2)
|
|
69
|
+
.max(16)
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("BCP-47 language tag. Defaults to \"en-US\"."),
|
|
72
|
+
},
|
|
73
|
+
}, async ({ projectId, keywords, appId, storeProvider, country, lang }) => {
|
|
74
|
+
try {
|
|
75
|
+
let resolvedStoreProvider = storeProvider;
|
|
76
|
+
let resolvedAppId = appId;
|
|
77
|
+
if (!resolvedAppId || !resolvedStoreProvider) {
|
|
78
|
+
const project = await client.getProject(projectId);
|
|
79
|
+
resolvedStoreProvider =
|
|
80
|
+
resolvedStoreProvider ??
|
|
81
|
+
project?.primaryStoreProvider ??
|
|
82
|
+
"app_store";
|
|
83
|
+
if (!resolvedAppId) {
|
|
84
|
+
resolvedAppId =
|
|
85
|
+
resolvedStoreProvider === "google_play"
|
|
86
|
+
? project?.googlePlayPackageName
|
|
87
|
+
: project?.appleAppId
|
|
88
|
+
? String(project.appleAppId)
|
|
89
|
+
: undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!resolvedAppId) {
|
|
93
|
+
return fail(new Error(`Project has no linked ${resolvedStoreProvider === "google_play" ? "Google Play package" : "Apple app id"}. ` +
|
|
94
|
+
"Link the app in the project settings or pass appId explicitly."));
|
|
95
|
+
}
|
|
96
|
+
return ok(await client.addKeywords({
|
|
97
|
+
projectId,
|
|
98
|
+
appId: resolvedAppId,
|
|
99
|
+
keywords,
|
|
100
|
+
storeProvider: resolvedStoreProvider,
|
|
101
|
+
country: country?.toLowerCase(),
|
|
102
|
+
lang,
|
|
103
|
+
}), "Added tracked keywords");
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
return fail(error);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
server.registerTool("get_keyword_history", {
|
|
110
|
+
title: "Get Keyword History",
|
|
111
|
+
description: "Fetch the rank time series for a single tracked keyword. Returns up to 30 days on Free and up to 365 days on Pro. " +
|
|
112
|
+
"Pass appId to compute the history for a competitor app instead of the user's own app — defaults to the tracked keyword's owner app.",
|
|
113
|
+
inputSchema: {
|
|
114
|
+
trackedKeywordId: z
|
|
115
|
+
.string()
|
|
116
|
+
.uuid()
|
|
117
|
+
.describe("The tracked-keyword row id (from list_keywords). Not the keyword string."),
|
|
118
|
+
appId: z
|
|
119
|
+
.string()
|
|
120
|
+
.min(1)
|
|
121
|
+
.optional()
|
|
122
|
+
.describe("Optional store app id to compute history for. Defaults to the tracked keyword's app."),
|
|
123
|
+
},
|
|
124
|
+
}, async ({ trackedKeywordId, appId }) => {
|
|
125
|
+
try {
|
|
126
|
+
return ok(await client.getKeywordHistory({ trackedKeywordId, appId }), "Fetched keyword history");
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
return fail(error);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|