@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
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# applaunchflow MCP
|
|
2
|
+
|
|
3
|
+
Local stdio MCP server for applaunchflow.
|
|
4
|
+
|
|
5
|
+
## Auth
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm --dir applaunchflow-mcp dev auth login --base-url http://localhost:3000
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Credentials are stored in `~/.applaunchflow/credentials.json`.
|
|
12
|
+
|
|
13
|
+
Environment overrides:
|
|
14
|
+
|
|
15
|
+
- `applaunchflow_BASE_URL`
|
|
16
|
+
- `applaunchflow_MCP_TOKEN`
|
|
17
|
+
- `applaunchflow_MCP_COOKIE_NAME`
|
|
18
|
+
|
|
19
|
+
## Run
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm --dir applaunchflow-mcp build
|
|
23
|
+
node applaunchflow-mcp/build/index.js
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The server uses stdio transport and exposes tools/resources for project, screenshot, layout, template, variant, graphics, and ASO workflows.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
const CONFIG_DIR = path.join(os.homedir(), ".applaunchflow");
|
|
5
|
+
const CREDENTIALS_PATH = path.join(CONFIG_DIR, "credentials.json");
|
|
6
|
+
function defaultCookieName(baseUrl) {
|
|
7
|
+
return baseUrl.startsWith("https://")
|
|
8
|
+
? "__Secure-authjs.session-token"
|
|
9
|
+
: "authjs.session-token";
|
|
10
|
+
}
|
|
11
|
+
export function normalizeBaseUrl(baseUrl) {
|
|
12
|
+
return baseUrl.replace(/\/+$/, "");
|
|
13
|
+
}
|
|
14
|
+
export function getCredentialsPath() {
|
|
15
|
+
return CREDENTIALS_PATH;
|
|
16
|
+
}
|
|
17
|
+
export async function loadStoredCredentials() {
|
|
18
|
+
try {
|
|
19
|
+
const raw = await fs.readFile(CREDENTIALS_PATH, "utf8");
|
|
20
|
+
const parsed = JSON.parse(raw);
|
|
21
|
+
return {
|
|
22
|
+
...parsed,
|
|
23
|
+
baseUrl: normalizeBaseUrl(parsed.baseUrl),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function saveStoredCredentials(credentials) {
|
|
31
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
32
|
+
await fs.writeFile(CREDENTIALS_PATH, `${JSON.stringify(credentials, null, 2)}\n`, "utf8");
|
|
33
|
+
}
|
|
34
|
+
export async function clearStoredCredentials() {
|
|
35
|
+
await fs.rm(CREDENTIALS_PATH, { force: true });
|
|
36
|
+
}
|
|
37
|
+
export async function resolveCredentials() {
|
|
38
|
+
const baseUrl = normalizeBaseUrl(process.env.APPLAUNCHFLOW_BASE_URL || "https://dashboard.applaunchflow.com");
|
|
39
|
+
const envToken = process.env.APPLAUNCHFLOW_MCP_TOKEN;
|
|
40
|
+
if (envToken) {
|
|
41
|
+
return {
|
|
42
|
+
baseUrl,
|
|
43
|
+
token: envToken,
|
|
44
|
+
cookieName: process.env.APPLAUNCHFLOW_MCP_COOKIE_NAME || defaultCookieName(baseUrl),
|
|
45
|
+
createdAt: new Date().toISOString(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const stored = await loadStoredCredentials();
|
|
49
|
+
if (!stored) {
|
|
50
|
+
throw new Error(`No AppLaunchFlow MCP credentials found. Run \`applaunchflow-mcp auth login --base-url ${baseUrl}\`.`);
|
|
51
|
+
}
|
|
52
|
+
return stored;
|
|
53
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createServer } from "http";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { normalizeBaseUrl, saveStoredCredentials, } from "./credentials.js";
|
|
4
|
+
function openBrowser(url) {
|
|
5
|
+
const platform = process.platform;
|
|
6
|
+
const command = platform === "darwin"
|
|
7
|
+
? "open"
|
|
8
|
+
: platform === "win32"
|
|
9
|
+
? "cmd"
|
|
10
|
+
: "xdg-open";
|
|
11
|
+
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
12
|
+
try {
|
|
13
|
+
const child = spawn(command, args, {
|
|
14
|
+
detached: true,
|
|
15
|
+
stdio: "ignore",
|
|
16
|
+
});
|
|
17
|
+
child.unref();
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
console.error(`Failed to open the browser automatically. Open this URL manually: ${url}`);
|
|
21
|
+
if (error instanceof Error) {
|
|
22
|
+
console.error(error.message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function createCallbackListener() {
|
|
27
|
+
const callbackPath = "/callback";
|
|
28
|
+
let resolveAddress = null;
|
|
29
|
+
const addressPromise = new Promise((resolve) => {
|
|
30
|
+
resolveAddress = resolve;
|
|
31
|
+
});
|
|
32
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
33
|
+
const server = createServer((request, response) => {
|
|
34
|
+
const requestUrl = new URL(request.url || "/", "http://127.0.0.1:0");
|
|
35
|
+
if (requestUrl.pathname !== callbackPath) {
|
|
36
|
+
response.statusCode = 404;
|
|
37
|
+
response.end("Not found");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const token = requestUrl.searchParams.get("token");
|
|
41
|
+
const cookieName = requestUrl.searchParams.get("cookieName");
|
|
42
|
+
const baseUrl = requestUrl.searchParams.get("baseUrl");
|
|
43
|
+
const expiresAt = requestUrl.searchParams.get("expiresAt") || undefined;
|
|
44
|
+
if (!token || !cookieName || !baseUrl) {
|
|
45
|
+
response.statusCode = 400;
|
|
46
|
+
response.end("Missing token payload");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
50
|
+
response.end("<!doctype html><html><body><h1>AppLaunchFlow MCP connected</h1><p>You can close this tab now.</p></body></html>");
|
|
51
|
+
clearTimeout(timeout);
|
|
52
|
+
server.close();
|
|
53
|
+
resolve({
|
|
54
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
55
|
+
token,
|
|
56
|
+
cookieName,
|
|
57
|
+
expiresAt,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
server.listen(0, "127.0.0.1", () => {
|
|
61
|
+
const address = server.address();
|
|
62
|
+
resolveAddress?.(address);
|
|
63
|
+
});
|
|
64
|
+
const timeout = setTimeout(() => {
|
|
65
|
+
server.close();
|
|
66
|
+
reject(new Error("Timed out waiting for browser authentication"));
|
|
67
|
+
}, 5 * 60 * 1000);
|
|
68
|
+
});
|
|
69
|
+
const address = await addressPromise;
|
|
70
|
+
return {
|
|
71
|
+
callbackUrl: `http://127.0.0.1:${address.port}${callbackPath}`,
|
|
72
|
+
responsePromise,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export async function login(baseUrl) {
|
|
76
|
+
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
77
|
+
const { callbackUrl, responsePromise } = await createCallbackListener();
|
|
78
|
+
const authorizeUrl = new URL("/auth/mcp", normalizedBaseUrl);
|
|
79
|
+
authorizeUrl.searchParams.set("callback", callbackUrl);
|
|
80
|
+
authorizeUrl.searchParams.set("name", "Local MCP");
|
|
81
|
+
console.error(`Opening AppLaunchFlow authorization in your browser...`);
|
|
82
|
+
console.error(`If nothing opens, visit: ${authorizeUrl.toString()}`);
|
|
83
|
+
openBrowser(authorizeUrl.toString());
|
|
84
|
+
const received = await responsePromise;
|
|
85
|
+
const credentials = {
|
|
86
|
+
...received,
|
|
87
|
+
createdAt: new Date().toISOString(),
|
|
88
|
+
};
|
|
89
|
+
await saveStoredCredentials(credentials);
|
|
90
|
+
console.error(`Login successful! Connected to ${credentials.baseUrl}`);
|
|
91
|
+
return credentials;
|
|
92
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
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("Cookie", `${this.credentials.cookieName}=${this.credentials.token}`);
|
|
37
|
+
headers.set("Authorization", `Bearer ${this.credentials.token}`);
|
|
38
|
+
return headers;
|
|
39
|
+
}
|
|
40
|
+
async requestJson(path, options = {}) {
|
|
41
|
+
const url = `${this.credentials.baseUrl}${path}${buildSearchParams(options.query)}`;
|
|
42
|
+
const headers = this.buildHeaders(options.headers);
|
|
43
|
+
if (options.body !== undefined && !headers.has("Content-Type")) {
|
|
44
|
+
headers.set("Content-Type", "application/json");
|
|
45
|
+
}
|
|
46
|
+
const response = await fetch(url, {
|
|
47
|
+
method: options.method || "GET",
|
|
48
|
+
headers,
|
|
49
|
+
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
50
|
+
});
|
|
51
|
+
const contentType = response.headers.get("content-type") || "";
|
|
52
|
+
const payload = contentType.includes("application/json")
|
|
53
|
+
? await response.json()
|
|
54
|
+
: await response.text();
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const message = typeof payload === "string"
|
|
57
|
+
? payload
|
|
58
|
+
: payload?.error ||
|
|
59
|
+
payload?.message ||
|
|
60
|
+
`Request failed with status ${response.status}`;
|
|
61
|
+
throw new AppLaunchFlowApiError(message, response.status, payload);
|
|
62
|
+
}
|
|
63
|
+
return payload;
|
|
64
|
+
}
|
|
65
|
+
async createSignedUpload(args) {
|
|
66
|
+
return this.requestJson("/api/assets/upload/signed-url", {
|
|
67
|
+
method: "POST",
|
|
68
|
+
body: args,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async uploadBinary(uploadUrl, buffer, contentType) {
|
|
72
|
+
const response = await fetch(uploadUrl, {
|
|
73
|
+
method: "PUT",
|
|
74
|
+
headers: {
|
|
75
|
+
"Content-Type": contentType,
|
|
76
|
+
},
|
|
77
|
+
body: new Uint8Array(buffer),
|
|
78
|
+
});
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new Error(`Upload failed with status ${response.status}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
listProjects() {
|
|
84
|
+
return this.requestJson("/api/projects");
|
|
85
|
+
}
|
|
86
|
+
createProject(body) {
|
|
87
|
+
return this.requestJson("/api/projects", {
|
|
88
|
+
method: "POST",
|
|
89
|
+
body,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
deleteProject(projectId) {
|
|
93
|
+
return this.requestJson(`/api/projects/${projectId}`, {
|
|
94
|
+
method: "DELETE",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
getProject(projectId) {
|
|
98
|
+
return this.requestJson(`/api/app/${projectId}`);
|
|
99
|
+
}
|
|
100
|
+
listScreenshots(query) {
|
|
101
|
+
return this.requestJson("/api/screenshots/list", { query });
|
|
102
|
+
}
|
|
103
|
+
generateLayouts(body) {
|
|
104
|
+
return this.requestJson("/api/screenshots/generate", {
|
|
105
|
+
method: "POST",
|
|
106
|
+
body,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
regenerateLayouts(body) {
|
|
110
|
+
return this.requestJson("/api/screenshots/regenerate", {
|
|
111
|
+
method: "POST",
|
|
112
|
+
body,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
getLayout(query) {
|
|
116
|
+
return this.requestJson("/api/translations", {
|
|
117
|
+
query: {
|
|
118
|
+
generationId: query.generationId,
|
|
119
|
+
language: query.language,
|
|
120
|
+
variantId: query.variantId,
|
|
121
|
+
sign: query.sign ? 1 : undefined,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
saveLayout(body) {
|
|
126
|
+
return this.requestJson("/api/translations", {
|
|
127
|
+
method: "POST",
|
|
128
|
+
body,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
transformLayout(body) {
|
|
132
|
+
return this.requestJson("/api/mcp/transform", {
|
|
133
|
+
method: "POST",
|
|
134
|
+
body,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
listTemplates() {
|
|
138
|
+
return this.requestJson("/api/mcp/templates");
|
|
139
|
+
}
|
|
140
|
+
getTemplate(templateId) {
|
|
141
|
+
return this.requestJson(`/api/mcp/templates/${templateId}`);
|
|
142
|
+
}
|
|
143
|
+
lookupAppStore(id, country = "us") {
|
|
144
|
+
return this.requestJson("/api/itunes/lookup", {
|
|
145
|
+
query: { id, country },
|
|
146
|
+
headers: {},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
translateLayouts(body) {
|
|
150
|
+
return this.requestJson("/api/screenshots/translate", {
|
|
151
|
+
method: "POST",
|
|
152
|
+
body,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
listVariants(generationId, contentType) {
|
|
156
|
+
return this.requestJson("/api/variants", {
|
|
157
|
+
query: { generationId, contentType },
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
createVariant(body) {
|
|
161
|
+
return this.requestJson("/api/variants", {
|
|
162
|
+
method: "POST",
|
|
163
|
+
body,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
switchVariant(variantId) {
|
|
167
|
+
return this.requestJson(`/api/variants/${variantId}`, {
|
|
168
|
+
method: "PATCH",
|
|
169
|
+
body: { isActive: true },
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
duplicateVariant(variantId) {
|
|
173
|
+
return this.requestJson(`/api/variants/${variantId}/duplicate`, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
deleteVariant(variantId) {
|
|
178
|
+
return this.requestJson(`/api/variants/${variantId}`, {
|
|
179
|
+
method: "DELETE",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
getGraphics(projectId, variantId) {
|
|
183
|
+
return this.requestJson("/api/graphics", {
|
|
184
|
+
query: { projectId, variantId },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
generateGraphics(body) {
|
|
188
|
+
return this.requestJson("/api/graphics/generate", {
|
|
189
|
+
method: "POST",
|
|
190
|
+
body,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
saveGraphics(body) {
|
|
194
|
+
return this.requestJson("/api/graphics", {
|
|
195
|
+
method: "POST",
|
|
196
|
+
body,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
getAsoCopy(generationId, variantId) {
|
|
200
|
+
return this.requestJson("/api/aso/copy", {
|
|
201
|
+
query: { generationId, variantId },
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
generateAsoCopy(body) {
|
|
205
|
+
return this.requestJson("/api/aso/copy", {
|
|
206
|
+
method: "POST",
|
|
207
|
+
body,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
updateAsoCopy(body) {
|
|
211
|
+
return this.requestJson("/api/aso/copy", {
|
|
212
|
+
method: "PUT",
|
|
213
|
+
body,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
translateAsoCopy(body) {
|
|
217
|
+
return this.requestJson("/api/aso/translate", {
|
|
218
|
+
method: "POST",
|
|
219
|
+
body,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
suggestCompetitors(body) {
|
|
223
|
+
return this.requestJson("/api/aso/competitors/suggest", {
|
|
224
|
+
method: "POST",
|
|
225
|
+
body,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
listSharedIllustrations(query) {
|
|
229
|
+
return this.requestJson("/api/illustrations/shared", { query });
|
|
230
|
+
}
|
|
231
|
+
listProjectIllustrations(projectId) {
|
|
232
|
+
return this.requestJson("/api/illustrations/list", {
|
|
233
|
+
query: { projectId },
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/** List files in a project's asset subfolder (panorama, illustrations, backgrounds, etc.) */
|
|
237
|
+
async listProjectAssetFolder(projectId, folder) {
|
|
238
|
+
// Use the app endpoint for known folders, or fall back to storage listing
|
|
239
|
+
if (folder === "illustrations") {
|
|
240
|
+
return this.listProjectIllustrations(projectId);
|
|
241
|
+
}
|
|
242
|
+
// For panorama/backgrounds/logo — use the screenshots list with a folder hint
|
|
243
|
+
// These are stored under {projectId}/{folder}/ in the screenshots bucket
|
|
244
|
+
return this.requestJson(`/api/app/${projectId}/assets`, {
|
|
245
|
+
query: { folder },
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { clearStoredCredentials, getCredentialsPath, loadStoredCredentials, resolveCredentials, } from "./auth/credentials.js";
|
|
5
|
+
import { login } from "./auth/login.js";
|
|
6
|
+
import { AppLaunchFlowClient } from "./client/api.js";
|
|
7
|
+
import { registerPrompts } from "./prompts/register.js";
|
|
8
|
+
import { registerResources } from "./resources/register.js";
|
|
9
|
+
import { TemplateSelectionCoordinator } from "./template-selection.js";
|
|
10
|
+
import { registerAssetTools } from "./tools/assets.js";
|
|
11
|
+
import { registerLayoutTools } from "./tools/layouts.js";
|
|
12
|
+
import { registerProjectTools } from "./tools/projects.js";
|
|
13
|
+
import { registerScreenshotTools } from "./tools/screenshots.js";
|
|
14
|
+
import { registerTemplateTools } from "./tools/templates.js";
|
|
15
|
+
import { registerLocalizationTools } from "./tools/localization.js";
|
|
16
|
+
import { registerVariantTools } from "./tools/variants.js";
|
|
17
|
+
const SERVER_INSTRUCTIONS = `
|
|
18
|
+
AppLaunchFlow MCP is screenshot-focused at this stage.
|
|
19
|
+
Only use it for screenshot project setup, screenshot uploads, screenshot template generation, variants, and direct screenshot layout editing.
|
|
20
|
+
Do not treat this MCP as an ASO, localization, graphics, or promo-video assistant.
|
|
21
|
+
|
|
22
|
+
Use AppLaunchFlow MCP as an execution tool, not a questionnaire.
|
|
23
|
+
|
|
24
|
+
Default behavior:
|
|
25
|
+
- For concrete requests on an existing project, act directly instead of asking follow-up questions.
|
|
26
|
+
- Only ask when a missing detail is required to avoid a materially wrong result, or when the request is genuinely ambiguous.
|
|
27
|
+
- Do not force menu-style "what would you like to do next?" steps after each tool call.
|
|
28
|
+
- The user can edit layouts in natural language. Translate those requests into direct MCP actions.
|
|
29
|
+
- If a tool returns a user-facing URL, repeat the exact URL in the assistant reply. Do not say "link above" or assume tool output is visible to the user.
|
|
30
|
+
|
|
31
|
+
Preferred workflows:
|
|
32
|
+
- New screenshot direction or template on an existing project: call generate_layouts WITHOUT a variantId — a new variant is always created automatically. Never overwrite existing variants.
|
|
33
|
+
- For small, precise edits to existing known nodes, transform_layout can be used directly.
|
|
34
|
+
- For any composition-sensitive edit, inspect the current layout first with get_layout. This includes adding screens, reusing screenshots, changing screenshot placement, moving text, changing spacing, or anything that should match the existing visual system.
|
|
35
|
+
- Use transform_layout as the primary tool for editing current screens once you have enough layout context.
|
|
36
|
+
- Default to layouts: ["mobile"] for transform_layout. Only include tablet/desktop if the user explicitly asks.
|
|
37
|
+
- When editing a single screen, scope the transform to just that screen using the screens parameter (e.g. screens: [2]). Do not transform the entire layout when only one screen needs changes.
|
|
38
|
+
- For adding new screens to an existing layout, prefer direct layout editing when the user wants to keep the current design. Only generate a fresh variant when the user asks for a new AI-generated layout/template.
|
|
39
|
+
- When adding or editing elements, ensure text and screenshots do not overlap. Verify that positions place elements in distinct, non-conflicting areas of the canvas.
|
|
40
|
+
- After composition-sensitive edits, inspect the returned translation or re-fetch the layout before reporting success. If elements overlap or are poorly positioned, fix them before telling the user the edit is done.
|
|
41
|
+
- ALWAYS use browse_templates when a template choice is needed. Never offer templates via text bullet points or AskUserQuestion. The gallery opens in the browser and returns the user's selection automatically.
|
|
42
|
+
- Use actual visual previews from the template tools/resources. Do not rely on adjective-heavy descriptions alone.
|
|
43
|
+
- Keep the full template catalog available when the user is browsing templates. Only narrow to a small shortlist if the user explicitly asks for recommendations or fewer options.
|
|
44
|
+
|
|
45
|
+
Translation and localization:
|
|
46
|
+
- When the user asks to translate, localize, or create a version in another language, ALWAYS use translate_layouts. Do NOT manually edit text nodes via transform_layout for translation.
|
|
47
|
+
- translate_layouts uses AI to translate all text while preserving layout, positioning, and styling.
|
|
48
|
+
- To apply the same transform across all screens, use transform_layout with screens: "all" in the target.
|
|
49
|
+
|
|
50
|
+
Project creation should be fast and simple:
|
|
51
|
+
1. Ask for the app name and platform (iOS or Android) using AskUserQuestion. Default platform to iOS.
|
|
52
|
+
2. Autofill category and description from context (e.g. "Skyscanner" → category "Travel"). Do not ask the user for these.
|
|
53
|
+
3. Call create_project immediately. Do not ask for confirmation or optional fields unless the user volunteers them.
|
|
54
|
+
4. After creation, recommend uploading screenshots as the next step.
|
|
55
|
+
`.trim();
|
|
56
|
+
async function runAuthCommand(args) {
|
|
57
|
+
const [subcommand, ...rest] = args;
|
|
58
|
+
if (subcommand === "login") {
|
|
59
|
+
const baseUrlFlagIndex = rest.findIndex((value) => value === "--base-url");
|
|
60
|
+
const baseUrl = baseUrlFlagIndex >= 0
|
|
61
|
+
? rest[baseUrlFlagIndex + 1]
|
|
62
|
+
: process.env.APPLAUNCHFLOW_BASE_URL || "https://dashboard.applaunchflow.com";
|
|
63
|
+
await login(baseUrl);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (subcommand === "logout") {
|
|
67
|
+
await clearStoredCredentials();
|
|
68
|
+
console.error(`Removed credentials at ${getCredentialsPath()}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (subcommand === "status") {
|
|
72
|
+
const credentials = await loadStoredCredentials();
|
|
73
|
+
if (!credentials) {
|
|
74
|
+
console.error("No stored AppLaunchFlow MCP credentials");
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
console.error(`Base URL: ${credentials.baseUrl}`);
|
|
79
|
+
console.error(`Cookie name: ${credentials.cookieName}`);
|
|
80
|
+
if (credentials.expiresAt) {
|
|
81
|
+
console.error(`Expires at: ${credentials.expiresAt}`);
|
|
82
|
+
}
|
|
83
|
+
console.error(`Credentials file: ${getCredentialsPath()}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
console.error("Usage: applaunchflow-mcp auth <login|logout|status> [--base-url <url>]");
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
}
|
|
89
|
+
async function startServer() {
|
|
90
|
+
const credentials = await resolveCredentials();
|
|
91
|
+
const client = new AppLaunchFlowClient(credentials);
|
|
92
|
+
const templateSelectionCoordinator = new TemplateSelectionCoordinator();
|
|
93
|
+
const server = new McpServer({
|
|
94
|
+
name: "applaunchflow-mcp",
|
|
95
|
+
version: "0.1.0",
|
|
96
|
+
}, {
|
|
97
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
98
|
+
});
|
|
99
|
+
registerPrompts(server);
|
|
100
|
+
registerResources(server, client);
|
|
101
|
+
registerProjectTools(server, client);
|
|
102
|
+
registerAssetTools(server, client);
|
|
103
|
+
registerScreenshotTools(server, client);
|
|
104
|
+
registerLayoutTools(server, client);
|
|
105
|
+
registerTemplateTools(server, client, templateSelectionCoordinator);
|
|
106
|
+
registerLocalizationTools(server, client);
|
|
107
|
+
registerVariantTools(server, client);
|
|
108
|
+
const transport = new StdioServerTransport();
|
|
109
|
+
await server.connect(transport);
|
|
110
|
+
}
|
|
111
|
+
async function main() {
|
|
112
|
+
const [, , command, ...args] = process.argv;
|
|
113
|
+
if (command === "auth") {
|
|
114
|
+
await runAuthCommand(args);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
await startServer();
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
void main();
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerPrompts(server) {
|
|
3
|
+
server.registerPrompt("create-project-wizard", {
|
|
4
|
+
title: "Create Project Wizard",
|
|
5
|
+
description: "Guide the user through quick project creation: ask app name + platform, then create immediately.",
|
|
6
|
+
argsSchema: {
|
|
7
|
+
userGoal: z
|
|
8
|
+
.string()
|
|
9
|
+
.optional()
|
|
10
|
+
.describe("Optional user request or project idea to keep in view."),
|
|
11
|
+
},
|
|
12
|
+
}, async ({ userGoal }) => ({
|
|
13
|
+
description: "Use this prompt when the user wants to create a project through AppLaunchFlow MCP.",
|
|
14
|
+
messages: [
|
|
15
|
+
{
|
|
16
|
+
role: "user",
|
|
17
|
+
content: {
|
|
18
|
+
type: "text",
|
|
19
|
+
text: [
|
|
20
|
+
"Create an AppLaunchFlow project quickly.",
|
|
21
|
+
"Ask for the app name and platform (iOS or Android) using AskUserQuestion.",
|
|
22
|
+
"Autofill category and description from context — do not ask the user for these.",
|
|
23
|
+
"Call create_project immediately after getting the name and platform.",
|
|
24
|
+
"After creation, recommend uploading screenshots as the next step.",
|
|
25
|
+
userGoal ? `User goal: ${userGoal}` : null,
|
|
26
|
+
]
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.join("\n"),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
}));
|
|
33
|
+
server.registerPrompt("direct-editing-workflow", {
|
|
34
|
+
title: "Direct Editing Workflow",
|
|
35
|
+
description: "Guide layout editing on an existing screenshot variant without inventing a new visual system.",
|
|
36
|
+
argsSchema: {
|
|
37
|
+
userGoal: z
|
|
38
|
+
.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Optional concrete editing request."),
|
|
41
|
+
},
|
|
42
|
+
}, async ({ userGoal }) => ({
|
|
43
|
+
description: "Use this prompt when the user wants to edit an existing layout or add screens while preserving the current design language.",
|
|
44
|
+
messages: [
|
|
45
|
+
{
|
|
46
|
+
role: "user",
|
|
47
|
+
content: {
|
|
48
|
+
type: "text",
|
|
49
|
+
text: [
|
|
50
|
+
"Operate directly on the existing AppLaunchFlow screenshot layout.",
|
|
51
|
+
"For small text-only edits to known nodes, transform_layout can be used directly.",
|
|
52
|
+
"For any composition-sensitive edit, call get_layout first and inspect the relevant existing screens before building transform operations.",
|
|
53
|
+
"Composition-sensitive edits include adding screens, reusing screenshots, changing screenshot placement, moving text, changing spacing, or any request that should match the current style.",
|
|
54
|
+
"When preserving the current design, copy actual numeric values from nearby screens: text positions, widths, zIndex, screenshot position, scale, rotation, and typography attributes.",
|
|
55
|
+
"Do not invent a fresh composition unless the user explicitly asks for a redesign.",
|
|
56
|
+
"After applying composition-sensitive transforms, inspect the returned translation or fetch the layout again and verify that the new screens match the existing style and do not overlap key content.",
|
|
57
|
+
userGoal ? `User goal: ${userGoal}` : null,
|
|
58
|
+
]
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.join("\n"),
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
}));
|
|
65
|
+
}
|