@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,138 @@
1
+ import { z } from "zod";
2
+ import { fail, ok } from "./utils.js";
3
+ const transformOperationSchema = z.object({
4
+ type: z.enum([
5
+ "update_node",
6
+ "delete_node",
7
+ "add_node",
8
+ "reorder",
9
+ "replace_color",
10
+ ]).describe("Operation type. replace_color is a find-and-replace for colors across the layout — use it for bulk color changes instead of updating each text node individually. " +
11
+ "Example: {type:'replace_color', target:{nodeType:'screen'}, changes:{find:'#F6EFE9', replace:'#7C3AED'}} replaces that color everywhere (text marks, icon colors, backgrounds). " +
12
+ "Use screens:'all' to replace across all screens, or screens:[6] for a single screen."),
13
+ target: z.object({
14
+ nodeType: z
15
+ .string()
16
+ .describe("REQUIRED. The node type to target: 'screen', 'text', 'screenshot', 'illustration', 'pill', 'badge', 'blob', 'rating', 'logo', 'emoji', 'header', 'panoramaBackground', 'backgroundImage'."),
17
+ nodeId: z
18
+ .string()
19
+ .optional()
20
+ .describe("Optional. Target a specific node by id. If omitted, the operation applies to ALL nodes of nodeType in the target screens."),
21
+ selector: z
22
+ .string()
23
+ .optional()
24
+ .describe("Optional. Target screens by id: 'screenId:<id>'. Do NOT use '#' prefix."),
25
+ screens: z
26
+ .union([z.literal("all"), z.array(z.number())])
27
+ .optional()
28
+ .describe("Optional. Target specific screens by index array (e.g. [0, 1, 2]) or 'all' for every screen. If omitted, targets all screens."),
29
+ }),
30
+ changes: z.record(z.any()),
31
+ }).superRefine((operation, ctx) => {
32
+ // Require nodeType for all operations except replace_color
33
+ if (!operation.target.nodeType && operation.type !== "replace_color") {
34
+ ctx.addIssue({
35
+ code: z.ZodIssueCode.custom,
36
+ path: ["target", "nodeType"],
37
+ message: "nodeType is required in target",
38
+ });
39
+ return;
40
+ }
41
+ const payload = operation.changes?.node &&
42
+ typeof operation.changes.node === "object" &&
43
+ !Array.isArray(operation.changes.node)
44
+ ? operation.changes.node
45
+ : operation.changes;
46
+ if (operation.type === "add_node") {
47
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
48
+ ctx.addIssue({
49
+ code: z.ZodIssueCode.custom,
50
+ path: ["changes"],
51
+ message: "add_node requires a node object in changes or changes.node",
52
+ });
53
+ return;
54
+ }
55
+ if (typeof payload.id !== "string" || payload.id.trim().length === 0) {
56
+ ctx.addIssue({
57
+ code: z.ZodIssueCode.custom,
58
+ path: ["changes", "id"],
59
+ message: "add_node requires a non-empty id field",
60
+ });
61
+ }
62
+ if (operation.target.nodeType === "screen" &&
63
+ ("text" in payload || "screenshotPath" in payload)) {
64
+ ctx.addIssue({
65
+ code: z.ZodIssueCode.custom,
66
+ path: ["changes"],
67
+ message: "When adding a screen, only provide the screen container fields. Add screenshot and text nodes with separate add_node operations targeting that screen index.",
68
+ });
69
+ }
70
+ }
71
+ });
72
+ export function registerLayoutTools(server, client) {
73
+ server.registerTool("get_layout", {
74
+ title: "Get Layout",
75
+ description: "Get layout JSON for the current translation before editing or reviewing a variant.",
76
+ inputSchema: {
77
+ generationId: z.string().uuid(),
78
+ language: z.string().optional(),
79
+ variantId: z.string().uuid().optional(),
80
+ sign: z.boolean().optional(),
81
+ },
82
+ }, async ({ generationId, language, variantId, sign }) => {
83
+ try {
84
+ return ok(await client.getLayout({ generationId, language, variantId, sign }), "Fetched layout data");
85
+ }
86
+ catch (error) {
87
+ return fail(error);
88
+ }
89
+ });
90
+ server.registerTool("save_layout", {
91
+ title: "Save Layout",
92
+ description: "Persist a full translation layout payload",
93
+ inputSchema: {
94
+ generationId: z.string().uuid(),
95
+ language: z.string(),
96
+ variantId: z.string().uuid().optional(),
97
+ mobileLayout: z.record(z.any()),
98
+ tabletLayout: z.record(z.any()),
99
+ desktopLayout: z.record(z.any()).nullable().optional(),
100
+ },
101
+ }, async (args) => {
102
+ try {
103
+ return ok(await client.saveLayout(args), "Saved layout");
104
+ }
105
+ catch (error) {
106
+ return fail(error);
107
+ }
108
+ });
109
+ server.registerTool("transform_layout", {
110
+ title: "Transform Layout",
111
+ description: "Apply transform operations to an existing layout. Primary editing tool for text, screenshots, colors, and structure changes. " +
112
+ "RULES: " +
113
+ "1. nodeType is REQUIRED in every operation target. The backend rejects operations without it. " +
114
+ "2. Omit nodeId to update ALL nodes of that type in the target screens. This is the efficient way to do bulk changes (e.g. change all illustration colors: {nodeType:'illustration', screens:'all'} with changes {primaryColor:'#xxx'}). " +
115
+ "3. Use screens:'all' to target every screen at once. Do NOT send one operation per screen when the same change applies to all. " +
116
+ "4. For add_node, changes MUST include an 'id' field. " +
117
+ "5. To add new screens, first add empty screen containers, then populate them in a SECOND call using selector 'screenId:<id>'. " +
118
+ "6. Default to layouts:['mobile']. Only include tablet/desktop if the user asks.",
119
+ inputSchema: {
120
+ generationId: z.string().uuid(),
121
+ language: z.string(),
122
+ variantId: z.string().uuid().optional(),
123
+ atomic: z.boolean().optional(),
124
+ layouts: z
125
+ .array(z.enum(["mobile", "tablet", "desktop"]))
126
+ .optional()
127
+ .describe("Which layout sizes to transform. Default to ['mobile'] unless the user explicitly asks for tablet or desktop."),
128
+ operations: z.array(transformOperationSchema).min(1),
129
+ },
130
+ }, async (args) => {
131
+ try {
132
+ return ok(await client.transformLayout(args), "Applied layout transform");
133
+ }
134
+ catch (error) {
135
+ return fail(error);
136
+ }
137
+ });
138
+ }
@@ -0,0 +1,58 @@
1
+ import { z } from "zod";
2
+ import { fail, ok } from "./utils.js";
3
+ const SUPPORTED_LANGUAGE_CODES = [
4
+ "en", "es", "fr", "de", "it", "pt", "pt-BR", "ja", "ko",
5
+ "zh-CN", "zh-TW", "nl", "ru", "ar", "tr", "pl", "sv",
6
+ "no", "da", "fi", "cs", "hi", "hu", "ro", "uk",
7
+ ];
8
+ export function registerLocalizationTools(server, client) {
9
+ server.registerTool("translate_layouts", {
10
+ title: "Translate Layouts",
11
+ description: "Translate screenshot layouts into one or more target languages using AI. " +
12
+ "This is the PREFERRED way to localize screenshots — do NOT manually edit text nodes for translation. " +
13
+ "The backend translates all text in the layout while preserving positioning, styling, and screenshots. " +
14
+ "Requires a source layout to already exist (generate_layouts must have been called first).",
15
+ inputSchema: {
16
+ generationId: z.string().uuid().describe("The project/generation UUID."),
17
+ variantId: z
18
+ .string()
19
+ .uuid()
20
+ .optional()
21
+ .describe("Variant to translate. If omitted, uses the active variant."),
22
+ targetLanguages: z
23
+ .array(z.enum(SUPPORTED_LANGUAGE_CODES))
24
+ .min(1)
25
+ .describe("Array of target language codes (e.g. ['en', 'ja', 'de']). The source language is auto-detected and excluded."),
26
+ layouts: z
27
+ .array(z.enum(["mobile", "tablet", "desktop"]))
28
+ .optional()
29
+ .describe("Which layout sizes to translate. Defaults to ['mobile', 'tablet']. Include 'desktop' only if the project has a desktop layout."),
30
+ },
31
+ }, async (args) => {
32
+ try {
33
+ const body = {
34
+ ...args,
35
+ layouts: args.layouts || ["mobile", "tablet"],
36
+ };
37
+ return ok(await client.translateLayouts(body), "Translated layouts");
38
+ }
39
+ catch (error) {
40
+ return fail(error);
41
+ }
42
+ });
43
+ server.registerTool("list_translations", {
44
+ title: "List Translations",
45
+ description: "List available translations for a project variant. Returns which languages have been translated.",
46
+ inputSchema: {
47
+ generationId: z.string().uuid(),
48
+ variantId: z.string().uuid().optional(),
49
+ },
50
+ }, async ({ generationId, variantId }) => {
51
+ try {
52
+ return ok(await client.getLayout({ generationId, variantId }), "Fetched translations");
53
+ }
54
+ catch (error) {
55
+ return fail(error);
56
+ }
57
+ });
58
+ }
@@ -0,0 +1,113 @@
1
+ import { z } from "zod";
2
+ import { fail, ok } from "./utils.js";
3
+ function stripUndefined(value) {
4
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
5
+ }
6
+ export function registerProjectTools(server, client) {
7
+ server.registerTool("list_projects", {
8
+ title: "List Projects",
9
+ description: "List AppLaunchFlow projects for the authenticated user",
10
+ }, async () => {
11
+ try {
12
+ return ok(await client.listProjects(), "Fetched projects");
13
+ }
14
+ catch (error) {
15
+ return fail(error);
16
+ }
17
+ });
18
+ server.registerTool("get_project", {
19
+ title: "Get Project",
20
+ description: "Get the full hub state for a project",
21
+ inputSchema: {
22
+ projectId: z.string().uuid(),
23
+ },
24
+ }, async ({ projectId }) => {
25
+ try {
26
+ return ok(await client.getProject(projectId), "Fetched project");
27
+ }
28
+ catch (error) {
29
+ return fail(error);
30
+ }
31
+ });
32
+ server.registerTool("create_project", {
33
+ title: "Create Project",
34
+ description: "Create a new AppLaunchFlow project. Only app name and platform are required. " +
35
+ "Autofill category and description from context when possible — do not ask the user for these unless genuinely ambiguous.",
36
+ inputSchema: {
37
+ appName: z
38
+ .string()
39
+ .trim()
40
+ .min(1)
41
+ .max(120)
42
+ .describe("The app name."),
43
+ platform: z
44
+ .enum(["ios", "android", "both"])
45
+ .optional()
46
+ .describe("Target platform. Defaults to iOS."),
47
+ category: z
48
+ .string()
49
+ .trim()
50
+ .max(120)
51
+ .optional()
52
+ .describe("App category. Infer from the app name/context when possible (e.g. 'Travel' for a flight app)."),
53
+ appDescription: z
54
+ .string()
55
+ .trim()
56
+ .max(4000)
57
+ .optional()
58
+ .describe("Brief app description. Infer from context when possible."),
59
+ defaultDeviceType: z
60
+ .enum(["phone", "tablet", "desktop", "watch"])
61
+ .optional()
62
+ .describe("Defaults to phone. Only set if the user explicitly asks."),
63
+ logoPath: z
64
+ .string()
65
+ .optional()
66
+ .describe("Optional stored logo path from upload_screenshots when fileType=logo."),
67
+ metadata: z
68
+ .record(z.any())
69
+ .optional()
70
+ .describe("Advanced escape hatch for extra metadata fields."),
71
+ },
72
+ }, async (args) => {
73
+ try {
74
+ const platform = args.platform || "ios";
75
+ const metadata = stripUndefined({
76
+ ...(args.metadata || {}),
77
+ appName: args.appName,
78
+ platform,
79
+ category: args.category,
80
+ appDescription: args.appDescription,
81
+ defaultDeviceType: args.defaultDeviceType || "phone",
82
+ logoPath: args.logoPath,
83
+ });
84
+ const requestBody = stripUndefined({
85
+ name: args.appName,
86
+ platform,
87
+ metadata,
88
+ });
89
+ const created = await client.createProject(requestBody);
90
+ return ok({
91
+ project: created.project,
92
+ nextRecommendedStep: "upload_screenshots",
93
+ }, "Created project");
94
+ }
95
+ catch (error) {
96
+ return fail(error);
97
+ }
98
+ });
99
+ server.registerTool("delete_project", {
100
+ title: "Delete Project",
101
+ description: "Delete a project",
102
+ inputSchema: {
103
+ projectId: z.string().uuid(),
104
+ },
105
+ }, async ({ projectId }) => {
106
+ try {
107
+ return ok(await client.deleteProject(projectId), "Deleted project");
108
+ }
109
+ catch (error) {
110
+ return fail(error);
111
+ }
112
+ });
113
+ }
@@ -0,0 +1,107 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { z } from "zod";
3
+ import { fail, ok } from "./utils.js";
4
+ export function registerScreenshotTools(server, client) {
5
+ server.registerTool("generate_layouts", {
6
+ title: "Generate Layouts",
7
+ description: "Generate screenshot layouts for a project. " +
8
+ "For existing projects, pass generationId (same as projectId) — the API fetches screenshots from storage automatically. " +
9
+ "Do NOT pass screenshots when using generationId. " +
10
+ "Only pass metadata + screenshots (without generationId) for the ephemeral upload flow. " +
11
+ "Do NOT pass variantId — omit it so a new variant is always created. Never overwrite an existing variant. " +
12
+ "Do not use this tool for small edits to an existing layout; use transform_layout instead.",
13
+ inputSchema: {
14
+ generationId: z
15
+ .string()
16
+ .uuid()
17
+ .optional()
18
+ .describe("The project/generation UUID. When provided, the API loads screenshots from storage and saves results to DB. This is the primary way to call this tool for existing projects."),
19
+ projectId: z
20
+ .string()
21
+ .uuid()
22
+ .optional()
23
+ .describe("Only needed for the upload flow (without generationId) to sign screenshot paths."),
24
+ metadata: z
25
+ .record(z.any())
26
+ .optional()
27
+ .describe("App metadata. Required only when generationId is NOT provided (upload flow)."),
28
+ screenshots: z
29
+ .array(z.object({
30
+ path: z.string().optional(),
31
+ url: z.string().optional(),
32
+ filename: z.string().optional(),
33
+ }))
34
+ .optional()
35
+ .describe("Screenshot list. Required only when generationId is NOT provided. Do NOT send when using generationId."),
36
+ templateId: z.string().optional(),
37
+ deviceType: z.enum(["phone", "tablet", "desktop"]).optional(),
38
+ variantId: z
39
+ .string()
40
+ .uuid()
41
+ .optional()
42
+ .describe("DO NOT pass this. Always omit so a new variant is created. Never overwrite existing variants."),
43
+ },
44
+ }, async (args) => {
45
+ try {
46
+ return ok(await client.generateLayouts(args), "Generated layouts");
47
+ }
48
+ catch (error) {
49
+ return fail(error);
50
+ }
51
+ });
52
+ server.registerTool("list_screenshots", {
53
+ title: "List Screenshots",
54
+ description: "List uploaded screenshot paths for a project",
55
+ inputSchema: {
56
+ projectId: z.string().uuid(),
57
+ deviceType: z.enum(["mobile", "tablet", "desktop"]).optional(),
58
+ platform: z.enum(["ios", "android"]).optional(),
59
+ },
60
+ }, async ({ projectId, deviceType, platform }) => {
61
+ try {
62
+ return ok(await client.listScreenshots({ projectId, deviceType, platform }), "Listed screenshots");
63
+ }
64
+ catch (error) {
65
+ return fail(error);
66
+ }
67
+ });
68
+ server.registerTool("view_screenshot", {
69
+ title: "View Screenshot",
70
+ description: "Fetch a screenshot image and return it for visual analysis. " +
71
+ "Use this to inspect screenshots — extract colors, read UI text, understand layout context, or identify visual elements. " +
72
+ "Pass projectId and the relative path from list_screenshots or get_layout (e.g. 'mobile/ios/1234-image.PNG').",
73
+ inputSchema: {
74
+ projectId: z.string().uuid().describe("The project UUID."),
75
+ path: z
76
+ .string()
77
+ .describe("Relative screenshot path (e.g. 'mobile/ios/1234-IMG.PNG') from list_screenshots or the layout's screenshot.path field."),
78
+ },
79
+ }, async ({ projectId, path }) => {
80
+ try {
81
+ const fullPath = `${projectId}/${path}`;
82
+ const previewUrl = `${client.credentials.baseUrl}/api/preview?path=${encodeURIComponent(fullPath)}&w=320`;
83
+ const headers = new Headers();
84
+ headers.set("Cookie", `${client.credentials.cookieName}=${client.credentials.token}`);
85
+ headers.set("Authorization", `Bearer ${client.credentials.token}`);
86
+ const response = await fetch(previewUrl, { headers });
87
+ if (!response.ok) {
88
+ throw new Error(`Failed to fetch image: ${response.status}`);
89
+ }
90
+ const arrayBuffer = await response.arrayBuffer();
91
+ const mimeType = response.headers.get("content-type") || "image/png";
92
+ const base64 = Buffer.from(arrayBuffer).toString("base64");
93
+ return {
94
+ content: [
95
+ {
96
+ type: "image",
97
+ data: base64,
98
+ mimeType,
99
+ },
100
+ ],
101
+ };
102
+ }
103
+ catch (error) {
104
+ return fail(error);
105
+ }
106
+ });
107
+ }
@@ -0,0 +1,66 @@
1
+ export function extractAppStoreId(input) {
2
+ if (/^\d+$/.test(input)) {
3
+ return input;
4
+ }
5
+ const match = input.match(/id(\d+)/);
6
+ return match ? match[1] : null;
7
+ }
8
+ export async function fetchGooglePlayMetadata(urlString) {
9
+ const url = new URL(urlString);
10
+ const packageName = url.searchParams.get("id");
11
+ if (!packageName) {
12
+ throw new Error("Google Play URL must include an id query parameter");
13
+ }
14
+ const response = await fetch(url.toString(), {
15
+ headers: {
16
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
17
+ "Accept-Language": "en-US,en;q=0.9",
18
+ },
19
+ });
20
+ if (!response.ok) {
21
+ throw new Error(`Google Play request failed with status ${response.status}`);
22
+ }
23
+ const html = await response.text();
24
+ const scriptMatch = html.match(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/);
25
+ if (!scriptMatch) {
26
+ throw new Error("Unable to parse Google Play metadata");
27
+ }
28
+ const payload = JSON.parse(scriptMatch[1]);
29
+ return {
30
+ provider: "google_play",
31
+ packageName,
32
+ appName: payload.name || null,
33
+ description: payload.description || null,
34
+ category: payload.applicationCategory || null,
35
+ iconUrl: payload.image || null,
36
+ rating: payload.aggregateRating?.ratingValue || null,
37
+ ratingCount: payload.aggregateRating?.ratingCount || null,
38
+ url: url.toString(),
39
+ };
40
+ }
41
+ export async function lookupStoreMetadata(client, app, country = "us") {
42
+ if (app.includes("play.google.com")) {
43
+ return fetchGooglePlayMetadata(app);
44
+ }
45
+ const appId = extractAppStoreId(app);
46
+ if (!appId) {
47
+ throw new Error("Provide an App Store URL, Google Play URL, or App Store numeric id");
48
+ }
49
+ const lookup = await client.lookupAppStore(appId, country);
50
+ const appResult = lookup.results?.[0];
51
+ if (!appResult) {
52
+ throw new Error("App Store app not found");
53
+ }
54
+ return {
55
+ provider: "apple",
56
+ appleAppId: appId,
57
+ appName: appResult.trackName || null,
58
+ description: appResult.description || null,
59
+ iconUrl: appResult.artworkUrl512 || appResult.artworkUrl100 || null,
60
+ category: appResult.primaryGenreName || null,
61
+ screenshotUrls: appResult.screenshotUrls || [],
62
+ averageUserRating: appResult.averageUserRating || null,
63
+ userRatingCount: appResult.userRatingCount || null,
64
+ appStoreUrl: appResult.trackViewUrl || null,
65
+ };
66
+ }