@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.
@@ -0,0 +1,330 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { buildTemplateGalleryUrl, TEMPLATE_PREVIEW_DEVICE_TYPES, decorateTemplatePayload, } from "../template-previews.js";
4
+ import { fail } from "./utils.js";
5
+ function formatTemplateLine(template, deviceType) {
6
+ const parts = [
7
+ `${template.name} (${template.id})`,
8
+ typeof template.screenCount === "number"
9
+ ? `${template.screenCount} screens`
10
+ : null,
11
+ template.categories?.length
12
+ ? `categories: ${template.categories.join(", ")}`
13
+ : null,
14
+ template.description || null,
15
+ `Preview URL (${deviceType}): ${template.previewUrls[deviceType]}`,
16
+ ];
17
+ return parts.filter(Boolean).join(" | ");
18
+ }
19
+ function buildListTemplatesResult(payload, deviceType, galleryUrl) {
20
+ return {
21
+ content: [
22
+ {
23
+ type: "text",
24
+ text: [
25
+ `Fetched ${payload.templates.length} screenshot templates.`,
26
+ `Open the visual gallery: ${galleryUrl}`,
27
+ `Use the ${deviceType} preview resources below to compare them visually.`,
28
+ "Keep the full catalog available unless the user explicitly asks for a shortlist.",
29
+ "",
30
+ ...payload.templates.map((template) => formatTemplateLine(template, deviceType)),
31
+ ].join("\n"),
32
+ },
33
+ {
34
+ type: "resource_link",
35
+ uri: galleryUrl,
36
+ name: "Open Template Gallery",
37
+ mimeType: "text/html",
38
+ description: "Hosted visual gallery with screenshot template previews for all available templates.",
39
+ },
40
+ ...payload.templates.map((template) => ({
41
+ type: "resource_link",
42
+ uri: template.previewResourceUris[deviceType],
43
+ name: `${template.name} (${deviceType} preview)`,
44
+ mimeType: "image/png",
45
+ description: template.description ||
46
+ `Visual preview for the ${template.name} screenshot template.`,
47
+ })),
48
+ ],
49
+ structuredContent: {
50
+ success: true,
51
+ data: {
52
+ ...payload,
53
+ previewDeviceType: deviceType,
54
+ galleryUrl,
55
+ },
56
+ message: "Fetched templates",
57
+ },
58
+ };
59
+ }
60
+ function buildTemplateDetailsResult(payload) {
61
+ const { template } = payload;
62
+ const previewSummary = TEMPLATE_PREVIEW_DEVICE_TYPES.map((deviceType) => `${deviceType}: ${template.previewUrls[deviceType]} | resource: ${template.previewResourceUris[deviceType]}`);
63
+ return {
64
+ content: [
65
+ {
66
+ type: "text",
67
+ text: [
68
+ `Fetched template ${template.name} (${template.id}).`,
69
+ template.description || null,
70
+ typeof template.screenCount === "number"
71
+ ? `Screen count: ${template.screenCount}`
72
+ : null,
73
+ template.categories?.length
74
+ ? `Categories: ${template.categories.join(", ")}`
75
+ : null,
76
+ "",
77
+ "Preview assets:",
78
+ ...previewSummary,
79
+ ]
80
+ .filter(Boolean)
81
+ .join("\n"),
82
+ },
83
+ ...TEMPLATE_PREVIEW_DEVICE_TYPES.map((deviceType) => ({
84
+ type: "resource_link",
85
+ uri: template.previewResourceUris[deviceType],
86
+ name: `${template.name} (${deviceType} preview)`,
87
+ mimeType: "image/png",
88
+ description: template.description ||
89
+ `Visual preview for the ${template.name} screenshot template.`,
90
+ })),
91
+ ],
92
+ structuredContent: {
93
+ success: true,
94
+ data: payload,
95
+ message: "Fetched template",
96
+ },
97
+ };
98
+ }
99
+ export function registerTemplateTools(server, client, selectionCoordinator) {
100
+ server.registerTool("browse_templates", {
101
+ title: "Browse & Select Template",
102
+ description: "ALWAYS use this tool when a template choice is needed. Opens the visual template gallery in the browser where the user can browse previews and click to select. Returns the selected template id. Never offer templates via text or AskUserQuestion — always open this gallery.",
103
+ inputSchema: {
104
+ deviceType: z
105
+ .enum(TEMPLATE_PREVIEW_DEVICE_TYPES)
106
+ .optional()
107
+ .describe("Which preview device the gallery should show first."),
108
+ templateIds: z
109
+ .array(z.string())
110
+ .optional()
111
+ .describe("Optional subset of template ids to show in the gallery."),
112
+ selectedTemplateId: z
113
+ .string()
114
+ .optional()
115
+ .describe("Optional template id to highlight in the gallery."),
116
+ title: z
117
+ .string()
118
+ .optional()
119
+ .describe("Optional gallery heading, for example the project name."),
120
+ },
121
+ }, async ({ deviceType = "phone", templateIds, selectedTemplateId, title, }, extra) => {
122
+ try {
123
+ const payload = decorateTemplatePayload(await client.listTemplates(), client.credentials.baseUrl);
124
+ const availableIds = new Set(payload.templates.map((template) => template.id));
125
+ const availableTemplateMap = new Map(payload.templates.map((template) => [template.id, template]));
126
+ const filteredTemplateIds = templateIds?.filter((templateId) => availableIds.has(templateId)) || [];
127
+ // If coordinator is available, set up a callback so clicking a template works
128
+ if (selectionCoordinator) {
129
+ const selection = await selectionCoordinator.createSelection({
130
+ baseUrl: client.credentials.baseUrl,
131
+ deviceType,
132
+ templateIds: filteredTemplateIds.length > 0 ? filteredTemplateIds : undefined,
133
+ selectedTemplateId: selectedTemplateId && availableIds.has(selectedTemplateId)
134
+ ? selectedTemplateId
135
+ : undefined,
136
+ title,
137
+ });
138
+ const fallbackGalleryUrl = buildTemplateGalleryUrl(client.credentials.baseUrl, {
139
+ deviceType,
140
+ templateIds: filteredTemplateIds.length > 0 ? filteredTemplateIds : undefined,
141
+ selectedTemplateId: selectedTemplateId && availableIds.has(selectedTemplateId)
142
+ ? selectedTemplateId
143
+ : undefined,
144
+ title,
145
+ });
146
+ try {
147
+ const elicitationId = randomUUID();
148
+ try {
149
+ selection.setCompletionNotifier(server.server.createElicitationCompletionNotifier(elicitationId));
150
+ }
151
+ catch {
152
+ selection.setCompletionNotifier(undefined);
153
+ }
154
+ const elicitationResult = await server.server.elicitInput({
155
+ mode: "url",
156
+ elicitationId,
157
+ message: "Browse the template gallery and click a template to select it.",
158
+ url: selection.galleryUrl,
159
+ }, { signal: extra.signal });
160
+ if (elicitationResult.action !== "accept") {
161
+ selection.cleanup();
162
+ return {
163
+ content: [
164
+ {
165
+ type: "text",
166
+ text: elicitationResult.action === "decline"
167
+ ? "Template browsing was dismissed."
168
+ : "Template browsing was cancelled.",
169
+ },
170
+ ],
171
+ structuredContent: {
172
+ success: false,
173
+ data: { action: elicitationResult.action, cancelled: true },
174
+ message: "Template browsing not completed",
175
+ },
176
+ };
177
+ }
178
+ const chosenTemplateId = await selection.waitForSelection(extra.signal);
179
+ const chosenTemplate = availableTemplateMap.get(chosenTemplateId);
180
+ if (!chosenTemplate) {
181
+ throw new Error(`Selected template ${chosenTemplateId} is not available`);
182
+ }
183
+ return {
184
+ content: [
185
+ {
186
+ type: "text",
187
+ text: `Selected template ${chosenTemplate.name} (${chosenTemplate.id}).`,
188
+ },
189
+ {
190
+ type: "resource_link",
191
+ uri: chosenTemplate.previewResourceUris[deviceType],
192
+ name: `${chosenTemplate.name} (${deviceType} preview)`,
193
+ mimeType: "image/png",
194
+ description: chosenTemplate.description ||
195
+ `Visual preview for the ${chosenTemplate.name} screenshot template.`,
196
+ },
197
+ ],
198
+ structuredContent: {
199
+ success: true,
200
+ data: {
201
+ template: chosenTemplate,
202
+ templateId: chosenTemplate.id,
203
+ templateName: chosenTemplate.name,
204
+ deviceType,
205
+ galleryUrl: selection.galleryUrl,
206
+ },
207
+ message: "Selected template",
208
+ },
209
+ };
210
+ }
211
+ catch (error) {
212
+ selection.cleanup();
213
+ const errorMessage = error instanceof Error ? error.message : String(error);
214
+ if (errorMessage.includes("Client does not support url elicitation") ||
215
+ errorMessage.includes("Client does not support URL elicitation")) {
216
+ return {
217
+ content: [
218
+ {
219
+ type: "text",
220
+ text: [
221
+ "This client does not support interactive URL selection.",
222
+ "Paste this exact gallery URL into the user-visible reply so the user can choose manually:",
223
+ fallbackGalleryUrl,
224
+ ].join("\n"),
225
+ },
226
+ {
227
+ type: "resource_link",
228
+ uri: fallbackGalleryUrl,
229
+ name: "Open Template Gallery",
230
+ mimeType: "text/html",
231
+ description: "Hosted visual gallery for browsing screenshot template previews.",
232
+ },
233
+ ],
234
+ structuredContent: {
235
+ success: false,
236
+ data: {
237
+ interactiveSelectionAvailable: false,
238
+ galleryUrl: fallbackGalleryUrl,
239
+ userFacingUrl: fallbackGalleryUrl,
240
+ deviceType,
241
+ templateIds: filteredTemplateIds,
242
+ },
243
+ message: "Interactive selection unavailable; falling back to manual gallery browsing",
244
+ },
245
+ };
246
+ }
247
+ throw error;
248
+ }
249
+ }
250
+ // No coordinator — fall back to returning the URL
251
+ const galleryUrl = buildTemplateGalleryUrl(client.credentials.baseUrl, {
252
+ deviceType,
253
+ templateIds: filteredTemplateIds.length > 0 ? filteredTemplateIds : undefined,
254
+ selectedTemplateId: selectedTemplateId && availableIds.has(selectedTemplateId)
255
+ ? selectedTemplateId
256
+ : undefined,
257
+ title,
258
+ });
259
+ return {
260
+ content: [
261
+ {
262
+ type: "text",
263
+ text: [
264
+ "Paste this exact gallery URL into the user-visible reply.",
265
+ `Template gallery URL: ${galleryUrl}`,
266
+ "Do not say 'link above' because tool results may be collapsed or hidden.",
267
+ "After you pick a template, reply with the template name or id.",
268
+ ].join("\n"),
269
+ },
270
+ {
271
+ type: "resource_link",
272
+ uri: galleryUrl,
273
+ name: "Open Template Gallery",
274
+ mimeType: "text/html",
275
+ description: "Hosted visual gallery for browsing screenshot template previews.",
276
+ },
277
+ ],
278
+ structuredContent: {
279
+ success: true,
280
+ data: {
281
+ galleryUrl,
282
+ userFacingUrl: galleryUrl,
283
+ deviceType,
284
+ templateIds: filteredTemplateIds,
285
+ },
286
+ message: "Prepared template gallery",
287
+ },
288
+ };
289
+ }
290
+ catch (error) {
291
+ return fail(error);
292
+ }
293
+ });
294
+ server.registerTool("list_templates", {
295
+ title: "List Templates",
296
+ description: "List all AppLaunchFlow screenshot templates with visual preview resources. Prefer visual comparison over text-only descriptions.",
297
+ inputSchema: {
298
+ deviceType: z
299
+ .enum(TEMPLATE_PREVIEW_DEVICE_TYPES)
300
+ .optional()
301
+ .describe("Which preview device to attach for each template."),
302
+ },
303
+ }, async ({ deviceType = "phone" }) => {
304
+ try {
305
+ const payload = decorateTemplatePayload(await client.listTemplates(), client.credentials.baseUrl);
306
+ const galleryUrl = buildTemplateGalleryUrl(client.credentials.baseUrl, {
307
+ deviceType,
308
+ });
309
+ return buildListTemplatesResult(payload, deviceType, galleryUrl);
310
+ }
311
+ catch (error) {
312
+ return fail(error);
313
+ }
314
+ });
315
+ server.registerTool("get_template_details", {
316
+ title: "Get Template Details",
317
+ description: "Get details and visual preview resources for a single screenshot template.",
318
+ inputSchema: {
319
+ templateId: z.string(),
320
+ },
321
+ }, async ({ templateId }) => {
322
+ try {
323
+ const payload = decorateTemplatePayload(await client.getTemplate(templateId), client.credentials.baseUrl);
324
+ return buildTemplateDetailsResult(payload);
325
+ }
326
+ catch (error) {
327
+ return fail(error);
328
+ }
329
+ });
330
+ }
@@ -0,0 +1,53 @@
1
+ import { AppLaunchFlowApiError } from "../client/api.js";
2
+ export function ok(data, message) {
3
+ const text = typeof data === "string"
4
+ ? message
5
+ ? `${message}\n\n${data}`
6
+ : data
7
+ : message
8
+ ? `${message}\n\n${JSON.stringify(data, null, 2)}`
9
+ : JSON.stringify(data, null, 2);
10
+ return {
11
+ content: [
12
+ {
13
+ type: "text",
14
+ text,
15
+ },
16
+ ],
17
+ structuredContent: {
18
+ success: true,
19
+ data,
20
+ ...(message ? { message } : {}),
21
+ },
22
+ };
23
+ }
24
+ export function fail(error) {
25
+ const normalized = error instanceof AppLaunchFlowApiError
26
+ ? {
27
+ code: error.body?.error?.code ||
28
+ error.body?.code ||
29
+ `HTTP_${error.status}`,
30
+ type: error.body?.error?.type || "server",
31
+ message: error.body?.error?.message || error.message,
32
+ status: error.status,
33
+ details: error.body,
34
+ }
35
+ : {
36
+ code: "UNKNOWN",
37
+ type: "server",
38
+ message: error instanceof Error ? error.message : String(error),
39
+ };
40
+ return {
41
+ isError: true,
42
+ content: [
43
+ {
44
+ type: "text",
45
+ text: normalized.message,
46
+ },
47
+ ],
48
+ structuredContent: {
49
+ success: false,
50
+ error: normalized,
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,79 @@
1
+ import { z } from "zod";
2
+ import { fail, ok } from "./utils.js";
3
+ const contentTypeEnum = z.enum(["screenshots"]);
4
+ export function registerVariantTools(server, client) {
5
+ server.registerTool("list_variants", {
6
+ title: "List Variants",
7
+ description: "List variants for a generation and content type",
8
+ inputSchema: {
9
+ generationId: z.string().uuid(),
10
+ contentType: contentTypeEnum,
11
+ },
12
+ }, async ({ generationId, contentType }) => {
13
+ try {
14
+ return ok(await client.listVariants(generationId, contentType), "Fetched variants");
15
+ }
16
+ catch (error) {
17
+ return fail(error);
18
+ }
19
+ });
20
+ server.registerTool("create_variant", {
21
+ title: "Create Variant",
22
+ description: "Create a new content variant. This is the preferred starting point when the user wants a new screenshot direction, a different template, or a fresh AI-generated take without overwriting the current variant.",
23
+ inputSchema: {
24
+ generationId: z.string().uuid(),
25
+ contentType: contentTypeEnum,
26
+ label: z.string().optional(),
27
+ setActive: z.boolean().optional(),
28
+ },
29
+ }, async (args) => {
30
+ try {
31
+ return ok(await client.createVariant(args), "Created variant");
32
+ }
33
+ catch (error) {
34
+ return fail(error);
35
+ }
36
+ });
37
+ server.registerTool("switch_variant", {
38
+ title: "Switch Variant",
39
+ description: "Set a variant as active",
40
+ inputSchema: {
41
+ variantId: z.string().uuid(),
42
+ },
43
+ }, async ({ variantId }) => {
44
+ try {
45
+ return ok(await client.switchVariant(variantId), "Switched variant");
46
+ }
47
+ catch (error) {
48
+ return fail(error);
49
+ }
50
+ });
51
+ server.registerTool("duplicate_variant", {
52
+ title: "Duplicate Variant",
53
+ description: "Duplicate an existing variant",
54
+ inputSchema: {
55
+ variantId: z.string().uuid(),
56
+ },
57
+ }, async ({ variantId }) => {
58
+ try {
59
+ return ok(await client.duplicateVariant(variantId), "Duplicated variant");
60
+ }
61
+ catch (error) {
62
+ return fail(error);
63
+ }
64
+ });
65
+ server.registerTool("delete_variant", {
66
+ title: "Delete Variant",
67
+ description: "Delete a content variant",
68
+ inputSchema: {
69
+ variantId: z.string().uuid(),
70
+ },
71
+ }, async ({ variantId }) => {
72
+ try {
73
+ return ok(await client.deleteVariant(variantId), "Deleted variant");
74
+ }
75
+ catch (error) {
76
+ return fail(error);
77
+ }
78
+ });
79
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@applaunchflow/mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for AppLaunchFlow — create App Store & Google Play screenshots with AI.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "applaunchflow-mcp": "./build/index.js"
9
+ },
10
+ "files": [
11
+ "build"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc -p tsconfig.json && chmod 755 build/index.js",
15
+ "dev": "tsx src/index.ts",
16
+ "start": "node build/index.js"
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.20.0",
20
+ "zod": "^3.25.76"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^24.6.0",
24
+ "tsx": "^4.20.5",
25
+ "typescript": "^5.9.3"
26
+ }
27
+ }