@just-every/design 0.1.31 → 0.1.33

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/dist/server.js CHANGED
@@ -4,11 +4,17 @@
4
4
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
6
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
7
+ import { statSync } from 'node:fs';
7
8
  import { readFile } from 'node:fs/promises';
8
- import { fileURLToPath } from 'node:url';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import { fileURLToPath, pathToFileURL } from 'node:url';
9
12
  import { DesignAppClient } from './design-client.js';
13
+ import { INSTRUCTIONS_MD } from './instructions.js';
14
+ import { runScreenshotCommand } from './screenshot.js';
10
15
  import { buildCreateRunRequest, NOT_AUTHENTICATED_HELP, watchRun } from './tool-logic.js';
11
- import { wrapToolResponse } from './response-guidance.js';
16
+ import { formatToolResponseMarkdown, wrapToolResponse } from './response-guidance.js';
17
+ import { createSyncProgressTracker, getProgressPct } from './progress.js';
12
18
  export async function startMcpServer(options) {
13
19
  const config = options.config;
14
20
  const sessionToken = config.sessionToken ?? '';
@@ -34,155 +40,51 @@ export async function startMcpServer(options) {
34
40
  tools: [
35
41
  {
36
42
  name: 'design.create',
37
- description: 'Create a design run in Every Design (async). Use this to generate things like: ' +
38
- 'web/landing page designs (HTML/CSS + assets), logos, icons, marketing graphics/illustrations, ' +
39
- 'photorealistic images, backgrounds/patterns, and even 3D assets (GLB). ' +
40
- 'Attach 1+ source images via `sourceImages` (local path, URL, or base64) or pass web URLs via `referenceUrls`/`references`. ' +
41
- 'Note: when using this MCP server/CLI, HTML output is opt-in (use output.designKind=interface and enable progression.autoGenerateHtml/autoExtractAssets or call `design.generateHtml`). If output.designKind is omitted, this server defaults to output.designKind=interface to avoid expensive code builds while still producing a UI design. For interface runs, asset extraction and HTML generation are deferred by default; use `design.extractAssets` and `design.generateHtml` when you explicitly want those outputs. To allow backend inference, explicitly set output.designKind=auto. ' +
42
- 'Returns a run id; follow up with `design.watch`, `design.get`, and `design.artifacts.*`.',
43
+ description: 'Create a new design. Returns run id. You must use design.create -> design.sync -> design.check in that order for interface creation or visual tasks (UI, Web, Mobile, Logos, Icons, 3D, Photos).',
43
44
  inputSchema: {
44
45
  type: 'object',
45
46
  properties: {
46
47
  prompt: { type: 'string', description: 'Prompt for the design run.' },
47
48
  parentRunId: {
48
49
  type: 'string',
49
- description: 'Optional parent run id to associate this run with an existing run/thread (useful when a CLI is working inside a run and wants related requests grouped).',
50
- },
51
- output: {
52
- type: 'object',
53
- description: 'Optional output controls. If omitted, the system tries to infer the best kind from the prompt.',
54
- properties: {
55
- designKind: {
56
- type: 'string',
57
- description: 'What to generate. Use interface for UI designs (image target by default; HTML/code is opt-in via progression flags); use 3dasset for GLB output.',
58
- enum: ['auto', 'logo', 'icon', 'interface', 'graphic', 'photo', 'background', '3dasset'],
59
- },
60
- format: {
61
- type: 'string',
62
- description: 'Optional format hint for image outputs.',
63
- enum: ['png', 'webp'],
64
- },
65
- assetFormats: {
66
- type: 'array',
67
- description: 'Optional formats for 3d assets (glb is always produced).',
68
- items: { type: 'string', enum: ['glb', 'fbx'] },
69
- },
70
- },
71
- additionalProperties: true,
72
- },
73
- style: {
74
- type: 'string',
75
- description: 'Optional style string appended to the prompt (e.g. “minimal, Swiss, high-contrast”).',
50
+ description: 'Optional request changes to a target design or new designs based on a previous run.',
76
51
  },
77
52
  referenceUrls: {
78
53
  type: 'array',
79
- description: 'Optional web-accessible URLs to use as visual reference/inspiration.',
54
+ description: 'Optional public web-accessible URLs to use as visual reference/inspiration.',
80
55
  items: { type: 'string' },
81
56
  maxItems: 8,
82
57
  },
83
- references: {
84
- type: 'array',
85
- description: 'Optional structured references (advanced). Most users should prefer `sourceImages` or `referenceUrls`.',
86
- items: {
87
- type: 'object',
88
- properties: {
89
- type: { type: 'string', enum: ['url', 'path'] },
90
- url: { type: 'string' },
91
- path: { type: 'string' },
92
- filename: { type: 'string' },
93
- userProvided: { type: 'boolean' },
94
- },
95
- required: ['type'],
96
- additionalProperties: true,
97
- },
98
- maxItems: 8,
99
- },
100
58
  sourceImages: {
101
59
  type: 'array',
102
- description: 'Optional images to upload to Every Design and use as references. Supports local file paths, URLs, and base64.',
60
+ description: 'Optional local images to upload to Every Design and use as references. Supports local file paths and URLs.',
103
61
  items: {
104
62
  type: 'object',
105
63
  properties: {
106
- type: { type: 'string', enum: ['path', 'url', 'base64'] },
107
- path: { type: 'string', description: 'Local file path (png/jpg).', },
108
- url: { type: 'string', description: 'Public URL of an image.', },
109
- data: { type: 'string', description: 'Base64 payload (optionally a data: URL).', },
110
- mime: { type: 'string', description: 'MIME type for base64 data (e.g. image/png).', },
111
- filename: { type: 'string', description: 'Optional filename override.', },
64
+ type: { type: 'string', enum: ['path', 'url'] },
65
+ path: { type: 'string', description: 'Local file path (png/jpg).' },
66
+ url: { type: 'string', description: 'Public URL of an image.' },
112
67
  },
113
68
  required: ['type'],
114
69
  additionalProperties: false,
115
70
  },
116
71
  maxItems: 8,
117
72
  },
118
- config: {
119
- type: 'object',
120
- description: 'Advanced pass-through config merged with the convenience fields above. Useful keys include: ' +
121
- '`output` (designKind/format/assetFormats), `references` (url/path refs), `imageLoop`, `codeLoop`, and `refinementLoop`.',
122
- additionalProperties: true,
123
- },
124
73
  },
125
74
  required: ['prompt'],
126
75
  additionalProperties: false,
127
76
  },
128
77
  },
129
78
  {
130
- name: 'design.list',
131
- description: 'List recent design runs for the currently selected organization (chosen during auth login).',
132
- inputSchema: {
133
- type: 'object',
134
- properties: {
135
- page: { type: 'number', description: 'Page number (default 1).' },
136
- limit: { type: 'number', description: 'Page size (default 20, max 50).' },
137
- },
138
- additionalProperties: false,
139
- },
140
- },
141
- {
142
- name: 'design.get',
143
- description: 'Fetch a single design run by id (status, prompt, timestamps, etc).',
79
+ name: 'design.sync',
80
+ description: 'Wait for a design to complete, extracts assets and sync to a folder. Takes up to 30 mins for a fresh design. MCP clients should call with a long timeout (>=30 minutes).',
144
81
  inputSchema: {
145
82
  type: 'object',
146
83
  properties: {
147
84
  runId: { type: 'string' },
148
- },
149
- required: ['runId'],
150
- additionalProperties: false,
151
- },
152
- },
153
- {
154
- name: 'design.watch',
155
- description: 'Watch a run until it finishes by polling status. Optionally sync useful artifacts to a local folder (downloads missing files and can trigger asset extraction when needed).',
156
- inputSchema: {
157
- type: 'object',
158
- properties: {
159
- runId: { type: 'string' },
160
- timeoutSeconds: { type: 'number', description: 'Max wait time (default 3600).' },
161
- intervalSeconds: { type: 'number', description: 'Polling interval (default 5).' },
162
- syncDir: { type: 'string', description: 'Optional local directory to sync artifacts into.' },
163
- ensureAssets: { type: 'boolean', description: 'Whether to trigger extract-assets if assets are missing when syncDir is set (default true).' },
164
- targetArtifactId: { type: 'string', description: 'Optional: select this artifact as the run target before extracting/syncing.' },
165
- },
166
- required: ['runId'],
167
- additionalProperties: false,
168
- },
169
- },
170
- {
171
- name: 'design.extractAssets',
172
- description: 'Trigger the asset extraction phase for an existing interface run (useful when runs are configured to stop after the target image by default). ' +
173
- 'If you pass targetArtifactId, this tool will first set the run\'s explicit target selection to that artifact (so subsequent progress actions use the same target unless cleared).',
174
- inputSchema: {
175
- type: 'object',
176
- properties: {
177
- runId: { type: 'string' },
178
- targetArtifactId: {
179
- type: 'string',
180
- description: 'Optional artifact id to explicitly set as the run\'s target image before extracting assets (must be an image draft artifact from the same run).',
181
- },
182
- designKind: {
85
+ syncDir: {
183
86
  type: 'string',
184
- description: 'Optional explicit kind to persist for the run when progressing (interface).',
185
- enum: ['interface'],
87
+ description: 'Optional local directory to sync artifacts into (default: ./every-design/<run-id>).',
186
88
  },
187
89
  },
188
90
  required: ['runId'],
@@ -190,187 +92,25 @@ export async function startMcpServer(options) {
190
92
  },
191
93
  },
192
94
  {
193
- name: 'design.generateHtml',
194
- description: 'Trigger HTML generation (assets + code) for an existing interface run. This is separate from design.create so HTML builds are not automatic by default. ' +
195
- 'If you pass targetArtifactId, this tool will first set the run\'s explicit target selection to that artifact.',
95
+ name: 'design.check',
96
+ description: 'Returns a screenshot and guidance on how close we are to matching a design run. Accepts either URL or path to html file. MCP clients should call with a long timeout (>=15 minutes).',
196
97
  inputSchema: {
197
98
  type: 'object',
198
99
  properties: {
199
100
  runId: { type: 'string' },
200
- targetArtifactId: {
201
- type: 'string',
202
- description: 'Optional artifact id to explicitly set as the run\'s target image before generating HTML (must be an image draft artifact from the same run).',
203
- },
101
+ url: { type: 'string', description: 'HTTP(S) URL to screenshot (local URL is allowed).' },
102
+ path: { type: 'string', description: 'Absolute or relative path to a local HTML file.' },
204
103
  },
205
104
  required: ['runId'],
206
105
  additionalProperties: false,
207
106
  },
208
107
  },
209
108
  {
210
- name: 'design.iterate',
211
- description: 'Iterate on an existing run using screenshot feedback. This creates an iterate operation under the run, waits for completion, and returns the iterate report JSON. ' +
212
- 'Provide 1+ render screenshots via `render`. Optionally include a `target` image; if omitted, the backend will use the run\'s latest target image.',
213
- inputSchema: {
214
- type: 'object',
215
- properties: {
216
- runId: { type: 'string' },
217
- concerns: { type: 'string', description: 'Optional: what to focus on (spacing, hierarchy, etc).' },
218
- viewport: {
219
- type: 'object',
220
- description: 'Optional viewport hint used for iteration context.',
221
- properties: {
222
- width: { type: 'number' },
223
- height: { type: 'number' },
224
- },
225
- additionalProperties: false,
226
- },
227
- target: {
228
- type: 'array',
229
- description: 'Optional target/reference images (path/url/base64). If omitted, the run\'s latest target-image is used.',
230
- items: {
231
- type: 'object',
232
- properties: {
233
- type: { type: 'string', enum: ['path', 'url', 'base64'] },
234
- path: { type: 'string' },
235
- url: { type: 'string' },
236
- data: { type: 'string' },
237
- mime: { type: 'string' },
238
- filename: { type: 'string' },
239
- },
240
- required: ['type'],
241
- additionalProperties: false,
242
- },
243
- maxItems: 4,
244
- },
245
- render: {
246
- type: 'array',
247
- description: 'Render/current screenshots to iterate on (path/url/base64).',
248
- items: {
249
- type: 'object',
250
- properties: {
251
- type: { type: 'string', enum: ['path', 'url', 'base64'] },
252
- path: { type: 'string' },
253
- url: { type: 'string' },
254
- data: { type: 'string' },
255
- mime: { type: 'string' },
256
- filename: { type: 'string' },
257
- },
258
- required: ['type'],
259
- additionalProperties: false,
260
- },
261
- maxItems: 4,
262
- },
263
- timeoutSeconds: { type: 'number', description: 'Max wait time for the iterate operation (default 900).' },
264
- intervalSeconds: { type: 'number', description: 'Polling interval (default 5).' },
265
- },
266
- required: ['runId', 'render'],
267
- additionalProperties: false,
268
- },
269
- },
270
- {
271
- name: 'design.refineDraft',
272
- description: 'Refine a specific draft image within the same run by generating a new refined draft (non-fork). ' +
273
- 'Use this for quick, single-pass iteration without forking the run thread.',
109
+ name: 'design.instructions',
110
+ description: 'Return the Every Design MCP workflow instructions for gudiance on how to use these tools.',
274
111
  inputSchema: {
275
112
  type: 'object',
276
- properties: {
277
- runId: { type: 'string' },
278
- artifactId: { type: 'string', description: 'Draft/refined draft (or target) artifact id to refine.' },
279
- prompt: { type: 'string', description: 'Refinement instructions (what to change).', minLength: 1 },
280
- },
281
- required: ['runId', 'artifactId', 'prompt'],
282
- additionalProperties: false,
283
- },
284
- },
285
- {
286
- name: 'design.uploadTarget',
287
- description: 'Upload an image and set it as the run\'s explicit target selection (used by extract-assets and generate-html).',
288
- inputSchema: {
289
- type: 'object',
290
- properties: {
291
- runId: { type: 'string' },
292
- sourceImage: {
293
- type: 'object',
294
- description: 'Image to upload (path/url/base64).',
295
- properties: {
296
- type: { type: 'string', enum: ['path', 'url', 'base64'] },
297
- path: { type: 'string' },
298
- url: { type: 'string' },
299
- data: { type: 'string' },
300
- mime: { type: 'string' },
301
- filename: { type: 'string' },
302
- },
303
- required: ['type'],
304
- additionalProperties: false,
305
- },
306
- },
307
- required: ['runId', 'sourceImage'],
308
- additionalProperties: false,
309
- },
310
- },
311
- {
312
- name: 'design.setTarget',
313
- description: 'Select which draft image should be treated as the canonical target for this run (used by extract-assets and generate-html). ' +
314
- 'This persists in the run config until changed or cleared.',
315
- inputSchema: {
316
- type: 'object',
317
- properties: {
318
- runId: { type: 'string' },
319
- artifactId: {
320
- type: 'string',
321
- description: 'Artifact id of a draft/refined draft image in this run.',
322
- },
323
- },
324
- required: ['runId', 'artifactId'],
325
- additionalProperties: false,
326
- },
327
- },
328
- {
329
- name: 'design.clearTarget',
330
- description: 'Clear any explicit target selection for this run (reverts to default target selection behavior on subsequent progress actions).',
331
- inputSchema: {
332
- type: 'object',
333
- properties: {
334
- runId: { type: 'string' },
335
- },
336
- required: ['runId'],
337
- additionalProperties: false,
338
- },
339
- },
340
- {
341
- name: 'design.events',
342
- description: 'Fetch structured events/logs for a run (useful for debugging failures).',
343
- inputSchema: {
344
- type: 'object',
345
- properties: {
346
- runId: { type: 'string' },
347
- },
348
- required: ['runId'],
349
- additionalProperties: false,
350
- },
351
- },
352
- {
353
- name: 'design.artifacts.list',
354
- description: 'List artifacts for a run (e.g. png/webp/html/css/glb, previews, logs).',
355
- inputSchema: {
356
- type: 'object',
357
- properties: {
358
- runId: { type: 'string' },
359
- },
360
- required: ['runId'],
361
- additionalProperties: false,
362
- },
363
- },
364
- {
365
- name: 'design.artifacts.download',
366
- description: 'Download an artifact to a local cache directory and return the file path (for opening/viewing locally).',
367
- inputSchema: {
368
- type: 'object',
369
- properties: {
370
- runId: { type: 'string' },
371
- artifactId: { type: 'string' },
372
- },
373
- required: ['runId', 'artifactId'],
113
+ properties: {},
374
114
  additionalProperties: false,
375
115
  },
376
116
  },
@@ -385,117 +125,122 @@ export async function startMcpServer(options) {
385
125
  const { prompt, config, parentRunId } = await buildCreateRunRequest(client, input ?? {});
386
126
  const created = await client.createRun({ prompt, config, ...(parentRunId ? { parentRunId } : {}) });
387
127
  const wrapped = wrap('design.create', created);
388
- return { content: [{ type: 'text', text: pretty(wrapped) }] };
389
- }
390
- case 'design.list': {
391
- const page = typeof input?.page === 'number' ? input.page : undefined;
392
- const limit = typeof input?.limit === 'number' ? input.limit : undefined;
393
- const runs = await client.listRuns({ page, limit });
394
- const wrapped = wrap('design.list', runs);
395
- return { content: [{ type: 'text', text: pretty(wrapped) }] };
396
- }
397
- case 'design.get': {
398
- const runId = String(input?.runId ?? '').trim();
399
- if (!runId)
400
- throw new Error('Missing required argument: runId');
401
- const run = await client.getRun(runId);
402
- const wrapped = wrap('design.get', run, { runId });
403
- return { content: [{ type: 'text', text: pretty(wrapped) }] };
128
+ return { content: [{ type: 'text', text: formatToolResponseMarkdown(wrapped) }] };
404
129
  }
405
- case 'design.watch': {
130
+ case 'design.sync': {
406
131
  const runId = String(input?.runId ?? '').trim();
407
132
  if (!runId)
408
133
  throw new Error('Missing required argument: runId');
409
- const timeoutSeconds = typeof input?.timeoutSeconds === 'number' ? input.timeoutSeconds : 3600;
410
- const intervalSeconds = typeof input?.intervalSeconds === 'number' ? input.intervalSeconds : 5;
411
- const syncDir = typeof input?.syncDir === 'string' ? input.syncDir.trim() : '';
412
- const ensureAssets = typeof input?.ensureAssets === 'boolean' ? input.ensureAssets : undefined;
413
- const targetArtifactId = typeof input?.targetArtifactId === 'string' ? input.targetArtifactId.trim() : '';
134
+ const syncDirInput = typeof input?.syncDir === 'string' ? input.syncDir.trim() : '';
135
+ const syncDir = syncDirInput || `./every-design/${runId}`;
136
+ const progressToken = request.params?.progressToken;
137
+ const hasProgressToken = progressToken !== null && progressToken !== undefined;
138
+ const progressLines = [];
139
+ const tracker = createSyncProgressTracker();
140
+ const emitProgress = (pct) => {
141
+ progressLines.push(`Progress: ${pct}%`);
142
+ if (!hasProgressToken)
143
+ return;
144
+ void server.notification({
145
+ method: 'notifications/progress',
146
+ params: {
147
+ progressToken,
148
+ progress: pct,
149
+ total: 100,
150
+ message: `Sync ${pct}%`,
151
+ },
152
+ });
153
+ };
154
+ const initialPct = tracker.next(0);
155
+ if (initialPct !== null) {
156
+ emitProgress(initialPct);
157
+ }
414
158
  const run = await watchRun(client, runId, {
415
- timeoutSeconds,
416
- intervalSeconds,
417
- log: (line) => console.error(line),
418
- ...(syncDir ? { syncDir } : {}),
419
- ...(ensureAssets !== undefined ? { ensureAssets } : {}),
420
- ...(targetArtifactId ? { targetArtifactId } : {}),
159
+ timeoutSeconds: 1800,
160
+ intervalSeconds: 5,
161
+ onUpdate: (snapshot) => {
162
+ const pct = getProgressPct(snapshot.run);
163
+ const mapped = tracker.next(pct);
164
+ if (mapped !== null) {
165
+ emitProgress(mapped);
166
+ }
167
+ },
168
+ onExtractStart: () => {
169
+ const mapped = tracker.markExtractStart();
170
+ if (mapped !== null) {
171
+ emitProgress(mapped);
172
+ }
173
+ },
174
+ syncDir,
175
+ ensureAssets: true,
421
176
  });
422
- const wrapped = wrap('design.watch', run, { runId, ...(syncDir ? { syncDir } : {}) });
423
- return { content: [{ type: 'text', text: pretty(wrapped) }] };
424
- }
425
- case 'design.extractAssets': {
426
- const runId = String(input?.runId ?? '').trim();
427
- if (!runId)
428
- throw new Error('Missing required argument: runId');
429
- const targetArtifactId = typeof input?.targetArtifactId === 'string' ? input.targetArtifactId.trim() : '';
430
- const designKind = typeof input?.designKind === 'string' ? input.designKind.trim().toLowerCase() : '';
431
- const normalizedKind = designKind === 'html' ? 'interface' : designKind;
432
- if (targetArtifactId) {
433
- await client.setRunTarget(runId, targetArtifactId);
177
+ const finalPct = getProgressPct(run);
178
+ const mappedFinal = tracker.finalize(finalPct);
179
+ if (mappedFinal !== null) {
180
+ emitProgress(mappedFinal);
434
181
  }
435
- const res = await client.progressRun(runId, {
436
- action: 'extract_assets',
437
- ...(normalizedKind === 'interface' ? { designKind: 'interface' } : {}),
438
- });
439
- const wrapped = wrap('design.extractAssets', res, {
440
- runId,
441
- ...(targetArtifactId ? { artifactId: targetArtifactId } : {}),
442
- });
443
- return { content: [{ type: 'text', text: pretty(wrapped) }] };
182
+ const payload = typeof run === 'object' && run ? { ...run, progress: progressLines } : { run, progress: progressLines };
183
+ const wrapped = wrap('design.sync', payload, { runId, syncDir });
184
+ return { content: [{ type: 'text', text: formatToolResponseMarkdown(wrapped) }] };
444
185
  }
445
- case 'design.generateHtml': {
186
+ case 'design.check': {
446
187
  const runId = String(input?.runId ?? '').trim();
447
188
  if (!runId)
448
189
  throw new Error('Missing required argument: runId');
449
- const targetArtifactId = typeof input?.targetArtifactId === 'string' ? input.targetArtifactId.trim() : '';
450
- if (targetArtifactId) {
451
- await client.setRunTarget(runId, targetArtifactId);
190
+ const urlInput = typeof input?.url === 'string' ? input.url.trim() : '';
191
+ const pathInput = typeof input?.path === 'string' ? input.path.trim() : '';
192
+ if (!urlInput && !pathInput)
193
+ throw new Error('Missing required argument: url or path');
194
+ if (urlInput && pathInput)
195
+ throw new Error('Provide only one of url or path');
196
+ let targetUrl = urlInput;
197
+ if (pathInput) {
198
+ const resolved = path.resolve(pathInput);
199
+ let stat;
200
+ try {
201
+ stat = statSync(resolved);
202
+ }
203
+ catch {
204
+ throw new Error(`File not found: ${resolved}`);
205
+ }
206
+ if (!stat.isFile()) {
207
+ throw new Error(`Path must be a file: ${resolved}`);
208
+ }
209
+ targetUrl = pathToFileURL(resolved).toString();
452
210
  }
453
- const res = await client.progressRun(runId, { action: 'generate_html', designKind: 'interface' });
454
- const wrapped = wrap('design.generateHtml', res, {
455
- runId,
456
- ...(targetArtifactId ? { artifactId: targetArtifactId } : {}),
211
+ const screenshotOut = path.join(os.tmpdir(), `every-design-check-${runId}-${Date.now()}.png`);
212
+ const screenshot = await runScreenshotCommand({
213
+ url: targetUrl,
214
+ outPath: screenshotOut,
215
+ width: 1696,
216
+ height: 2528,
217
+ waitMs: 4000,
457
218
  });
458
- return { content: [{ type: 'text', text: pretty(wrapped) }] };
459
- }
460
- case 'design.iterate': {
461
- const runId = String(input?.runId ?? '').trim();
462
- if (!runId)
463
- throw new Error('Missing required argument: runId');
464
- const concerns = typeof input?.concerns === 'string' ? input.concerns.trim() : '';
465
- const viewportInput = input?.viewport && typeof input.viewport === 'object' ? input.viewport : null;
466
- const viewport = {
467
- width: Number(viewportInput?.width) || 1696,
468
- height: Number(viewportInput?.height) || 2528,
469
- };
470
- const render = Array.isArray(input?.render) ? input?.render : [];
471
- if (render.length === 0)
472
- throw new Error('Missing required argument: render (array of images)');
473
- const target = Array.isArray(input?.target) ? input?.target : [];
474
- const renderRefs = await client.uploadSourceImages(render);
475
- const targetRefs = target.length > 0 ? await client.uploadSourceImages(target) : [];
219
+ const renderRefs = await client.uploadSourceImages([{ type: 'path', path: screenshot.screenshotPath }]);
476
220
  const created = await client.createRunOperation(runId, {
477
221
  type: 'iterate',
478
- ...(concerns ? { concerns } : {}),
479
- viewport,
222
+ viewport: { width: screenshot.width, height: screenshot.height },
480
223
  render: renderRefs,
481
- ...(targetRefs.length > 0 ? { target: targetRefs } : {}),
482
224
  });
483
225
  const operationId = String(created?.operation?.id ?? '').trim();
484
226
  if (!operationId)
485
227
  throw new Error('Iterate operation created but missing operation id');
486
- const timeoutSeconds = typeof input?.timeoutSeconds === 'number' ? input.timeoutSeconds : 900;
487
- const intervalSeconds = typeof input?.intervalSeconds === 'number' ? input.intervalSeconds : 5;
488
- const deadlineMs = Date.now() + Math.max(1, timeoutSeconds) * 1000;
228
+ const deadlineMs = Date.now() + 900 * 1000;
229
+ let terminalStatus = '';
489
230
  while (true) {
490
231
  const op = await client.getOperation(operationId);
491
232
  const status = String(op?.operation?.status ?? '').trim().toLowerCase();
492
233
  if (status === 'completed' || status === 'failed' || status === 'cancelled') {
234
+ terminalStatus = status;
493
235
  break;
494
236
  }
495
237
  if (Date.now() > deadlineMs) {
496
238
  throw new Error(`Timed out waiting for iterate operation (${operationId})`);
497
239
  }
498
- await new Promise((r) => setTimeout(r, Math.max(1, intervalSeconds) * 1000));
240
+ await new Promise((r) => setTimeout(r, 5_000));
241
+ }
242
+ if (terminalStatus !== 'completed') {
243
+ throw new Error(`Iterate operation ${operationId} ended with status ${terminalStatus || 'unknown'}`);
499
244
  }
500
245
  const detail = await client.getRun(runId);
501
246
  const outputs = Array.isArray(detail?.run?.outputs) ? detail.run.outputs : [];
@@ -518,12 +263,17 @@ export async function startMcpServer(options) {
518
263
  runId,
519
264
  operationId,
520
265
  status: 'completed',
266
+ screenshot: {
267
+ filePath: screenshot.screenshotPath,
268
+ width: screenshot.width,
269
+ height: screenshot.height,
270
+ },
521
271
  iterate: null,
522
272
  generatedAssets,
523
273
  error: 'Missing critique artifact for iterate operation',
524
274
  };
525
- const wrapped = wrap('design.iterate', payload, { runId });
526
- return { content: [{ type: 'text', text: pretty(wrapped) }] };
275
+ const wrapped = wrap('design.check', payload, { runId });
276
+ return { content: [{ type: 'text', text: formatToolResponseMarkdown(wrapped) }] };
527
277
  }
528
278
  const url = typeof critiqueArtifact?.url === 'string' ? String(critiqueArtifact.url) : '';
529
279
  if (!url)
@@ -539,103 +289,21 @@ export async function startMcpServer(options) {
539
289
  }
540
290
  const payload = {
541
291
  runId,
542
- operationId,
292
+ screenshot: {
293
+ filePath: screenshot.screenshotPath,
294
+ width: screenshot.width,
295
+ height: screenshot.height,
296
+ },
543
297
  iterate: parsed,
544
298
  artifact: { id: critiqueArtifact.id, filePath: downloaded.filePath },
545
299
  generatedAssets,
546
300
  };
547
- const wrapped = wrap('design.iterate', payload, { runId });
548
- return { content: [{ type: 'text', text: pretty(wrapped) }] };
549
- }
550
- case 'design.uploadTarget': {
551
- const runId = String(input?.runId ?? '').trim();
552
- if (!runId)
553
- throw new Error('Missing required argument: runId');
554
- const sourceImage = input?.sourceImage;
555
- if (!sourceImage || typeof sourceImage !== 'object') {
556
- throw new Error('Missing required argument: sourceImage');
557
- }
558
- const res = await client.uploadRunTarget(runId, sourceImage);
559
- const wrapped = wrap('design.uploadTarget', res, { runId });
560
- return { content: [{ type: 'text', text: pretty(wrapped) }] };
561
- }
562
- case 'design.refineDraft': {
563
- const runId = String(input?.runId ?? '').trim();
564
- const artifactId = String(input?.artifactId ?? '').trim();
565
- const prompt = String(input?.prompt ?? '').trim();
566
- if (!runId)
567
- throw new Error('Missing required argument: runId');
568
- if (!artifactId)
569
- throw new Error('Missing required argument: artifactId');
570
- if (!prompt)
571
- throw new Error('Missing required argument: prompt');
572
- const res = await client.refineDraft(runId, artifactId, prompt);
573
- const wrapped = wrap('design.refineDraft', res, { runId, artifactId });
574
- return { content: [{ type: 'text', text: pretty(wrapped) }] };
575
- }
576
- case 'design.setTarget': {
577
- const runId = String(input?.runId ?? '').trim();
578
- const artifactId = String(input?.artifactId ?? '').trim();
579
- if (!runId)
580
- throw new Error('Missing required argument: runId');
581
- if (!artifactId)
582
- throw new Error('Missing required argument: artifactId');
583
- const res = await client.setRunTarget(runId, artifactId);
584
- const wrapped = wrap('design.setTarget', res, { runId, artifactId });
585
- return { content: [{ type: 'text', text: pretty(wrapped) }] };
586
- }
587
- case 'design.clearTarget': {
588
- const runId = String(input?.runId ?? '').trim();
589
- if (!runId)
590
- throw new Error('Missing required argument: runId');
591
- const res = await client.clearRunTarget(runId);
592
- const wrapped = wrap('design.clearTarget', res, { runId });
593
- return { content: [{ type: 'text', text: pretty(wrapped) }] };
594
- }
595
- case 'design.events': {
596
- const runId = String(input?.runId ?? '').trim();
597
- if (!runId)
598
- throw new Error('Missing required argument: runId');
599
- const events = await client.getRunEvents(runId);
600
- const wrapped = wrap('design.events', events, { runId });
601
- return { content: [{ type: 'text', text: pretty(wrapped) }] };
602
- }
603
- case 'design.artifacts.list': {
604
- const runId = String(input?.runId ?? '').trim();
605
- if (!runId)
606
- throw new Error('Missing required argument: runId');
607
- const detail = await client.getRun(runId);
608
- const outputs = Array.isArray(detail?.run?.outputs) ? detail.run.outputs : [];
609
- const wrapped = wrap('design.artifacts.list', { artifacts: outputs }, { runId });
610
- return { content: [{ type: 'text', text: pretty(wrapped) }] };
301
+ const wrapped = wrap('design.check', payload, { runId });
302
+ return { content: [{ type: 'text', text: formatToolResponseMarkdown(wrapped) }] };
611
303
  }
612
- case 'design.artifacts.download': {
613
- const runId = String(input?.runId ?? '').trim();
614
- const artifactId = String(input?.artifactId ?? '').trim();
615
- if (!runId)
616
- throw new Error('Missing required argument: runId');
617
- if (!artifactId)
618
- throw new Error('Missing required argument: artifactId');
619
- const detail = await client.getRun(runId);
620
- const outputs = Array.isArray(detail?.run?.outputs) ? detail.run.outputs : [];
621
- const found = outputs.find((entry) => String(entry?.id ?? '') === artifactId) || null;
622
- const url = typeof found?.url === 'string' ? found.url : '';
623
- if (!url)
624
- throw new Error(`Artifact not found in standard outputs: ${artifactId}`);
625
- const hint = (() => {
626
- try {
627
- const u = url.startsWith('http://') || url.startsWith('https://')
628
- ? new URL(url)
629
- : new URL(url, 'https://example.invalid');
630
- return u.pathname.split('/').filter(Boolean).pop() || undefined;
631
- }
632
- catch {
633
- return url.split('/').filter(Boolean).pop() || undefined;
634
- }
635
- })();
636
- const artifact = await client.downloadUrlToCache(runId, artifactId, url, { fileNameHint: hint });
637
- const wrapped = wrap('design.artifacts.download', artifact, { runId, artifactId });
638
- return { content: [{ type: 'text', text: pretty(wrapped) }] };
304
+ case 'design.instructions': {
305
+ const wrapped = wrap('design.instructions', { instructions: INSTRUCTIONS_MD });
306
+ return { content: [{ type: 'text', text: formatToolResponseMarkdown(wrapped) }] };
639
307
  }
640
308
  default:
641
309
  throw new Error(`Unknown tool: ${tool}`);
@@ -649,9 +317,6 @@ export async function startMcpServer(options) {
649
317
  const transport = new StdioServerTransport();
650
318
  await server.connect(transport);
651
319
  }
652
- function pretty(value) {
653
- return JSON.stringify(value, null, 2);
654
- }
655
320
  async function resolvePackageVersion() {
656
321
  try {
657
322
  const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url));