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 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
- | `check_account` | Key status, credits, plan | 0 |
96
- | `list_options` | Platforms, styles, languages, limits | 0 |
97
- | `choose_screenshot_source` | Asks you: files or simulator | 0 |
98
- | `find_screenshots` | Lists screenshot-sized images in a folder | 0 |
99
- | `list_devices` | Booted iOS Simulators / Android emulators | 0 |
100
- | `capture_simulator` | Saves the current screen of one device | 0 |
101
- | `preview_titles` | Proposed marketing copy per screenshot | 0 |
102
- | `generate_screenshots` | Creates the store screenshots (background job) | 1 per screenshot |
103
- | `get_job` | Progress and results (long-polls up to 55 s) | 0 |
104
- | `cancel_job` | Stops a job; unstarted screenshots aren't charged | 0 |
105
-
106
- Platforms: `ios`, `android`, `ipad`, `duo` (iPhone Duo is on the Pro and Studio plans). Up to 10 screenshots per run, 20 MB each, PNG/JPEG/WebP.
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 # full pipeline against a mocked API
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 === 400 || status === 413) {
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 if generation fails). Never auto-retried after sending. */
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
- const url = res.mockup?.assets?.origUrl;
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', 'The server finished without returning an image.', `Check ${HISTORY_URL}; if nothing is there, retry this screenshot.`);
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.mockup?.title ?? '',
230
- subtitle: res.mockup?.subtitle ?? '',
231
- width: Number(res.mockup?.assets?.width ?? 0),
232
- height: Number(res.mockup?.assets?.height ?? 0),
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);
@@ -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.0.0';
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';
@@ -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 turns raw app screenshots into App Store / Google Play marketing screenshots.
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. Confirm the platform (ios, android, ipad, duo).
15
- 3. Optionally run preview_titles (free) and let the user edit the copy.
16
- 4. generate_screenshots costs 1 credit per screenshot: confirm the cost with the user first. It runs in the background; follow it with get_job (wait_seconds 55) until finished, then report the saved file paths.
17
- 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 History first.`;
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 {