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/build/http.js ADDED
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ import { createServer } from "node:http";
3
+ import { pathToFileURL } from "node:url";
4
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
+ import { createAppLaunchFlowServer } from "./index.js";
6
+ const DEFAULT_PORT = 8787;
7
+ const DEFAULT_DASHBOARD_URL = "https://dashboard.applaunchflow.com";
8
+ const REQUIRED_SCOPES = [
9
+ "projects:read",
10
+ "projects:write",
11
+ "assets:write",
12
+ "generations:write",
13
+ ];
14
+ function normalizeBaseUrl(value) {
15
+ return value.replace(/\/+$/, "");
16
+ }
17
+ function dashboardBaseUrl() {
18
+ return normalizeBaseUrl(process.env.APPLAUNCHFLOW_BASE_URL || DEFAULT_DASHBOARD_URL);
19
+ }
20
+ function publicBaseUrl(request) {
21
+ const configured = process.env.APPLAUNCHFLOW_MCP_PUBLIC_URL;
22
+ if (configured) {
23
+ const parsed = new URL(configured);
24
+ if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
25
+ throw new Error("APPLAUNCHFLOW_MCP_PUBLIC_URL must use HTTPS in production");
26
+ }
27
+ parsed.pathname = parsed.pathname.replace(/\/mcp\/?$/, "");
28
+ parsed.search = "";
29
+ parsed.hash = "";
30
+ return normalizeBaseUrl(parsed.toString());
31
+ }
32
+ const host = request?.headers.host || `127.0.0.1:${DEFAULT_PORT}`;
33
+ const protocol = process.env.NODE_ENV === "production" ? "https" : "http";
34
+ return `${protocol}://${host}`;
35
+ }
36
+ function resourceIdentifier(request) {
37
+ return `${publicBaseUrl(request)}/mcp`;
38
+ }
39
+ function resourceMetadataUrl(request) {
40
+ return `${publicBaseUrl(request)}/.well-known/oauth-protected-resource`;
41
+ }
42
+ function json(response, status, payload, headers = {}) {
43
+ response.writeHead(status, {
44
+ "content-type": "application/json; charset=utf-8",
45
+ "cache-control": "no-store",
46
+ ...headers,
47
+ });
48
+ response.end(JSON.stringify(payload));
49
+ }
50
+ function bearerToken(request) {
51
+ const header = request.headers.authorization;
52
+ if (!header)
53
+ return null;
54
+ const match = /^Bearer\s+(.+)$/i.exec(header);
55
+ return match?.[1]?.trim() || null;
56
+ }
57
+ function unauthorized(request, response) {
58
+ json(response, 401, {
59
+ jsonrpc: "2.0",
60
+ error: { code: -32001, message: "Authorization required" },
61
+ id: null,
62
+ }, {
63
+ "www-authenticate": `Bearer resource_metadata="${resourceMetadataUrl(request)}"`,
64
+ });
65
+ }
66
+ async function introspectToken(request, token) {
67
+ const response = await fetch(`${dashboardBaseUrl()}/api/auth/mcp/introspect`, {
68
+ headers: {
69
+ authorization: `Bearer ${token}`,
70
+ accept: "application/json",
71
+ },
72
+ });
73
+ if (!response.ok)
74
+ return null;
75
+ const payload = (await response.json());
76
+ if (!payload.active ||
77
+ !payload.userId ||
78
+ REQUIRED_SCOPES.some((scope) => !payload.scopes?.includes(scope))) {
79
+ return null;
80
+ }
81
+ const expectedResource = resourceIdentifier(request);
82
+ if (payload.resource && payload.resource !== expectedResource)
83
+ return null;
84
+ return {
85
+ token,
86
+ clientId: payload.clientId || "applaunchflow-mcp",
87
+ scopes: payload.scopes || [],
88
+ expiresAt: payload.expiresAt,
89
+ resource: new URL(expectedResource),
90
+ extra: { userId: payload.userId },
91
+ };
92
+ }
93
+ async function handleMcp(request, response) {
94
+ const token = bearerToken(request);
95
+ if (!token) {
96
+ unauthorized(request, response);
97
+ return;
98
+ }
99
+ let auth = null;
100
+ try {
101
+ auth = await introspectToken(request, token);
102
+ }
103
+ catch (error) {
104
+ console.error("Token introspection failed", error);
105
+ json(response, 503, {
106
+ jsonrpc: "2.0",
107
+ error: { code: -32002, message: "Authorization service unavailable" },
108
+ id: null,
109
+ });
110
+ return;
111
+ }
112
+ if (!auth) {
113
+ unauthorized(request, response);
114
+ return;
115
+ }
116
+ const server = createAppLaunchFlowServer({
117
+ baseUrl: dashboardBaseUrl(),
118
+ token,
119
+ });
120
+ const transport = new StreamableHTTPServerTransport({
121
+ sessionIdGenerator: undefined,
122
+ });
123
+ request.auth = auth;
124
+ try {
125
+ await server.connect(transport);
126
+ await transport.handleRequest(request, response);
127
+ }
128
+ catch (error) {
129
+ console.error("MCP request failed", error);
130
+ if (!response.headersSent) {
131
+ json(response, 500, {
132
+ jsonrpc: "2.0",
133
+ error: { code: -32603, message: "Internal server error" },
134
+ id: null,
135
+ });
136
+ }
137
+ }
138
+ finally {
139
+ await transport.close().catch(() => undefined);
140
+ await server.close().catch(() => undefined);
141
+ }
142
+ }
143
+ export function createHttpServer() {
144
+ process.env.APPLAUNCHFLOW_MCP_REMOTE = "1";
145
+ return createServer(async (request, response) => {
146
+ const url = new URL(request.url || "/", publicBaseUrl(request));
147
+ if (request.method === "GET" && url.pathname === "/healthz") {
148
+ json(response, 200, { ok: true, service: "applaunchflow-mcp" });
149
+ return;
150
+ }
151
+ if (request.method === "GET" &&
152
+ url.pathname === "/.well-known/oauth-protected-resource") {
153
+ json(response, 200, {
154
+ resource: resourceIdentifier(request),
155
+ authorization_servers: [dashboardBaseUrl()],
156
+ scopes_supported: REQUIRED_SCOPES,
157
+ bearer_methods_supported: ["header"],
158
+ resource_documentation: `${dashboardBaseUrl()}/docs/mcp`,
159
+ });
160
+ return;
161
+ }
162
+ if (url.pathname === "/mcp") {
163
+ await handleMcp(request, response);
164
+ return;
165
+ }
166
+ json(response, 404, { error: "Not found" });
167
+ });
168
+ }
169
+ async function main() {
170
+ const port = Number(process.env.PORT || DEFAULT_PORT);
171
+ const server = createHttpServer();
172
+ server.listen(port, "0.0.0.0", () => {
173
+ console.error(`AppLaunchFlow MCP HTTP server listening on port ${port}`);
174
+ });
175
+ }
176
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
177
+ void main();
178
+ }
@@ -0,0 +1,111 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createServer as createNodeServer } from "node:http";
4
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
6
+ import { createHttpServer } from "./http.js";
7
+ async function withServer(callback) {
8
+ const server = createHttpServer();
9
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
10
+ const address = server.address();
11
+ try {
12
+ await callback(`http://127.0.0.1:${address.port}`);
13
+ }
14
+ finally {
15
+ await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
16
+ }
17
+ }
18
+ test("HTTP server exposes health and protected-resource metadata", async () => {
19
+ await withServer(async (baseUrl) => {
20
+ const health = await fetch(`${baseUrl}/healthz`);
21
+ assert.equal(health.status, 200);
22
+ assert.deepEqual(await health.json(), {
23
+ ok: true,
24
+ service: "applaunchflow-mcp",
25
+ });
26
+ const metadata = await fetch(`${baseUrl}/.well-known/oauth-protected-resource`);
27
+ assert.equal(metadata.status, 200);
28
+ const payload = (await metadata.json());
29
+ assert.equal(payload.resource, `${baseUrl}/mcp`);
30
+ assert.deepEqual(payload.authorization_servers, [
31
+ "https://dashboard.applaunchflow.com",
32
+ ]);
33
+ });
34
+ });
35
+ test("MCP endpoint challenges unauthenticated callers with resource metadata", async () => {
36
+ await withServer(async (baseUrl) => {
37
+ const response = await fetch(`${baseUrl}/mcp`, {
38
+ method: "POST",
39
+ headers: { "content-type": "application/json" },
40
+ body: JSON.stringify({
41
+ jsonrpc: "2.0",
42
+ id: 1,
43
+ method: "initialize",
44
+ params: {
45
+ protocolVersion: "2025-06-18",
46
+ capabilities: {},
47
+ clientInfo: { name: "test", version: "1.0.0" },
48
+ },
49
+ }),
50
+ });
51
+ assert.equal(response.status, 401);
52
+ assert.equal(response.headers.get("www-authenticate"), `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"`);
53
+ });
54
+ });
55
+ test("authenticated Streamable HTTP clients can initialize and discover tools", async () => {
56
+ const introspectionServer = createNodeServer((request, response) => {
57
+ if (request.url === "/api/auth/mcp/introspect" &&
58
+ request.headers.authorization === "Bearer test-access-token") {
59
+ response.writeHead(200, { "content-type": "application/json" });
60
+ response.end(JSON.stringify({
61
+ active: true,
62
+ userId: "00000000-0000-4000-8000-000000000001",
63
+ clientId: "test-client",
64
+ scopes: [
65
+ "projects:read",
66
+ "projects:write",
67
+ "assets:write",
68
+ "generations:write",
69
+ ],
70
+ expiresAt: Math.floor(Date.now() / 1000) + 3600,
71
+ }));
72
+ return;
73
+ }
74
+ response.writeHead(401).end();
75
+ });
76
+ await new Promise((resolve) => introspectionServer.listen(0, "127.0.0.1", resolve));
77
+ const introspectionAddress = introspectionServer.address();
78
+ const previousDashboard = process.env.APPLAUNCHFLOW_BASE_URL;
79
+ const previousPublicUrl = process.env.APPLAUNCHFLOW_MCP_PUBLIC_URL;
80
+ process.env.APPLAUNCHFLOW_BASE_URL = `http://127.0.0.1:${introspectionAddress.port}`;
81
+ try {
82
+ await withServer(async (baseUrl) => {
83
+ process.env.APPLAUNCHFLOW_MCP_PUBLIC_URL = `${baseUrl}/mcp`;
84
+ const transport = new StreamableHTTPClientTransport(new URL(`${baseUrl}/mcp`), {
85
+ requestInit: {
86
+ headers: { authorization: "Bearer test-access-token" },
87
+ },
88
+ });
89
+ const client = new Client({ name: "http-test", version: "1.0.0" });
90
+ await client.connect(transport);
91
+ try {
92
+ const { tools } = await client.listTools();
93
+ assert.equal(tools.length, 43);
94
+ }
95
+ finally {
96
+ await client.close();
97
+ }
98
+ });
99
+ }
100
+ finally {
101
+ if (previousDashboard === undefined)
102
+ delete process.env.APPLAUNCHFLOW_BASE_URL;
103
+ else
104
+ process.env.APPLAUNCHFLOW_BASE_URL = previousDashboard;
105
+ if (previousPublicUrl === undefined)
106
+ delete process.env.APPLAUNCHFLOW_MCP_PUBLIC_URL;
107
+ else
108
+ process.env.APPLAUNCHFLOW_MCP_PUBLIC_URL = previousPublicUrl;
109
+ await new Promise((resolve, reject) => introspectionServer.close((error) => (error ? reject(error) : resolve())));
110
+ }
111
+ });
package/build/index.js ADDED
@@ -0,0 +1,127 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { AppLaunchFlowClient, } from "./client/api.js";
3
+ import { registerPrompts } from "./prompts/register.js";
4
+ import { registerResources } from "./resources/register.js";
5
+ import { registerAssetTools } from "./tools/assets.js";
6
+ import { registerLayoutTools } from "./tools/layouts.js";
7
+ import { registerProjectTools } from "./tools/projects.js";
8
+ import { registerScreenshotTools } from "./tools/screenshots.js";
9
+ import { registerTemplateTools } from "./tools/templates.js";
10
+ import { registerGraphicsTools } from "./tools/graphics.js";
11
+ import { registerPromoVideoTools } from "./tools/promovideo.js";
12
+ import { registerMockupTools } from "./tools/mockups.js";
13
+ import { registerLocalizationTools } from "./tools/localization.js";
14
+ import { registerVariantTools } from "./tools/variants.js";
15
+ import { registerKeywordTools } from "./tools/keywords.js";
16
+ import { installToolMetadataPolicy } from "./tool-metadata.js";
17
+ export const SERVER_INSTRUCTIONS = `
18
+ AppLaunchFlow MCP supports four content types: app store screenshots, social graphics, promo videos, and mockup animations.
19
+ Use it for project setup, screenshot uploads, AI generation of screenshots/graphics/videos, mockup animation editing, variant management, direct layout editing, and translation.
20
+ Do not treat this MCP as an ASO or generic graphics-design assistant — every tool is scoped to one of those four content types.
21
+
22
+ Use AppLaunchFlow MCP as an execution tool, not a questionnaire.
23
+
24
+ Default behavior:
25
+ - When the user wants help and no project has been selected yet, the first branch is always: create a new app or edit an existing app.
26
+ - Do not start with template browsing before that project choice is resolved.
27
+ - For concrete requests on an existing project, act directly instead of asking follow-up questions.
28
+ - Only ask when a missing detail is required to avoid a materially wrong result, or when the request is genuinely ambiguous.
29
+ - Do not force menu-style "what would you like to do next?" steps after each tool call.
30
+ - The user can edit layouts in natural language. Translate those requests into direct MCP actions.
31
+ - 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.
32
+ - Initial creation tools return an editor URL. Include that exact URL in the reply so the user can open the result.
33
+ - For edit tools (transform_layout, save_graphics_format, update_promo_video, update_mockup_animation, save_layout, save_graphics), include the editor URL as a reference without claiming a new browser tab was opened.
34
+
35
+ Schema references (MCP resources — read them, they are not loaded automatically):
36
+ - applaunchflow://schema/layout — every field of the layout JSON: all 11 node types, their properties, and valid value ranges. Used by BOTH screenshots and social graphics (a social layout is the same shape with exactly one screen and the format's canvas size).
37
+ - applaunchflow://schema/transforms — the transform_layout operations, selector syntax, and the selector pitfalls that silently match every screen.
38
+ - applaunchflow://schema/video-config — the full Remotion VideoConfig: six scene types and their content shapes, theme, text styles, ken burns, choreography preset ids, device/text overlays, audio.
39
+ - For mockup animations the equivalent is the list_mockup_presets TOOL, not a resource.
40
+ - Read the relevant resource before hand-writing or non-trivially editing JSON. A get_* response only shows what is currently set — it does not tell you what is possible.
41
+
42
+ Screenshot workflows:
43
+ - Entry point without a known project: ask whether the user wants to create a new app or edit an existing project. If they want existing, list/select projects. If they want new, create the project first.
44
+ - For the normal style-choice flow, call list_source_screenshots, choose 3-7 real screenshots in story order, then call prepare_screenshot_styles. This generates or reuses one personalized catalog containing every template for phone, tablet, and desktop.
45
+ - After preparation, call browse_templates with exactly the returned templateIds plus generationId and catalogKey so the gallery renders the real personalized results. Then immediately call apply_screenshot_style with the returned catalogKey and selected templateId. Applying creates a new variant from cache without another AI call. Never overwrite an existing variant.
46
+ - Use generate_layouts only for an explicitly requested legacy/direct single-template generation. Do not use it for the normal visual style chooser.
47
+ - For small, precise edits to existing known nodes, transform_layout can be used directly.
48
+ - 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.
49
+ - Use transform_layout as the primary tool for editing current screens once you have enough layout context.
50
+ - Default to layouts: ["mobile"] for transform_layout. Only include tablet/desktop if the user explicitly asks.
51
+ - 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.
52
+ - 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.
53
+ - 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.
54
+ - 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.
55
+ - get_layout is mandatory before every direct transform_layout call. Do not edit a layout without a fresh read of the current state first.
56
+ - ALWAYS use browse_templates after prepare_screenshot_styles when a screenshot template choice is needed. Pass the prepared templateIds, generationId, and catalogKey. Never offer templates via text bullet points. The connector returns a gallery URL, which you must show to the user before waiting for the selected template id.
57
+ - When you need visual context about a screenshot (e.g. to extract colors, understand the app UI, or make context-specific edits), use view_screenshot to look at the actual image.
58
+ - After generating a new variant, include the editor URL in the reply.
59
+
60
+ Social graphics workflows (mirror the screenshot flow):
61
+ - Call list_source_screenshots, select 3-7 real screenshots in story order, then call prepare_social_graphics_styles. This generates or reuses every social template across all six formats in one catalog.
62
+ - Call browse_social_templates with the returned templateIds plus generationId and catalogKey so the gallery renders the personalized graphics, then immediately call apply_social_graphics_style with the selected templateId and catalogKey. Applying creates a fresh variant containing all six formats without another AI call.
63
+ - Use generate_graphics only for an explicitly requested legacy/direct single-template generation. Never overwrite an existing graphics variant.
64
+ - For edits to existing social graphics, ALWAYS call get_graphics_format first, then save_graphics_format. Mutate JSON for exactly one format in memory and save only that one format. The same get-before-edit receipt rule applies as for screenshots.
65
+ - Default to the variant's primary format unless the user explicitly asks to edit another format.
66
+ - Do not edit multiple formats in one pass. The user can sync the design to other formats later in the graphics editor UI.
67
+ - When sharing or returning a graphics editor URL after an edit, include \`&format=<format>\` so the browser shows the format that was changed.
68
+ - Use create_variant with contentType:"socialGraphics" or duplicate_variant to create a copy / fresh take without overwriting the current graphics variant.
69
+
70
+ Promo video workflows:
71
+ - generate_promo_video runs the AI generation pipeline against the project's screenshots and produces a complete Remotion video config. Omit variantId to create a new variant.
72
+ - For edits to an existing promo video, ALWAYS call get_promo_video first to fetch the current config, mutate the config object in memory, then call update_promo_video with the full updated config. There is no granular transform tool for promo videos at this stage — full-config replace is the supported edit path. The same get-before-edit receipt rule as graphics applies: update_promo_video is locked until a fresh get_promo_video has been called for the same project/variant.
73
+ - Use clear_promo_video to wipe a variant's video config when the user wants to start over.
74
+ - Use create_variant with contentType:"promoVideo" or duplicate_variant for copies / A-B tests. duplicate_variant clones the source promo-video variant, including its config.
75
+ - Promo video has no template gallery — the LLM produces the full config end-to-end. Do NOT call browse_templates / browse_social_templates for promo videos.
76
+
77
+ Mockup animation workflows:
78
+ - create_mockup_animation seeds a fresh mockup variant from a SCENE_PRESETS preset and a specific screenshot/recording path. Always omit variantId; a new variant is always created. Call list_mockup_media first to pick a screenshotPath, and list_mockup_presets to pick a presetId and learn the valid enum + bound values. Editor opens automatically.
79
+ - For edits to an existing mockup animation, ALWAYS call get_mockup_animation first to fetch the current state, mutate the MockupProjectState object in memory, then call update_mockup_animation with the full updated state. There is no granular per-keyframe transform — full-state replace is the supported edit path. The same get-before-edit receipt rule applies: update_mockup_animation is locked until a fresh get_mockup_animation has been called for the same project/variant. Edits propagate live to any open mockup editor tab via realtime — do not open the editor again.
80
+ - Use create_variant with contentType:"mockups" or duplicate_variant for additional A/B variants. duplicate_variant clones the source mockup variant, including its state. To start a variant over, call create_mockup_animation for a fresh variant instead.
81
+ - Mockup animation has no template gallery — the LLM constructs the MockupProjectState end-to-end using values from list_mockup_presets. Do NOT call browse_templates / browse_social_templates for mockup animations.
82
+
83
+ Translation and localization (screenshots only):
84
+ - When the user asks to translate, localize, or create a version in another language for screenshots, ALWAYS use translate_layouts. Do NOT manually edit text nodes via transform_layout for translation.
85
+ - translate_layouts uses AI to translate all text while preserving layout, positioning, and styling.
86
+ - To apply the same transform across all screens, use transform_layout with screens: "all" in the target.
87
+ - Translation is not currently exposed for social graphics or promo videos.
88
+
89
+ Project creation should be fast and simple:
90
+ 1. Ask for the app name and platform (iOS or Android) using AskUserQuestion. Default platform to iOS.
91
+ 2. Autofill category and description from context (e.g. "Skyscanner" → category "Travel"). Do not ask the user for these.
92
+ 3. Call create_project immediately. Do not ask for confirmation or optional fields unless the user volunteers them.
93
+ 4. After creation, recommend uploading screenshots as the next step (screenshots are the input for both social graphics and promo video generation too).
94
+ `.trim();
95
+ const HOSTED_SERVER_INSTRUCTIONS = `
96
+ HOSTED CONNECTOR SAFETY RULES:
97
+ - Never reveal, repeat, log, or place OAuth access tokens, refresh tokens, authorization codes, PKCE verifiers, or read receipts in user-facing text.
98
+ - A readReceipt returned inside structured tool data is an opaque safety input. Pass it only to the matching edit tool, for the exact same project, variant, language, and format.
99
+ - Before deleting, clearing, overwriting, or replacing user content, require clear user intent for that exact action. Do not infer destructive intent from a broad request.
100
+ - Hosted gallery tools return a URL instead of opening a local browser. Show the exact URL, stop, and wait for the user's chosen template id. Do not guess or apply a style before the user selects it.
101
+
102
+ ${SERVER_INSTRUCTIONS}
103
+ `.trim();
104
+ export function createAppLaunchFlowServer(credentials) {
105
+ const client = new AppLaunchFlowClient(credentials);
106
+ const server = new McpServer({
107
+ name: "applaunchflow-mcp",
108
+ version: "0.3.0",
109
+ }, {
110
+ instructions: HOSTED_SERVER_INSTRUCTIONS,
111
+ });
112
+ installToolMetadataPolicy(server, { hosted: true });
113
+ registerPrompts(server);
114
+ registerResources(server, client);
115
+ registerProjectTools(server, client);
116
+ registerAssetTools(server, client);
117
+ registerScreenshotTools(server, client);
118
+ registerLayoutTools(server, client);
119
+ registerTemplateTools(server, client);
120
+ registerGraphicsTools(server, client);
121
+ registerPromoVideoTools(server, client);
122
+ registerMockupTools(server, client);
123
+ registerLocalizationTools(server, client);
124
+ registerVariantTools(server, client);
125
+ registerKeywordTools(server, client);
126
+ return server;
127
+ }
@@ -0,0 +1,66 @@
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
+ "If no project is selected yet, the first branching question is whether the user wants to create a new app or edit an existing one.",
22
+ "Ask for the app name and platform (iOS or Android) using AskUserQuestion.",
23
+ "Autofill category and description from context — do not ask the user for these.",
24
+ "Call create_project immediately after getting the name and platform.",
25
+ "After creation, recommend uploading screenshots as the next step.",
26
+ userGoal ? `User goal: ${userGoal}` : null,
27
+ ]
28
+ .filter(Boolean)
29
+ .join("\n"),
30
+ },
31
+ },
32
+ ],
33
+ }));
34
+ server.registerPrompt("direct-editing-workflow", {
35
+ title: "Direct Editing Workflow",
36
+ description: "Guide layout editing on an existing screenshot variant without inventing a new visual system.",
37
+ argsSchema: {
38
+ userGoal: z
39
+ .string()
40
+ .optional()
41
+ .describe("Optional concrete editing request."),
42
+ },
43
+ }, async ({ userGoal }) => ({
44
+ description: "Use this prompt when the user wants to edit an existing layout or add screens while preserving the current design language.",
45
+ messages: [
46
+ {
47
+ role: "user",
48
+ content: {
49
+ type: "text",
50
+ text: [
51
+ "Operate directly on the existing AppLaunchFlow screenshot layout.",
52
+ "Call get_layout first and inspect the relevant existing screens before every transform_layout call.",
53
+ "This read-before-edit rule is mandatory even for small direct edits.",
54
+ "Composition-sensitive edits include adding screens, reusing screenshots, changing screenshot placement, moving text, changing spacing, or any request that should match the current style.",
55
+ "When preserving the current design, copy actual numeric values from nearby screens: text positions, widths, zIndex, screenshot position, scale, rotation, and typography attributes.",
56
+ "Do not invent a fresh composition unless the user explicitly asks for a redesign.",
57
+ "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.",
58
+ userGoal ? `User goal: ${userGoal}` : null,
59
+ ]
60
+ .filter(Boolean)
61
+ .join("\n"),
62
+ },
63
+ },
64
+ ],
65
+ }));
66
+ }
@@ -0,0 +1,36 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createHostedReadReceipt, verifyHostedReadReceipt, } from "./tools/utils.js";
4
+ import { isPrivateOrReservedIp } from "./tools/assets.js";
5
+ test("hosted read receipts are token-bound, target-bound, and expire", () => {
6
+ const issuedAt = 1_000_000;
7
+ const receipt = createHostedReadReceipt("layout::one", "access-token-a", issuedAt);
8
+ assert.equal(verifyHostedReadReceipt(receipt, "layout::one", "access-token-a", issuedAt + 60_000), true);
9
+ assert.equal(verifyHostedReadReceipt(receipt, "layout::two", "access-token-a", issuedAt + 60_000), false);
10
+ assert.equal(verifyHostedReadReceipt(receipt, "layout::one", "access-token-b", issuedAt + 60_000), false);
11
+ assert.equal(verifyHostedReadReceipt(receipt, "layout::one", "access-token-a", issuedAt + 10 * 60_000 + 1), false);
12
+ });
13
+ test("hosted read receipts reject malformed and future-issued values", () => {
14
+ assert.equal(verifyHostedReadReceipt("invalid", "target", "token"), false);
15
+ const receipt = createHostedReadReceipt("target", "token", 50_000);
16
+ assert.equal(verifyHostedReadReceipt(receipt, "target", "token", 49_999), false);
17
+ });
18
+ test("hosted asset fetches block private, mapped, and documentation networks", () => {
19
+ for (const address of [
20
+ "127.0.0.1",
21
+ "10.1.2.3",
22
+ "100.64.1.2",
23
+ "169.254.169.254",
24
+ "192.168.1.2",
25
+ "198.51.100.7",
26
+ "::1",
27
+ "::ffff:127.0.0.1",
28
+ "fc00::1",
29
+ "fe80::1",
30
+ "2001:db8::1",
31
+ ]) {
32
+ assert.equal(isPrivateOrReservedIp(address), true, address);
33
+ }
34
+ assert.equal(isPrivateOrReservedIp("8.8.8.8"), false);
35
+ assert.equal(isPrivateOrReservedIp("2606:4700:4700::1111"), false);
36
+ });