appmockupgenerator-mcp 1.0.1 → 1.1.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 +49 -13
- package/dist/api.js +60 -9
- package/dist/catalog.js +79 -0
- package/dist/config.js +1 -1
- package/dist/helpers.js +130 -0
- package/dist/index.js +10 -6
- package/dist/jobs.js +133 -3
- package/dist/nextsteps.js +149 -0
- package/dist/shared/legalDocs/catalog.js +99 -0
- package/dist/shared/legalDocs/generator.js +547 -0
- package/dist/shared/legalDocs/questions.js +505 -0
- package/dist/shared/legalDocs/render.js +111 -0
- package/dist/shared/legalDocs/slug.js +39 -0
- package/dist/shared/legalDocs/types.js +16 -0
- package/dist/tools-extra.js +517 -0
- package/dist/tools-legal.js +138 -0
- package/dist/tools-tracking.js +139 -0
- package/dist/tools.js +162 -117
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -72,6 +72,12 @@ On Windows, if `npx` isn't found, use `"command": "cmd", "args": ["/c", "npx", "
|
|
|
72
72
|
- "Make App Store screenshots from the PNGs in ./fastlane/screenshots"
|
|
73
73
|
- "Create Play Store screenshots for these 4 images, Vibrant Gradient style, in German"
|
|
74
74
|
- "Capture my iOS Simulator screens and turn them into store screenshots"
|
|
75
|
+
- "Make a panorama set from these 5 screens and A/B test minimal vs editorial"
|
|
76
|
+
- "Localize my last 4 screenshots into German, Japanese and Portuguese (Brazil) with cultural adaptation"
|
|
77
|
+
- "Convert these iPhone mockups to iPad and iPhone Duo"
|
|
78
|
+
- "Score my current App Store screenshots" · "Make a Play Store feature graphic"
|
|
79
|
+
- "Write and host a privacy policy, terms and support page for my app"
|
|
80
|
+
- "Track my app and tell me when it hits a milestone"
|
|
75
81
|
- Claude Code: type `/` and pick the `create_store_screenshots` prompt from the appmockup server for a guided flow.
|
|
76
82
|
|
|
77
83
|
### Where screenshots come from
|
|
@@ -90,20 +96,47 @@ Before generating, the assistant asks you which source to use:
|
|
|
90
96
|
|
|
91
97
|
## Tools
|
|
92
98
|
|
|
99
|
+
Ask *"What can AppMockup do?"* and the assistant calls `capabilities` for the full list. Plan limits match the web app and are enforced by the server.
|
|
100
|
+
|
|
101
|
+
**Store screenshots**
|
|
102
|
+
|
|
103
|
+
| Tool | What it does | Credits | Plan |
|
|
104
|
+
|---|---|---|---|
|
|
105
|
+
| `generate_screenshots` | Raw screens → store screenshots. `mode` clean/dynamic, 7 styles, 19 copy languages, pop-out effect, title/subtitle colours, reference mockup, iPad native/compatibility, Magic Convert (iPad/Duo) | 1 each (+1 Magic Convert) | All (Duo: Pro+) |
|
|
106
|
+
| `preview_titles` | AI headline + subtitle per screen, editable before paying | 0 | All |
|
|
107
|
+
| `generate_panorama` | One continuous design across the set, sliced into panels. Art directions `dynamic`, `clean`, `frameless`, `editorial`; optional hero panel; 2–4 directions = A/B test | 1 per panel per direction | All (A/B: Starter+) |
|
|
108
|
+
| `generate_hero_image` | Lifestyle hero / feature poster | 1 | All |
|
|
109
|
+
|
|
110
|
+
**Edit, convert, localize**
|
|
111
|
+
|
|
112
|
+
| Tool | What it does | Credits | Plan |
|
|
113
|
+
|---|---|---|---|
|
|
114
|
+
| `edit_screenshot` | Replace headline/subtitle, background, style, or any instruction | 1 each | Starter+ |
|
|
115
|
+
| `match_style` | Copy the look of a reference image, URL or competitor listing | 1 each | Pro+ |
|
|
116
|
+
| `convert_to_ipad` | iPhone mockup → iPad Pro 13", optional UI reflow | 1 each | Pro+ |
|
|
117
|
+
| `convert_to_duo` | iPhone mockup → iPhone Duo inner/outer display | 1 per output | Pro+ |
|
|
118
|
+
| `localize_screenshots` | 74 languages, native-sounding copy, optional cultural adaptation (colours, density, RTL) | 1 per screenshot per language | Pro+ |
|
|
119
|
+
|
|
120
|
+
**Listing, legal, growth**
|
|
121
|
+
|
|
93
122
|
| Tool | What it does | Credits |
|
|
94
123
|
|---|---|---|
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
124
|
+
| `generate_feature_graphic` | Google Play 1024×500 feature graphic (Starter+) | 1 |
|
|
125
|
+
| `analyze_screenshots` | Score 0–100, grade, strengths, fixes | 0 |
|
|
126
|
+
| `legal_pages_questions`, `generate_legal_pages`, `publish_legal_pages`, `list_legal_sites` | Privacy policy, terms, support and delete-account pages; saved locally, or hosted at `<name>.appfront.page` (hosting: monthly plans) | 0 |
|
|
127
|
+
| `track_app`, `list_tracked_apps`, `refresh_tracked_app`, `app_milestones`, `set_tracking_alerts`, `untrack_app`, `tracking_limits` | Daily ratings/reviews/installs and milestone alerts (Free 1 app, Starter 2, Pro 5, Studio 30) | 0 |
|
|
128
|
+
| `submit_to_showcase`, `my_showcase_submissions`, `withdraw_showcase_submission` | Feature your live app on the AppMockup Showcase | 0 |
|
|
129
|
+
|
|
130
|
+
**Workflow & account**
|
|
131
|
+
|
|
132
|
+
| Tool | What it does |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `capabilities`, `list_options`, `check_account` | What's possible, exact option values, credits and plan |
|
|
135
|
+
| `choose_screenshot_source`, `find_screenshots`, `list_devices`, `capture_simulator` | Pick files or capture from a simulator |
|
|
136
|
+
| `get_job`, `cancel_job` | Follow any background job (long-polls up to 55 s); cancelled items aren't charged |
|
|
137
|
+
| `list_history`, `download_assets` | Everything you generated (web or MCP). The URLs can be passed straight into edit/convert/localize/match |
|
|
138
|
+
|
|
139
|
+
Platforms: `ios`, `android`, `ipad`, `duo`. Up to 10 images per run, 20 MB each, PNG/JPEG/WebP. Web-only: landing page builder, icon resizer, store listing preview.
|
|
107
140
|
|
|
108
141
|
## Configuration
|
|
109
142
|
|
|
@@ -147,5 +180,8 @@ Debug with the MCP Inspector: `npm run build && npm run inspect`.
|
|
|
147
180
|
npm install
|
|
148
181
|
npm run build
|
|
149
182
|
node scripts/smoke-test.mjs ../public # offline tool checks
|
|
150
|
-
node scripts/pipeline-test.mjs a.png b.png c.png #
|
|
183
|
+
node scripts/pipeline-test.mjs a.png b.png c.png # screenshot pipeline against a mocked API
|
|
184
|
+
node scripts/features-test.mjs a.png # every feature tool against a mocked API
|
|
151
185
|
```
|
|
186
|
+
|
|
187
|
+
`npm run build` first copies the legal-document generator from `../services/legalDocs` (`scripts/sync-shared.mjs`), so legal pages match the web wizard exactly.
|
package/dist/api.js
CHANGED
|
@@ -33,6 +33,9 @@ function mapApiError(status, body, context) {
|
|
|
33
33
|
}
|
|
34
34
|
return new AppMockupError('API_KEY_INVALID', 'AppMockup rejected the API key.', `Copy a valid key from ${API_KEYS_URL} into APPMOCKUP_API_KEY and restart your AI client.`);
|
|
35
35
|
}
|
|
36
|
+
if (status === 402 && (code === 'APP_LIMIT' || code === 'PAID_REQUIRED')) {
|
|
37
|
+
return new AppMockupError('PLAN_REQUIRED', serverMsg || 'This needs a higher plan.', `See plans at ${PRICING_URL}.`, typeof body.limit === 'number' ? { limit: body.limit } : undefined);
|
|
38
|
+
}
|
|
36
39
|
if (status === 402) {
|
|
37
40
|
const credits = Number(body.currentCredits ?? 0);
|
|
38
41
|
return new AppMockupError('INSUFFICIENT_CREDITS', `Not enough credits (you have ${credits}).`, `Each screenshot costs 1 credit. Buy credits or upgrade at ${PRICING_URL}, then retry.`, { credits });
|
|
@@ -47,17 +50,30 @@ function mapApiError(status, body, context) {
|
|
|
47
50
|
const mins = retryAfter ? Math.ceil(retryAfter / 60) : undefined;
|
|
48
51
|
return new AppMockupError('RATE_LIMITED', serverMsg || 'Rate limit reached.', mins ? `Wait about ${mins} minute(s) and retry.` : 'Wait a few minutes and retry.', retryAfter ? { retryAfterSeconds: retryAfter } : undefined);
|
|
49
52
|
}
|
|
53
|
+
if (status === 404 && body.error && !/session/i.test(serverMsg ?? '') && typeof body.error === 'string' && !/endpoint/i.test(body.error)) {
|
|
54
|
+
// A JSON 404 from a real endpoint means "that thing does not exist" (app, batch, site).
|
|
55
|
+
return new AppMockupError('INVALID_INPUT', `${context}: ${serverMsg}`, 'Check the id or URL and try again.');
|
|
56
|
+
}
|
|
50
57
|
if (status === 404 && /session/i.test(serverMsg ?? '')) {
|
|
51
58
|
return new AppMockupError('SESSION_EXPIRED', 'The title preview expired (previews last 1 hour).', 'Run preview_titles again, or call generate_screenshots with the files directly.');
|
|
52
59
|
}
|
|
53
60
|
if (status === 404) {
|
|
54
61
|
return new AppMockupError('SERVICE_UNAVAILABLE', `${context}: this AppMockup API endpoint was not found.`, 'The server may not support MCP yet, or APPMOCKUP_API_URL points to the wrong environment (amk_test_ keys use /test-api, amk_live_ keys use /api).');
|
|
55
62
|
}
|
|
56
|
-
if (status ===
|
|
63
|
+
if (status === 409) {
|
|
64
|
+
return new AppMockupError('INVALID_INPUT', serverMsg || `${context}: this already exists.`, 'Nothing was changed.');
|
|
65
|
+
}
|
|
66
|
+
if (status === 410) {
|
|
67
|
+
return new AppMockupError('SESSION_EXPIRED', serverMsg || `${context}: this has expired.`, 'Start the operation again.');
|
|
68
|
+
}
|
|
69
|
+
if (status === 424) {
|
|
70
|
+
return new AppMockupError('SERVICE_UNAVAILABLE', serverMsg || 'The app store did not respond.', 'Try again in a few minutes.');
|
|
71
|
+
}
|
|
72
|
+
if (status === 400 || status === 413 || status === 422) {
|
|
57
73
|
return new AppMockupError('INVALID_INPUT', serverMsg ? `${context}: ${serverMsg}` : `${context}: the request was rejected.`, 'Check the screenshots and options, then retry.');
|
|
58
74
|
}
|
|
59
75
|
if (status === 503 || status === 502) {
|
|
60
|
-
return new AppMockupError('SERVICE_UNAVAILABLE', 'AppMockup is temporarily unavailable.', 'Wait a minute and retry.');
|
|
76
|
+
return new AppMockupError('SERVICE_UNAVAILABLE', serverMsg || 'AppMockup is temporarily unavailable.', 'Wait a minute and retry. Failed generations are refunded automatically.');
|
|
61
77
|
}
|
|
62
78
|
if (status === 504) {
|
|
63
79
|
return new AppMockupError('TIMEOUT', `${context} timed out on the server.`, `If a screenshot was created it appears in ${HISTORY_URL}; check there before retrying so you don't spend another credit.`);
|
|
@@ -124,8 +140,11 @@ export class ApiClient {
|
|
|
124
140
|
throw err;
|
|
125
141
|
}
|
|
126
142
|
}
|
|
143
|
+
/** Plan from the most recent account check (for plan-aware suggestions). */
|
|
144
|
+
lastPlan;
|
|
127
145
|
async account() {
|
|
128
146
|
const acc = await this.request('mcp-account.php', 'Account check', { timeoutMs: 20_000, idempotent: true });
|
|
147
|
+
this.lastPlan = acc.plan;
|
|
129
148
|
if (!this.accountChecked && acc.minClientVersion && semverLt(VERSION, acc.minClientVersion)) {
|
|
130
149
|
throw new AppMockupError('CLIENT_OUTDATED', `This AppMockup MCP server (v${VERSION}) is out of date.`, 'Restart your AI client; npx -y appmockupgenerator-mcp fetches the latest version.');
|
|
131
150
|
}
|
|
@@ -200,7 +219,7 @@ export class ApiClient {
|
|
|
200
219
|
});
|
|
201
220
|
return Number(res.updated ?? 0);
|
|
202
221
|
}
|
|
203
|
-
/** Spends 1 credit (refunded server-side
|
|
222
|
+
/** Spends 1 credit (+1 for Magic Convert); refunded server-side on failure. Never auto-retried after sending. */
|
|
204
223
|
async generate(args) {
|
|
205
224
|
const res = await this.request('generate-mockup.php', 'Screenshot generation', {
|
|
206
225
|
body: {
|
|
@@ -214,25 +233,57 @@ export class ApiClient {
|
|
|
214
233
|
customInstructions: args.customInstructions ?? '',
|
|
215
234
|
twistEnabled: args.twist,
|
|
216
235
|
twistType: args.twist ? 'ai-decides' : 'none',
|
|
236
|
+
specialEffect: args.specialEffect ?? 'none',
|
|
237
|
+
...(args.colors && (args.colors.title || args.colors.subtitle) ? { colors: args.colors } : {}),
|
|
238
|
+
...(args.ipadMode ? { ipadMode: args.ipadMode } : {}),
|
|
239
|
+
adaptUiToIpad: Boolean(args.magicConvert),
|
|
240
|
+
...(args.reference ? { referenceAssetBase64: args.reference } : {}),
|
|
217
241
|
originalFileName: args.fileName,
|
|
218
242
|
},
|
|
219
243
|
timeoutMs: 330_000,
|
|
220
244
|
idempotent: false,
|
|
221
245
|
});
|
|
222
|
-
|
|
246
|
+
return this.assetFrom(res, 'Screenshot generation');
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Generic call to an AppMockup endpoint that accepts API keys.
|
|
250
|
+
* `idempotent: false` for anything that can spend credits.
|
|
251
|
+
*/
|
|
252
|
+
async post(endpoint, context, body, opts = {}) {
|
|
253
|
+
return this.request(endpoint, context, {
|
|
254
|
+
body: { supportsAssetUrls: true, ...body },
|
|
255
|
+
timeoutMs: opts.timeoutMs ?? 120_000,
|
|
256
|
+
idempotent: opts.idempotent ?? false,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
/** Pull the stored image out of any generation response (flat or nested `mockup`). */
|
|
260
|
+
assetFrom(res, context) {
|
|
261
|
+
const mockup = (res.mockup ?? {});
|
|
262
|
+
const assets = (res.assets ?? mockup.assets ?? {});
|
|
263
|
+
const url = assets.origUrl;
|
|
223
264
|
if (!url) {
|
|
224
|
-
throw new AppMockupError('GENERATION_FAILED',
|
|
265
|
+
throw new AppMockupError('GENERATION_FAILED', `${context} finished without returning a stored image.`, `Check ${HISTORY_URL}; if nothing is there, retry.`);
|
|
225
266
|
}
|
|
226
267
|
this.assertCdnUrl(url);
|
|
227
268
|
return {
|
|
228
269
|
url,
|
|
229
|
-
title: res.
|
|
230
|
-
subtitle: res.
|
|
231
|
-
width: Number(
|
|
232
|
-
height: Number(
|
|
270
|
+
title: String(mockup.title ?? res.title ?? ''),
|
|
271
|
+
subtitle: String(mockup.subtitle ?? res.subtitle ?? ''),
|
|
272
|
+
width: Number(assets.width ?? 0),
|
|
273
|
+
height: Number(assets.height ?? 0),
|
|
233
274
|
creditsRemaining: typeof res.creditsRemaining === 'number' ? res.creditsRemaining : null,
|
|
234
275
|
};
|
|
235
276
|
}
|
|
277
|
+
/** True for https URLs on AppMockup's own CDN (the only URLs the API accepts as image input). */
|
|
278
|
+
isOwnCdnUrl(raw) {
|
|
279
|
+
try {
|
|
280
|
+
const u = new URL(raw);
|
|
281
|
+
return u.protocol === 'https:' && this.config.allowedCdnHosts.includes(u.hostname);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
236
287
|
/** Download a finished asset. Only our own CDN, bounded size, verified bytes. */
|
|
237
288
|
async download(url) {
|
|
238
289
|
this.assertCdnUrl(url);
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product catalog exposed to the model. Mirrors the web app:
|
|
3
|
+
* - styles: components/StyleSelector.tsx
|
|
4
|
+
* - panorama variants: services/geminiPhpService.ts PANORAMA_VARIANTS / api-php/panorama-prompt.php
|
|
5
|
+
* - feature graphic: pages/FeatureGraphicPage.tsx
|
|
6
|
+
* - localize langs: pages/LocalizeScreenshotsPage.tsx SUPPORTED_LANGUAGES
|
|
7
|
+
* - plan gating: services/tierService.ts (enforced server-side for API keys)
|
|
8
|
+
*/
|
|
9
|
+
export const STYLES = [
|
|
10
|
+
'Default',
|
|
11
|
+
'Futuristic Dark Mode',
|
|
12
|
+
'Clean & Minimal Light',
|
|
13
|
+
'Vibrant Gradient',
|
|
14
|
+
'Professional Corporate Blue',
|
|
15
|
+
'Claymorphism 3D',
|
|
16
|
+
'Elegant Pastel',
|
|
17
|
+
];
|
|
18
|
+
/** Languages for AI-written copy during generation. */
|
|
19
|
+
export const LANGUAGES = ['en', 'es', 'fr', 'de', 'it', 'pt', 'nl', 'ja', 'ko', 'zh', 'ar', 'hi', 'ru', 'tr', 'pl', 'sv', 'id', 'th', 'vi'];
|
|
20
|
+
export const PANORAMA_VARIANTS = [
|
|
21
|
+
{ id: 'dynamic', label: 'Dynamic scene', description: 'Layered photoreal scene with 3D objects and features popping out of the phone. The boldest.' },
|
|
22
|
+
{ id: 'clean', label: 'Minimal', description: 'One refined surface, devices and type. No props, no effects. Calm and premium.' },
|
|
23
|
+
{ id: 'frameless', label: 'Frameless UI', description: 'No phone frame: the screen as a large rounded card on a soft backdrop. Most readable UI.' },
|
|
24
|
+
{ id: 'editorial', label: 'Bold editorial', description: 'Flat poster design: colour blocking, oversized type, graphic shapes. Built to stop a scroll.' },
|
|
25
|
+
];
|
|
26
|
+
export const PANORAMA_MAX_PANELS = 10;
|
|
27
|
+
export const FEATURE_GRAPHIC_STYLES = ['auto', 'minimal', 'bold', '3d-premium', 'lifestyle', 'dark'];
|
|
28
|
+
/** Store localization languages (code → name). The name is what the renderer is told. */
|
|
29
|
+
export const LOCALIZE_LANGUAGES = {
|
|
30
|
+
en: 'English', zh: 'Chinese (Simplified)', 'zh-TW': 'Chinese (Traditional)', ja: 'Japanese', ko: 'Korean', de: 'German',
|
|
31
|
+
es: 'Spanish', fr: 'French', 'pt-BR': 'Portuguese (Brazil)', ru: 'Russian', it: 'Italian', ar: 'Arabic', hi: 'Hindi',
|
|
32
|
+
tr: 'Turkish', nl: 'Dutch', id: 'Indonesian', th: 'Thai', vi: 'Vietnamese', pl: 'Polish', sv: 'Swedish',
|
|
33
|
+
af: 'Afrikaans', am: 'Amharic', hy: 'Armenian', az: 'Azerbaijani', eu: 'Basque', be: 'Belarusian', bn: 'Bangla',
|
|
34
|
+
bg: 'Bulgarian', my: 'Burmese', ca: 'Catalan', hr: 'Croatian', cs: 'Czech', da: 'Danish', et: 'Estonian',
|
|
35
|
+
fil: 'Filipino', fi: 'Finnish', 'fr-CA': 'French (Canada)', gl: 'Galician', ka: 'Georgian', el: 'Greek',
|
|
36
|
+
gu: 'Gujarati', he: 'Hebrew', hu: 'Hungarian', is: 'Icelandic', kn: 'Kannada', kk: 'Kazakh', km: 'Khmer',
|
|
37
|
+
ky: 'Kyrgyz', lo: 'Lao', lv: 'Latvian', lt: 'Lithuanian', mk: 'Macedonian', ms: 'Malay', ml: 'Malayalam',
|
|
38
|
+
mr: 'Marathi', mn: 'Mongolian', ne: 'Nepali', no: 'Norwegian', or: 'Odia', fa: 'Persian', pt: 'Portuguese (Portugal)',
|
|
39
|
+
pa: 'Punjabi', ro: 'Romanian', sr: 'Serbian', si: 'Sinhala', sk: 'Slovak', sl: 'Slovenian', 'es-MX': 'Spanish (Mexico)',
|
|
40
|
+
sw: 'Swahili', ta: 'Tamil', te: 'Telugu', uk: 'Ukrainian', ur: 'Urdu', zu: 'Zulu',
|
|
41
|
+
};
|
|
42
|
+
/** Human summary returned by the `capabilities` tool. Keep in sync with the tools. */
|
|
43
|
+
export const CAPABILITIES = `AppMockup can do all of this from your AI assistant (1 credit = 1 generated image unless noted):
|
|
44
|
+
|
|
45
|
+
CREATE STORE SCREENSHOTS
|
|
46
|
+
• generate_screenshots: raw screenshots → App Store / Google Play marketing screenshots with device frame, background and headline.
|
|
47
|
+
Platforms: iPhone (ios), Android, iPad, iPhone Duo (Pro/Studio). Modes: clean (minimal) or dynamic (themed 3D scenes).
|
|
48
|
+
Options: 7 visual styles, copy in ${LANGUAGES.length} languages, pop-out effect, custom title/subtitle colours, art-direction instructions,
|
|
49
|
+
match a reference mockup, iPad native vs compatibility, Magic Convert (rebuild a phone UI as a real iPad/Duo layout, +1 credit).
|
|
50
|
+
• preview_titles (free): AI headline + subtitle per screen to review and edit before paying.
|
|
51
|
+
• generate_panorama: the whole set as ONE continuous wide design, sliced into panels. Art directions: dynamic, clean (minimal),
|
|
52
|
+
frameless, editorial. Optional hero first panel. A/B test 2-4 art directions in one go (Starter+). Up to ${PANORAMA_MAX_PANELS} panels.
|
|
53
|
+
• generate_hero_image: a lifestyle "feature poster" hero panel for the listing.
|
|
54
|
+
|
|
55
|
+
EDIT & REUSE
|
|
56
|
+
• edit_screenshot (Starter+): change the headline/subtitle, background, style, or any free-form edit on a finished mockup.
|
|
57
|
+
• match_style (Pro+): copy the look of a reference screenshot (yours or a competitor's, by image or store URL).
|
|
58
|
+
• convert_to_ipad (Pro+): turn an iPhone mockup into an iPad Pro 13" store screenshot, optionally reflowing the UI.
|
|
59
|
+
• convert_to_duo (Pro/Studio): turn an iPhone mockup into iPhone Duo inner or outer display screenshots.
|
|
60
|
+
|
|
61
|
+
GO GLOBAL
|
|
62
|
+
• localize_screenshots (Pro+): translate finished screenshots into ${Object.keys(LOCALIZE_LANGUAGES).length} store languages with native-sounding copy,
|
|
63
|
+
optional cultural adaptation (colours, density, typography, RTL). 1 credit per screenshot per language.
|
|
64
|
+
|
|
65
|
+
STORE ASSETS & LISTING
|
|
66
|
+
• generate_feature_graphic (Starter+): Google Play 1024×500 feature graphic.
|
|
67
|
+
• analyze_screenshots (free): score 0-100, grade, strengths and fixes for your current screenshots.
|
|
68
|
+
• legal_pages_questions / generate_legal_pages / publish_legal_pages: privacy policy, terms, support and delete-account pages
|
|
69
|
+
(free to generate; hosting on yourapp.appfront.page is a monthly-plan feature).
|
|
70
|
+
|
|
71
|
+
GROWTH
|
|
72
|
+
• App tracking: track_app, list_tracked_apps, refresh_tracked_app, app_milestones, untrack_app, tracking_limits — daily ratings/
|
|
73
|
+
reviews/installs and milestone alerts (Free: 1 app, Starter 2, Pro 5, Studio 30).
|
|
74
|
+
• Showcase: submit_to_showcase, my_showcase_submissions, withdraw_showcase_submission — feature your live app on appmockupgenerator.com.
|
|
75
|
+
|
|
76
|
+
HISTORY
|
|
77
|
+
• list_history / download_assets: everything you generated (web or MCP), reusable as input to edit/convert/localize.
|
|
78
|
+
|
|
79
|
+
WEB-ONLY (open appmockupgenerator.com): AI landing page builder, icon resizer, store listing preview.`;
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { AppMockupError, API_KEYS_URL } from './errors.js';
|
|
4
|
-
export const VERSION = '1.
|
|
4
|
+
export const VERSION = '1.1.0';
|
|
5
5
|
const KEY_RE = /^amk_(live|test)_[A-Za-z0-9]{40}$/;
|
|
6
6
|
const PRODUCTION_API = 'https://api.appmockupgenerator.com/api';
|
|
7
7
|
const STAGING_API = 'https://api.appmockupgenerator.com/test-api';
|
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { AppMockupError, PRICING_URL, toAppMockupError } from './errors.js';
|
|
6
|
+
import { MAX_FILES_PER_JOB, readLocalImage, resolveUserPath } from './images.js';
|
|
7
|
+
export const ok = (text, structured) => ({
|
|
8
|
+
content: [{ type: 'text', text }],
|
|
9
|
+
...(structured ? { structuredContent: structured } : {}),
|
|
10
|
+
});
|
|
11
|
+
export const fail = (err) => ({
|
|
12
|
+
content: [{ type: 'text', text: toAppMockupError(err).toText() }],
|
|
13
|
+
isError: true,
|
|
14
|
+
});
|
|
15
|
+
/** Wraps every handler so no exception (or secret) escapes unformatted. */
|
|
16
|
+
export function safe(log, name, fn) {
|
|
17
|
+
return async (args) => {
|
|
18
|
+
try {
|
|
19
|
+
return await fn(args);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
const e = toAppMockupError(err);
|
|
23
|
+
if (e.code === 'SERVER_ERROR' && !(err instanceof AppMockupError)) {
|
|
24
|
+
log(`tool ${name} unexpected error: ${err?.message ?? String(err)}`);
|
|
25
|
+
}
|
|
26
|
+
return fail(e);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Workspace root from the client (MCP roots), falling back to the process cwd. */
|
|
31
|
+
export async function baseDir(ctx) {
|
|
32
|
+
try {
|
|
33
|
+
if (ctx.server.server.getClientCapabilities()?.roots) {
|
|
34
|
+
const { roots } = await ctx.server.server.listRoots(undefined, { timeout: 3000 });
|
|
35
|
+
const first = roots.find((r) => r.uri.startsWith('file://'));
|
|
36
|
+
if (first)
|
|
37
|
+
return fileURLToPath(first.uri);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch { /* client without roots support */ }
|
|
41
|
+
return process.cwd();
|
|
42
|
+
}
|
|
43
|
+
export async function writableDir(dir) {
|
|
44
|
+
try {
|
|
45
|
+
await fs.mkdir(dir, { recursive: true });
|
|
46
|
+
await fs.access(dir, fs.constants.W_OK);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Where results go: explicit arg > APPMOCKUP_OUTPUT_DIR > <workspace>/appmockup-output > ~/AppMockup. */
|
|
54
|
+
export async function outputRoot(ctx, explicit) {
|
|
55
|
+
const base = await baseDir(ctx);
|
|
56
|
+
if (explicit)
|
|
57
|
+
return resolveUserPath(explicit, base);
|
|
58
|
+
if (ctx.config.outputDir)
|
|
59
|
+
return ctx.config.outputDir;
|
|
60
|
+
const isSystemDir = base === path.parse(base).root || base === os.homedir();
|
|
61
|
+
const candidate = path.join(base, 'appmockup-output');
|
|
62
|
+
if (!isSystemDir && await writableDir(candidate))
|
|
63
|
+
return candidate;
|
|
64
|
+
return path.join(os.homedir(), 'AppMockup');
|
|
65
|
+
}
|
|
66
|
+
export const stamp = () => new Date().toISOString().replace(/[-:]/g, '').replace('T', '-').slice(0, 15);
|
|
67
|
+
/** <root>/<timestamp>-<label> unless the user picked a folder. */
|
|
68
|
+
export async function jobOutputDir(ctx, explicit, label) {
|
|
69
|
+
const root = await outputRoot(ctx, explicit);
|
|
70
|
+
return explicit ? root : path.join(root, `${stamp()}-${label}`);
|
|
71
|
+
}
|
|
72
|
+
export function canElicit(ctx) {
|
|
73
|
+
return Boolean(ctx.server.server.getClientCapabilities()?.elicitation);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Credit gate shared by every paid tool: checks the balance, then asks the user
|
|
77
|
+
* (elicitation dialog when supported, otherwise via the assistant + confirm flag).
|
|
78
|
+
* Returns a tool result to send back when the run must NOT start, or null to proceed.
|
|
79
|
+
*/
|
|
80
|
+
export async function confirmSpend(ctx, opts) {
|
|
81
|
+
const acc = await ctx.api.account();
|
|
82
|
+
if (acc.credits < opts.cost) {
|
|
83
|
+
throw new AppMockupError('INSUFFICIENT_CREDITS', `${opts.what} needs ${opts.cost} credit(s) but you have ${acc.credits}.`, `Buy credits or upgrade at ${acc.pricingUrl || PRICING_URL}, or reduce the request.`, { credits: acc.credits, needed: opts.cost });
|
|
84
|
+
}
|
|
85
|
+
const summary = `${opts.what} for ${opts.cost} credit(s)? You have ${acc.credits}.`;
|
|
86
|
+
if (canElicit(ctx)) {
|
|
87
|
+
const res = await ctx.server.server.elicitInput({
|
|
88
|
+
mode: 'form',
|
|
89
|
+
message: summary,
|
|
90
|
+
requestedSchema: { type: 'object', properties: { confirm: { type: 'boolean', title: 'Spend credits and continue', default: true } }, required: ['confirm'] },
|
|
91
|
+
});
|
|
92
|
+
if (res.action !== 'accept' || res.content?.confirm !== true) {
|
|
93
|
+
return ok('The user declined. Nothing was generated and no credits were spent.', { started: false });
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (!opts.confirmed) {
|
|
98
|
+
return ok(`Confirmation needed. Ask the user: "${summary}" If they agree, call ${opts.toolName} again with the same arguments and confirm_credit_spend: true.`, { started: false, needs_confirmation: true, credits: acc.credits, cost: opts.cost });
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Accepts local file paths and AppMockup CDN URLs (e.g. a URL returned by an
|
|
104
|
+
* earlier job or list_history). Local files are validated and uploaded once.
|
|
105
|
+
*/
|
|
106
|
+
export async function resolveImages(ctx, inputs, opts = {}) {
|
|
107
|
+
const max = opts.max ?? MAX_FILES_PER_JOB;
|
|
108
|
+
if (!Array.isArray(inputs) || inputs.length === 0) {
|
|
109
|
+
throw new AppMockupError('INVALID_INPUT', `No ${opts.label ?? 'images'} were given.`, 'Pass local file paths, or AppMockup image URLs from a previous result or list_history.');
|
|
110
|
+
}
|
|
111
|
+
if (inputs.length > max) {
|
|
112
|
+
throw new AppMockupError('TOO_MANY_FILES', `${inputs.length} images given; the maximum here is ${max}.`, 'Select fewer images.');
|
|
113
|
+
}
|
|
114
|
+
const base = await baseDir(ctx);
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const raw of inputs) {
|
|
117
|
+
const value = String(raw ?? '').trim();
|
|
118
|
+
if (/^https?:\/\//i.test(value)) {
|
|
119
|
+
if (!ctx.api.isOwnCdnUrl(value)) {
|
|
120
|
+
throw new AppMockupError('INVALID_INPUT', `Only AppMockup image URLs can be used directly (got ${new URL(value).hostname}).`, 'Download the image and pass the local file path instead.');
|
|
121
|
+
}
|
|
122
|
+
out.push({ ref: value, name: path.basename(new URL(value).pathname) || 'asset.png', source: value });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const img = await readLocalImage(value, base);
|
|
126
|
+
const url = await ctx.api.uploadScreenshot(img.data, img.mime);
|
|
127
|
+
out.push({ ref: url ?? `data:${img.mime};base64,${img.data.toString('base64')}`, name: img.name, source: img.path, local: img });
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -8,13 +8,17 @@ import { JobManager } from './jobs.js';
|
|
|
8
8
|
import { registerTools } from './tools.js';
|
|
9
9
|
// stdout is the MCP protocol channel: all diagnostics go to stderr, and never include secrets.
|
|
10
10
|
const log = (msg) => process.stderr.write(`[appmockupgenerator-mcp] ${msg}\n`);
|
|
11
|
-
const INSTRUCTIONS = `AppMockup
|
|
12
|
-
Workflow:
|
|
11
|
+
const INSTRUCTIONS = `AppMockup creates App Store / Google Play marketing assets: store screenshots, panorama sets, hero images, feature graphics, edits, style matches, iPad/iPhone Duo conversions, localization into 70+ languages, plus a free screenshot analyzer, legal pages, app tracking and the Showcase. Call capabilities when the user asks what is possible.
|
|
12
|
+
Workflow for new screenshots:
|
|
13
13
|
1. If the user has not given screenshot files, call choose_screenshot_source and follow the user's choice. Recommend existing files; only capture from a simulator when the user asks, one screen at a time, after the user navigates to it.
|
|
14
|
-
2.
|
|
15
|
-
3.
|
|
16
|
-
4.
|
|
17
|
-
|
|
14
|
+
2. Call choose_design so the user picks the layout, art direction and platform. Do not skip this: many users don't know panorama exists. Panorama (one continuous design across the set, plus a hero panel) is the recommended default, the same as on appmockupgenerator.com; individual screenshots (clean or dynamic) are the alternative. Skip it only if the user already named the layout.
|
|
15
|
+
3. Panorama → generate_panorama. Individual → optionally preview_titles (free) so the user can edit the copy, then generate_screenshots.
|
|
16
|
+
4. Every paid tool states its credit cost and needs the user's confirmation. Paid tools run in the background: follow them with get_job (wait_seconds 55) until finished, then report the saved file paths.
|
|
17
|
+
Results include AppMockup URLs that can be fed straight into edit_screenshot, convert_to_ipad, convert_to_duo, localize_screenshots or match_style.
|
|
18
|
+
NEXT STEPS: finished jobs and most tools end with a "NEXT STEPS" block tailored to what the user just made, their plan and what they already did. Always show it to the user as a short numbered menu at the end of your reply (e.g. "Want me to localize these into Japanese and German, make the iPad versions, or change a headline?"). Never run a suggestion without the user's go-ahead. If the user picks one, the block already lists the tool and the image URLs to pass.
|
|
19
|
+
When a tool answers with needs_input (e.g. which languages, which reference, what to change), ask the user that question; nothing was charged.
|
|
20
|
+
Some tools need a plan (edit/feature graphic: Starter+; match style, iPad/Duo conversion, localization: Pro+). If one returns PLAN_REQUIRED, tell the user which plan unlocks it.
|
|
21
|
+
Errors include a code and a "How to fix" line: relay the fix to the user instead of retrying blindly. Never retry a generation that timed out without checking list_history first.`;
|
|
18
22
|
async function main() {
|
|
19
23
|
let config;
|
|
20
24
|
try {
|