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 +110 -0
- package/build/catalog.js +5 -0
- package/build/catalog.test.js +13 -0
- package/build/cli-core.js +20 -0
- package/build/cli-core.test.js +24 -0
- package/build/cli.js +71 -0
- package/build/client/api.js +341 -0
- package/build/http.js +178 -0
- package/build/http.test.js +111 -0
- package/build/index.js +127 -0
- package/build/prompts/register.js +66 -0
- package/build/read-receipt.test.js +36 -0
- package/build/resources/data.js +723 -0
- package/build/resources/register.js +115 -0
- package/build/social-template-previews.js +84 -0
- package/build/template-previews.js +83 -0
- package/build/tool-metadata.js +112 -0
- package/build/tool-metadata.test.js +32 -0
- package/build/tools/assets.js +311 -0
- package/build/tools/graphics.js +475 -0
- package/build/tools/keywords.js +132 -0
- package/build/tools/layouts.js +283 -0
- package/build/tools/localization.js +59 -0
- package/build/tools/mockups.js +324 -0
- package/build/tools/projects.js +113 -0
- package/build/tools/promovideo.js +199 -0
- package/build/tools/screenshots.js +307 -0
- package/build/tools/templates.js +195 -0
- package/build/tools/utils.js +212 -0
- package/build/tools/variants.js +71 -0
- package/package.json +33 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { buildTemplateGalleryUrl, TEMPLATE_PREVIEW_DEVICE_TYPES, decorateTemplatePayload, } from "../template-previews.js";
|
|
3
|
+
import { fail } from "./utils.js";
|
|
4
|
+
function formatTemplateLine(template, deviceType) {
|
|
5
|
+
const parts = [
|
|
6
|
+
`${template.name} (${template.id})`,
|
|
7
|
+
typeof template.screenCount === "number"
|
|
8
|
+
? `${template.screenCount} screens`
|
|
9
|
+
: null,
|
|
10
|
+
template.categories?.length
|
|
11
|
+
? `categories: ${template.categories.join(", ")}`
|
|
12
|
+
: null,
|
|
13
|
+
template.description || null,
|
|
14
|
+
`Preview URL (${deviceType}): ${template.previewUrls[deviceType]}`,
|
|
15
|
+
];
|
|
16
|
+
return parts.filter(Boolean).join(" | ");
|
|
17
|
+
}
|
|
18
|
+
function buildListTemplatesResult(payload, deviceType, galleryUrl) {
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: "text",
|
|
23
|
+
text: [
|
|
24
|
+
`Fetched ${payload.templates.length} screenshot templates.`,
|
|
25
|
+
`Open the visual gallery: ${galleryUrl}`,
|
|
26
|
+
`Use the ${deviceType} preview resources below to compare them visually.`,
|
|
27
|
+
"Keep the full catalog available unless the user explicitly asks for a shortlist.",
|
|
28
|
+
"",
|
|
29
|
+
...payload.templates.map((template) => formatTemplateLine(template, deviceType)),
|
|
30
|
+
].join("\n"),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
type: "resource_link",
|
|
34
|
+
uri: galleryUrl,
|
|
35
|
+
name: "Open Template Gallery",
|
|
36
|
+
mimeType: "text/html",
|
|
37
|
+
description: "Hosted visual gallery with screenshot template previews for all available templates.",
|
|
38
|
+
},
|
|
39
|
+
...payload.templates.map((template) => ({
|
|
40
|
+
type: "resource_link",
|
|
41
|
+
uri: template.previewResourceUris[deviceType],
|
|
42
|
+
name: `${template.name} (${deviceType} preview)`,
|
|
43
|
+
mimeType: "image/png",
|
|
44
|
+
description: template.description ||
|
|
45
|
+
`Visual preview for the ${template.name} screenshot template.`,
|
|
46
|
+
})),
|
|
47
|
+
],
|
|
48
|
+
structuredContent: {
|
|
49
|
+
success: true,
|
|
50
|
+
data: {
|
|
51
|
+
...payload,
|
|
52
|
+
previewDeviceType: deviceType,
|
|
53
|
+
galleryUrl,
|
|
54
|
+
},
|
|
55
|
+
message: "Fetched templates",
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function buildTemplateDetailsResult(payload) {
|
|
60
|
+
const { template } = payload;
|
|
61
|
+
const previewSummary = TEMPLATE_PREVIEW_DEVICE_TYPES.map((deviceType) => `${deviceType}: ${template.previewUrls[deviceType]} | resource: ${template.previewResourceUris[deviceType]}`);
|
|
62
|
+
return {
|
|
63
|
+
content: [
|
|
64
|
+
{
|
|
65
|
+
type: "text",
|
|
66
|
+
text: [
|
|
67
|
+
`Fetched template ${template.name} (${template.id}).`,
|
|
68
|
+
template.description || null,
|
|
69
|
+
typeof template.screenCount === "number"
|
|
70
|
+
? `Screen count: ${template.screenCount}`
|
|
71
|
+
: null,
|
|
72
|
+
template.categories?.length
|
|
73
|
+
? `Categories: ${template.categories.join(", ")}`
|
|
74
|
+
: null,
|
|
75
|
+
"",
|
|
76
|
+
"Preview assets:",
|
|
77
|
+
...previewSummary,
|
|
78
|
+
]
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.join("\n"),
|
|
81
|
+
},
|
|
82
|
+
...TEMPLATE_PREVIEW_DEVICE_TYPES.map((deviceType) => ({
|
|
83
|
+
type: "resource_link",
|
|
84
|
+
uri: template.previewResourceUris[deviceType],
|
|
85
|
+
name: `${template.name} (${deviceType} preview)`,
|
|
86
|
+
mimeType: "image/png",
|
|
87
|
+
description: template.description ||
|
|
88
|
+
`Visual preview for the ${template.name} screenshot template.`,
|
|
89
|
+
})),
|
|
90
|
+
],
|
|
91
|
+
structuredContent: {
|
|
92
|
+
success: true,
|
|
93
|
+
data: payload,
|
|
94
|
+
message: "Fetched template",
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export function registerTemplateTools(server, client) {
|
|
99
|
+
server.registerTool("browse_templates", {
|
|
100
|
+
title: "Browse & Select Template",
|
|
101
|
+
description: "Use this visual style gallery after prepare_screenshot_styles, restricted to its returned templateIds, so the user can choose which prepared result to apply. It can also be used independently for static style discovery. Returns the selected template id. Never offer templates via text or AskUserQuestion — always open this gallery.",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
deviceType: z
|
|
104
|
+
.enum(TEMPLATE_PREVIEW_DEVICE_TYPES)
|
|
105
|
+
.optional()
|
|
106
|
+
.describe("Which preview device the gallery should show first."),
|
|
107
|
+
templateIds: z
|
|
108
|
+
.array(z.string())
|
|
109
|
+
.optional()
|
|
110
|
+
.describe("Optional subset of template ids to show in the gallery."),
|
|
111
|
+
selectedTemplateId: z
|
|
112
|
+
.string()
|
|
113
|
+
.optional()
|
|
114
|
+
.describe("Optional template id to highlight in the gallery."),
|
|
115
|
+
title: z
|
|
116
|
+
.string()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe("Optional gallery heading, for example the project name."),
|
|
119
|
+
generationId: z
|
|
120
|
+
.string()
|
|
121
|
+
.uuid()
|
|
122
|
+
.optional()
|
|
123
|
+
.describe("Project UUID from prepare_screenshot_styles. Pass together with catalogKey to show the real personalized previews."),
|
|
124
|
+
catalogKey: z
|
|
125
|
+
.string()
|
|
126
|
+
.min(1)
|
|
127
|
+
.max(128)
|
|
128
|
+
.optional()
|
|
129
|
+
.describe("Catalog key from prepare_screenshot_styles. Pass together with generationId."),
|
|
130
|
+
},
|
|
131
|
+
}, async ({ deviceType = "phone", templateIds, selectedTemplateId, title, generationId, catalogKey, }) => {
|
|
132
|
+
try {
|
|
133
|
+
const payload = decorateTemplatePayload(await client.listTemplates(), client.credentials.baseUrl);
|
|
134
|
+
const availableIds = new Set(payload.templates.map((template) => template.id));
|
|
135
|
+
const filteredTemplateIds = templateIds?.filter((templateId) => availableIds.has(templateId)) || [];
|
|
136
|
+
const droppedTemplateIds = templateIds?.filter((templateId) => !availableIds.has(templateId)) ||
|
|
137
|
+
[];
|
|
138
|
+
if (droppedTemplateIds.length > 0) {
|
|
139
|
+
console.error(`[browse_templates] Ignoring unknown template ids not in the registry: ${droppedTemplateIds.join(", ")}`);
|
|
140
|
+
}
|
|
141
|
+
// If a caller restricted the gallery to specific ids but NONE of them
|
|
142
|
+
// are known, silently showing every template would misrepresent the
|
|
143
|
+
// prepared catalog. Fail loudly instead so the flow can re-prepare.
|
|
144
|
+
if (templateIds &&
|
|
145
|
+
templateIds.length > 0 &&
|
|
146
|
+
filteredTemplateIds.length === 0) {
|
|
147
|
+
return fail(new Error(`None of the requested template ids match the available templates (${droppedTemplateIds.join(", ")}). ` +
|
|
148
|
+
"The prepared catalog and the template registry may be out of sync — re-run prepare_screenshot_styles and pass its returned templateIds."));
|
|
149
|
+
}
|
|
150
|
+
const galleryUrl = buildTemplateGalleryUrl(client.credentials.baseUrl, {
|
|
151
|
+
deviceType,
|
|
152
|
+
templateIds: filteredTemplateIds.length > 0 ? filteredTemplateIds : undefined,
|
|
153
|
+
selectedTemplateId: selectedTemplateId && availableIds.has(selectedTemplateId)
|
|
154
|
+
? selectedTemplateId
|
|
155
|
+
: undefined,
|
|
156
|
+
title,
|
|
157
|
+
generationId,
|
|
158
|
+
catalogKey,
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
content: [
|
|
162
|
+
{
|
|
163
|
+
type: "text",
|
|
164
|
+
text: [
|
|
165
|
+
"Paste this exact gallery URL into the user-visible reply.",
|
|
166
|
+
`Template gallery URL: ${galleryUrl}`,
|
|
167
|
+
"Do not say 'link above' because tool results may be collapsed or hidden.",
|
|
168
|
+
"After you pick a template, reply with the template name or id.",
|
|
169
|
+
].join("\n"),
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
type: "resource_link",
|
|
173
|
+
uri: galleryUrl,
|
|
174
|
+
name: "Open Template Gallery",
|
|
175
|
+
mimeType: "text/html",
|
|
176
|
+
description: "Hosted visual gallery for browsing screenshot template previews.",
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
structuredContent: {
|
|
180
|
+
success: true,
|
|
181
|
+
data: {
|
|
182
|
+
galleryUrl,
|
|
183
|
+
userFacingUrl: galleryUrl,
|
|
184
|
+
deviceType,
|
|
185
|
+
templateIds: filteredTemplateIds,
|
|
186
|
+
},
|
|
187
|
+
message: "Prepared template gallery",
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
return fail(error);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { ElicitResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { AppLaunchFlowApiError } from "../client/api.js";
|
|
4
|
+
/**
|
|
5
|
+
* Bypass the SDK's `elicitation.url` capability gate by calling the underlying
|
|
6
|
+
* protocol `request()` method directly. This lets us send URL-mode elicitations
|
|
7
|
+
* to clients (like Claude Code) that support them at the protocol level even when
|
|
8
|
+
* the SDK's newer capability check doesn't recognise the advertised capabilities.
|
|
9
|
+
*
|
|
10
|
+
* Falls back gracefully: callers should catch errors and offer the URL in text.
|
|
11
|
+
*/
|
|
12
|
+
export async function elicitUrl(server, params, options) {
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
const proto = server.server;
|
|
15
|
+
return proto.request({ method: "elicitation/create", params }, ElicitResultSchema, options);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Ask the host to open a URL. Hosted connectors return false when the client
|
|
19
|
+
* does not support URL elicitation so the tool can still return the URL.
|
|
20
|
+
*/
|
|
21
|
+
export async function openUrl(server, url, message, options) {
|
|
22
|
+
try {
|
|
23
|
+
const result = await elicitUrl(server, {
|
|
24
|
+
mode: "url",
|
|
25
|
+
elicitationId: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
26
|
+
message,
|
|
27
|
+
url,
|
|
28
|
+
}, options);
|
|
29
|
+
return result.action === "accept";
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Emit periodic progress notifications while a long-running operation (e.g. the
|
|
37
|
+
* one-shot AI catalog generation behind prepare_*) is awaited. Clients that
|
|
38
|
+
* reset their request timeout on progress stay connected instead of timing out
|
|
39
|
+
* during the dead air. Returns a stop function to call in a `finally`.
|
|
40
|
+
*/
|
|
41
|
+
export function startProgressHeartbeat(extra, message, intervalMs = 8000) {
|
|
42
|
+
const progressToken = extra?._meta?.progressToken;
|
|
43
|
+
const sendNotification = extra?.sendNotification;
|
|
44
|
+
if (progressToken === undefined || typeof sendNotification !== "function") {
|
|
45
|
+
return () => { };
|
|
46
|
+
}
|
|
47
|
+
let progress = 0;
|
|
48
|
+
const send = () => {
|
|
49
|
+
progress += 1;
|
|
50
|
+
void sendNotification({
|
|
51
|
+
method: "notifications/progress",
|
|
52
|
+
params: { progressToken, progress, message },
|
|
53
|
+
}).catch(() => {
|
|
54
|
+
// Best-effort keepalive; a failed notification must not break the tool.
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
send();
|
|
58
|
+
const interval = setInterval(send, intervalMs);
|
|
59
|
+
if (typeof interval.unref === "function") {
|
|
60
|
+
interval.unref();
|
|
61
|
+
}
|
|
62
|
+
return () => clearInterval(interval);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* In-memory get-before-edit gate keyed by generationId + variantId. The timestamp
|
|
66
|
+
* is retained for a future TTL sweep; current callers only check presence.
|
|
67
|
+
*/
|
|
68
|
+
export function createReadReceiptStore() {
|
|
69
|
+
const receipts = new Map();
|
|
70
|
+
const keyFor = (args) => [args.generationId, args.variantId || "active"].join("::");
|
|
71
|
+
return {
|
|
72
|
+
record(args) {
|
|
73
|
+
receipts.set(keyFor(args), Date.now());
|
|
74
|
+
},
|
|
75
|
+
has(args) {
|
|
76
|
+
return receipts.has(keyFor(args));
|
|
77
|
+
},
|
|
78
|
+
consume(args) {
|
|
79
|
+
receipts.delete(keyFor(args));
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const READ_RECEIPT_TTL_MS = 10 * 60 * 1000;
|
|
84
|
+
/**
|
|
85
|
+
* Hosted MCP requests are intentionally stateless, so the local in-memory
|
|
86
|
+
* get-before-edit gate cannot span two tool calls. These signed, short-lived
|
|
87
|
+
* receipts prove that the same access token fetched the exact target shortly
|
|
88
|
+
* before attempting an edit, without storing bearer tokens or server state.
|
|
89
|
+
*/
|
|
90
|
+
export function createHostedReadReceipt(target, bearerToken, now = Date.now()) {
|
|
91
|
+
const payload = Buffer.from(JSON.stringify({ target, issuedAt: now })).toString("base64url");
|
|
92
|
+
const signature = createHmac("sha256", bearerToken)
|
|
93
|
+
.update(payload)
|
|
94
|
+
.digest("base64url");
|
|
95
|
+
return `${payload}.${signature}`;
|
|
96
|
+
}
|
|
97
|
+
export function verifyHostedReadReceipt(receipt, target, bearerToken, now = Date.now()) {
|
|
98
|
+
if (!receipt)
|
|
99
|
+
return false;
|
|
100
|
+
const [payload, signature, extra] = receipt.split(".");
|
|
101
|
+
if (!payload || !signature || extra)
|
|
102
|
+
return false;
|
|
103
|
+
const expected = createHmac("sha256", bearerToken)
|
|
104
|
+
.update(payload)
|
|
105
|
+
.digest("base64url");
|
|
106
|
+
const providedBuffer = Buffer.from(signature);
|
|
107
|
+
const expectedBuffer = Buffer.from(expected);
|
|
108
|
+
if (providedBuffer.length !== expectedBuffer.length ||
|
|
109
|
+
!timingSafeEqual(providedBuffer, expectedBuffer)) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
114
|
+
return (decoded.target === target &&
|
|
115
|
+
typeof decoded.issuedAt === "number" &&
|
|
116
|
+
decoded.issuedAt <= now &&
|
|
117
|
+
now - decoded.issuedAt <= READ_RECEIPT_TTL_MS);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export function hostedMcpEnabled() {
|
|
124
|
+
return process.env.APPLAUNCHFLOW_MCP_REMOTE === "1";
|
|
125
|
+
}
|
|
126
|
+
export function ok(data, message) {
|
|
127
|
+
const text = typeof data === "string"
|
|
128
|
+
? message
|
|
129
|
+
? `${message}\n\n${data}`
|
|
130
|
+
: data
|
|
131
|
+
: message
|
|
132
|
+
? `${message}\n\n${JSON.stringify(data, null, 2)}`
|
|
133
|
+
: JSON.stringify(data, null, 2);
|
|
134
|
+
return {
|
|
135
|
+
content: [
|
|
136
|
+
{
|
|
137
|
+
type: "text",
|
|
138
|
+
text,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
structuredContent: {
|
|
142
|
+
success: true,
|
|
143
|
+
data,
|
|
144
|
+
...(message ? { message } : {}),
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function extractValidationIssues(error) {
|
|
149
|
+
if (!(error instanceof AppLaunchFlowApiError))
|
|
150
|
+
return null;
|
|
151
|
+
const body = error.body;
|
|
152
|
+
const details = body?.details ?? body?.error?.details;
|
|
153
|
+
if (!Array.isArray(details))
|
|
154
|
+
return null;
|
|
155
|
+
return details.filter((issue) => typeof issue === "object" && issue !== null);
|
|
156
|
+
}
|
|
157
|
+
function formatValidationIssue(issue) {
|
|
158
|
+
const path = Array.isArray(issue.path)
|
|
159
|
+
? issue.path.join(".")
|
|
160
|
+
: (issue.path ?? "");
|
|
161
|
+
const where = path ? `\`${path}\`` : "(root)";
|
|
162
|
+
const bounds = [
|
|
163
|
+
issue.expected !== undefined ? `expected ${JSON.stringify(issue.expected)}` : null,
|
|
164
|
+
issue.received !== undefined ? `received ${JSON.stringify(issue.received)}` : null,
|
|
165
|
+
issue.maximum !== undefined ? `max ${JSON.stringify(issue.maximum)}` : null,
|
|
166
|
+
issue.minimum !== undefined ? `min ${JSON.stringify(issue.minimum)}` : null,
|
|
167
|
+
]
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
.join(", ");
|
|
170
|
+
const parts = [where, issue.message ?? issue.code ?? "invalid"];
|
|
171
|
+
if (bounds)
|
|
172
|
+
parts.push(`(${bounds})`);
|
|
173
|
+
return `• ${parts.join(" — ")}`;
|
|
174
|
+
}
|
|
175
|
+
export function fail(error) {
|
|
176
|
+
const issues = extractValidationIssues(error);
|
|
177
|
+
const normalized = error instanceof AppLaunchFlowApiError
|
|
178
|
+
? {
|
|
179
|
+
code: error.body?.error?.code ||
|
|
180
|
+
error.body?.code ||
|
|
181
|
+
`HTTP_${error.status}`,
|
|
182
|
+
type: error.body?.error?.type || "server",
|
|
183
|
+
message: error.body?.error?.message || error.message,
|
|
184
|
+
status: error.status,
|
|
185
|
+
details: error.body,
|
|
186
|
+
...(issues ? { issues } : {}),
|
|
187
|
+
}
|
|
188
|
+
: {
|
|
189
|
+
code: "UNKNOWN",
|
|
190
|
+
type: "server",
|
|
191
|
+
message: error instanceof Error ? error.message : String(error),
|
|
192
|
+
};
|
|
193
|
+
// For validation errors, append a compact issue summary to the visible
|
|
194
|
+
// text so the calling LLM can see exactly which field tripped Zod and
|
|
195
|
+
// self-correct on the next call instead of guessing.
|
|
196
|
+
const text = issues?.length
|
|
197
|
+
? [normalized.message, "Issues:", ...issues.map(formatValidationIssue)].join("\n")
|
|
198
|
+
: normalized.message;
|
|
199
|
+
return {
|
|
200
|
+
isError: true,
|
|
201
|
+
content: [
|
|
202
|
+
{
|
|
203
|
+
type: "text",
|
|
204
|
+
text,
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
structuredContent: {
|
|
208
|
+
success: false,
|
|
209
|
+
error: normalized,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { openUrl, fail, ok } from "./utils.js";
|
|
3
|
+
const contentTypeEnum = z.enum(["screenshots", "socialGraphics", "promoVideo"]);
|
|
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, extra) => {
|
|
30
|
+
try {
|
|
31
|
+
const result = await client.createVariant(args);
|
|
32
|
+
const variantId = result?.variant?.id;
|
|
33
|
+
const generationId = args.generationId;
|
|
34
|
+
if (variantId && generationId) {
|
|
35
|
+
const editorPath = args.contentType === "socialGraphics"
|
|
36
|
+
? "/graphics"
|
|
37
|
+
: args.contentType === "promoVideo"
|
|
38
|
+
? "/promovideo"
|
|
39
|
+
: "/editor";
|
|
40
|
+
const params = new URLSearchParams({
|
|
41
|
+
projectId: generationId,
|
|
42
|
+
variantId,
|
|
43
|
+
});
|
|
44
|
+
if (args.contentType === "screenshots") {
|
|
45
|
+
params.set("device", "phone");
|
|
46
|
+
}
|
|
47
|
+
const editorUrl = `${client.credentials.baseUrl}${editorPath}?${params.toString()}`;
|
|
48
|
+
await openUrl(server, editorUrl, "Opening the new variant in the editor.", { signal: extra.signal });
|
|
49
|
+
return ok({ ...result, editorUrl }, "Created variant");
|
|
50
|
+
}
|
|
51
|
+
return ok(result, "Created variant");
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return fail(error);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
server.registerTool("duplicate_variant", {
|
|
58
|
+
title: "Duplicate Variant",
|
|
59
|
+
description: "Duplicate an existing variant",
|
|
60
|
+
inputSchema: {
|
|
61
|
+
variantId: z.string().uuid(),
|
|
62
|
+
},
|
|
63
|
+
}, async ({ variantId }) => {
|
|
64
|
+
try {
|
|
65
|
+
return ok(await client.duplicateVariant(variantId), "Duplicated variant");
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return fail(error);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "applaunchflow",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Hosted OAuth MCP connector for AppLaunchFlow.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/ynnickw/applaunchflow-mcp.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"bin": {
|
|
12
|
+
"applaunchflow": "build/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"build"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"clean": "node -e \"require('node:fs').rmSync('build', { recursive: true, force: true })\"",
|
|
19
|
+
"build": "npm run clean && tsc -p tsconfig.json && chmod 755 build/cli.js",
|
|
20
|
+
"dev:http": "tsx src/http.ts",
|
|
21
|
+
"start:http": "node build/http.js",
|
|
22
|
+
"test": "npm run build && node --test build/*.test.js scripts/*.test.mjs"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
26
|
+
"zod": "^3.25.76"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^24.6.0",
|
|
30
|
+
"tsx": "^4.20.5",
|
|
31
|
+
"typescript": "^5.9.3"
|
|
32
|
+
}
|
|
33
|
+
}
|