@applaunchflow/mcp 0.1.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/README.md +26 -0
- package/build/auth/credentials.js +53 -0
- package/build/auth/login.js +92 -0
- package/build/client/api.js +248 -0
- package/build/index.js +125 -0
- package/build/prompts/register.js +65 -0
- package/build/resources/data.js +466 -0
- package/build/resources/register.js +111 -0
- package/build/template-previews.js +79 -0
- package/build/template-selection.js +230 -0
- package/build/tools/analysis.js +23 -0
- package/build/tools/aso.js +84 -0
- package/build/tools/assets.js +200 -0
- package/build/tools/graphics.js +69 -0
- package/build/tools/layouts.js +138 -0
- package/build/tools/localization.js +58 -0
- package/build/tools/projects.js +113 -0
- package/build/tools/screenshots.js +107 -0
- package/build/tools/store-metadata.js +66 -0
- package/build/tools/templates.js +330 -0
- package/build/tools/utils.js +53 -0
- package/build/tools/variants.js +79 -0
- package/package.json +27 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
export const LAYOUT_SCHEMA_RESOURCE = {
|
|
2
|
+
layout: {
|
|
3
|
+
description: "Root layout object. One per device size (mobile, tablet, desktop).",
|
|
4
|
+
fields: {
|
|
5
|
+
template: { type: "string", description: "Template id used to generate this layout." },
|
|
6
|
+
platform: { type: '"ios" | "android" | "both"', description: "Target platform." },
|
|
7
|
+
canvasWidth: { type: "number", description: "Canvas width in pixels for a single screen." },
|
|
8
|
+
canvasHeight: { type: "number", description: "Canvas height in pixels for a single screen." },
|
|
9
|
+
backgroundColor: { type: "hex color", description: "Default background color for all screens." },
|
|
10
|
+
backgroundGradient: { type: "Gradient?", description: "Optional default gradient for all screens. Has type ('linear'|'radial'), colors[] and optional direction (degrees)." },
|
|
11
|
+
panoramaBackground: { type: "PanoramaBackground?", description: "A single wide image that spans across all screens as a continuous backdrop. Has imageUrl, optional storagePath, verticalOffset (0-100, 50=center), fitMode ('cover'|'contain'), blur (0-20)." },
|
|
12
|
+
themeColors: { type: "ThemeColors?", description: "Template color palette: primary, secondary, background, text, textSecondary. Used by the renderer for consistent styling." },
|
|
13
|
+
headers: { type: "HeaderNode[]?", description: "Shared header bars across screens (rare). Each has id, position, width, height, backgroundColor, zIndex." },
|
|
14
|
+
screens: { type: "Screen[]", description: "Ordered array of screens. Each screen is one App Store screenshot." },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
screen: {
|
|
18
|
+
description: "A single screenshot frame. Contains all visual elements for one App Store screenshot.",
|
|
19
|
+
fields: {
|
|
20
|
+
id: { type: "string", required: true, description: "Unique screen identifier." },
|
|
21
|
+
index: { type: "number", required: true, description: "Display order (0-based)." },
|
|
22
|
+
screenType: { type: "number", required: true, description: "Determines render variation (0-4 typically). Preserved on reorder." },
|
|
23
|
+
screenshots: { type: "ScreenshotNode[]", required: true, description: "Device mockups showing the app. Usually 1, sometimes 2+ for multi-phone compositions." },
|
|
24
|
+
texts: { type: "TextNode[]", required: true, description: "Text elements (headlines, subtitles, body copy)." },
|
|
25
|
+
pills: { type: "PillNode[]?", description: "Tag/button-style labels." },
|
|
26
|
+
badges: { type: "BadgeNode[]?", description: "Circular seals or count badges." },
|
|
27
|
+
blobs: { type: "BlobNode[]?", description: "Organic decorative shapes." },
|
|
28
|
+
ratings: { type: "RatingNode[]?", description: "Star rating displays with optional label." },
|
|
29
|
+
logo: { type: "LogoNode?", description: "App logo (image or text-based)." },
|
|
30
|
+
illustrations: { type: "IllustrationNode[]?", description: "Decorative images/stickers." },
|
|
31
|
+
magnifiers: { type: "MagnifierNode[]?", description: "Zoomed-in insets of a screenshot region." },
|
|
32
|
+
emojis: { type: "EmojiNode[]?", description: "Decorative emoji characters." },
|
|
33
|
+
backgroundColor: { type: "hex color?", description: "Per-screen background color override." },
|
|
34
|
+
backgroundGradient: { type: "Gradient?", description: "Per-screen gradient override." },
|
|
35
|
+
backgroundImage: { type: "object?", description: "Per-screen background image with url, optional blur (0-20), verticalOffset/horizontalOffset (0-100, 50=center)." },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
components: {
|
|
39
|
+
ScreenshotNode: {
|
|
40
|
+
description: "A device mockup displaying an app screenshot inside a phone/tablet/desktop frame.",
|
|
41
|
+
visual: "The screenshot is rendered inside a realistic device bezel. The frame style is controlled by variant3D.",
|
|
42
|
+
frameStyles: {
|
|
43
|
+
description: "Available frame styles via the variant3D field. Each gives the phone a different 3D perspective.",
|
|
44
|
+
options: {
|
|
45
|
+
none: "No frame — raw screenshot without device bezel. Use for edge-to-edge or frameless designs.",
|
|
46
|
+
flat: "Flat front-facing phone frame. Clean, straight-on view. This is the default.",
|
|
47
|
+
left: "Phone angled to the left with subtle 3D perspective.",
|
|
48
|
+
"left-2": "Phone angled more steeply to the left.",
|
|
49
|
+
right: "Phone angled to the right with subtle 3D perspective.",
|
|
50
|
+
"right-2": "Phone angled more steeply to the right.",
|
|
51
|
+
handheld: "Phone held in a hand (dark/silhouette hand).",
|
|
52
|
+
handheld2: "Phone held in a hand (medium skin tone, realistic).",
|
|
53
|
+
handheld3: "Phone held in a hand (light skin tone, realistic).",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
overflow: {
|
|
57
|
+
description: "When overflow is true, the device frame visually extends beyond the screen boundary into adjacent screens. " +
|
|
58
|
+
"This creates a seamless multi-screen effect where a phone appears to span across two screenshots. " +
|
|
59
|
+
"Commonly used with phones positioned at the edge of a screen so they partially appear on the neighboring screen.",
|
|
60
|
+
},
|
|
61
|
+
fields: {
|
|
62
|
+
id: "string — unique node id",
|
|
63
|
+
path: "string — relative storage path e.g. 'mobile/ios/1234-image.png'",
|
|
64
|
+
position: "{ x, y } — pixel position on canvas",
|
|
65
|
+
scale: "number — size multiplier (1.0 = natural size)",
|
|
66
|
+
rotation: "number — degrees",
|
|
67
|
+
zIndex: "number — stacking order",
|
|
68
|
+
overflow: "boolean? — allow device to visually extend into adjacent screens (see overflow description above)",
|
|
69
|
+
tiltAngle: "number? — tilts the entire device mockup (-30 to 30 degrees). Independent of variant3D.",
|
|
70
|
+
phoneId: "string? — phone frame id: 'iphone17' or 'googlepixel'",
|
|
71
|
+
tabletId: "string? — tablet frame id: 'ipad'",
|
|
72
|
+
desktopId: "string? — desktop frame id: 'macbook-pro-16'",
|
|
73
|
+
variant3D: "string? — frame style (see frameStyles above). Controls the 3D perspective of the device.",
|
|
74
|
+
fitMode: "'stretch' | 'cover' — how the screenshot maps inside the device frame (default: stretch)",
|
|
75
|
+
showCamera: "boolean? — show camera punch hole/notch (default true)",
|
|
76
|
+
opacity: "number? — 0-100 (default 100)",
|
|
77
|
+
shadow: "ShadowConfig? — drop shadow { color, blur, offsetX, offsetY }",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
TextNode: {
|
|
81
|
+
description: "A text element — headline, subtitle, or body copy.",
|
|
82
|
+
visual: "Rendered text with full rich-text support (bold, italic, colors, mixed fonts). Most templates use a large title + smaller subtitle per screen.",
|
|
83
|
+
fields: {
|
|
84
|
+
id: "string — unique node id",
|
|
85
|
+
richContent: "TiptapJSON — rich text content (see richContent format below)",
|
|
86
|
+
position: "{ x, y } — pixel position",
|
|
87
|
+
zIndex: "number — stacking order",
|
|
88
|
+
align: "'left' | 'center' | 'right'? — text alignment",
|
|
89
|
+
type: "'title' | 'subtitle' | 'body' | 'brands'? — semantic role",
|
|
90
|
+
lineHeight: "number? — line spacing multiplier (e.g. 1.2)",
|
|
91
|
+
width: "number? — max width for text wrapping",
|
|
92
|
+
overflow: "boolean? — allow overflow to adjacent screens",
|
|
93
|
+
rotation: "number? — degrees",
|
|
94
|
+
opacity: "number? — 0-100",
|
|
95
|
+
shadow: "ShadowConfig? — drop shadow",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
PillNode: {
|
|
99
|
+
description: "A rounded tag or button-style label.",
|
|
100
|
+
visual: "Colored rounded rectangle with text inside. Looks like a tag, chip, or CTA button. Can have an arrow icon.",
|
|
101
|
+
fields: {
|
|
102
|
+
id: "string",
|
|
103
|
+
richContent: "TiptapJSON — pill text content",
|
|
104
|
+
position: "{ x, y }",
|
|
105
|
+
zIndex: "number",
|
|
106
|
+
backgroundColor: "hex color — pill background",
|
|
107
|
+
textColor: "hex color — text inside the pill",
|
|
108
|
+
width: "number — pill width",
|
|
109
|
+
height: "number — pill height",
|
|
110
|
+
cornerRadius: "number? — defaults to fully rounded",
|
|
111
|
+
showArrow: "boolean? — show arrow icon on the right",
|
|
112
|
+
fontSize: "number?",
|
|
113
|
+
lineHeight: "number?",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
BadgeNode: {
|
|
117
|
+
description: "A circular seal or count badge.",
|
|
118
|
+
visual: "Round badge with text (e.g. '#1', '4.9★', 'NEW'). Often used for social proof.",
|
|
119
|
+
fields: {
|
|
120
|
+
id: "string",
|
|
121
|
+
text: "string — main badge text",
|
|
122
|
+
subtext: "string? — smaller secondary text",
|
|
123
|
+
size: "number — badge diameter",
|
|
124
|
+
position: "{ x, y }",
|
|
125
|
+
zIndex: "number",
|
|
126
|
+
backgroundColor: "hex color?",
|
|
127
|
+
textColor: "hex color?",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
BlobNode: {
|
|
131
|
+
description: "An organic decorative shape.",
|
|
132
|
+
visual: "Soft, rounded organic blob shape. Used as a background accent or decorative element behind other nodes.",
|
|
133
|
+
fields: {
|
|
134
|
+
id: "string",
|
|
135
|
+
width: "number",
|
|
136
|
+
height: "number",
|
|
137
|
+
color: "hex color",
|
|
138
|
+
position: "{ x, y }",
|
|
139
|
+
zIndex: "number",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
RatingNode: {
|
|
143
|
+
description: "A star rating display.",
|
|
144
|
+
visual: "Shows filled/partial stars (e.g. ★★★★★ 4.8). Can include a text label like '12K ratings'. Stars can appear below the label.",
|
|
145
|
+
fields: {
|
|
146
|
+
id: "string",
|
|
147
|
+
rating: "number — star rating value (e.g. 4.8)",
|
|
148
|
+
label: "string? — text label (e.g. '12,345 ratings')",
|
|
149
|
+
labelColor: "hex color? — label text color",
|
|
150
|
+
size: "number? — overall size scale",
|
|
151
|
+
starsBelow: "boolean? — if true, stars appear below the label",
|
|
152
|
+
position: "{ x, y }",
|
|
153
|
+
zIndex: "number",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
LogoNode: {
|
|
157
|
+
description: "The app logo.",
|
|
158
|
+
visual: "Shows the app icon (from uploaded logo image) or text-based logo. Usually appears on the first or last screen.",
|
|
159
|
+
fields: {
|
|
160
|
+
id: "string",
|
|
161
|
+
text: "string? — text-based logo fallback",
|
|
162
|
+
path: "string? — stored logo image path",
|
|
163
|
+
imageUrl: "string? — legacy URL field",
|
|
164
|
+
fontSize: "number? — text logo font size",
|
|
165
|
+
width: "number? — fixed width for logo box",
|
|
166
|
+
cornerRadius: "number? — rounded corners for image logos",
|
|
167
|
+
position: "{ x, y }",
|
|
168
|
+
zIndex: "number",
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
IllustrationNode: {
|
|
172
|
+
description: "A decorative image or sticker.",
|
|
173
|
+
visual: "An overlay image (PNG/SVG). Used for decorative elements like arrows, sparkles, stickers, or custom graphics.",
|
|
174
|
+
fields: {
|
|
175
|
+
id: "string",
|
|
176
|
+
path: "string? — stored illustration path",
|
|
177
|
+
imageUrl: "string? — legacy URL",
|
|
178
|
+
scale: "number? — size multiplier",
|
|
179
|
+
width: "number?",
|
|
180
|
+
height: "number?",
|
|
181
|
+
rotation: "number? — degrees",
|
|
182
|
+
tiltAngle: "number? — 3D perspective tilt (-30 to 30)",
|
|
183
|
+
primaryColor: "hex color? — SVG tint color",
|
|
184
|
+
position: "{ x, y }",
|
|
185
|
+
zIndex: "number",
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
MagnifierNode: {
|
|
189
|
+
description: "A zoomed-in inset of a screenshot region.",
|
|
190
|
+
visual: "A rounded rectangle showing a magnified portion of a screenshot. Like a 'zoom bubble' highlighting a specific UI detail.",
|
|
191
|
+
fields: {
|
|
192
|
+
id: "string",
|
|
193
|
+
screenshotId: "string — id of the screenshot node to magnify",
|
|
194
|
+
sourceRegion: "{ x, y, width, height } — relative coordinates 0-1 defining the zoom area",
|
|
195
|
+
scale: "number — zoom level (e.g. 2.0 = 200%)",
|
|
196
|
+
cornerRadius: "number — rounded corner radius in px",
|
|
197
|
+
borderWidth: "number — border thickness in px",
|
|
198
|
+
borderColor: "hex color",
|
|
199
|
+
shadowEnabled: "boolean? — drop shadow on the magnifier",
|
|
200
|
+
position: "{ x, y }",
|
|
201
|
+
zIndex: "number",
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
EmojiNode: {
|
|
205
|
+
description: "A decorative emoji character.",
|
|
206
|
+
visual: "A single emoji rendered at a given size. Used as playful accents or decorative elements.",
|
|
207
|
+
fields: {
|
|
208
|
+
id: "string",
|
|
209
|
+
emoji: "string — the emoji character(s) e.g. '🚀'",
|
|
210
|
+
size: "number — font size in pixels",
|
|
211
|
+
rotation: "number? — degrees",
|
|
212
|
+
position: "{ x, y }",
|
|
213
|
+
zIndex: "number",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
richContentFormat: {
|
|
218
|
+
description: "Text content uses TiptapJSON (ProseMirror-compatible rich text). This is the format for TextNode.richContent and PillNode.richContent.",
|
|
219
|
+
structure: {
|
|
220
|
+
type: '"doc"',
|
|
221
|
+
attrs: "optional { defaultColor, defaultFontFamily, defaultFontSize, defaultFontWeight }",
|
|
222
|
+
content: "TiptapParagraph[] — array of paragraphs",
|
|
223
|
+
},
|
|
224
|
+
paragraph: {
|
|
225
|
+
type: '"paragraph"',
|
|
226
|
+
content: "array of text runs and hard breaks",
|
|
227
|
+
},
|
|
228
|
+
textRun: {
|
|
229
|
+
type: '"text"',
|
|
230
|
+
text: "string — the actual text content",
|
|
231
|
+
marks: "optional array of marks for styling",
|
|
232
|
+
},
|
|
233
|
+
marks: [
|
|
234
|
+
{ type: "bold", description: "Bold text" },
|
|
235
|
+
{ type: "italic", description: "Italic text" },
|
|
236
|
+
{ type: "underline", description: "Underlined text" },
|
|
237
|
+
{ type: "textStyle", attrs: "{ color?, fontFamily?, fontSize? }", description: "Inline style overrides" },
|
|
238
|
+
],
|
|
239
|
+
example: {
|
|
240
|
+
type: "doc",
|
|
241
|
+
attrs: { defaultColor: "#ffffff", defaultFontFamily: "Inter", defaultFontSize: 64, defaultFontWeight: 800 },
|
|
242
|
+
content: [
|
|
243
|
+
{
|
|
244
|
+
type: "paragraph",
|
|
245
|
+
content: [
|
|
246
|
+
{ type: "text", text: "Find Cheap", marks: [] },
|
|
247
|
+
{ type: "hardBreak" },
|
|
248
|
+
{ type: "text", text: "Flights", marks: [{ type: "textStyle", attrs: { color: "#00D4AA" } }] },
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
shadowConfig: {
|
|
255
|
+
description: "Drop shadow applied to screenshots, text, or decorative nodes.",
|
|
256
|
+
fields: {
|
|
257
|
+
color: "hex color (e.g. '#000000')",
|
|
258
|
+
blur: "number — blur radius 0-50",
|
|
259
|
+
offsetX: "number — horizontal offset -50 to 50",
|
|
260
|
+
offsetY: "number — vertical offset -50 to 50",
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
gradient: {
|
|
264
|
+
description: "Background gradient for layout or individual screens.",
|
|
265
|
+
fields: {
|
|
266
|
+
type: "'linear' | 'radial'",
|
|
267
|
+
colors: "string[] — array of hex colors (min 2)",
|
|
268
|
+
direction: "number? — angle in degrees (for linear gradients)",
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
export const TRANSFORM_SCHEMA_RESOURCE = {
|
|
273
|
+
operations: [
|
|
274
|
+
"update_node",
|
|
275
|
+
"delete_node",
|
|
276
|
+
"add_node",
|
|
277
|
+
"reorder",
|
|
278
|
+
"replace_color",
|
|
279
|
+
],
|
|
280
|
+
nodeTypes: [
|
|
281
|
+
"screen",
|
|
282
|
+
"header",
|
|
283
|
+
"panoramaBackground",
|
|
284
|
+
"screenshot",
|
|
285
|
+
"text",
|
|
286
|
+
"pill",
|
|
287
|
+
"badge",
|
|
288
|
+
"blob",
|
|
289
|
+
"rating",
|
|
290
|
+
"logo",
|
|
291
|
+
"illustration",
|
|
292
|
+
"magnifier",
|
|
293
|
+
"emoji",
|
|
294
|
+
"backgroundImage",
|
|
295
|
+
],
|
|
296
|
+
target: {
|
|
297
|
+
nodeType: "required for precise updates",
|
|
298
|
+
nodeId: "optional specific node identifier",
|
|
299
|
+
selector: {
|
|
300
|
+
format: "string — one of the following patterns",
|
|
301
|
+
examples: [
|
|
302
|
+
"screen:0 — target screen by index",
|
|
303
|
+
"screen_1 — target screen by index (alternate syntax)",
|
|
304
|
+
"screenId:my-screen-id — target screen by its id field (use this for newly added screens)",
|
|
305
|
+
"all_headers — target all headers",
|
|
306
|
+
"all_texts — target all texts",
|
|
307
|
+
],
|
|
308
|
+
warning: "Do NOT use '#' prefix or any other format. Invalid selectors silently match ALL screens.",
|
|
309
|
+
},
|
|
310
|
+
screens: "array of screen indexes or 'all' — preferred way to target screens by index",
|
|
311
|
+
},
|
|
312
|
+
notes: [
|
|
313
|
+
"Default to layouts: ['mobile']. Only include tablet/desktop if the user explicitly asks.",
|
|
314
|
+
"Use get_layout first whenever the edit changes composition or should closely match the current styling.",
|
|
315
|
+
"add_node expects changes.node or changes to contain a full node payload with an id.",
|
|
316
|
+
"The backend validates the resulting layout before saving. Incomplete nodes such as text without position or screenshot without placement will be rejected.",
|
|
317
|
+
"When adding screens and then populating them, use two separate transform_layout calls: first add the empty screens, then target them by id using selector 'screenId:<id>' to add text and screenshot nodes.",
|
|
318
|
+
"Ensure added or moved elements do not overlap other elements on the same screen. Text must not cover screenshots and vice versa.",
|
|
319
|
+
"reorder expects changes.order or changes.nodeIds with node ids in the desired order.",
|
|
320
|
+
],
|
|
321
|
+
};
|
|
322
|
+
export const SUPPORTED_LANGUAGES = [
|
|
323
|
+
"en",
|
|
324
|
+
"es",
|
|
325
|
+
"fr",
|
|
326
|
+
"de",
|
|
327
|
+
"it",
|
|
328
|
+
"pt",
|
|
329
|
+
"pt-BR",
|
|
330
|
+
"ja",
|
|
331
|
+
"ko",
|
|
332
|
+
"zh-CN",
|
|
333
|
+
"zh-TW",
|
|
334
|
+
"nl",
|
|
335
|
+
"ru",
|
|
336
|
+
"ar",
|
|
337
|
+
"tr",
|
|
338
|
+
"pl",
|
|
339
|
+
"sv",
|
|
340
|
+
"no",
|
|
341
|
+
"da",
|
|
342
|
+
"fi",
|
|
343
|
+
"cs",
|
|
344
|
+
"hu",
|
|
345
|
+
"ro",
|
|
346
|
+
"uk",
|
|
347
|
+
];
|
|
348
|
+
export const SUPPORTED_DEVICES = {
|
|
349
|
+
phone: [
|
|
350
|
+
{ id: "iphone17", label: "iPhone 17 Pro Max", aspectRatio: 1320 / 2868 },
|
|
351
|
+
{ id: "googlepixel", label: "Google Pixel", aspectRatio: 1080 / 2400 },
|
|
352
|
+
],
|
|
353
|
+
tablet: [
|
|
354
|
+
{ id: "ipad", label: 'iPad Pro 12.9"', aspectRatio: 2048 / 2732 },
|
|
355
|
+
],
|
|
356
|
+
desktop: [
|
|
357
|
+
{
|
|
358
|
+
id: "macbook-pro-16",
|
|
359
|
+
label: 'MacBook Pro 16"',
|
|
360
|
+
aspectRatio: 2880 / 1800,
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
};
|
|
364
|
+
export const PROJECT_CREATION_WIZARD_RESOURCE = {
|
|
365
|
+
title: "Create Project Wizard",
|
|
366
|
+
matchesUi: "dashboard upload flow",
|
|
367
|
+
steps: [
|
|
368
|
+
{
|
|
369
|
+
order: 1,
|
|
370
|
+
id: "platform",
|
|
371
|
+
label: "Default Platform",
|
|
372
|
+
required: true,
|
|
373
|
+
prompt: "Ask whether the project should start on iOS or Android. Only offer both if the user explicitly asks for it.",
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
order: 2,
|
|
377
|
+
id: "store-import",
|
|
378
|
+
label: "Import from App Store",
|
|
379
|
+
required: false,
|
|
380
|
+
prompt: "Offer optional App Store import by URL or numeric Apple app id before manual entry.",
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
order: 3,
|
|
384
|
+
id: "app-name",
|
|
385
|
+
label: "App Name",
|
|
386
|
+
required: true,
|
|
387
|
+
prompt: "Confirm the final app name after import or manual entry. Do not create the project without this.",
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
order: 4,
|
|
391
|
+
id: "category",
|
|
392
|
+
label: "App Category",
|
|
393
|
+
required: false,
|
|
394
|
+
prompt: "Offer the category field as optional.",
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
order: 5,
|
|
398
|
+
id: "logo",
|
|
399
|
+
label: "Logo",
|
|
400
|
+
required: false,
|
|
401
|
+
prompt: "Offer an optional logo step. Store import can prefill the icon; manual flow can skip it.",
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
order: 6,
|
|
405
|
+
id: "app-context",
|
|
406
|
+
label: "App Context",
|
|
407
|
+
required: false,
|
|
408
|
+
prompt: "Offer optional app context/description to improve downstream screenshot and ASO generation.",
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
defaults: {
|
|
412
|
+
platform: "ios",
|
|
413
|
+
defaultDeviceType: "phone",
|
|
414
|
+
},
|
|
415
|
+
createProjectRule: "Call create_project only after the platform choice, optional import decision, and app name confirmation are complete.",
|
|
416
|
+
nextStepAfterCreate: "Upload screenshots",
|
|
417
|
+
};
|
|
418
|
+
export const WORKFLOW_GUIDE_RESOURCE = {
|
|
419
|
+
title: "Preferred MCP Workflows",
|
|
420
|
+
principle: "Prefer direct execution for concrete requests. Ask follow-up questions only when needed to avoid a materially wrong result.",
|
|
421
|
+
workflows: [
|
|
422
|
+
{
|
|
423
|
+
name: "Browse templates visually",
|
|
424
|
+
preferredSteps: [
|
|
425
|
+
"choose_template",
|
|
426
|
+
"browse_templates",
|
|
427
|
+
"get_template_details",
|
|
428
|
+
],
|
|
429
|
+
notes: "Use choose_template when the user needs to pick one template and continue automatically after clicking it in the hosted gallery. Use browse_templates for manual browsing only. Keep the full template catalog available unless the user asks for a shortlist.",
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: "New template on existing project",
|
|
433
|
+
preferredSteps: [
|
|
434
|
+
"create_variant",
|
|
435
|
+
"generate_layouts",
|
|
436
|
+
],
|
|
437
|
+
notes: "Use this when the user wants a fresh screenshot direction or a different template without overwriting the current variant.",
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
name: "Edit current layout directly",
|
|
441
|
+
preferredSteps: [
|
|
442
|
+
"get_layout",
|
|
443
|
+
"transform_layout",
|
|
444
|
+
],
|
|
445
|
+
notes: "Use get_layout first for edits that affect layout, spacing, or visual hierarchy. Only skip it for small, clearly targeted changes to known existing nodes such as replacing headline text.",
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "Add new screens to current layout",
|
|
449
|
+
preferredSteps: [
|
|
450
|
+
"get_layout",
|
|
451
|
+
"transform_layout",
|
|
452
|
+
],
|
|
453
|
+
notes: "Prefer adding screens directly to the current layout when the user wants to keep the same design language. Inspect the existing screens first, then copy the nearest matching screen structure and numeric styling values so the new screens align with the established composition.",
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
avoid: [
|
|
457
|
+
"Do not ask a generic 'what next' question after every successful operation.",
|
|
458
|
+
"Do not force menu-based interaction when the user already gave a concrete natural-language edit request.",
|
|
459
|
+
"Do not reduce the template catalog to a tiny recommendation list unless the user explicitly asks for that.",
|
|
460
|
+
"Do not describe templates without showing preview resources when template previews are available.",
|
|
461
|
+
"Do not read template previews one by one when browse_templates can show the full gallery in one step.",
|
|
462
|
+
"Do not ask the user to type a template name when choose_template can collect the choice directly from the hosted gallery.",
|
|
463
|
+
"Do not invent fresh x/y positions, widths, screenshot scale values, or headline styling for new screens when the user wants to preserve the current design language.",
|
|
464
|
+
"Do not report success for new screens or composition edits without checking that the new nodes match the surrounding screens and do not overlap key content.",
|
|
465
|
+
],
|
|
466
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { buildTemplatePreviewUrl, decorateTemplatePayload, isSafeTemplateId, isTemplatePreviewDeviceType, } from "../template-previews.js";
|
|
4
|
+
import { LAYOUT_SCHEMA_RESOURCE, PROJECT_CREATION_WIZARD_RESOURCE, SUPPORTED_DEVICES, TRANSFORM_SCHEMA_RESOURCE, WORKFLOW_GUIDE_RESOURCE, } from "./data.js";
|
|
5
|
+
function asJsonResource(uri, payload) {
|
|
6
|
+
return {
|
|
7
|
+
contents: [
|
|
8
|
+
{
|
|
9
|
+
uri,
|
|
10
|
+
mimeType: "application/json",
|
|
11
|
+
text: JSON.stringify(payload, null, 2),
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function asBinaryResource(uri, mimeType, blob) {
|
|
17
|
+
return {
|
|
18
|
+
contents: [
|
|
19
|
+
{
|
|
20
|
+
uri,
|
|
21
|
+
mimeType,
|
|
22
|
+
blob,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function registerResources(server, client) {
|
|
28
|
+
server.registerResource("layout-schema", "applaunchflow://schema/layout", {
|
|
29
|
+
title: "Layout Schema",
|
|
30
|
+
description: "AppLaunchFlow layout JSON structure",
|
|
31
|
+
mimeType: "application/json",
|
|
32
|
+
}, async (uri) => asJsonResource(uri.href, LAYOUT_SCHEMA_RESOURCE));
|
|
33
|
+
server.registerResource("transform-schema", "applaunchflow://schema/transforms", {
|
|
34
|
+
title: "Transform Schema",
|
|
35
|
+
description: "Supported layout transform operations",
|
|
36
|
+
mimeType: "application/json",
|
|
37
|
+
}, async (uri) => asJsonResource(uri.href, TRANSFORM_SCHEMA_RESOURCE));
|
|
38
|
+
server.registerResource("template-catalog", "applaunchflow://templates", {
|
|
39
|
+
title: "Template Catalog",
|
|
40
|
+
description: "Current screenshot template catalog",
|
|
41
|
+
mimeType: "application/json",
|
|
42
|
+
}, async (uri) => asJsonResource(uri.href, decorateTemplatePayload(await client.listTemplates(), client.credentials.baseUrl)));
|
|
43
|
+
server.registerResource("template-preview", new ResourceTemplate("applaunchflow://templates/{templateId}/preview/{deviceType}", {
|
|
44
|
+
list: undefined,
|
|
45
|
+
}), {
|
|
46
|
+
title: "Template Preview",
|
|
47
|
+
description: "Rendered preview image for a screenshot template and device type.",
|
|
48
|
+
mimeType: "image/png",
|
|
49
|
+
}, async (uri, { templateId, deviceType }) => {
|
|
50
|
+
const resolvedTemplateId = Array.isArray(templateId)
|
|
51
|
+
? templateId[0]
|
|
52
|
+
: templateId;
|
|
53
|
+
const resolvedDeviceType = Array.isArray(deviceType)
|
|
54
|
+
? deviceType[0]
|
|
55
|
+
: deviceType;
|
|
56
|
+
if (!isSafeTemplateId(resolvedTemplateId) ||
|
|
57
|
+
!isTemplatePreviewDeviceType(resolvedDeviceType)) {
|
|
58
|
+
throw new Error("Invalid template preview request");
|
|
59
|
+
}
|
|
60
|
+
const headers = new Headers();
|
|
61
|
+
headers.set("Cookie", `${client.credentials.cookieName}=${client.credentials.token}`);
|
|
62
|
+
headers.set("Authorization", `Bearer ${client.credentials.token}`);
|
|
63
|
+
const previewUrl = buildTemplatePreviewUrl(client.credentials.baseUrl, resolvedTemplateId, resolvedDeviceType);
|
|
64
|
+
const response = await fetch(previewUrl, {
|
|
65
|
+
headers,
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw new Error(`Failed to fetch template preview ${resolvedTemplateId}/${resolvedDeviceType}: HTTP ${response.status}`);
|
|
69
|
+
}
|
|
70
|
+
const mimeType = response.headers.get("content-type") || "image/png";
|
|
71
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
72
|
+
const blob = Buffer.from(arrayBuffer).toString("base64");
|
|
73
|
+
return asBinaryResource(uri.href, mimeType, blob);
|
|
74
|
+
});
|
|
75
|
+
server.registerResource("project-creation-wizard", "applaunchflow://wizard/project-creation", {
|
|
76
|
+
title: "Project Creation Wizard",
|
|
77
|
+
description: "The exact guided flow used by the UI create-project wizard",
|
|
78
|
+
mimeType: "application/json",
|
|
79
|
+
}, async (uri) => asJsonResource(uri.href, PROJECT_CREATION_WIZARD_RESOURCE));
|
|
80
|
+
server.registerResource("workflow-guide", "applaunchflow://guide/workflows", {
|
|
81
|
+
title: "Workflow Guide",
|
|
82
|
+
description: "Preferred MCP workflows for generating and editing projects",
|
|
83
|
+
mimeType: "application/json",
|
|
84
|
+
}, async (uri) => asJsonResource(uri.href, WORKFLOW_GUIDE_RESOURCE));
|
|
85
|
+
server.registerResource("devices", "applaunchflow://devices", {
|
|
86
|
+
title: "Supported Devices",
|
|
87
|
+
description: "Device frame identifiers and dimensions",
|
|
88
|
+
mimeType: "application/json",
|
|
89
|
+
}, async (uri) => asJsonResource(uri.href, SUPPORTED_DEVICES));
|
|
90
|
+
server.registerResource("project-state", new ResourceTemplate("applaunchflow://project/{projectId}/state", {
|
|
91
|
+
list: undefined,
|
|
92
|
+
}), {
|
|
93
|
+
title: "Project State",
|
|
94
|
+
description: "Project state summary with variants and translations",
|
|
95
|
+
mimeType: "application/json",
|
|
96
|
+
}, async (uri, { projectId }) => {
|
|
97
|
+
const resolvedProjectId = Array.isArray(projectId) ? projectId[0] : projectId;
|
|
98
|
+
const [project, translations, screenshotVariants] = await Promise.all([
|
|
99
|
+
client.getProject(resolvedProjectId),
|
|
100
|
+
client.getLayout({ generationId: resolvedProjectId }),
|
|
101
|
+
client.listVariants(resolvedProjectId, "screenshots"),
|
|
102
|
+
]);
|
|
103
|
+
return asJsonResource(uri.href, {
|
|
104
|
+
project,
|
|
105
|
+
translations,
|
|
106
|
+
variants: {
|
|
107
|
+
screenshots: screenshotVariants,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export const TEMPLATE_PREVIEW_DEVICE_TYPES = [
|
|
2
|
+
"phone",
|
|
3
|
+
"tablet",
|
|
4
|
+
"desktop",
|
|
5
|
+
];
|
|
6
|
+
export function isTemplatePreviewDeviceType(value) {
|
|
7
|
+
return TEMPLATE_PREVIEW_DEVICE_TYPES.includes(value);
|
|
8
|
+
}
|
|
9
|
+
export function isSafeTemplateId(templateId) {
|
|
10
|
+
return /^[a-z0-9-]+$/i.test(templateId);
|
|
11
|
+
}
|
|
12
|
+
export function buildTemplatePreviewPath(templateId, deviceType) {
|
|
13
|
+
return `/template-examples/${deviceType}/${templateId}.png`;
|
|
14
|
+
}
|
|
15
|
+
export function buildTemplatePreviewUrl(baseUrl, templateId, deviceType) {
|
|
16
|
+
return new URL(buildTemplatePreviewPath(templateId, deviceType), baseUrl).toString();
|
|
17
|
+
}
|
|
18
|
+
export function buildTemplatePreviewResourceUri(templateId, deviceType) {
|
|
19
|
+
return `applaunchflow://templates/${encodeURIComponent(templateId)}/preview/${deviceType}`;
|
|
20
|
+
}
|
|
21
|
+
export function buildTemplateGalleryUrl(baseUrl, options) {
|
|
22
|
+
const url = new URL("/template-gallery", baseUrl);
|
|
23
|
+
if (options?.deviceType) {
|
|
24
|
+
url.searchParams.set("device", options.deviceType);
|
|
25
|
+
}
|
|
26
|
+
if (options?.templateIds?.length) {
|
|
27
|
+
url.searchParams.set("ids", options.templateIds.join(","));
|
|
28
|
+
}
|
|
29
|
+
if (options?.selectedTemplateId) {
|
|
30
|
+
url.searchParams.set("selected", options.selectedTemplateId);
|
|
31
|
+
}
|
|
32
|
+
if (options?.title) {
|
|
33
|
+
url.searchParams.set("title", options.title);
|
|
34
|
+
}
|
|
35
|
+
if (options?.returnTo) {
|
|
36
|
+
url.searchParams.set("returnTo", options.returnTo);
|
|
37
|
+
}
|
|
38
|
+
return url.toString();
|
|
39
|
+
}
|
|
40
|
+
function decorateTemplate(template, baseUrl) {
|
|
41
|
+
const previewUrls = Object.fromEntries(TEMPLATE_PREVIEW_DEVICE_TYPES.map((deviceType) => [
|
|
42
|
+
deviceType,
|
|
43
|
+
template.previewUrls?.[deviceType] ||
|
|
44
|
+
buildTemplatePreviewUrl(baseUrl, template.id, deviceType),
|
|
45
|
+
]));
|
|
46
|
+
const previewResourceUris = Object.fromEntries(TEMPLATE_PREVIEW_DEVICE_TYPES.map((deviceType) => [
|
|
47
|
+
deviceType,
|
|
48
|
+
buildTemplatePreviewResourceUri(template.id, deviceType),
|
|
49
|
+
]));
|
|
50
|
+
return {
|
|
51
|
+
...template,
|
|
52
|
+
previewUrls,
|
|
53
|
+
previewResourceUris,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function decorateTemplatePayload(payload, baseUrl) {
|
|
57
|
+
if (payload &&
|
|
58
|
+
typeof payload === "object" &&
|
|
59
|
+
"templates" in payload &&
|
|
60
|
+
Array.isArray(payload.templates)) {
|
|
61
|
+
const typedPayload = payload;
|
|
62
|
+
return {
|
|
63
|
+
...typedPayload,
|
|
64
|
+
templates: typedPayload.templates.map((template) => decorateTemplate(template, baseUrl)),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (payload &&
|
|
68
|
+
typeof payload === "object" &&
|
|
69
|
+
"template" in payload &&
|
|
70
|
+
payload.template &&
|
|
71
|
+
typeof payload.template === "object") {
|
|
72
|
+
const typedPayload = payload;
|
|
73
|
+
return {
|
|
74
|
+
...typedPayload,
|
|
75
|
+
template: decorateTemplate(typedPayload.template, baseUrl),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return payload;
|
|
79
|
+
}
|