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 ADDED
@@ -0,0 +1,110 @@
1
+ # AppLaunchFlow MCP
2
+
3
+ MCP server for AppLaunchFlow — create App Store & Google Play screenshots with AI.
4
+
5
+ ## Connect
6
+
7
+ AppLaunchFlow is a hosted MCP connector with OAuth 2.1 and PKCE. No API key,
8
+ local token, or scoped npm package name is required.
9
+
10
+ ### Codex
11
+
12
+ The shortest setup command configures the hosted connector and opens OAuth:
13
+
14
+ ```bash
15
+ npx -y applaunchflow connect codex
16
+ ```
17
+
18
+ Useful follow-up commands:
19
+
20
+ ```bash
21
+ npx -y applaunchflow status
22
+ npx -y applaunchflow disconnect
23
+ ```
24
+
25
+ The equivalent native Codex commands are:
26
+
27
+ ```bash
28
+ codex mcp add applaunchflow --url https://mcp.applaunchflow.com/mcp
29
+ codex mcp login applaunchflow
30
+ codex mcp get applaunchflow
31
+ codex mcp remove applaunchflow
32
+ ```
33
+
34
+ ### ChatGPT
35
+
36
+ ```bash
37
+ npx -y applaunchflow connect chatgpt
38
+ ```
39
+
40
+ Paste the displayed URL when creating a custom MCP connector in ChatGPT. The
41
+ same URL is also available at any time with `npx -y applaunchflow url`.
42
+
43
+ ### Other MCP clients
44
+
45
+ Use this Streamable HTTP endpoint and enable OAuth when prompted:
46
+
47
+ ```text
48
+ https://mcp.applaunchflow.com/mcp
49
+ ```
50
+
51
+ ## Hosted service
52
+
53
+ The public Streamable HTTP service uses OAuth 2.1 authorization code flow with
54
+ PKCE through the AppLaunchFlow dashboard.
55
+
56
+ ```bash
57
+ npm ci
58
+ npm run build
59
+ APPLAUNCHFLOW_BASE_URL=https://dashboard.applaunchflow.com \
60
+ APPLAUNCHFLOW_MCP_PUBLIC_URL=https://mcp.applaunchflow.com \
61
+ PORT=8787 \
62
+ npm run start:http
63
+ ```
64
+
65
+ Public endpoints:
66
+
67
+ - MCP: `https://mcp.applaunchflow.com/mcp`
68
+ - Protected resource metadata: `https://mcp.applaunchflow.com/.well-known/oauth-protected-resource`
69
+ - Health: `https://mcp.applaunchflow.com/healthz`
70
+
71
+ `APPLAUNCHFLOW_MCP_PUBLIC_URL` may be either the origin or the full `/mcp`
72
+ URL; both services normalize it to the same canonical resource URL. Set
73
+ `APPLAUNCHFLOW_MCP_PUBLIC_URL=https://mcp.applaunchflow.com/mcp` and
74
+ `NEXT_PUBLIC_APP_URL=https://dashboard.applaunchflow.com` on the dashboard.
75
+
76
+ The included `Dockerfile` produces a non-root OCI image for the hosted server.
77
+ The MCP host and dashboard must both be served through public HTTPS in
78
+ production. Do not expose the Node process directly without a TLS-terminating
79
+ platform or reverse proxy.
80
+
81
+ ## Personalized style workflow
82
+
83
+ Screenshot and social-graphics styles are prepared once for the selected app screenshots, then the chosen style is applied from cache. This lets users compare styles without paying for another AI call when they choose one.
84
+
85
+ For App Store screenshots:
86
+
87
+ 1. `list_source_screenshots`
88
+ 2. `prepare_screenshot_styles` with 3-7 ordered paths
89
+ 3. `browse_templates` with the returned `templateIds`, `generationId`, and `catalogKey`
90
+ 4. `apply_screenshot_style` with the returned `catalogKey`
91
+
92
+ For social graphics:
93
+
94
+ 1. `list_source_screenshots`
95
+ 2. `prepare_social_graphics_styles` with 3-7 ordered paths
96
+ 3. `browse_social_templates` with the returned `templateIds`, `generationId`, and `catalogKey`
97
+ 4. `apply_social_graphics_style` with the returned `catalogKey`
98
+
99
+ The screenshot result includes phone, tablet, and desktop. The social result includes all six supported formats. `generate_layouts` and `generate_graphics` remain available for legacy direct single-template calls.
100
+
101
+ ## Development
102
+
103
+ ```bash
104
+ npm ci
105
+ npm run dev
106
+ ```
107
+
108
+ Run `npm test` before publishing or deploying. See
109
+ [`docs/openai-submission.md`](docs/openai-submission.md) for the final OpenAI
110
+ submission checklist and manual test cases.
@@ -0,0 +1,5 @@
1
+ export function listPublicTemplateIds(templatePayloads) {
2
+ return Object.keys(templatePayloads ?? {})
3
+ .filter((templateId) => !templateId.startsWith("__"))
4
+ .sort();
5
+ }
@@ -0,0 +1,13 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { listPublicTemplateIds } from "./catalog.js";
4
+ test("listPublicTemplateIds returns sorted public templates", () => {
5
+ assert.deepEqual(listPublicTemplateIds({
6
+ beauty: {},
7
+ __layoutVersion: "v1",
8
+ action: {},
9
+ }), ["action", "beauty"]);
10
+ });
11
+ test("listPublicTemplateIds handles an absent catalog", () => {
12
+ assert.deepEqual(listPublicTemplateIds(undefined), []);
13
+ });
@@ -0,0 +1,20 @@
1
+ export const APPLAUNCHFLOW_MCP_NAME = "applaunchflow";
2
+ export const APPLAUNCHFLOW_MCP_URL = "https://mcp.applaunchflow.com/mcp";
3
+ export function codexAddArgs() {
4
+ return [
5
+ "mcp",
6
+ "add",
7
+ APPLAUNCHFLOW_MCP_NAME,
8
+ "--url",
9
+ APPLAUNCHFLOW_MCP_URL,
10
+ ];
11
+ }
12
+ export function codexLoginArgs() {
13
+ return ["mcp", "login", APPLAUNCHFLOW_MCP_NAME];
14
+ }
15
+ export function codexStatusArgs() {
16
+ return ["mcp", "get", APPLAUNCHFLOW_MCP_NAME];
17
+ }
18
+ export function codexDisconnectArgs() {
19
+ return ["mcp", "remove", APPLAUNCHFLOW_MCP_NAME];
20
+ }
@@ -0,0 +1,24 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { APPLAUNCHFLOW_MCP_NAME, APPLAUNCHFLOW_MCP_URL, codexAddArgs, codexDisconnectArgs, codexLoginArgs, codexStatusArgs, } from "./cli-core.js";
4
+ test("Codex convenience commands use the hosted OAuth connector", () => {
5
+ assert.deepEqual(codexAddArgs(), [
6
+ "mcp",
7
+ "add",
8
+ APPLAUNCHFLOW_MCP_NAME,
9
+ "--url",
10
+ APPLAUNCHFLOW_MCP_URL,
11
+ ]);
12
+ assert.deepEqual(codexLoginArgs(), ["mcp", "login", "applaunchflow"]);
13
+ assert.deepEqual(codexStatusArgs(), ["mcp", "get", "applaunchflow"]);
14
+ assert.deepEqual(codexDisconnectArgs(), [
15
+ "mcp",
16
+ "remove",
17
+ "applaunchflow",
18
+ ]);
19
+ });
20
+ test("the public connector URL is a secure MCP endpoint", () => {
21
+ const url = new URL(APPLAUNCHFLOW_MCP_URL);
22
+ assert.equal(url.protocol, "https:");
23
+ assert.equal(url.pathname, "/mcp");
24
+ });
package/build/cli.js ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { APPLAUNCHFLOW_MCP_URL, codexAddArgs, codexDisconnectArgs, codexLoginArgs, codexStatusArgs, } from "./cli-core.js";
4
+ function printHelp() {
5
+ console.log(`AppLaunchFlow MCP
6
+
7
+ Usage:
8
+ applaunchflow connect codex
9
+ applaunchflow connect chatgpt
10
+ applaunchflow status
11
+ applaunchflow disconnect
12
+ applaunchflow url`);
13
+ }
14
+ function runCodex(args) {
15
+ const result = spawnSync("codex", args, { stdio: "inherit" });
16
+ if (result.error) {
17
+ throw new Error(`Could not run Codex: ${result.error.message}`);
18
+ }
19
+ if (result.status !== 0) {
20
+ throw new Error(`Codex exited with status ${result.status ?? "unknown"}`);
21
+ }
22
+ }
23
+ function codexIsConfigured() {
24
+ const result = spawnSync("codex", codexStatusArgs(), { stdio: "ignore" });
25
+ if (result.error) {
26
+ throw new Error(`Could not run Codex: ${result.error.message}`);
27
+ }
28
+ return result.status === 0;
29
+ }
30
+ function connectCodex() {
31
+ if (!codexIsConfigured()) {
32
+ runCodex(codexAddArgs());
33
+ }
34
+ runCodex(codexLoginArgs());
35
+ }
36
+ function connectChatGpt() {
37
+ console.log(`Add a custom MCP connector in ChatGPT using this URL:\n${APPLAUNCHFLOW_MCP_URL}`);
38
+ }
39
+ function main() {
40
+ const [command, target] = process.argv.slice(2);
41
+ if (command === "connect" && target === "codex") {
42
+ connectCodex();
43
+ return;
44
+ }
45
+ if (command === "connect" && target === "chatgpt") {
46
+ connectChatGpt();
47
+ return;
48
+ }
49
+ if (command === "status") {
50
+ runCodex(codexStatusArgs());
51
+ return;
52
+ }
53
+ if (command === "disconnect") {
54
+ runCodex(codexDisconnectArgs());
55
+ return;
56
+ }
57
+ if (command === "url") {
58
+ console.log(APPLAUNCHFLOW_MCP_URL);
59
+ return;
60
+ }
61
+ printHelp();
62
+ if (command)
63
+ process.exitCode = 1;
64
+ }
65
+ try {
66
+ main();
67
+ }
68
+ catch (error) {
69
+ console.error(error instanceof Error ? error.message : String(error));
70
+ process.exitCode = 1;
71
+ }
@@ -0,0 +1,341 @@
1
+ export class AppLaunchFlowApiError extends Error {
2
+ status;
3
+ body;
4
+ constructor(message, status, body) {
5
+ super(message);
6
+ this.name = "AppLaunchFlowApiError";
7
+ this.status = status;
8
+ this.body = body;
9
+ }
10
+ }
11
+ function buildSearchParams(query) {
12
+ if (!query) {
13
+ return "";
14
+ }
15
+ const params = new URLSearchParams();
16
+ for (const [key, value] of Object.entries(query)) {
17
+ if (value === undefined || value === null) {
18
+ continue;
19
+ }
20
+ if (Array.isArray(value)) {
21
+ value.forEach((item) => params.append(key, String(item)));
22
+ continue;
23
+ }
24
+ params.set(key, String(value));
25
+ }
26
+ const serialized = params.toString();
27
+ return serialized ? `?${serialized}` : "";
28
+ }
29
+ export class AppLaunchFlowClient {
30
+ credentials;
31
+ constructor(credentials) {
32
+ this.credentials = credentials;
33
+ }
34
+ buildHeaders(extraHeaders) {
35
+ const headers = new Headers(extraHeaders);
36
+ headers.set("Authorization", `Bearer ${this.credentials.token}`);
37
+ return headers;
38
+ }
39
+ async requestJson(path, options = {}) {
40
+ const url = `${this.credentials.baseUrl}${path}${buildSearchParams(options.query)}`;
41
+ const headers = this.buildHeaders(options.headers);
42
+ if (options.body !== undefined && !headers.has("Content-Type")) {
43
+ headers.set("Content-Type", "application/json");
44
+ }
45
+ const response = await fetch(url, {
46
+ method: options.method || "GET",
47
+ headers,
48
+ body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
49
+ });
50
+ const contentType = response.headers.get("content-type") || "";
51
+ const payload = contentType.includes("application/json")
52
+ ? await response.json()
53
+ : await response.text();
54
+ if (!response.ok) {
55
+ const message = typeof payload === "string"
56
+ ? payload
57
+ : payload?.error ||
58
+ payload?.message ||
59
+ `Request failed with status ${response.status}`;
60
+ throw new AppLaunchFlowApiError(message, response.status, payload);
61
+ }
62
+ return payload;
63
+ }
64
+ async createSignedUpload(args) {
65
+ return this.requestJson("/api/assets/upload/signed-url", {
66
+ method: "POST",
67
+ body: args,
68
+ });
69
+ }
70
+ async uploadBinary(uploadUrl, buffer, contentType) {
71
+ const response = await fetch(uploadUrl, {
72
+ method: "PUT",
73
+ headers: {
74
+ "Content-Type": contentType,
75
+ },
76
+ body: new Uint8Array(buffer),
77
+ });
78
+ if (!response.ok) {
79
+ throw new Error(`Upload failed with status ${response.status}`);
80
+ }
81
+ }
82
+ listProjects() {
83
+ return this.requestJson("/api/projects");
84
+ }
85
+ createProject(body) {
86
+ return this.requestJson("/api/projects", {
87
+ method: "POST",
88
+ body,
89
+ });
90
+ }
91
+ deleteProject(projectId) {
92
+ return this.requestJson(`/api/projects/${projectId}`, {
93
+ method: "DELETE",
94
+ });
95
+ }
96
+ getProject(projectId) {
97
+ return this.requestJson(`/api/app/${projectId}`);
98
+ }
99
+ listScreenshots(query) {
100
+ return this.requestJson("/api/screenshots/list", { query });
101
+ }
102
+ listProjectScreenshots(projectId) {
103
+ return this.requestJson(`/api/projects/${projectId}/screenshots`);
104
+ }
105
+ generateLayouts(body) {
106
+ return this.requestJson("/api/screenshots/generate", {
107
+ method: "POST",
108
+ body,
109
+ });
110
+ }
111
+ applyScreenshotTemplate(body) {
112
+ return this.requestJson("/api/screenshots/apply-template", {
113
+ method: "POST",
114
+ body,
115
+ });
116
+ }
117
+ regenerateLayouts(body) {
118
+ return this.requestJson("/api/screenshots/regenerate", {
119
+ method: "POST",
120
+ body,
121
+ });
122
+ }
123
+ getLayout(query) {
124
+ return this.requestJson("/api/translations", {
125
+ query: {
126
+ generationId: query.generationId,
127
+ language: query.language,
128
+ variantId: query.variantId,
129
+ sign: query.sign ? 1 : undefined,
130
+ },
131
+ });
132
+ }
133
+ saveLayout(body) {
134
+ return this.requestJson("/api/translations", {
135
+ method: "POST",
136
+ body,
137
+ });
138
+ }
139
+ transformLayout(body) {
140
+ return this.requestJson("/api/mcp/transform", {
141
+ method: "POST",
142
+ body,
143
+ });
144
+ }
145
+ listTemplates() {
146
+ return this.requestJson("/api/mcp/templates");
147
+ }
148
+ getTemplate(templateId) {
149
+ return this.requestJson(`/api/mcp/templates/${templateId}`);
150
+ }
151
+ listSocialTemplates() {
152
+ return this.requestJson("/api/mcp/social-templates");
153
+ }
154
+ getSocialTemplate(templateId) {
155
+ return this.requestJson(`/api/mcp/social-templates/${templateId}`);
156
+ }
157
+ getPromoVideo(generationId, variantId) {
158
+ return this.requestJson("/api/promovideo/load", {
159
+ query: { generationId, variantId },
160
+ });
161
+ }
162
+ generatePromoVideo(body) {
163
+ return this.requestJson("/api/promovideo/generate", {
164
+ method: "POST",
165
+ body,
166
+ });
167
+ }
168
+ updatePromoVideo(body) {
169
+ return this.requestJson("/api/promovideo/update", {
170
+ method: "POST",
171
+ body,
172
+ });
173
+ }
174
+ clearPromoVideo(body) {
175
+ return this.requestJson("/api/promovideo/clear", {
176
+ method: "POST",
177
+ body,
178
+ });
179
+ }
180
+ createMockupAnimation(body) {
181
+ return this.requestJson("/api/mockups/setup", {
182
+ method: "POST",
183
+ body,
184
+ });
185
+ }
186
+ getMockupAnimation(generationId, variantId) {
187
+ return this.requestJson("/api/mockups", {
188
+ query: { generationId, variantId },
189
+ });
190
+ }
191
+ updateMockupAnimation(body) {
192
+ return this.requestJson("/api/mockups", {
193
+ method: "POST",
194
+ body,
195
+ });
196
+ }
197
+ listMockupMedia(projectId) {
198
+ return this.requestJson("/api/mockup-media/list", {
199
+ query: { projectId },
200
+ });
201
+ }
202
+ getMockupThemeColors(generationId) {
203
+ return this.requestJson("/api/mockups/theme-colors", {
204
+ query: { generationId },
205
+ });
206
+ }
207
+ lookupAppStore(id, country = "us") {
208
+ return this.requestJson("/api/itunes/lookup", {
209
+ query: { id, country },
210
+ headers: {},
211
+ });
212
+ }
213
+ translateLayouts(body) {
214
+ return this.requestJson("/api/screenshots/translate", {
215
+ method: "POST",
216
+ body,
217
+ });
218
+ }
219
+ listVariants(generationId, contentType) {
220
+ return this.requestJson("/api/variants", {
221
+ query: { generationId, contentType },
222
+ });
223
+ }
224
+ createVariant(body) {
225
+ return this.requestJson("/api/variants", {
226
+ method: "POST",
227
+ body,
228
+ });
229
+ }
230
+ switchVariant(variantId) {
231
+ return this.requestJson(`/api/variants/${variantId}`, {
232
+ method: "PATCH",
233
+ body: { isActive: true },
234
+ });
235
+ }
236
+ duplicateVariant(variantId) {
237
+ return this.requestJson(`/api/variants/${variantId}/duplicate`, {
238
+ method: "POST",
239
+ });
240
+ }
241
+ deleteVariant(variantId) {
242
+ return this.requestJson(`/api/variants/${variantId}`, {
243
+ method: "DELETE",
244
+ });
245
+ }
246
+ getGraphics(projectId, variantId) {
247
+ return this.requestJson("/api/graphics", {
248
+ query: { projectId, variantId },
249
+ });
250
+ }
251
+ getGraphicsFormat(projectId, format, variantId) {
252
+ return this.requestJson("/api/graphics", {
253
+ query: { projectId, variantId, format },
254
+ });
255
+ }
256
+ generateGraphics(body) {
257
+ return this.requestJson("/api/graphics/generate", {
258
+ method: "POST",
259
+ body,
260
+ });
261
+ }
262
+ applyGraphicsTemplate(body) {
263
+ return this.requestJson("/api/graphics/apply-template", {
264
+ method: "POST",
265
+ body,
266
+ });
267
+ }
268
+ saveGraphics(body) {
269
+ return this.requestJson("/api/graphics", {
270
+ method: "POST",
271
+ body,
272
+ });
273
+ }
274
+ saveGraphicsFormat(body) {
275
+ return this.requestJson("/api/graphics/format", {
276
+ method: "POST",
277
+ body,
278
+ });
279
+ }
280
+ getAsoCopy(generationId, variantId) {
281
+ return this.requestJson("/api/aso/copy", {
282
+ query: { generationId, variantId },
283
+ });
284
+ }
285
+ generateAsoCopy(body) {
286
+ return this.requestJson("/api/aso/copy", {
287
+ method: "POST",
288
+ body,
289
+ });
290
+ }
291
+ updateAsoCopy(body) {
292
+ return this.requestJson("/api/aso/copy", {
293
+ method: "PUT",
294
+ body,
295
+ });
296
+ }
297
+ translateAsoCopy(body) {
298
+ return this.requestJson("/api/aso/translate", {
299
+ method: "POST",
300
+ body,
301
+ });
302
+ }
303
+ suggestCompetitors(body) {
304
+ return this.requestJson("/api/aso/competitors/suggest", {
305
+ method: "POST",
306
+ body,
307
+ });
308
+ }
309
+ listSharedIllustrations(query) {
310
+ return this.requestJson("/api/illustrations/shared", { query });
311
+ }
312
+ listProjectIllustrations(projectId) {
313
+ return this.requestJson("/api/illustrations/list", {
314
+ query: { projectId },
315
+ });
316
+ }
317
+ listKeywords(query) {
318
+ return this.requestJson("/api/keywords", { query });
319
+ }
320
+ listKeywordCompetitors(query) {
321
+ return this.requestJson("/api/keywords/competitors", { query });
322
+ }
323
+ getKeywordHistory(query) {
324
+ return this.requestJson("/api/keywords/history", { query });
325
+ }
326
+ addKeywords(body) {
327
+ return this.requestJson("/api/keywords", { method: "POST", body });
328
+ }
329
+ /** List files in a project's asset subfolder (panorama, illustrations, backgrounds, etc.) */
330
+ async listProjectAssetFolder(projectId, folder) {
331
+ // Use the app endpoint for known folders, or fall back to storage listing
332
+ if (folder === "illustrations") {
333
+ return this.listProjectIllustrations(projectId);
334
+ }
335
+ // For panorama/backgrounds/logo — use the screenshots list with a folder hint
336
+ // These are stored under {projectId}/{folder}/ in the screenshots bucket
337
+ return this.requestJson(`/api/app/${projectId}/assets`, {
338
+ query: { folder },
339
+ });
340
+ }
341
+ }